create-oke 0.2.6 → 0.2.8

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 (70) hide show
  1. package/README.md +32 -20
  2. package/examples/linkly/.env.example +15 -0
  3. package/examples/linkly/drizzle.config.ts +11 -0
  4. package/examples/linkly/oke.config.ts +6 -4
  5. package/examples/linkly/package.json +7 -4
  6. package/examples/linkly/src/flows/analytics/index.ts +29 -14
  7. package/examples/linkly/src/flows/links/index.ts +58 -35
  8. package/examples/linkly/src/flows/links/signals.ts +4 -3
  9. package/examples/linkly/src/gates.ts +4 -2
  10. package/examples/linkly/src/schema.decl.ts +39 -0
  11. package/examples/linkly/src/schema.generated.ts +30 -0
  12. package/examples/linkly/src/schema.ts +2 -17
  13. package/examples/linkly/tests/linkly.test.ts +9 -9
  14. package/examples/notes/.env.example +12 -0
  15. package/examples/notes/drizzle.config.ts +11 -0
  16. package/examples/notes/oke.config.ts +2 -2
  17. package/examples/notes/package.json +7 -4
  18. package/examples/notes/src/app.ts +1 -1
  19. package/examples/notes/src/flows/notes/index.ts +44 -30
  20. package/examples/notes/src/schema.ts +3 -3
  21. package/examples/notes/tests/notes.test.ts +1 -1
  22. package/examples/provisions/.env.example +19 -0
  23. package/examples/provisions/drizzle.config.ts +11 -0
  24. package/examples/provisions/oke.config.ts +10 -8
  25. package/examples/provisions/package.json +7 -4
  26. package/examples/provisions/src/app.ts +3 -3
  27. package/examples/provisions/src/channels.ts +4 -3
  28. package/examples/provisions/src/flows/notifications/index.ts +13 -7
  29. package/examples/provisions/src/flows/orders/index.ts +58 -31
  30. package/examples/provisions/src/flows/payments/index.ts +12 -6
  31. package/examples/provisions/src/schema.ts +7 -7
  32. package/examples/provisions/src/vault.ts +2 -2
  33. package/examples/provisions/tests/orders.test.ts +9 -8
  34. package/examples/skyport/.env.example +34 -0
  35. package/examples/skyport/drizzle.config.ts +11 -0
  36. package/examples/skyport/oke.config.ts +25 -22
  37. package/examples/skyport/package.json +8 -5
  38. package/examples/skyport/src/ai.ts +4 -4
  39. package/examples/skyport/src/app.ts +2 -13
  40. package/examples/skyport/src/flows/bookings/index.ts +45 -22
  41. package/examples/skyport/src/flows/bookings/shapes.ts +4 -4
  42. package/examples/skyport/src/flows/bookings/signals.ts +6 -2
  43. package/examples/skyport/src/flows/support/index.ts +37 -25
  44. package/examples/skyport/src/schema.ts +11 -8
  45. package/examples/skyport/src/vault.ts +1 -1
  46. package/examples/skyport/tests/support.test.ts +6 -6
  47. package/package.json +9 -9
  48. package/src/cli.test.ts +132 -64
  49. package/src/cli.ts +58 -33
  50. package/src/scaffold.ts +53 -15
  51. package/src/sync-templates.ts +2 -11
  52. package/src/templates.ts +1 -2
  53. package/src/transform.ts +59 -9
  54. package/templates/full/.env.example +24 -0
  55. package/templates/full/README.md +22 -6
  56. package/templates/full/drizzle.config.ts +14 -0
  57. package/templates/full/oke.config.ts +14 -26
  58. package/templates/full/package.json +3 -0
  59. package/templates/hello/.env.example +9 -0
  60. package/templates/hello/README.md +6 -4
  61. package/templates/minimal/.env.example +13 -0
  62. package/templates/minimal/README.md +15 -7
  63. package/templates/minimal/drizzle.config.ts +14 -0
  64. package/templates/minimal/oke.config.ts +2 -2
  65. package/templates/minimal/package.json +3 -0
  66. package/templates/standard/.env.example +21 -0
  67. package/templates/standard/README.md +25 -8
  68. package/templates/standard/drizzle.config.ts +14 -0
  69. package/templates/standard/oke.config.ts +12 -24
  70. package/templates/standard/package.json +3 -0
@@ -0,0 +1,19 @@
1
+ # Environment template — copy to `.env.local` for local overrides.
2
+ # create-oke copies this file to `.env.local` when you scaffold a new project.
3
+ #
4
+ # Resolution order (first hit wins):
5
+ # process.env → .env.local → docker/.env.docker → vault driver → dev fallback
6
+ #
7
+ # --- Vault (app secrets) ---
8
+
9
+ # STRIPE_KEY — Stripe secret API key for payments; from https://dashboard.stripe.com/apikeys (test keys start with sk_test_)
10
+ STRIPE_KEY=
11
+
12
+ # DATABASE_URL — primary Postgres URL (vault contract in src/vault.ts); from your host or `docker/.env.docker` after `oke dev --docker`
13
+ DATABASE_URL=
14
+
15
+ # --- Infrastructure (optional) ---
16
+ # This app pins redis for prod kv in oke.config.ts (local dev uses in-memory kv).
17
+
18
+ # REDIS_URL — Redis/Valkey connection for store.kv; from your host (Upstash, ElastiCache, …)
19
+ # REDIS_URL=redis://:password@host:6379
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ /** Domain schema sync for `oke db push|generate|migrate`. */
4
+ export default defineConfig({
5
+ dialect: "sqlite",
6
+ schema: "./src/schema.ts",
7
+ out: "./drizzle",
8
+ dbCredentials: {
9
+ url: process.env.OKE_SQLITE_URL ?? "file:.oke/app.sqlite",
10
+ },
11
+ });
@@ -2,15 +2,17 @@ import { defineConfig } from "okengine/config";
2
2
 
3
3
  export default defineConfig({
4
4
  drivers: {
5
- store: { sql: { dev: "sqlite", test: "memory", prod: "postgres" },
6
- kv: { dev: "memory", test: "memory", prod: "redis" } },
7
- signal: { dev: "memory", test: "memory", prod: "postgres" },
8
- clock: { dev: "memory", test: "frozen", prod: "postgres" },
9
- vault: { dev: "dotenv", test: "memory", prod: "sops" },
5
+ store: {
6
+ sql: { local: "sqlite", test: "memory", prod: "postgres" },
7
+ kv: { local: "memory", test: "memory", prod: "redis" },
8
+ },
9
+ signal: { local: "memory", test: "memory", prod: "postgres" },
10
+ clock: { local: "memory", test: "frozen", prod: "postgres" },
11
+ vault: { local: "dotenv", test: "memory", prod: "sops" },
10
12
  channel: {
11
- email: { dev: "console", test: "console", prod: "smtp" },
12
- sms: { dev: "console", test: "console", prod: "unifonic" },
13
- whatsapp: { dev: "console", test: "console", prod: "wa-cloud" },
13
+ email: { local: "console", test: "console", prod: "smtp" },
14
+ sms: { local: "console", test: "console", prod: "unifonic" },
15
+ whatsapp: { local: "console", test: "console", prod: "wa-cloud" },
14
16
  },
15
17
  },
16
18
  i18n: { locales: ["en", "ar"], default: "ar", dir: { ar: "rtl" } },
@@ -3,13 +3,16 @@
3
3
  "version": "0.0.1",
4
4
  "private": true,
5
5
  "type": "module",
6
+ "scripts": {
7
+ "test": "bun test",
8
+ "dev": "oke dev"
9
+ },
6
10
  "dependencies": {
7
- "okengine": "file:../..",
8
11
  "drizzle-orm": "^1.0.0-rc.4",
12
+ "okengine": "file:../..",
9
13
  "zod": "^4.4.3"
10
14
  },
11
- "scripts": {
12
- "test": "bun test",
13
- "dev": "oke dev"
15
+ "devDependencies": {
16
+ "drizzle-kit": "^1.0.0-rc.4"
14
17
  }
15
18
  }
@@ -16,11 +16,11 @@ import * as notifications from "./flows/notifications";
16
16
 
17
17
  export const app = oke({ name: "provisions" })
18
18
  .adopt({ orders, payments, notifications })
19
- .plug(auth()) // zero ceremony: uses your configured store
20
- .plug(audit) // app-wide
19
+ .plug(auth()) // zero ceremony: uses your configured store
20
+ .plug(audit) // app-wide
21
21
  .hook("onError", (ctx, err, fx) => fx.log.error(err));
22
22
 
23
- app.unit("orders").plug(rateLimit({ max: 30 })); // this unit only
23
+ app.unit("orders").plug(rateLimit({ max: 30 })); // this unit only
24
24
 
25
25
  export type App = typeof app;
26
26
 
@@ -2,13 +2,14 @@ import { channel } from "okengine";
2
2
  import { z } from "zod";
3
3
 
4
4
  export const mail = channel.email({ from: "Provisions <no-reply@provisions.sa>" });
5
- export const sms = channel.sms({ sender: "PROVISIONS" });
6
- export const wa = channel.whatsapp();
5
+ export const sms = channel.sms({ sender: "PROVISIONS" });
6
+ export const wa = channel.whatsapp();
7
7
 
8
8
  export const orderConfirmed = mail.template("order-confirmed", {
9
9
  schema: z.object({ name: z.string(), orderId: z.string(), total: z.number() }),
10
10
  });
11
11
 
12
- export const otpCode = channel.template("otp-code", { // medium-agnostic
12
+ export const otpCode = channel.template("otp-code", {
13
+ // medium-agnostic
13
14
  schema: z.object({ code: z.string() }),
14
15
  });
@@ -4,13 +4,19 @@ import { orderNews } from "../orders/signals";
4
4
  import { getOrder } from "../orders";
5
5
  import { orderConfirmed, otpCode, wa, sms } from "../../channels";
6
6
 
7
- on(orderNews, flow({
8
- do: async ({ orderId, status }, fx) => {
9
- if (status !== "confirmed") return;
10
- const o = await fx.call(getOrder, { id: orderId });
11
- await fx.send(orderConfirmed, { to: o.userId, data: { name: o.userName, orderId, total: o.total } });
12
- },
13
- }));
7
+ on(
8
+ orderNews,
9
+ flow({
10
+ do: async ({ orderId, status }, fx) => {
11
+ if (status !== "confirmed") return;
12
+ const o = await fx.call(getOrder, { id: orderId });
13
+ await fx.send(orderConfirmed, {
14
+ to: o.userId,
15
+ data: { name: o.userName, orderId, total: o.total },
16
+ });
17
+ },
18
+ }),
19
+ );
14
20
 
15
21
  export const sendOtp = flow({
16
22
  in: z.object({ userId: z.string(), code: z.string() }),
@@ -9,31 +9,49 @@ import { orders, products } from "../../schema";
9
9
 
10
10
  const canOrder = gate.policy("order:create", ({ auth }) => auth.scopes.has("order:create"));
11
11
 
12
- export const create = on(http.post("/orders").gate(member, canOrder), flow({
13
- in: NewOrder, out: OrderId, errors: { OutOfStock },
14
- do: async (input, fx) => {
15
- const [product] = await fx.store(db).select({ stock: products.stock })
16
- .from(products).where(eq(products.sku, input.sku)).limit(1);
17
- if (!product || product.stock < input.qty) return fx.fail("OutOfStock",
18
- { left: product?.stock ?? 0 },
19
- { message: fx.t("order.outOfStock", { left: product?.stock ?? 0 }) });
12
+ export const create = on(
13
+ http.post("/orders").gate(member, canOrder),
14
+ flow({
15
+ in: NewOrder,
16
+ out: OrderId,
17
+ errors: { OutOfStock },
18
+ do: async (input, fx) => {
19
+ const [product] = await fx
20
+ .store(db)
21
+ .select({ stock: products.stock })
22
+ .from(products)
23
+ .where(eq(products.sku, input.sku))
24
+ .limit(1);
25
+ if (!product || product.stock < input.qty)
26
+ return fx.fail(
27
+ "OutOfStock",
28
+ { left: product?.stock ?? 0 },
29
+ { message: fx.t("order.outOfStock", { left: product?.stock ?? 0 }) },
30
+ );
20
31
 
21
- const id = fx.id();
22
- await fx.store(db).insert(orders).values(
23
- { id, userId: fx.auth.userId, ...input, status: "pending", createdAt: Date.now() });
24
- await fx.emit(orderPlaced, { orderId: id });
25
- return { id };
26
- },
27
- }));
32
+ const id = fx.id();
33
+ await fx
34
+ .store(db)
35
+ .insert(orders)
36
+ .values({ id, userId: fx.auth.userId, ...input, status: "pending", createdAt: Date.now() });
37
+ await fx.emit(orderPlaced, { orderId: id });
38
+ return { id };
39
+ },
40
+ }),
41
+ );
28
42
 
29
43
  // LIVE QUERY — realtime and auto-caching from one flag
30
- export const mine = on(http.get("/orders").gate(member).live(), flow({
31
- out: OrderRow.array(),
32
- do: (_, fx) => fx.store(db).select().from(orders).where(eq(orders.userId, fx.auth.userId)),
33
- }));
44
+ export const mine = on(
45
+ http.get("/orders").gate(member).live(),
46
+ flow({
47
+ out: OrderRow.array(),
48
+ do: (_, fx) => fx.store(db).select().from(orders).where(eq(orders.userId, fx.auth.userId)),
49
+ }),
50
+ );
34
51
 
35
52
  export const getOrder = flow({
36
- in: OrderId, out: OrderRow,
53
+ in: OrderId,
54
+ out: OrderRow,
37
55
  do: async ({ id }, fx) => {
38
56
  const [order] = await fx.store(db).select().from(orders).where(eq(orders.id, id)).limit(1);
39
57
  return order;
@@ -41,19 +59,28 @@ export const getOrder = flow({
41
59
  });
42
60
 
43
61
  // SIGNAL consumer
44
- on(orderPlaced, flow({
45
- do: async ({ orderId }, fx) => {
46
- const paid = await fx.call(chargeOrder, { orderId });
47
- await fx.store(db).update(orders).set({ status: paid ? "confirmed" : "failed" })
48
- .where(eq(orders.id, orderId));
49
- await fx.emit(orderNews, { orderId, status: paid ? "confirmed" : "failed" });
50
- },
51
- }));
62
+ on(
63
+ orderPlaced,
64
+ flow({
65
+ do: async ({ orderId }, fx) => {
66
+ const paid = await fx.call(chargeOrder, { orderId });
67
+ await fx
68
+ .store(db)
69
+ .update(orders)
70
+ .set({ status: paid ? "confirmed" : "failed" })
71
+ .where(eq(orders.id, orderId));
72
+ await fx.emit(orderNews, { orderId, status: paid ? "confirmed" : "failed" });
73
+ },
74
+ }),
75
+ );
52
76
 
53
77
  // CHANGE trigger — CDC, built in
54
- on(db.table(orders).changed("status"), flow({
55
- do: ({ before, after }, fx) => fx.log.info("status", { from: before.status, to: after.status }),
56
- }));
78
+ on(
79
+ db.table(orders).changed("status"),
80
+ flow({
81
+ do: ({ before, after }, fx) => fx.log.info("status", { from: before.status, to: after.status }),
82
+ }),
83
+ );
57
84
 
58
85
  export { canOrder };
59
86
 
@@ -10,13 +10,16 @@ import { stripeKey } from "../../vault";
10
10
  import { OrderRef } from "./shapes";
11
11
 
12
12
  export const chargeOrder = flow({
13
- durable: true, // every fx call below is journaled
14
- in: OrderRef, out: z.boolean(),
13
+ durable: true, // every fx call below is journaled
14
+ in: OrderRef,
15
+ out: z.boolean(),
15
16
  do: async ({ orderId }, fx) => {
16
- const intent = await fx.step("create-intent", () => // never re-runs on replay
17
- stripe(fx.vault(stripeKey)).create(orderId));
17
+ const intent = await fx.step("create-intent", () =>
18
+ // never re-runs on replay
19
+ stripe(fx.vault(stripeKey)).create(orderId),
20
+ );
18
21
 
19
- await fx.clock.sleep("verify-window", "2m"); // survives restart and deploy
22
+ await fx.clock.sleep("verify-window", "2m"); // survives restart and deploy
20
23
 
21
24
  return fx.step("confirm", () => stripe(fx.vault(stripeKey)).confirm(intent));
22
25
  },
@@ -27,7 +30,10 @@ const charged = chargeOrder.do;
27
30
  (chargeOrder as { do: typeof charged }).do = async (input, fx) => {
28
31
  const paid = await charged(input, fx);
29
32
  if (paid) {
30
- await fx.store(db).update(orders).set({ status: "confirmed" })
33
+ await fx
34
+ .store(db)
35
+ .update(orders)
36
+ .set({ status: "confirmed" })
31
37
  .where(eq(orders.id, input.orderId));
32
38
  await fx.emit(orderNews, { orderId: input.orderId, status: "confirmed" });
33
39
  }
@@ -1,16 +1,16 @@
1
1
  import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
2
2
 
3
3
  export const products = sqliteTable("products", {
4
- sku: text("sku").primaryKey(),
5
- name: text("name").notNull(),
4
+ sku: text("sku").primaryKey(),
5
+ name: text("name").notNull(),
6
6
  stock: integer("stock").notNull().default(0),
7
7
  });
8
8
 
9
9
  export const orders = sqliteTable("orders", {
10
- id: text("id").primaryKey(),
11
- userId: text("user_id").notNull(),
12
- sku: text("sku").notNull(),
13
- qty: integer("qty").notNull(),
14
- status: text("status").notNull().default("pending"),
10
+ id: text("id").primaryKey(),
11
+ userId: text("user_id").notNull(),
12
+ sku: text("sku").notNull(),
13
+ qty: integer("qty").notNull(),
14
+ status: text("status").notNull().default("pending"),
15
15
  createdAt: integer("created_at").notNull(),
16
16
  });
@@ -2,7 +2,7 @@ import { vault } from "okengine";
2
2
  import { z } from "zod";
3
3
 
4
4
  // A declaration is a CONTRACT, not a value.
5
- // Resolution: process.env → .env.local → .env.stack → vault driver → dev fallback
5
+ // Resolution: process.env → .env.local → docker/.env.docker → vault driver → dev fallback
6
6
  export const stripeKey = vault.secret("STRIPE_KEY", {
7
7
  schema: z.string().startsWith("sk_"),
8
8
  description: "Payments gateway key",
@@ -12,5 +12,5 @@ export const stripeKey = vault.secret("STRIPE_KEY", {
12
12
 
13
13
  export const dbUrl = vault.secret("DATABASE_URL", {
14
14
  schema: z.string().url(),
15
- dev: vault.fromStack("store.sql"), // generated by `oke dev --stack` — zero manual setup
15
+ dev: vault.fromDocker("store.sql"), // generated by `oke dev --docker` — zero manual setup
16
16
  });
@@ -3,16 +3,17 @@ import { createTestApp } from "okengine/test";
3
3
  import { app } from "../src/app";
4
4
 
5
5
  test("order → charge → notify", async () => {
6
- const t = await createTestApp(app); // memory drivers, frozen clock
7
- const u = await t.auth.loginAs({ scopes: ["order:create"] });
6
+ const t = await createTestApp(app); // memory drivers, frozen clock
7
+ const u = await t.auth.loginAs({ scopes: ["order:create"] });
8
8
 
9
- const { data } = await t.api.orders.create({ sku: "COFFEE", qty: 2 }, { as: u });
10
- await t.signals.drain();
11
- await t.clock.advance("2m"); // the durable sleep elapses instantly
12
- await t.signals.drain();
9
+ const { data } = await t.api.orders.create({ sku: "COFFEE", qty: 2 }, { as: u });
10
+ await t.signals.drain();
11
+ await t.clock.advance("2m"); // the durable sleep elapses instantly
12
+ await t.signals.drain();
13
13
 
14
- expect(t.channels.sent()).toContainEqual(
15
- expect.objectContaining({ template: "order-confirmed", to: u.id, locale: "ar" }));
14
+ expect(t.channels.sent()).toContainEqual(
15
+ expect.objectContaining({ template: "order-confirmed", to: u.id, locale: "ar" }),
16
+ );
16
17
  expect(data?.id).toBeDefined();
17
18
  await t.close();
18
19
  });
@@ -0,0 +1,34 @@
1
+ # Environment template — copy to `.env.local` for local overrides.
2
+ # create-oke copies this file to `.env.local` when you scaffold a new project.
3
+ #
4
+ # Resolution order (first hit wins):
5
+ # process.env → .env.local → docker/.env.docker → vault driver → dev fallback
6
+ #
7
+ # --- Vault (app secrets) ---
8
+
9
+ # DATABASE_URL — primary Postgres URL (vault contract in src/vault.ts); from your host or `docker/.env.docker` after `oke dev --docker`
10
+ DATABASE_URL=
11
+
12
+ # DATABASE_REPLICA_1 — read-replica Postgres URL for read-only flow routing; from your host's replica endpoint
13
+ DATABASE_REPLICA_1=
14
+
15
+ # ANTHROPIC_KEY — Anthropic API key for prod AI driver; from https://console.anthropic.com/settings/keys
16
+ ANTHROPIC_KEY=
17
+
18
+ # STRIPE_KEY — Stripe secret API key for payments; from https://dashboard.stripe.com/apikeys (test keys start with sk_test_)
19
+ STRIPE_KEY=
20
+
21
+ # --- Infrastructure (optional) ---
22
+ # Commented — this app pins redis (store.kv) and s3 (store.files) for prod in oke.config.ts.
23
+
24
+ # REDIS_URL — Redis/Valkey connection for store.kv; from your host or `docker/.env.docker` after `oke dev --docker`
25
+ # REDIS_URL=redis://:password@127.0.0.1:6379
26
+
27
+ # AWS_ACCESS_KEY_ID — S3-compatible object storage access key; from AWS IAM, Cloudflare R2, or MinIO console
28
+ # AWS_ACCESS_KEY_ID=
29
+
30
+ # AWS_SECRET_ACCESS_KEY — matching secret for object storage; never commit real values
31
+ # AWS_SECRET_ACCESS_KEY=
32
+
33
+ # AWS_REGION — bucket region (e.g. us-east-1); from your object storage provider
34
+ # AWS_REGION=us-east-1
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ /** Domain schema sync for `oke db push|generate|migrate`. */
4
+ export default defineConfig({
5
+ dialect: "sqlite",
6
+ schema: "./src/schema.ts",
7
+ out: "./drizzle",
8
+ dbCredentials: {
9
+ url: process.env.OKE_SQLITE_URL ?? "file:.oke/app.sqlite",
10
+ },
11
+ });
@@ -6,39 +6,42 @@ export default defineConfig({
6
6
  // (Bun.sql, bun:sqlite, Bun.redis, Bun.S3) — zero npm client dependencies.
7
7
  drivers: {
8
8
  store: {
9
- sql: { dev: "sqlite", test: "memory",
10
- prod: { driver: "postgres", url: dbUrl, pool: { max: 20 },
11
- replicas: [dbReplica1] } }, // read-only flows auto-route here
12
- kv: { dev: "memory", test: "memory", prod: "redis" }, // Redis · Valkey · Dragonfly
13
- files: { dev: "fs", test: "memory", prod: "s3" }, // S3 · R2 · SeaweedFS · MinIO
14
- index: { dev: "pgvector", test: "memory", prod: "pgvector" },
9
+ sql: {
10
+ local: "sqlite",
11
+ test: "memory",
12
+ prod: { driver: "postgres", url: dbUrl, pool: { max: 20 }, replicas: [dbReplica1] },
13
+ }, // read-only flows auto-route here
14
+ kv: { local: "memory", test: "memory", prod: "redis" }, // Redis · Valkey · Dragonfly
15
+ files: { local: "fs", test: "memory", prod: "s3" }, // S3 · R2 · SeaweedFS · MinIO
16
+ index: { local: "pgvector", test: "memory", prod: "pgvector" },
15
17
  },
16
- signal: { dev: "memory", test: "memory", prod: "postgres" },
17
- clock: { dev: "memory", test: "frozen", prod: "postgres" },
18
- vault: { dev: "dotenv", test: "memory", prod: "sops" }, // SOPS/age — committable
19
- runs: { dev: "files", test: "memory", prod: "files" }, // Parquet + DuckDB
18
+ signal: { local: "memory", test: "memory", prod: "postgres" },
19
+ clock: { local: "memory", test: "frozen", prod: "postgres" },
20
+ vault: { local: "dotenv", test: "memory", prod: "sops" }, // SOPS/age — committable
21
+ runs: { local: "files", test: "memory", prod: "files" }, // Parquet + DuckDB
20
22
  channel: {
21
- email: { dev: "console", prod: "smtp" },
22
- sms: { dev: "console", prod: "unifonic" },
23
- whatsapp: { dev: "console", prod: "wa-cloud" },
24
- push: { dev: "console", prod: "fcm" },
23
+ email: { local: "console", prod: "smtp" },
24
+ sms: { local: "console", prod: "unifonic" },
25
+ whatsapp: { local: "console", prod: "wa-cloud" },
26
+ push: { local: "console", prod: "fcm" },
25
27
  },
26
28
  ai: {
27
- dev: "mock", // deterministic — tests never call out
29
+ local: "mock", // deterministic — tests never call out
28
30
  prod: { driver: "anthropic", key: anthropicKey },
29
31
  // no prod default: model choice is never guessed.
30
32
  // "openai-compatible" covers vLLM · Groq · Together · LM Studio · most self-hosted
31
33
  },
32
34
  },
33
35
 
34
- images: { // vendor choice, keyed by ROLE
36
+ images: {
37
+ // vendor choice, keyed by ROLE
35
38
  "store.sql": "pgvector/pgvector:pg17",
36
- "store.kv": "valkey/valkey:8-alpine",
39
+ "store.kv": "valkey/valkey:8-alpine",
37
40
  },
38
41
 
39
- i18n: { locales: ["en", "ar"], default: "ar", dir: { ar: "rtl" } },
40
- tenancy: { resolve: (ctx) => ctx.auth.orgId, isolation: "row" },
41
- topology: "monolith", // flip to "services" — code unchanged
42
- ports: { app: 6530, console: 6533, mcp: 6535 }, // O·K·E = 6·5·3
43
- console: { prod: { enabled: true, auth: "required" } },
42
+ i18n: { locales: ["en", "ar"], default: "ar", dir: { ar: "rtl" } },
43
+ tenancy: { resolve: (ctx) => ctx.auth.orgId, isolation: "row" },
44
+ topology: "monolith", // flip to "services" — code unchanged
45
+ ports: { app: 6530, console: 6533, mcp: 6535 }, // O·K·E = 6·5·3
46
+ console: { prod: { enabled: true, auth: "required" } },
44
47
  });
@@ -3,14 +3,17 @@
3
3
  "version": "0.0.1",
4
4
  "private": true,
5
5
  "type": "module",
6
- "dependencies": {
7
- "okengine": "file:../..",
8
- "drizzle-orm": "^1.0.0-rc.4",
9
- "zod": "^4.4.3"
10
- },
11
6
  "scripts": {
12
7
  "test": "bun test",
13
8
  "dev": "oke dev",
14
9
  "docker": "oke docker"
10
+ },
11
+ "dependencies": {
12
+ "drizzle-orm": "^1.0.0-rc.4",
13
+ "okengine": "file:../..",
14
+ "zod": "^4.4.3"
15
+ },
16
+ "devDependencies": {
17
+ "drizzle-kit": "^1.0.0-rc.4"
15
18
  }
16
19
  }
@@ -3,15 +3,15 @@ import { z } from "zod";
3
3
  import { getBooking, refundBooking } from "./flows/bookings";
4
4
 
5
5
  export const smart = ai.model("smart", { provider: "anthropic", tier: "opus" });
6
- export const fast = ai.model("fast", { provider: "anthropic", tier: "haiku" });
6
+ export const fast = ai.model("fast", { provider: "anthropic", tier: "haiku" });
7
7
 
8
8
  // A prompt is a VERSIONED ARTIFACT with a validated output shape — not a string in a handler
9
9
  export const triage = smart.prompt("ticket-triage", {
10
- in: z.object({ subject: z.string(), body: z.string() }),
10
+ in: z.object({ subject: z.string(), body: z.string() }),
11
11
  out: z.object({ urgency: z.enum(["low", "high"]), team: z.string(), summary: z.string() }),
12
12
  version: 3,
13
- evals: "./evals/triage.jsonl", // regression-gated in CI via `oke eval`
14
- budget: { maxCostPerCall: 0.02 }, // cost is a first-class dimension
13
+ evals: "./evals/triage.jsonl", // regression-gated in CI via `oke eval`
14
+ budget: { maxCostPerCall: 0.02 }, // cost is a first-class dimension
15
15
  });
16
16
 
17
17
  export const embed = ai.embed("docs", { model: fast, into: store.index("kb") });
@@ -1,20 +1,9 @@
1
1
  import { db } from "./core";
2
2
  import { member, fair } from "./gates";
3
- import {
4
- dbUrl,
5
- dbReplica1,
6
- anthropicKey,
7
- stripeKey,
8
- } from "./vault";
3
+ import { dbUrl, dbReplica1, anthropicKey, stripeKey } from "./vault";
9
4
  import { bookingConfirmed } from "./channels";
10
5
  import { orderPlaced, seatFeed } from "./flows/bookings/signals";
11
- import {
12
- smart,
13
- fast,
14
- triage,
15
- embed,
16
- support as supportAgentDecl,
17
- } from "./ai";
6
+ import { smart, fast, triage, embed, support as supportAgentDecl } from "./ai";
18
7
  import { canBook } from "./flows/bookings";
19
8
  import "./journeys";
20
9
  import "./ai";
@@ -6,32 +6,54 @@ import { orderPlaced, seatFeed } from "./signals";
6
6
  import { NewBooking, BookingId, BookingRow, FlightFull } from "./shapes";
7
7
  import { bookings, flights } from "../../schema";
8
8
 
9
- export const canBook = gate.policy("booking:create", ({ auth }) => auth.scopes.has("booking:create"));
9
+ export const canBook = gate.policy("booking:create", ({ auth }) =>
10
+ auth.scopes.has("booking:create"),
11
+ );
10
12
 
11
- export const create = on(http.post("/bookings").gate(member, canBook, fair), flow({
12
- slo: { availability: "99.9%", latency: { p99: "200ms" } },
13
- in: NewBooking, out: BookingId, errors: { FlightFull },
14
- do: async ({ flightId, seats }, fx) => {
15
- const [flight] = await fx.store(db).select().from(flights).where(eq(flights.id, flightId)).limit(1);
16
- if (!flight || flight.seatsAvailable < seats)
17
- return fx.fail("FlightFull", { seatsLeft: flight?.seatsAvailable ?? 0 });
13
+ export const create = on(
14
+ http.post("/bookings").gate(member, canBook, fair),
15
+ flow({
16
+ slo: { availability: "99.9%", latency: { p99: "200ms" } },
17
+ in: NewBooking,
18
+ out: BookingId,
19
+ errors: { FlightFull },
20
+ do: async ({ flightId, seats }, fx) => {
21
+ const [flight] = await fx
22
+ .store(db)
23
+ .select()
24
+ .from(flights)
25
+ .where(eq(flights.id, flightId))
26
+ .limit(1);
27
+ if (!flight || flight.seatsAvailable < seats)
28
+ return fx.fail("FlightFull", { seatsLeft: flight?.seatsAvailable ?? 0 });
18
29
 
19
- const id = fx.id();
20
- await fx.store(db).insert(bookings).values(
21
- { id, userId: fx.auth.userId, flightId, seats, status: "pending", createdAt: Date.now() });
22
- await fx.emit(orderPlaced, { orderId: id });
23
- await fx.emit(seatFeed, { flightId, left: flight.seatsAvailable - seats });
24
- return { id };
25
- },
26
- }));
30
+ const id = fx.id();
31
+ await fx.store(db).insert(bookings).values({
32
+ id,
33
+ userId: fx.auth.userId,
34
+ flightId,
35
+ seats,
36
+ status: "pending",
37
+ createdAt: Date.now(),
38
+ });
39
+ await fx.emit(orderPlaced, { orderId: id });
40
+ await fx.emit(seatFeed, { flightId, left: flight.seatsAvailable - seats });
41
+ return { id };
42
+ },
43
+ }),
44
+ );
27
45
 
28
- export const mine = on(http.get("/bookings").gate(member).live(), flow({
29
- out: BookingRow.array(),
30
- do: (_, fx) => fx.store(db).select().from(bookings).where(eq(bookings.userId, fx.auth.userId)),
31
- }));
46
+ export const mine = on(
47
+ http.get("/bookings").gate(member).live(),
48
+ flow({
49
+ out: BookingRow.array(),
50
+ do: (_, fx) => fx.store(db).select().from(bookings).where(eq(bookings.userId, fx.auth.userId)),
51
+ }),
52
+ );
32
53
 
33
54
  export const getBooking = flow({
34
- in: BookingId, out: BookingRow,
55
+ in: BookingId,
56
+ out: BookingRow,
35
57
  do: async ({ id }, fx) => {
36
58
  const [b] = await fx.store(db).select().from(bookings).where(eq(bookings.id, id)).limit(1);
37
59
  return b;
@@ -41,7 +63,8 @@ export const getBooking = flow({
41
63
  // The agent's second tool — refunding is a distinct, gated capability, never the same
42
64
  // permission as reading a booking, since the agent's tool list is exactly its authority.
43
65
  export const refundBooking = flow({
44
- in: BookingId, out: BookingRow,
66
+ in: BookingId,
67
+ out: BookingRow,
45
68
  do: async ({ id }, fx) => {
46
69
  await fx.store(db).update(bookings).set({ status: "refunded" }).where(eq(bookings.id, id));
47
70
  const [b] = await fx.store(db).select().from(bookings).where(eq(bookings.id, id)).limit(1);
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
 
3
- export const NewBooking = z.object({ flightId: z.string(), seats: z.number().min(1).max(9) });
4
- export const BookingId = z.object({ id: z.string() });
5
- export const BookingRow = z.object({ id: z.string(), status: z.string(), seats: z.number() });
6
- export const FlightFull = z.object({ seatsLeft: z.number() });
3
+ export const NewBooking = z.object({ flightId: z.string(), seats: z.number().min(1).max(9) });
4
+ export const BookingId = z.object({ id: z.string() });
5
+ export const BookingRow = z.object({ id: z.string(), status: z.string(), seats: z.number() });
6
+ export const FlightFull = z.object({ seatsLeft: z.number() });