create-oke 0.2.3 → 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.
- package/README.md +32 -20
- package/examples/linkly/.env.example +15 -0
- package/examples/linkly/drizzle.config.ts +11 -0
- package/examples/linkly/oke.config.ts +6 -4
- package/examples/linkly/package.json +7 -4
- package/examples/linkly/src/flows/analytics/index.ts +29 -14
- package/examples/linkly/src/flows/links/index.ts +58 -35
- package/examples/linkly/src/flows/links/signals.ts +4 -3
- package/examples/linkly/src/gates.ts +4 -2
- package/examples/linkly/src/schema.decl.ts +39 -0
- package/examples/linkly/src/schema.generated.ts +30 -0
- package/examples/linkly/src/schema.ts +2 -17
- package/examples/linkly/tests/linkly.test.ts +9 -9
- package/examples/notes/.env.example +12 -0
- package/examples/notes/drizzle.config.ts +11 -0
- package/examples/notes/oke.config.ts +8 -1
- package/examples/notes/package.json +7 -4
- package/examples/notes/src/app.ts +1 -1
- package/examples/notes/src/flows/notes/index.ts +44 -30
- package/examples/notes/src/schema.ts +3 -3
- package/examples/notes/tests/notes.test.ts +1 -1
- package/examples/provisions/.env.example +19 -0
- package/examples/provisions/drizzle.config.ts +11 -0
- package/examples/provisions/oke.config.ts +10 -8
- package/examples/provisions/package.json +7 -4
- package/examples/provisions/src/app.ts +3 -3
- package/examples/provisions/src/channels.ts +4 -3
- package/examples/provisions/src/flows/notifications/index.ts +13 -7
- package/examples/provisions/src/flows/orders/index.ts +58 -31
- package/examples/provisions/src/flows/payments/index.ts +12 -6
- package/examples/provisions/src/schema.ts +7 -7
- package/examples/provisions/src/vault.ts +2 -2
- package/examples/provisions/tests/orders.test.ts +9 -8
- package/examples/skyport/.env.example +34 -0
- package/examples/skyport/drizzle.config.ts +11 -0
- package/examples/skyport/oke.config.ts +25 -22
- package/examples/skyport/package.json +8 -5
- package/examples/skyport/src/ai.ts +4 -4
- package/examples/skyport/src/app.ts +2 -13
- package/examples/skyport/src/flows/bookings/index.ts +45 -22
- package/examples/skyport/src/flows/bookings/shapes.ts +4 -4
- package/examples/skyport/src/flows/bookings/signals.ts +6 -2
- package/examples/skyport/src/flows/support/index.ts +37 -25
- package/examples/skyport/src/schema.ts +11 -8
- package/examples/skyport/src/vault.ts +1 -1
- package/examples/skyport/tests/support.test.ts +6 -6
- package/package.json +9 -9
- package/src/cli.test.ts +132 -64
- package/src/cli.ts +58 -33
- package/src/index.ts +0 -0
- package/src/scaffold.ts +53 -15
- package/src/sync-templates.ts +2 -11
- package/src/templates.ts +1 -2
- package/src/transform.ts +59 -9
- package/templates/full/.env.example +24 -0
- package/templates/full/README.md +22 -6
- package/templates/full/drizzle.config.ts +14 -0
- package/templates/full/oke.config.ts +38 -9
- package/templates/full/package.json +3 -0
- package/templates/hello/.env.example +9 -0
- package/templates/hello/README.md +6 -4
- package/templates/minimal/.env.example +13 -0
- package/templates/minimal/README.md +15 -7
- package/templates/minimal/drizzle.config.ts +14 -0
- package/templates/minimal/oke.config.ts +8 -1
- package/templates/minimal/package.json +3 -0
- package/templates/standard/.env.example +21 -0
- package/templates/standard/README.md +25 -8
- package/templates/standard/drizzle.config.ts +14 -0
- package/templates/standard/oke.config.ts +36 -8
- 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: {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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: {
|
|
12
|
-
sms: {
|
|
13
|
-
whatsapp: {
|
|
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
|
-
"
|
|
12
|
-
"
|
|
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())
|
|
20
|
-
.plug(audit)
|
|
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 }));
|
|
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
|
|
6
|
-
export const wa
|
|
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", {
|
|
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(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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(
|
|
31
|
-
|
|
32
|
-
|
|
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,
|
|
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(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.
|
|
49
|
-
|
|
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(
|
|
55
|
-
|
|
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,
|
|
14
|
-
in: OrderRef,
|
|
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", () =>
|
|
17
|
-
|
|
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");
|
|
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
|
|
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:
|
|
5
|
-
name:
|
|
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:
|
|
11
|
-
userId:
|
|
12
|
-
sku:
|
|
13
|
-
qty:
|
|
14
|
-
status:
|
|
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 →
|
|
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.
|
|
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);
|
|
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");
|
|
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
|
-
|
|
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: {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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:
|
|
17
|
-
clock:
|
|
18
|
-
vault:
|
|
19
|
-
runs:
|
|
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:
|
|
22
|
-
sms:
|
|
23
|
-
whatsapp: {
|
|
24
|
-
push:
|
|
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
|
-
|
|
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: {
|
|
36
|
+
images: {
|
|
37
|
+
// vendor choice, keyed by ROLE
|
|
35
38
|
"store.sql": "pgvector/pgvector:pg17",
|
|
36
|
-
"store.kv":
|
|
39
|
+
"store.kv": "valkey/valkey:8-alpine",
|
|
37
40
|
},
|
|
38
41
|
|
|
39
|
-
i18n:
|
|
40
|
-
tenancy:
|
|
41
|
-
topology: "monolith",
|
|
42
|
-
ports:
|
|
43
|
-
console:
|
|
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
|
|
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:
|
|
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",
|
|
14
|
-
budget: { maxCostPerCall: 0.02 },
|
|
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 }) =>
|
|
9
|
+
export const canBook = gate.policy("booking:create", ({ auth }) =>
|
|
10
|
+
auth.scopes.has("booking:create"),
|
|
11
|
+
);
|
|
10
12
|
|
|
11
|
-
export const create = on(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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(
|
|
29
|
-
|
|
30
|
-
|
|
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,
|
|
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,
|
|
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
|
|
4
|
-
export const BookingId
|
|
5
|
-
export const BookingRow
|
|
6
|
-
export const FlightFull
|
|
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() });
|