create-oke 0.1.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 (74) hide show
  1. package/package.json +36 -0
  2. package/src/cli.test.ts +110 -0
  3. package/src/cli.ts +156 -0
  4. package/src/index.ts +8 -0
  5. package/src/scaffold.ts +160 -0
  6. package/src/sync-templates.ts +65 -0
  7. package/src/templates.ts +85 -0
  8. package/src/transform.ts +124 -0
  9. package/templates/linkly/oke.config.ts +10 -0
  10. package/templates/linkly/package.json +15 -0
  11. package/templates/linkly/src/app.ts +21 -0
  12. package/templates/linkly/src/core.ts +4 -0
  13. package/templates/linkly/src/flows/analytics/index.ts +24 -0
  14. package/templates/linkly/src/flows/links/index.ts +59 -0
  15. package/templates/linkly/src/flows/links/shapes.ts +22 -0
  16. package/templates/linkly/src/flows/links/signals.ts +13 -0
  17. package/templates/linkly/src/gates.ts +8 -0
  18. package/templates/linkly/src/schema.ts +17 -0
  19. package/templates/linkly/tests/linkly.test.ts +19 -0
  20. package/templates/notes/oke.config.ts +7 -0
  21. package/templates/notes/package.json +16 -0
  22. package/templates/notes/src/app.ts +13 -0
  23. package/templates/notes/src/core.ts +4 -0
  24. package/templates/notes/src/flows/notes/index.ts +44 -0
  25. package/templates/notes/src/schema.ts +9 -0
  26. package/templates/notes/tests/notes.test.ts +10 -0
  27. package/templates/provisions/oke.config.ts +17 -0
  28. package/templates/provisions/package.json +15 -0
  29. package/templates/provisions/src/app.ts +60 -0
  30. package/templates/provisions/src/channels.ts +14 -0
  31. package/templates/provisions/src/core.ts +4 -0
  32. package/templates/provisions/src/flows/notifications/index.ts +19 -0
  33. package/templates/provisions/src/flows/orders/index.ts +70 -0
  34. package/templates/provisions/src/flows/orders/shapes.ts +21 -0
  35. package/templates/provisions/src/flows/orders/signals.ts +19 -0
  36. package/templates/provisions/src/flows/payments/index.ts +37 -0
  37. package/templates/provisions/src/flows/payments/shapes.ts +3 -0
  38. package/templates/provisions/src/flows/payments/stripe.ts +18 -0
  39. package/templates/provisions/src/gates.ts +4 -0
  40. package/templates/provisions/src/locales/ar.ts +1 -0
  41. package/templates/provisions/src/locales/en.ts +1 -0
  42. package/templates/provisions/src/plugins/audit-schema.ts +2 -0
  43. package/templates/provisions/src/plugins/audit.ts +14 -0
  44. package/templates/provisions/src/schema.ts +16 -0
  45. package/templates/provisions/src/vault.ts +16 -0
  46. package/templates/provisions/tests/orders.test.ts +18 -0
  47. package/templates/skyport/evals/triage.jsonl +1 -0
  48. package/templates/skyport/oke.config.ts +44 -0
  49. package/templates/skyport/oke.images.lock +4 -0
  50. package/templates/skyport/package.json +16 -0
  51. package/templates/skyport/src/ai.ts +25 -0
  52. package/templates/skyport/src/app.ts +57 -0
  53. package/templates/skyport/src/channels.ts +12 -0
  54. package/templates/skyport/src/core.ts +4 -0
  55. package/templates/skyport/src/flows/bookings/index.ts +50 -0
  56. package/templates/skyport/src/flows/bookings/shapes.ts +6 -0
  57. package/templates/skyport/src/flows/bookings/signals.ts +9 -0
  58. package/templates/skyport/src/flows/notifications/index.ts +16 -0
  59. package/templates/skyport/src/flows/payments/index.ts +14 -0
  60. package/templates/skyport/src/flows/payments/shapes.ts +5 -0
  61. package/templates/skyport/src/flows/support/index.ts +38 -0
  62. package/templates/skyport/src/flows/users/elements.ts +2 -0
  63. package/templates/skyport/src/flows/users/index.ts +12 -0
  64. package/templates/skyport/src/flows/users/shapes.ts +6 -0
  65. package/templates/skyport/src/gates.ts +10 -0
  66. package/templates/skyport/src/journeys.ts +9 -0
  67. package/templates/skyport/src/locales/ar.ts +4 -0
  68. package/templates/skyport/src/locales/en.ts +4 -0
  69. package/templates/skyport/src/plugins/audit-schema.ts +7 -0
  70. package/templates/skyport/src/plugins/audit.ts +14 -0
  71. package/templates/skyport/src/plugins/panel.tsx +4 -0
  72. package/templates/skyport/src/schema.ts +22 -0
  73. package/templates/skyport/src/vault.ts +24 -0
  74. package/templates/skyport/tests/support.test.ts +16 -0
@@ -0,0 +1,70 @@
1
+ import { on, flow, gate, http } from "okengine";
2
+ import { eq } from "drizzle-orm";
3
+ import { db } from "../../core";
4
+ import { member } from "../../gates";
5
+ import { orderPlaced, orderNews } from "./signals";
6
+ import { chargeOrder } from "../payments";
7
+ import { NewOrder, OrderId, OrderRow, OutOfStock } from "./shapes";
8
+ import { orders, products } from "../../schema";
9
+
10
+ const canOrder = gate.policy("order:create", ({ auth }) => auth.scopes.has("order:create"));
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 }) });
20
+
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
+ }));
28
+
29
+ // 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
+ }));
34
+
35
+ export const getOrder = flow({
36
+ in: OrderId, out: OrderRow,
37
+ do: async ({ id }, fx) => {
38
+ const [order] = await fx.store(db).select().from(orders).where(eq(orders.id, id)).limit(1);
39
+ return order;
40
+ },
41
+ });
42
+
43
+ // 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
+ }));
52
+
53
+ // 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
+ }));
57
+
58
+ export { canOrder };
59
+
60
+ // Enrich for notifications (userName / total are not storage columns).
61
+ const rawGet = getOrder.do;
62
+ (getOrder as { do: typeof rawGet }).do = async (input, fx) => {
63
+ const order = await rawGet(input, fx);
64
+ if (!order) return order;
65
+ return {
66
+ ...order,
67
+ userName: "Customer",
68
+ total: Number(order.qty ?? 0) * 10,
69
+ };
70
+ };
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+
3
+ export const NewOrder = z.object({
4
+ sku: z.string(),
5
+ qty: z.number().int().positive(),
6
+ });
7
+
8
+ export const OrderId = z.object({ id: z.string() });
9
+
10
+ export const OrderRow = z.object({
11
+ id: z.string(),
12
+ userId: z.string(),
13
+ sku: z.string(),
14
+ qty: z.number(),
15
+ status: z.string(),
16
+ createdAt: z.number(),
17
+ userName: z.string().optional(),
18
+ total: z.number().optional(),
19
+ });
20
+
21
+ export const OutOfStock = z.object({ left: z.number() });
@@ -0,0 +1,19 @@
1
+ import { signal } from "okengine";
2
+ import { z } from "zod";
3
+
4
+ export const orderPlaced = signal("order-placed", {
5
+ schema: z.object({ orderId: z.string() }),
6
+ delivery: "once",
7
+ retries: 3,
8
+ deadLetter: true,
9
+ });
10
+
11
+ export const orderNews = signal("order-news", {
12
+ schema: z.object({
13
+ orderId: z.string(),
14
+ status: z.enum(["confirmed", "failed"]),
15
+ }),
16
+ delivery: "once",
17
+ retries: 3,
18
+ deadLetter: true,
19
+ });
@@ -0,0 +1,37 @@
1
+ import { stripe } from "./stripe";
2
+ import { eq } from "drizzle-orm";
3
+ import { orderNews } from "../orders/signals";
4
+ import { db } from "../../core";
5
+ import { orders } from "../../schema";
6
+
7
+ import { flow } from "okengine";
8
+ import { z } from "zod";
9
+ import { stripeKey } from "../../vault";
10
+ import { OrderRef } from "./shapes";
11
+
12
+ export const chargeOrder = flow({
13
+ durable: true, // every fx call below is journaled
14
+ in: OrderRef, out: z.boolean(),
15
+ do: async ({ orderId }, fx) => {
16
+ const intent = await fx.step("create-intent", () => // never re-runs on replay
17
+ stripe(fx.vault(stripeKey)).create(orderId));
18
+
19
+ await fx.clock.sleep("verify-window", "2m"); // survives restart and deploy
20
+
21
+ return fx.step("confirm", () => stripe(fx.vault(stripeKey)).confirm(intent));
22
+ },
23
+ });
24
+
25
+ // Durable sleep returns early from fx.call; finish status + notify on resume.
26
+ const charged = chargeOrder.do;
27
+ (chargeOrder as { do: typeof charged }).do = async (input, fx) => {
28
+ const paid = await charged(input, fx);
29
+ if (paid) {
30
+ await fx.store(db).update(orders).set({ status: "confirmed" })
31
+ .where(eq(orders.id, input.orderId));
32
+ await fx.emit(orderNews, { orderId: input.orderId, status: "confirmed" });
33
+ }
34
+ return paid;
35
+ };
36
+
37
+ Object.assign(chargeOrder, { name: "payments.chargeOrder" });
@@ -0,0 +1,3 @@
1
+ import { z } from "zod";
2
+
3
+ export const OrderRef = z.object({ orderId: z.string() });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Tiny Stripe stub used by the durable charge flow.
3
+ *
4
+ * @param key - Secret key from the vault
5
+ */
6
+ export function stripe(_key: string): {
7
+ create(orderId: string): { id: string };
8
+ confirm(intent: { id: string }): boolean;
9
+ } {
10
+ return {
11
+ create(orderId) {
12
+ return { id: `pi_${orderId}` };
13
+ },
14
+ confirm(_intent) {
15
+ return true;
16
+ },
17
+ };
18
+ }
@@ -0,0 +1,4 @@
1
+ import { gate } from "okengine";
2
+
3
+ /** Verified member. */
4
+ export const member = gate.policy("member", ({ auth }) => !!auth?.verified);
@@ -0,0 +1 @@
1
+ export default { "order.outOfStock": "لم يتبقَّ سوى {left} قطع" };
@@ -0,0 +1 @@
1
+ export default { "order.outOfStock": "Only {left} left" };
@@ -0,0 +1,2 @@
1
+ /** Opaque audit schema placeholder for the audit plugin element. */
2
+ export default {};
@@ -0,0 +1,14 @@
1
+ import { plugin, store } from "okengine";
2
+ import { z } from "zod";
3
+
4
+ export const audit = plugin("audit", { version: "1.0.0" })
5
+ .config(z.object({ retain: z.string().default("2y") }))
6
+ .element(store.sql("audit", { schema: () => import("./audit-schema") }))
7
+ .needs("store.kv")
8
+ .decorate("audit", { enabled: true })
9
+ .hook("afterHandle", async (ctx, fx) => {
10
+ if (ctx.trigger.meta?.audit) await fx.store("audit").log(ctx);
11
+ })
12
+ .errors({ AuditWriteFailed: z.object({ reason: z.string() }) })
13
+ .consolePanel({ id: "audit", title: "Audit Trail", entry: "./panel.tsx" })
14
+ .cli("audit:export", ({ fx }) => fx.store("audit").exportCsv());
@@ -0,0 +1,16 @@
1
+ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
2
+
3
+ export const products = sqliteTable("products", {
4
+ sku: text("sku").primaryKey(),
5
+ name: text("name").notNull(),
6
+ stock: integer("stock").notNull().default(0),
7
+ });
8
+
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"),
15
+ createdAt: integer("created_at").notNull(),
16
+ });
@@ -0,0 +1,16 @@
1
+ import { vault } from "okengine";
2
+ import { z } from "zod";
3
+
4
+ // A declaration is a CONTRACT, not a value.
5
+ // Resolution: process.env → .env.local → .env.stack → vault driver → dev fallback
6
+ export const stripeKey = vault.secret("STRIPE_KEY", {
7
+ schema: z.string().startsWith("sk_"),
8
+ description: "Payments gateway key",
9
+ rotate: "90d",
10
+ dev: "sk_test_local",
11
+ });
12
+
13
+ export const dbUrl = vault.secret("DATABASE_URL", {
14
+ schema: z.string().url(),
15
+ dev: vault.fromStack("store.sql"), // generated by `oke dev --stack` — zero manual setup
16
+ });
@@ -0,0 +1,18 @@
1
+ import { test, expect } from "bun:test";
2
+ import { createTestApp } from "okengine/test";
3
+ import { app } from "../src/app";
4
+
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"] });
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();
13
+
14
+ expect(t.channels.sent()).toContainEqual(
15
+ expect.objectContaining({ template: "order-confirmed", to: u.id, locale: "ar" }));
16
+ expect(data?.id).toBeDefined();
17
+ await t.close();
18
+ });
@@ -0,0 +1 @@
1
+ {"input":{"subject":"seat dispute","body":"wrong seat"},"expected":{"urgency":"high","team":"ops"}}
@@ -0,0 +1,44 @@
1
+ import { defineConfig } from "okengine/config";
2
+ import { dbUrl, dbReplica1, anthropicKey } from "./src/vault";
3
+
4
+ export default defineConfig({
5
+ // Drivers are named after PROTOCOLS and bind through Bun's native clients
6
+ // (Bun.sql, bun:sqlite, Bun.redis, Bun.S3) — zero npm client dependencies.
7
+ drivers: {
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" },
15
+ },
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
20
+ 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" },
25
+ },
26
+ ai: {
27
+ dev: "mock", // deterministic — tests never call out
28
+ prod: { driver: "anthropic", key: anthropicKey },
29
+ // no prod default: model choice is never guessed.
30
+ // "openai-compatible" covers vLLM · Groq · Together · LM Studio · most self-hosted
31
+ },
32
+ },
33
+
34
+ images: { // vendor choice, keyed by ROLE
35
+ "store.sql": "pgvector/pgvector:pg17",
36
+ "store.kv": "valkey/valkey:8-alpine",
37
+ },
38
+
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" } },
44
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "store.sql": "pgvector/pgvector:pg17",
3
+ "store.kv": "valkey/valkey:8-alpine"
4
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@oke/example-skyport",
3
+ "version": "0.1.4",
4
+ "private": true,
5
+ "type": "module",
6
+ "dependencies": {
7
+ "okengine": "file:../..",
8
+ "drizzle-orm": "^1.0.0-rc.4",
9
+ "zod": "^4.4.3"
10
+ },
11
+ "scripts": {
12
+ "test": "bun test",
13
+ "dev": "oke dev",
14
+ "docker": "oke docker"
15
+ }
16
+ }
@@ -0,0 +1,25 @@
1
+ import { ai, store } from "okengine";
2
+ import { z } from "zod";
3
+ import { getBooking, refundBooking } from "./flows/bookings";
4
+
5
+ export const smart = ai.model("smart", { provider: "anthropic", tier: "opus" });
6
+ export const fast = ai.model("fast", { provider: "anthropic", tier: "haiku" });
7
+
8
+ // A prompt is a VERSIONED ARTIFACT with a validated output shape — not a string in a handler
9
+ export const triage = smart.prompt("ticket-triage", {
10
+ in: z.object({ subject: z.string(), body: z.string() }),
11
+ out: z.object({ urgency: z.enum(["low", "high"]), team: z.string(), summary: z.string() }),
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
15
+ });
16
+
17
+ export const embed = ai.embed("docs", { model: fast, into: store.index("kb") });
18
+
19
+ // An agent whose tools are YOUR OWN FLOWS — each carrying its gates and effects
20
+ export const support = ai.agent("support", {
21
+ model: smart,
22
+ tools: [getBooking, refundBooking],
23
+ maxSteps: 6,
24
+ budget: { maxCostPerRun: 0.25 },
25
+ });
@@ -0,0 +1,57 @@
1
+ import { db } from "./core";
2
+ import { member, fair } from "./gates";
3
+ import {
4
+ dbUrl,
5
+ dbReplica1,
6
+ anthropicKey,
7
+ stripeKey,
8
+ } from "./vault";
9
+ import { bookingConfirmed } from "./channels";
10
+ import { orderPlaced, seatFeed } from "./flows/bookings/signals";
11
+ import {
12
+ smart,
13
+ fast,
14
+ triage,
15
+ embed,
16
+ support as supportAgentDecl,
17
+ } from "./ai";
18
+ import { canBook } from "./flows/bookings";
19
+ import "./journeys";
20
+ import "./ai";
21
+ import "./channels";
22
+ import "./gates";
23
+
24
+ import { oke } from "okengine";
25
+ import { auth } from "okengine/auth";
26
+ import { audit } from "./plugins/audit";
27
+ import * as bookings from "./flows/bookings";
28
+ import * as payments from "./flows/payments";
29
+ import * as notifications from "./flows/notifications";
30
+ import * as support from "./flows/support";
31
+ import * as users from "./flows/users";
32
+
33
+ export const app = oke({ name: "skyport" })
34
+ .adopt({ bookings, payments, notifications, support, users })
35
+ .plug(auth())
36
+ .plug(audit)
37
+ .hook("onError", (ctx, err, fx) => fx.log.error(err));
38
+
39
+ export type App = typeof app;
40
+
41
+ Object.assign(app.$options, {
42
+ env: "test",
43
+ gates: [member, canBook, fair],
44
+ secrets: [dbUrl, dbReplica1, anthropicKey, stripeKey],
45
+ signals: [orderPlaced, seatFeed],
46
+ stores: [db],
47
+ channel: {
48
+ templates: [bookingConfirmed],
49
+ defaultLocale: "ar",
50
+ },
51
+ ai: {
52
+ models: [smart, fast],
53
+ prompts: [triage],
54
+ embeds: [embed],
55
+ agents: [supportAgentDecl],
56
+ },
57
+ });
@@ -0,0 +1,12 @@
1
+ import { channel } from "okengine";
2
+ import { z } from "zod";
3
+
4
+ export const mail = channel.email({ from: "Skyport <no-reply@skyport.sa>" });
5
+
6
+ export const bookingConfirmed = mail.template("booking-confirmed", {
7
+ schema: z.object({
8
+ name: z.string(),
9
+ bookingId: z.string(),
10
+ }),
11
+ locales: ["en", "ar"],
12
+ });
@@ -0,0 +1,4 @@
1
+ import { store } from "okengine";
2
+ import * as schema from "./schema";
3
+
4
+ export const db = store.sql("skyport", { schema });
@@ -0,0 +1,50 @@
1
+ import { on, flow, gate, http } from "okengine";
2
+ import { eq } from "drizzle-orm";
3
+ import { db } from "../../core";
4
+ import { member, fair } from "../../gates";
5
+ import { orderPlaced, seatFeed } from "./signals";
6
+ import { NewBooking, BookingId, BookingRow, FlightFull } from "./shapes";
7
+ import { bookings, flights } from "../../schema";
8
+
9
+ export const canBook = gate.policy("booking:create", ({ auth }) => auth.scopes.has("booking:create"));
10
+
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 });
18
+
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
+ }));
27
+
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
+ }));
32
+
33
+ export const getBooking = flow({
34
+ in: BookingId, out: BookingRow,
35
+ do: async ({ id }, fx) => {
36
+ const [b] = await fx.store(db).select().from(bookings).where(eq(bookings.id, id)).limit(1);
37
+ return b;
38
+ },
39
+ });
40
+
41
+ // The agent's second tool — refunding is a distinct, gated capability, never the same
42
+ // permission as reading a booking, since the agent's tool list is exactly its authority.
43
+ export const refundBooking = flow({
44
+ in: BookingId, out: BookingRow,
45
+ do: async ({ id }, fx) => {
46
+ await fx.store(db).update(bookings).set({ status: "refunded" }).where(eq(bookings.id, id));
47
+ const [b] = await fx.store(db).select().from(bookings).where(eq(bookings.id, id)).limit(1);
48
+ return b;
49
+ },
50
+ });
@@ -0,0 +1,6 @@
1
+ import { z } from "zod";
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() });
@@ -0,0 +1,9 @@
1
+ import { signal } from "okengine";
2
+ import { z } from "zod";
3
+
4
+ export const orderPlaced = signal("order-placed", {
5
+ schema: z.object({ orderId: z.string() }), delivery: "once", retries: 5, deadLetter: true,
6
+ });
7
+ export const seatFeed = signal("seat-feed", {
8
+ schema: z.object({ flightId: z.string(), left: z.number() }), delivery: "live",
9
+ });
@@ -0,0 +1,16 @@
1
+ import { on, flow } from "okengine";
2
+ import { bookingConfirmed } from "../../channels";
3
+ import { orderPlaced } from "../bookings/signals";
4
+
5
+ export const send = on(
6
+ orderPlaced,
7
+ flow({
8
+ name: "notifications.send",
9
+ do: async ({ id }, fx) => {
10
+ await fx.send(bookingConfirmed, {
11
+ to: fx.auth.userId ?? "guest",
12
+ data: { name: "Traveler", bookingId: id },
13
+ });
14
+ },
15
+ }),
16
+ );
@@ -0,0 +1,14 @@
1
+ import { flow } from "okengine";
2
+ import { stripeKey } from "../../vault";
3
+ import { ChargeIn } from "./shapes";
4
+
5
+ export const chargeBooking = flow({
6
+ name: "payments.chargeBooking",
7
+ durable: true,
8
+ in: ChargeIn,
9
+ do: async ({ orderId }, fx) => {
10
+ const intent = await fx.step("create-intent", () => fx.vault(stripeKey));
11
+ await fx.clock.sleep("verify-window", "2m");
12
+ return fx.step("confirm", () => ({ orderId, intent }));
13
+ },
14
+ });
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+
3
+ export const ChargeIn = z.object({
4
+ orderId: z.string(),
5
+ });
@@ -0,0 +1,38 @@
1
+ import { on, flow, http } from "okengine";
2
+ import { z } from "zod";
3
+ import { triage, support, embed, smart, fast } from "../../ai";
4
+ import { member } from "../../gates";
5
+ import { db } from "../../core";
6
+ import { tickets } from "../../schema";
7
+
8
+ // ① A prompt call with a provider fallback chain and a validated result
9
+ export const createTicket = on(http.post("/tickets").gate(member), flow({
10
+ in: z.object({ subject: z.string(), body: z.string() }),
11
+ out: z.object({ id: z.string(), urgency: z.string() }),
12
+ do: async (input, fx) => {
13
+ const t = await fx.ask(triage, input, { via: [smart, fast] });
14
+ const id = fx.id();
15
+ await fx.store(db).insert(tickets).values({ id, ...input, ...t });
16
+ return { id, urgency: t.urgency };
17
+ },
18
+ }));
19
+ // effects → writes[sql:tickets] asks[ticket-triage v3] cost[~$0.01] nondeterministic
20
+
21
+ // ② RAG — retrieve, then answer with streaming tokens
22
+ export const askDocs = on(http.post("/ask").gate(member).live(), flow({
23
+ in: z.object({ question: z.string() }),
24
+ do: async ({ question }, fx) => {
25
+ const context = await fx.search(embed, question, { topK: 5 });
26
+ return fx.stream(smart, { prompt: "answer-with-context", data: { question, context } });
27
+ // streaming reaches the client through the Signal element — no separate socket layer
28
+ },
29
+ }));
30
+
31
+ // ③ A durable, bounded agent
32
+ export const supportAgent = on(http.post("/support").gate(member), flow({
33
+ durable: true, // nondeterministic calls are ALWAYS journaled
34
+ in: z.object({ message: z.string() }),
35
+ do: ({ message }, fx) => fx.run(support, { message }),
36
+ // the agent can only call getBooking and refundBooking, and only within THIS user's
37
+ // gates and tenant scope — it cannot exceed what the code declares
38
+ }));
@@ -0,0 +1,2 @@
1
+ /** User-unit element stubs (Skyport tree). */
2
+ export const usersUnit = "users";
@@ -0,0 +1,12 @@
1
+ import { on, flow, http } from "okengine";
2
+ import { member } from "../../gates";
3
+ import { UserProfile } from "./shapes";
4
+
5
+ export const me = on(
6
+ http.get("/me").gate(member),
7
+ flow({
8
+ name: "users.me",
9
+ out: UserProfile,
10
+ do: (_input, fx) => ({ id: fx.auth.userId ?? "anon" }),
11
+ }),
12
+ );
@@ -0,0 +1,6 @@
1
+ import { z } from "zod";
2
+
3
+ export const UserProfile = z.object({
4
+ id: z.string(),
5
+ email: z.string().email().optional(),
6
+ });
@@ -0,0 +1,10 @@
1
+ import { gate } from "okengine";
2
+
3
+ export const member = gate.policy("member", ({ auth }) => !!auth?.verified);
4
+
5
+ export const fair = gate.rate({
6
+ strategy: "sliding-window-counter",
7
+ max: 300,
8
+ per: "1m",
9
+ keyBy: "user",
10
+ });
@@ -0,0 +1,9 @@
1
+ import { journey } from "okengine";
2
+ import { create } from "./flows/bookings";
3
+ import { chargeBooking } from "./flows/payments";
4
+ import { send } from "./flows/notifications";
5
+
6
+ journey("book-a-flight", {
7
+ path: [create, chargeBooking, send],
8
+ slo: { availability: "99.5%" },
9
+ });
@@ -0,0 +1,4 @@
1
+ export default {
2
+ "booking.confirmed": "تم تأكيد حجزك",
3
+ "support.ticket": "تم استلام تذكرة الدعم",
4
+ } as const;
@@ -0,0 +1,4 @@
1
+ export default {
2
+ "booking.confirmed": "Your booking is confirmed",
3
+ "support.ticket": "Support ticket received",
4
+ } as const;
@@ -0,0 +1,7 @@
1
+ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
2
+
3
+ export const auditLog = sqliteTable("audit_log", {
4
+ id: text("id").primaryKey(),
5
+ at: integer("at").notNull(),
6
+ payload: text("payload"),
7
+ });