okengine 0.17.2 → 0.18.3

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 (197) hide show
  1. package/README.md +9 -5
  2. package/manifest.v1.schema.json +61 -2
  3. package/package.json +18 -4
  4. package/site/content/docs/elements/clock.mdx +1 -1
  5. package/site/content/docs/elements/flow.mdx +25 -1
  6. package/site/content/docs/elements/store.mdx +287 -341
  7. package/site/content/docs/elements/vault.mdx +5 -5
  8. package/site/content/docs/get-started/installation.mdx +1 -2
  9. package/site/content/docs/get-started/introduction.mdx +54 -110
  10. package/site/content/docs/get-started/meta.json +9 -1
  11. package/site/content/docs/get-started/testing.mdx +328 -0
  12. package/site/content/docs/get-started/why.mdx +94 -70
  13. package/site/content/docs/index.mdx +1 -1
  14. package/site/content/docs/plugins/apple.mdx +151 -0
  15. package/site/content/docs/plugins/discord.mdx +139 -0
  16. package/site/content/docs/plugins/facebook.mdx +134 -0
  17. package/site/content/docs/plugins/figma.mdx +138 -0
  18. package/site/content/docs/plugins/github.mdx +138 -0
  19. package/site/content/docs/plugins/google.mdx +153 -0
  20. package/site/content/docs/plugins/index.mdx +47 -1
  21. package/site/content/docs/plugins/meta.json +10 -0
  22. package/site/content/docs/plugins/microsoft.mdx +151 -0
  23. package/site/content/docs/plugins/oauth.mdx +188 -0
  24. package/site/content/docs/plugins/x.mdx +125 -0
  25. package/site/content/docs/reference/cli.md +3 -2
  26. package/site/content/docs/reference/client.mdx +58 -1
  27. package/site/content/docs/reference/configuration.mdx +2 -4
  28. package/site/content/docs/reference/fx.mdx +3 -1
  29. package/site/content/docs/reference/index.mdx +0 -5
  30. package/site/content/docs/reference/meta.json +2 -2
  31. package/site/content/docs/reference/okid.mdx +137 -0
  32. package/src/auth/bindings.ts +1 -1
  33. package/src/auth/config.ts +9 -0
  34. package/src/auth/identity-sql.ts +314 -0
  35. package/src/auth/identity.ts +140 -2
  36. package/src/auth/index.ts +17 -1
  37. package/src/auth/method-context.ts +3 -0
  38. package/src/auth/oauth-as/cimd.ts +132 -0
  39. package/src/auth/oauth-as/crypto.test.ts +101 -0
  40. package/src/auth/oauth-as/crypto.ts +393 -0
  41. package/src/auth/oauth-as/errors.ts +68 -0
  42. package/src/auth/oauth-as/http.test.ts +419 -0
  43. package/src/auth/oauth-as/http.ts +842 -0
  44. package/src/auth/oauth-as/stores.ts +61 -0
  45. package/src/auth/oauth-as/tables.ts +142 -0
  46. package/src/auth/tables.ts +0 -11
  47. package/src/bench/README.md +83 -0
  48. package/src/bench/REPORT.md +176 -0
  49. package/src/bench/g01-rls-stamp.bench.ts +194 -0
  50. package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
  51. package/src/bench/g03-signal-once.bench.ts +157 -0
  52. package/src/bench/g03-signal-reconnect.bench.ts +254 -0
  53. package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
  54. package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
  55. package/src/bench/g05-sustained-full.bench.ts +265 -0
  56. package/src/bench/g06-mixed-load.bench.ts +260 -0
  57. package/src/bench/g07-vault-crypto.bench.ts +100 -0
  58. package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
  59. package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
  60. package/src/bench/g08-store-kv-durable.bench.ts +133 -0
  61. package/src/bench/g08-store-sql.bench.ts +178 -0
  62. package/src/bench/g09-journal-sustained.bench.ts +203 -0
  63. package/src/bench/g10-observability-contention.bench.ts +246 -0
  64. package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
  65. package/src/bench/g13-elements.bench.ts +427 -0
  66. package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
  67. package/src/bench/g15-postgres-degradation.bench.ts +264 -0
  68. package/src/bench/g16-live-query-fanout.bench.ts +206 -0
  69. package/src/bench/lib/event-loop-lag.ts +26 -0
  70. package/src/bench/lib/infra.ts +60 -0
  71. package/src/bench/lib/report.ts +52 -0
  72. package/src/bench/lib/rss-sampler.ts +61 -0
  73. package/src/bench/lib/signal-pg.ts +88 -0
  74. package/src/bench/load-app.ts +337 -0
  75. package/src/bench/load-child.ts +108 -0
  76. package/src/bench/smoke.bench.ts +43 -0
  77. package/src/cli/competitor-mention-removal.test.ts +28 -0
  78. package/src/cli/doctor-fd.ts +117 -0
  79. package/src/cli/doctor.test.ts +192 -0
  80. package/src/cli/doctor.ts +129 -1
  81. package/src/client/create.ts +95 -1
  82. package/src/client/index.ts +9 -2
  83. package/src/client/transport.ts +11 -4
  84. package/src/client/use-live-query.ts +154 -0
  85. package/src/client-react/index.ts +15 -1
  86. package/src/client-react/live-resource.ts +246 -0
  87. package/src/client-react/use-live-query.test.ts +475 -0
  88. package/src/client-react/use-live-query.ts +530 -0
  89. package/src/compiler/extract.test.ts +518 -0
  90. package/src/compiler/extract.ts +386 -19
  91. package/src/console/server/invoke-user-flow.ts +2 -1
  92. package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-DY4N6nnk.js} +1 -1
  93. package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-CsPDMrVM.js} +1 -1
  94. package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-CcTDXHuz.js} +3 -3
  95. package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-CKR595wP.js} +1 -1
  96. package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
  97. package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-CpPFFKyE.js} +1 -1
  98. package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-BsMf-9_W.js} +1 -1
  99. package/src/console/ui-next/dist/index.html +1 -1
  100. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
  101. package/src/drivers/cdc-outbox.ts +389 -0
  102. package/src/drivers/memory.ts +20 -0
  103. package/src/drivers/oauth-apple.ts +156 -0
  104. package/src/drivers/oauth-discord.ts +79 -0
  105. package/src/drivers/oauth-facebook.ts +80 -0
  106. package/src/drivers/oauth-figma.ts +116 -0
  107. package/src/drivers/oauth-github.ts +92 -0
  108. package/src/drivers/oauth-google.ts +142 -0
  109. package/src/drivers/oauth-microsoft.ts +174 -0
  110. package/src/drivers/oauth-oidc.ts +293 -0
  111. package/src/drivers/oauth-shared.ts +326 -0
  112. package/src/drivers/oauth-types.ts +159 -0
  113. package/src/drivers/oauth-x.ts +77 -0
  114. package/src/drivers/oauth2-common.ts +95 -0
  115. package/src/drivers/oauth2-token.ts +61 -0
  116. package/src/drivers/pg-rls-row-passes.ts +251 -0
  117. package/src/drivers/pg-rls.ts +2 -0
  118. package/src/drivers/postgres.ts +45 -2
  119. package/src/drivers/signal-postgres.ts +2 -1
  120. package/src/elements/channel/runtime.ts +29 -2
  121. package/src/elements/channel.test.ts +52 -0
  122. package/src/elements/gate/boot.ts +29 -2
  123. package/src/elements/store/emit-drizzle.ts +147 -14
  124. package/src/elements/store/field-ddl.test.ts +118 -0
  125. package/src/elements/store/field-types.test.ts +455 -0
  126. package/src/elements/store/list-query.golden.json +777 -0
  127. package/src/elements/store/list-query.parity.test.ts +396 -0
  128. package/src/elements/store/list-query.ts +792 -0
  129. package/src/elements/store/live-default.test.ts +136 -0
  130. package/src/elements/store/live-http.test.ts +160 -0
  131. package/src/elements/store/live-isolation.test.ts +291 -0
  132. package/src/elements/store/live-query-runtime.test.ts +323 -0
  133. package/src/elements/store/live-query-runtime.ts +403 -0
  134. package/src/elements/store/live-query-server.test.ts +377 -0
  135. package/src/elements/store/live-query-server.ts +102 -0
  136. package/src/elements/store/live-query.ts +97 -0
  137. package/src/elements/store/resource.ts +189 -680
  138. package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
  139. package/src/elements/store/schema-decl.ts +539 -41
  140. package/src/elements/store/sql-rls-stamp.test.ts +27 -0
  141. package/src/elements/store/sql-session.ts +297 -35
  142. package/src/elements/store/table.ts +102 -21
  143. package/src/elements/store.test.ts +3 -1
  144. package/src/elements/store.ts +12 -1
  145. package/src/elements/vault/chaos-child.ts +74 -1
  146. package/src/elements/vault/chaos.test.ts +4 -2
  147. package/src/elements/vault/storage.ts +4 -2
  148. package/src/index.ts +4 -1
  149. package/src/kernel/app-auth.ts +1 -0
  150. package/src/kernel/app.ts +116 -2
  151. package/src/kernel/auth-sharing.test.ts +196 -0
  152. package/src/kernel/boot.test.ts +3 -3
  153. package/src/kernel/errors.ts +8 -0
  154. package/src/kernel/fx.test.ts +1 -0
  155. package/src/kernel/fx.ts +14 -2
  156. package/src/kernel/horizontal-child.ts +2 -1
  157. package/src/kernel/http-resource.ts +33 -7
  158. package/src/kernel/identity-host-persist.test.ts +119 -0
  159. package/src/kernel/instance-id.ts +4 -2
  160. package/src/kernel/journal.ts +2 -1
  161. package/src/kernel/mcp-tool.test.ts +95 -0
  162. package/src/kernel/on.ts +9 -0
  163. package/src/kernel/realtime-bind.ts +326 -0
  164. package/src/kernel/resource-live.ts +117 -0
  165. package/src/kernel/triggers.ts +86 -4
  166. package/src/manifest/diff.ts +37 -0
  167. package/src/manifest/types.ts +64 -2
  168. package/src/okid.bench.test.ts +64 -0
  169. package/src/okid.test.ts +338 -0
  170. package/src/okid.ts +245 -0
  171. package/src/plugins/anonymous.ts +19 -1
  172. package/src/plugins/auth/shared.ts +15 -0
  173. package/src/plugins/index.ts +2 -0
  174. package/src/plugins/magic-link.ts +10 -8
  175. package/src/plugins/mcp-oauth.ts +208 -0
  176. package/src/plugins/oauth/flow-store.ts +117 -0
  177. package/src/plugins/oauth/link.ts +69 -0
  178. package/src/plugins/oauth/shared.ts +108 -0
  179. package/src/plugins/oauth/token-vault.ts +100 -0
  180. package/src/plugins/oauth.security.test.ts +535 -0
  181. package/src/plugins/oauth.ts +532 -0
  182. package/src/plugins/otp.ts +48 -6
  183. package/src/plugins/passkey.ts +20 -1
  184. package/src/plugins/two-factor.ts +11 -0
  185. package/src/plugins/username.ts +40 -7
  186. package/src/release/build-lib.ts +7 -1
  187. package/src/release/measure.ts +1 -0
  188. package/src/release/official-plugins.ts +4 -1
  189. package/src/runs/collect.ts +2 -1
  190. package/src/runs/drivers/files.ts +2 -1
  191. package/src/test/create-test-app.ts +114 -5
  192. package/src/test/export-bundle.test.ts +33 -0
  193. package/src/test/live-signals.test.ts +83 -0
  194. package/src/test/tenant-isolation.test.ts +175 -0
  195. package/src/testing.ts +26 -0
  196. package/src/upgrade/codemods.ts +1 -1
  197. package/site/content/docs/reference/migrating-environments.mdx +0 -158
@@ -272,6 +272,103 @@ export const db = store.sql("app", { schema: { bookings } });
272
272
  expect(table?.policies?.owner_owner_all?.using).toContain("oke.user()");
273
273
  });
274
274
 
275
+ test("extracts widened field heads and option bags into manifest columns", async () => {
276
+ const source = `
277
+ import { store, field, id } from "okengine";
278
+
279
+ export const events = store.schema.table("events", {
280
+ id: field.uuid().primaryKey().defaultFn(id),
281
+ status: field.varchar({ length: 64, enum: ["new", "archived"] }),
282
+ doc: field.jsonb(),
283
+ at: field.timestamp({ mode: "date", precision: 6, withTimezone: true }).notNull(),
284
+ day: field.date(),
285
+ amount: field.numeric({ precision: 12, scale: 2 }).notNull(),
286
+ ratio: field.numeric({ mode: "number" }),
287
+ span: field.interval({ fields: "day to second" }),
288
+ loc: field.point({ mode: "xy" }),
289
+ ip: field.inet(),
290
+ bin: field.bytea(),
291
+ big: field.bigserial({ mode: "bigint" }),
292
+ flag: field.boolean(),
293
+ });
294
+
295
+ export const db = store.sql("app", { schema: { events } });
296
+ `;
297
+ const manifest = await extractFromSources({
298
+ "src/schema.decl.ts": source,
299
+ });
300
+ const cols = manifest.stores?.app?.tables?.events?.columns;
301
+
302
+ expect(cols?.id).toMatchObject({ type: "uuid", primaryKey: true });
303
+ expect(cols?.status).toMatchObject({
304
+ type: "varchar",
305
+ length: 64,
306
+ enumValues: ["new", "archived"],
307
+ });
308
+ expect(cols?.doc).toMatchObject({ type: "jsonb", nullable: true });
309
+ expect(cols?.at).toMatchObject({
310
+ type: "timestamp",
311
+ mode: "date",
312
+ precision: 6,
313
+ withTimezone: true,
314
+ nullable: false,
315
+ });
316
+ expect(cols?.day).toMatchObject({ type: "date", nullable: true });
317
+ expect(cols?.amount).toMatchObject({
318
+ type: "numeric",
319
+ precision: 12,
320
+ scale: 2,
321
+ nullable: false,
322
+ });
323
+ expect(cols?.ratio).toMatchObject({ type: "numeric", mode: "number" });
324
+ expect(cols?.span).toMatchObject({ type: "interval", fields: "day to second" });
325
+ expect(cols?.loc).toMatchObject({ type: "point", mode: "xy" });
326
+ expect(cols?.ip).toMatchObject({ type: "inet" });
327
+ expect(cols?.bin).toMatchObject({ type: "bytea" });
328
+ expect(cols?.big).toMatchObject({ type: "bigserial", mode: "bigint" });
329
+ expect(cols?.flag).toMatchObject({ type: "boolean" });
330
+ });
331
+
332
+ test("prepared .okid()/.now() defaults mark the column dynamic like defaultFn", async () => {
333
+ const source = `
334
+ import { store, field } from "okengine";
335
+
336
+ export const assets = store.schema.table("assets", {
337
+ id: field.text().primaryKey().okid(),
338
+ createdAt: field.integer().notNull().now(),
339
+ updatedAt: field.timestamp({ mode: "date" }),
340
+ });
341
+
342
+ export const db = store.sql("app", { schema: { assets } });
343
+ `;
344
+ const manifest = await extractFromSources({
345
+ "src/schema.decl.ts": source,
346
+ });
347
+ const cols = manifest.stores?.app?.tables?.assets?.columns;
348
+ expect(cols?.id).toMatchObject({ type: "text", primaryKey: true, default: null });
349
+ expect(cols?.createdAt).toMatchObject({ type: "integer", nullable: false, default: null });
350
+ });
351
+
352
+ test("decimal head collapses to numeric in the manifest", async () => {
353
+ const source = `
354
+ import { store, field } from "okengine";
355
+
356
+ export const rates = store.schema.table("rates", {
357
+ rate: field.decimal({ precision: 6, scale: 3 }),
358
+ });
359
+
360
+ export const db = store.sql("app", { schema: { rates } });
361
+ `;
362
+ const manifest = await extractFromSources({
363
+ "src/schema.decl.ts": source,
364
+ });
365
+ expect(manifest.stores?.app?.tables?.rates?.columns?.rate).toMatchObject({
366
+ type: "numeric",
367
+ precision: 6,
368
+ scale: 3,
369
+ });
370
+ });
371
+
275
372
  test("extracts optional description fields additively", async () => {
276
373
  const source = `
277
374
  import { store, field, signal, channel, clock, gate, vault } from "okengine";
@@ -534,6 +631,242 @@ export const remove = mounted.remove;
534
631
  expect(manifest.flows?.get?.live).toBeUndefined();
535
632
  expect(manifest.flows?.create?.live).toBeUndefined();
536
633
  });
634
+
635
+ test("live: true synthesizes the internal signal + GET /live flow", async () => {
636
+ const source = `
637
+ import { on, http, store } from "okengine";
638
+
639
+ export const db = store.sql("notes", { schema: {} });
640
+
641
+ export const notesTable = store.schema.table("notes", {});
642
+ const notesR = store.resource(db, notesTable, { live: true });
643
+
644
+ const mounted = on(http.resource("/notes", notesR.all()));
645
+
646
+ export const list = mounted.list;
647
+ export const get = mounted.get;
648
+ `;
649
+ const manifest = await extractFromSources({ "src/flows/notes.ts": source });
650
+
651
+ // Internal signal derived from the physical table.
652
+ expect(manifest.signals?.["oke/live/sql:notes"]?.delivery).toBe("live");
653
+ expect(manifest.flows?.["_live_notes"]?.trigger).toEqual({
654
+ http: { method: "GET", path: "/notes/live" },
655
+ });
656
+ expect(manifest.flows?.["_live_notes"]?.effects?.reads).toEqual([
657
+ "sql:notes",
658
+ "signal:oke/live/sql:notes",
659
+ ]);
660
+ });
661
+
662
+ test("live signal name follows a schema-table binding, not the JS binding", async () => {
663
+ const source = `
664
+ import { on, http, store } from "okengine";
665
+
666
+ export const db = store.sql("notes", { schema: {} });
667
+
668
+ export const tasksTable = store.schema.table("tasks", {});
669
+ const tasksR = store.resource(db, tasksTable, { live: true });
670
+ const mounted = on(http.resource("/tasks", tasksR.all()));
671
+
672
+ export const list = mounted.list;
673
+ `;
674
+ const manifest = await extractFromSources({ "src/flows/tasks.ts": source });
675
+
676
+ expect(manifest.signals?.["oke/live/sql:tasks"]).toBeDefined();
677
+ expect(manifest.flows?.["_live_tasks"]?.trigger?.http?.path).toBe("/tasks/live");
678
+ });
679
+ });
680
+
681
+ describe("extractManifest — live guardrails (DX Pack A)", () => {
682
+ test("live: true without a PK column fails loud at extract", async () => {
683
+ const source = `
684
+ import { on, http, store } from "okengine";
685
+
686
+ export const db = store.sql("notes", { schema: {} });
687
+
688
+ export const notesTable = store.schema.table(
689
+ "notes",
690
+ { body: field.text().notNull() },
691
+ );
692
+ const notesR = store.resource(db, notesTable, { live: true });
693
+ const mounted = on(http.resource("/notes", notesR.all()));
694
+
695
+ export const list = mounted.list;
696
+ `;
697
+ expect(extractFromSources({ "src/flows/notes.ts": source })).rejects.toThrow(
698
+ /requires a primary key/,
699
+ );
700
+ });
701
+
702
+ test("warns when the table has no updatedAt / updated_at column", async () => {
703
+ const source = `
704
+ import { on, http, store } from "okengine";
705
+
706
+ export const db = store.sql("notes", { schema: {} });
707
+
708
+ export const notesTable = store.schema.table(
709
+ "notes",
710
+ { id: field.text().primaryKey() },
711
+ );
712
+ const notesR = store.resource(db, notesTable, { live: true });
713
+ const mounted = on(http.resource("/notes", notesR.all()));
714
+
715
+ export const list = mounted.list;
716
+ `;
717
+ const warnings: string[] = [];
718
+ const originalWarn = console.warn;
719
+ console.warn = (msg: unknown): void => {
720
+ warnings.push(String(msg));
721
+ };
722
+ try {
723
+ await extractFromSources({ "src/flows/notes.ts": source });
724
+ } finally {
725
+ console.warn = originalWarn;
726
+ }
727
+ expect(warnings.some((w) => w.includes("updatedAt"))).toBe(true);
728
+ // RLS warn also fires here — declared table carries no policies.
729
+ expect(warnings.some((w) => w.includes("RLS policies"))).toBe(true);
730
+ });
731
+
732
+ test("no updatedAt warn when a timestamp variant exists; RLS warn alone fires", async () => {
733
+ const source = `
734
+ import { on, http, store } from "okengine";
735
+
736
+ export const db = store.sql("notes", { schema: {} });
737
+
738
+ export const notesTable = store.schema.table(
739
+ "notes",
740
+ { id: field.text().primaryKey(), updated_at: field.timestamp({ mode: "string" }).notNull() },
741
+ );
742
+ const notesR = store.resource(db, notesTable, { live: true });
743
+ const mounted = on(http.resource("/notes", notesR.all()));
744
+
745
+ export const list = mounted.list;
746
+ `;
747
+ const warnings: string[] = [];
748
+ const originalWarn = console.warn;
749
+ console.warn = (msg: unknown): void => {
750
+ warnings.push(String(msg));
751
+ };
752
+ try {
753
+ await extractFromSources({ "src/flows/notes.ts": source });
754
+ } finally {
755
+ console.warn = originalWarn;
756
+ }
757
+ expect(warnings.some((w) => w.includes("updatedAt"))).toBe(false);
758
+ expect(warnings.some((w) => w.includes("RLS policies"))).toBe(true);
759
+ });
760
+
761
+ test("silent when the table is complete: PK + updatedAt + policies", async () => {
762
+ const source = `
763
+ import { on, http, store } from "okengine";
764
+
765
+ export const db = store.sql("notes", { schema: {} });
766
+
767
+ export const notesTable = store.schema.table(
768
+ "notes",
769
+ { id: field.text().primaryKey(), updated_at: field.timestamp({ mode: "string" }).notNull() },
770
+ [store.schema.policy.gate("member")],
771
+ );
772
+ const notesR = store.resource(db, notesTable, { live: true });
773
+ const mounted = on(http.resource("/notes", notesR.all()).gate(member));
774
+
775
+ export const list = mounted.list;
776
+ `;
777
+ const warnings: string[] = [];
778
+ const originalWarn = console.warn;
779
+ console.warn = (msg: unknown): void => {
780
+ warnings.push(String(msg));
781
+ };
782
+ try {
783
+ await extractFromSources({ "src/flows/notes.ts": source });
784
+ } finally {
785
+ console.warn = originalWarn;
786
+ }
787
+ expect(warnings).toEqual([]);
788
+ });
789
+ });
790
+
791
+ describe("extractManifest — manual live (.live(table) on a hand-written flow)", () => {
792
+ test("synthesizes the internal signal + live CustomMatch flow on the hand-written GET", async () => {
793
+ const source = `
794
+ import { on, http, store, flow, liveQuery } from "okengine";
795
+
796
+ export const db = store.sql("tasks", { schema: {} });
797
+
798
+ export const tasksTable = store.schema.table(
799
+ "tasks",
800
+ { id: field.text().primaryKey(), updated_at: field.timestamp({ mode: "string" }).notNull() },
801
+ [store.schema.policy.gate("member")],
802
+ );
803
+
804
+ export const tasksLive = on(
805
+ http.get("/tasks/live").gate(member).live(tasksTable),
806
+ flow("tasks.live", {
807
+ in: { unknown: true },
808
+ do: async (_input, fx) => liveQuery(fx, tasksTable, _input),
809
+ }),
810
+ );
811
+ `;
812
+ const manifest = await extractFromSources({ "src/flows/tasks.ts": source });
813
+
814
+ expect(manifest.signals?.["oke/live/sql:tasks"]?.delivery).toBe("live");
815
+ const flow = manifest.flows?.["tasks_live"] ?? manifest.flows?.["tasks.live"];
816
+ expect(flow?.live).toBe("oke/live/sql:tasks");
817
+ expect(flow?.trigger).toEqual({ http: { method: "GET", path: "/tasks/live" } });
818
+ expect(flow?.gates).toEqual(["member"]);
819
+ });
820
+
821
+ test("guardrails fire for .live(table): no PK → extract error; missing updatedAt → warn", async () => {
822
+ const noPk = `
823
+ import { on, http, store, flow, liveQuery } from "okengine";
824
+
825
+ export const db = store.sql("tasks", { schema: {} });
826
+
827
+ export const tasksTable = store.schema.table(
828
+ "tasks",
829
+ { body: field.text().notNull() },
830
+ [store.schema.policy.gate("member")],
831
+ );
832
+
833
+ export const tasksLive = on(
834
+ http.get("/tasks/live").gate(member).live(tasksTable),
835
+ flow("tasks.live", { do: async (_input, fx) => liveQuery(fx, tasksTable, _input) }),
836
+ );
837
+ `;
838
+ await expect(extractFromSources({ "src/flows/tasks.ts": noPk })).rejects.toThrow(
839
+ /requires a primary key/,
840
+ );
841
+
842
+ const warnSource = `
843
+ import { on, http, store, flow, liveQuery } from "okengine";
844
+
845
+ export const db = store.sql("tasks", { schema: {} });
846
+
847
+ export const tasksTable = store.schema.table(
848
+ "tasks",
849
+ { id: field.text().primaryKey() },
850
+ [store.schema.policy.gate("member")],
851
+ );
852
+
853
+ export const tasksLive = on(
854
+ http.get("/tasks/live").gate(member).live(tasksTable),
855
+ flow("tasks.live", { do: async (_input, fx) => liveQuery(fx, tasksTable, _input) }),
856
+ );
857
+ `;
858
+ const warnings: string[] = [];
859
+ const originalWarn = console.warn;
860
+ console.warn = (msg: unknown): void => {
861
+ warnings.push(String(msg));
862
+ };
863
+ try {
864
+ await extractFromSources({ "src/flows/tasks.ts": warnSource });
865
+ } finally {
866
+ console.warn = originalWarn;
867
+ }
868
+ expect(warnings.some((w) => w.includes("updatedAt"))).toBe(true);
869
+ });
537
870
  });
538
871
 
539
872
  describe("extractManifest — vault.config", () => {
@@ -1337,3 +1670,188 @@ export const app = oke({
1337
1670
  );
1338
1671
  });
1339
1672
  });
1673
+
1674
+ describe("extractManifest — project-wide store.live default", () => {
1675
+ test("oke({ store: { live: true } }) makes new tables live by default", async () => {
1676
+ const source = `
1677
+ import { on, http, store, field, oke } from "okengine";
1678
+
1679
+ export const db = store.sql("app", { schema: {} });
1680
+
1681
+ export const notes = store.schema.table("notes", {
1682
+ id: field.text().primaryKey(),
1683
+ updatedAt: field.integer().notNull(),
1684
+ }, [
1685
+ store.schema.policy.owner("owner"),
1686
+ ]);
1687
+
1688
+ const notesR = store.resource(db, notes, {});
1689
+ const mounted = on(http.resource("/notes", notesR.all()));
1690
+
1691
+ export const list = mounted.list;
1692
+ export const app = oke({
1693
+ name: "shop",
1694
+ store: { live: true },
1695
+ });
1696
+ `;
1697
+ const manifest = await extractFromSources({ "src/app.ts": source });
1698
+
1699
+ // Manifest.store stamps the project flag.
1700
+ expect(manifest.store).toEqual({ live: true });
1701
+
1702
+ // The table itself is live by default (opt-in flip, no per-table flag).
1703
+ expect(manifest.stores?.app?.tables?.notes?.live).toBe(true);
1704
+
1705
+ // Resource synthesized GET /notes/live + internal signal without an explicit live: true.
1706
+ expect(manifest.signals?.["oke/live/sql:notes"]?.delivery).toBe("live");
1707
+ expect(manifest.flows?.["_live_notes"]?.trigger?.http?.path).toBe("/notes/live");
1708
+ });
1709
+
1710
+ test("store.schema.live(false) opts a table out under the project flag", async () => {
1711
+ const source = `
1712
+ import { on, http, store, field, oke } from "okengine";
1713
+
1714
+ export const db = store.sql("app", { schema: {} });
1715
+
1716
+ export const notes = store.schema.table("notes", {
1717
+ id: field.text().primaryKey(),
1718
+ updatedAt: field.integer().notNull(),
1719
+ }, [
1720
+ store.schema.policy.owner("owner"),
1721
+ store.schema.live(false),
1722
+ ]);
1723
+
1724
+ const notesR = store.resource(db, notes, {});
1725
+ const mounted = on(http.resource("/notes", notesR.all()));
1726
+
1727
+ export const list = mounted.list;
1728
+ export const app = oke({
1729
+ name: "shop",
1730
+ store: { live: true },
1731
+ });
1732
+ `;
1733
+ const manifest = await extractFromSources({ "src/app.ts": source });
1734
+
1735
+ expect(manifest.store).toEqual({ live: true });
1736
+ // Opted-out table stays not live — no default flip, no synthesis.
1737
+ expect(manifest.stores?.app?.tables?.notes?.live).toBe(false);
1738
+ expect(manifest.signals?.["oke/live/sql:notes"]).toBeUndefined();
1739
+ expect(manifest.flows?.["_live_notes"]).toBeUndefined();
1740
+ });
1741
+
1742
+ test("live: false on the resource is equivalent to store.schema.live(false)", async () => {
1743
+ const source = `
1744
+ import { on, http, store, field, oke } from "okengine";
1745
+
1746
+ export const db = store.sql("app", { schema: {} });
1747
+
1748
+ export const notes = store.schema.table("notes", {
1749
+ id: field.text().primaryKey(),
1750
+ updatedAt: field.integer().notNull(),
1751
+ }, [
1752
+ store.schema.policy.owner("owner"),
1753
+ ]);
1754
+
1755
+ const notesR = store.resource(db, notes, { live: false });
1756
+ const mounted = on(http.resource("/notes", notesR.all()));
1757
+
1758
+ export const list = mounted.list;
1759
+ export const app = oke({
1760
+ name: "shop",
1761
+ store: { live: true },
1762
+ });
1763
+ `;
1764
+ const manifest = await extractFromSources({ "src/app.ts": source });
1765
+
1766
+ // The table-level default applies (live-by-default project-wide); only
1767
+ // this mount's /live route is suppressed by the resource opt-out.
1768
+ expect(manifest.stores?.app?.tables?.notes?.live).toBe(true);
1769
+ expect(manifest.signals?.["oke/live/sql:notes"]?.delivery).toBe("live");
1770
+ expect(manifest.flows?.["_live_notes"]).toBeUndefined();
1771
+ });
1772
+
1773
+ test("explicit live: true wins over store.schema.live(false)", async () => {
1774
+ const source = `
1775
+ import { on, http, store, field, oke } from "okengine";
1776
+
1777
+ export const db = store.sql("app", { schema: {} });
1778
+
1779
+ export const notes = store.schema.table("notes", {
1780
+ id: field.text().primaryKey(),
1781
+ updatedAt: field.integer().notNull(),
1782
+ }, [
1783
+ store.schema.policy.owner("owner"),
1784
+ store.schema.live(false),
1785
+ ]);
1786
+
1787
+ const notesR = store.resource(db, notes, { live: true });
1788
+ const mounted = on(http.resource("/notes", notesR.all()));
1789
+
1790
+ export const list = mounted.list;
1791
+ export const app = oke({
1792
+ name: "shop",
1793
+ store: { live: true },
1794
+ });
1795
+ `;
1796
+ const manifest = await extractFromSources({ "src/app.ts": source });
1797
+
1798
+ // The explicit opt-in is separate from the default flip — it still wins.
1799
+ expect(manifest.stores?.app?.tables?.notes?.live).toBe(false);
1800
+ expect(manifest.signals?.["oke/live/sql:notes"]?.delivery).toBe("live");
1801
+ expect(manifest.flows?.["_live_notes"]?.trigger?.http?.path).toBe("/notes/live");
1802
+ });
1803
+
1804
+ test("without the flag, an omitted live stays today's explicit-only behavior", async () => {
1805
+ const source = `
1806
+ import { on, http, store, field } from "okengine";
1807
+
1808
+ export const db = store.sql("app", { schema: {} });
1809
+
1810
+ export const notes = store.schema.table("notes", {
1811
+ id: field.text().primaryKey(),
1812
+ updatedAt: field.integer().notNull(),
1813
+ }, [
1814
+ store.schema.policy.owner("owner"),
1815
+ ]);
1816
+
1817
+ const notesR = store.resource(db, notes, {});
1818
+ const mounted = on(http.resource("/notes", notesR.all()));
1819
+
1820
+ export const list = mounted.list;
1821
+ export const app = oke({
1822
+ name: "shop",
1823
+ });
1824
+ `;
1825
+ const manifest = await extractFromSources({ "src/app.ts": source });
1826
+
1827
+ expect(manifest.store).toBeUndefined();
1828
+ // Table not live, resource without live: true synthesizes nothing.
1829
+ expect(manifest.stores?.app?.tables?.notes?.live).toBeUndefined();
1830
+ expect(manifest.signals?.["oke/live/sql:notes"]).toBeUndefined();
1831
+ expect(manifest.flows?.["_live_notes"]).toBeUndefined();
1832
+ });
1833
+
1834
+ test("the project default fires the same DX Pack A guardrails as explicit live: true", async () => {
1835
+ const source = `
1836
+ import { on, http, store, field, oke } from "okengine";
1837
+
1838
+ export const db = store.sql("app", { schema: {} });
1839
+
1840
+ export const notes = store.schema.table("notes", {
1841
+ body: field.text().notNull(),
1842
+ });
1843
+
1844
+ const notesR = store.resource(db, notes, {});
1845
+ const mounted = on(http.resource("/notes", notesR.all()));
1846
+
1847
+ export const list = mounted.list;
1848
+ export const app = oke({
1849
+ name: "shop",
1850
+ store: { live: true },
1851
+ });
1852
+ `;
1853
+ await expect(extractFromSources({ "src/app.ts": source })).rejects.toThrow(
1854
+ /requires a primary key/,
1855
+ );
1856
+ });
1857
+ });