okengine 0.7.0 → 0.9.0
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/package.json +3 -2
- package/site/content/docs/deployment/docker-swarm.mdx +228 -0
- package/site/content/docs/deployment/docker.mdx +212 -0
- package/site/content/docs/deployment/index.mdx +83 -0
- package/site/content/docs/deployment/kubernetes.mdx +176 -0
- package/site/content/docs/deployment/meta.json +5 -0
- package/site/content/docs/deployment/reverse-proxy.mdx +216 -0
- package/site/content/docs/elements/channel.mdx +25 -12
- package/site/content/docs/elements/clock.mdx +17 -15
- package/site/content/docs/elements/flow.mdx +6 -2
- package/site/content/docs/elements/signal.mdx +10 -8
- package/site/content/docs/elements/store.mdx +165 -0
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/get-started/installation.mdx +18 -16
- package/site/content/docs/index.mdx +5 -0
- package/site/content/docs/meta.json +10 -1
- package/site/content/docs/plugins/index.mdx +1 -2
- package/site/content/docs/plugins/magic-link.mdx +44 -2
- package/site/content/docs/plugins/meta.json +1 -2
- package/site/content/docs/plugins/otp.mdx +202 -0
- package/site/content/docs/plugins/two-factor.mdx +2 -1
- package/site/content/docs/reference/cli.md +5 -2
- package/site/content/docs/reference/configuration.mdx +21 -3
- package/site/content/docs/reference/environment-variables.mdx +21 -8
- package/site/content/docs/reference/plugins.mdx +1 -1
- package/src/auth/auth.test.ts +36 -0
- package/src/auth/bindings.ts +3 -12
- package/src/auth/identity.ts +33 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/otp-capability.ts +119 -0
- package/src/auth/otp-seal.test.ts +61 -0
- package/src/auth/otp-seal.ts +84 -0
- package/src/auth/schema.ts +3 -0
- package/src/auth/sessions.ts +26 -27
- package/src/auth/tables.ts +4 -0
- package/src/auth/verification.ts +61 -1
- package/src/cli/db-seed.ts +359 -0
- package/src/cli/db.test.ts +341 -3
- package/src/cli/db.ts +75 -8
- package/src/cli/dev-app-runner.ts +4 -0
- package/src/cli/docker.ts +4 -1
- package/src/cli/load-config.images.test.ts +26 -0
- package/src/cli/load-config.ts +10 -2
- package/src/cli/registry.ts +38 -2
- package/src/compiler/effects-infer.ts +1 -0
- package/src/config/index.ts +4 -0
- package/src/console/server/operator-db.ts +34 -9
- package/src/docker/compose.ts +162 -6
- package/src/docker/derive.ts +60 -3
- package/src/docker/docker.test.ts +374 -1
- package/src/docker/helpers.ts +2 -0
- package/src/docker/index.ts +11 -0
- package/src/docker/recipes/caddy.ts +51 -0
- package/src/docker/recipes/dragonfly.ts +31 -0
- package/src/docker/recipes/index.ts +25 -2
- package/src/docker/recipes/pgdog.ts +84 -0
- package/src/docker/recipes/redis.ts +6 -3
- package/src/docker/recipes/traefik.ts +83 -0
- package/src/docker/recipes/valkey.ts +30 -0
- package/src/docker/stack-id.ts +5 -0
- package/src/docker/types.ts +18 -0
- package/src/drivers/channel-sently.test.ts +8 -0
- package/src/drivers/channel-taqnyat-mail.ts +34 -0
- package/src/drivers/channel-taqnyat-whatsapp.ts +94 -0
- package/src/drivers/channel-types.ts +72 -0
- package/src/drivers/clock-postgres.test.ts +258 -0
- package/src/drivers/clock-postgres.ts +410 -0
- package/src/drivers/index.ts +18 -0
- package/src/drivers/journal-postgres.test.ts +175 -0
- package/src/drivers/journal-postgres.ts +492 -0
- package/src/elements/channel/otp-delivery.test.ts +76 -0
- package/src/elements/channel/otp-delivery.ts +291 -0
- package/src/elements/channel/runtime.ts +203 -114
- package/src/elements/channel.test.ts +71 -0
- package/src/elements/channel.ts +12 -2
- package/src/elements/clock/chaos-child.ts +280 -41
- package/src/elements/clock/durable.ts +7 -0
- package/src/elements/clock/reconcile.ts +2 -2
- package/src/elements/clock/runtime.ts +5 -3
- package/src/elements/clock.ts +1 -1
- package/src/elements/store/seed.test.ts +27 -0
- package/src/elements/store/seed.ts +68 -0
- package/src/elements/store/sql-session.test.ts +39 -0
- package/src/elements/store/sql-session.ts +55 -0
- package/src/elements/store/upsert-app.test.ts +103 -0
- package/src/elements/store.ts +5 -0
- package/src/index.ts +18 -0
- package/src/kernel/app.ts +221 -14
- package/src/kernel/boot-bind/channel.test.ts +16 -0
- package/src/kernel/boot-bind/channel.ts +64 -0
- package/src/kernel/boot-bind/clock.ts +17 -6
- package/src/kernel/boot-bind/gate.ts +14 -19
- package/src/kernel/boot-bind/honor-config.test.ts +123 -4
- package/src/kernel/boot-bind/journal.ts +89 -0
- package/src/kernel/boot-bind/signal.ts +20 -0
- package/src/kernel/boot-bind/store.test.ts +82 -0
- package/src/kernel/boot-bind/store.ts +22 -0
- package/src/kernel/boot.test.ts +6 -4
- package/src/kernel/boot.ts +53 -13
- package/src/kernel/concurrency.ts +1 -1
- package/src/kernel/fx.test.ts +9 -0
- package/src/kernel/fx.ts +175 -5
- package/src/kernel/graceful-shutdown.test.ts +76 -0
- package/src/kernel/graceful-shutdown.ts +106 -0
- package/src/kernel/horizontal-child.ts +257 -0
- package/src/kernel/horizontal.integration.test.ts +229 -0
- package/src/kernel/index.ts +14 -0
- package/src/kernel/journal-boot.test.ts +397 -0
- package/src/kernel/journal-suspend.ts +35 -0
- package/src/kernel/journal.test.ts +142 -0
- package/src/kernel/journal.ts +202 -27
- package/src/kernel/ready.test.ts +76 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +5 -5
- package/src/plugins/auth-methods.security.test.ts +20 -27
- package/src/plugins/auth-methods.test.ts +7 -6
- package/src/plugins/index.ts +12 -8
- package/src/plugins/magic-link.ts +1 -23
- package/src/plugins/otp.test.ts +236 -0
- package/src/plugins/otp.ts +570 -0
- package/src/plugins/taqnyat.live.test.ts +172 -0
- package/src/release/official-plugins.ts +1 -2
- package/site/content/docs/plugins/email-otp.mdx +0 -117
- package/site/content/docs/plugins/phone-number.mdx +0 -111
- package/src/plugins/email-otp.ts +0 -214
- package/src/plugins/phone-number.ts +0 -149
|
@@ -239,6 +239,151 @@ not when emit rewrites `schema.generated.ts`.
|
|
|
239
239
|
Opt out with `--no-db-push` or `db: { autoPush: false }`. Docker/prod **never**
|
|
240
240
|
auto-apply DDL; a missing table fails as **OKE1101** (`oke db migrate`).
|
|
241
241
|
|
|
242
|
+
#### Multiple environments
|
|
243
|
+
|
|
244
|
+
Local can `oke db push` freely — safe to wipe and regenerate.
|
|
245
|
+
|
|
246
|
+
Staging/prod accumulate versioned SQL under `drizzle/` (`oke db generate`).
|
|
247
|
+
`oke db migrate` applies only unrecorded files in order — behind DBs catch up.
|
|
248
|
+
|
|
249
|
+
| Environment | Sync |
|
|
250
|
+
| -------------- | -------------------------------------------------------------- |
|
|
251
|
+
| Local | `oke db push` (or auto-push from `oke dev`) |
|
|
252
|
+
| Staging / prod | `oke db generate` → review files → `oke db migrate` on that DB |
|
|
253
|
+
|
|
254
|
+
<Callout title="Connection pooling is infrastructure, not app code" type="info">
|
|
255
|
+
Bun.SQL defaults to **10** connections per process — fine per instance. Scale out and `N × pool`
|
|
256
|
+
can exceed Postgres `max_connections`. docker/prod puts **PgDog** in front; `DATABASE_URL` → port
|
|
257
|
+
`6432`. No app code changes.
|
|
258
|
+
</Callout>
|
|
259
|
+
|
|
260
|
+
**Why PgDog (not PgBouncer as the default).** Transaction pooling fixes the
|
|
261
|
+
math; naive poolers can leak session state (`SET`, RLS vars, `LISTEN`/`NOTIFY`)
|
|
262
|
+
across clients. PgDog re-applies those under transaction mode (PgBouncer is fine — not shipped).
|
|
263
|
+
|
|
264
|
+
**Read replicas later, zero app changes.** Add `role = "replica"` in
|
|
265
|
+
`pgdog.toml` later: `BEGIN READ ONLY` → replica, failover on promotion.
|
|
266
|
+
Not wired this round — readiness only.
|
|
267
|
+
|
|
268
|
+
#### Seeding
|
|
269
|
+
|
|
270
|
+
`oke db seed` loads `defineSeed` from `src/seed/index.ts` — **never at boot**.
|
|
271
|
+
Seed proves existence (bootstrap rows); it does not correct schema-adjacent data over time.
|
|
272
|
+
|
|
273
|
+
<StoreSeeding />
|
|
274
|
+
|
|
275
|
+
| env | `essential` | `dev` | `prod` |
|
|
276
|
+
| -------- | ----------- | ----- | ------ |
|
|
277
|
+
| `local` | yes | yes | no |
|
|
278
|
+
| `docker` | yes | yes | no |
|
|
279
|
+
| `test` | yes | no | no |
|
|
280
|
+
| `prod` | yes | no | yes |
|
|
281
|
+
|
|
282
|
+
`docker` is a laptop profile with prod-shaped drivers — still development, so `dev` runs.
|
|
283
|
+
Outside `test`, exactly one of `dev` / `prod` runs with `essential`.
|
|
284
|
+
|
|
285
|
+
**Simple form** — everything inline:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
import { defineSeed, type Fx } from "okengine";
|
|
289
|
+
import { db } from "../core";
|
|
290
|
+
import { notes } from "../schema.decl";
|
|
291
|
+
|
|
292
|
+
export default defineSeed({
|
|
293
|
+
essential: async (fx: Fx) => {
|
|
294
|
+
await fx
|
|
295
|
+
.store(db)
|
|
296
|
+
.upsert(
|
|
297
|
+
notes,
|
|
298
|
+
{ id: "welcome" },
|
|
299
|
+
{ id: "welcome", title: "Welcome", body: "…", createdAt: 1 },
|
|
300
|
+
);
|
|
301
|
+
},
|
|
302
|
+
dev: async (fx: Fx) => {
|
|
303
|
+
await fx
|
|
304
|
+
.store(db)
|
|
305
|
+
.upsert(
|
|
306
|
+
notes,
|
|
307
|
+
{ id: "sample-ideas" },
|
|
308
|
+
{ id: "sample-ideas", title: "Ideas", body: "…", createdAt: 2 },
|
|
309
|
+
);
|
|
310
|
+
},
|
|
311
|
+
// prod: async (fx) => { /* e.g. register a real external webhook URL */ },
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Complex form** — one function per file, compose with arrays (order = run order):
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
import { defineSeed } from "okengine";
|
|
319
|
+
import { seedWelcome } from "./essential/welcome";
|
|
320
|
+
import { seedSamples } from "./dev/samples";
|
|
321
|
+
import { seedWebhook } from "./prod/webhook";
|
|
322
|
+
|
|
323
|
+
export default defineSeed({
|
|
324
|
+
essential: [seedWelcome],
|
|
325
|
+
dev: [seedSamples],
|
|
326
|
+
prod: [seedWebhook],
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
| `upsert` outcome | When |
|
|
331
|
+
| ----------------- | ------------------------------------------ |
|
|
332
|
+
| `upserted` | no row matched `matchOn` — insert |
|
|
333
|
+
| `already-existed` | match found; default leaves it alone |
|
|
334
|
+
| `changed` | match found and `{ onExisting: "update" }` |
|
|
335
|
+
|
|
336
|
+
Default upsert is **insert-if-missing**. Pass `{ onExisting: "update" }` only when this
|
|
337
|
+
call should also rewrite other columns on a match — opt-in per call, never global.
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
// Safe bootstrap — second seed run leaves the row alone
|
|
341
|
+
await fx.store(db).upsert(
|
|
342
|
+
notes,
|
|
343
|
+
{ id: "welcome" },
|
|
344
|
+
{
|
|
345
|
+
id: "welcome",
|
|
346
|
+
title: "Welcome",
|
|
347
|
+
body: "Your Notes API is ready.",
|
|
348
|
+
createdAt: 1,
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// Refresh copy on every seed — match stays; title/body are rewritten
|
|
353
|
+
await fx.store(db).upsert(
|
|
354
|
+
notes,
|
|
355
|
+
{ id: "welcome" },
|
|
356
|
+
{
|
|
357
|
+
id: "welcome",
|
|
358
|
+
title: "Welcome (updated)",
|
|
359
|
+
body: "New bootstrap copy.",
|
|
360
|
+
createdAt: 1,
|
|
361
|
+
},
|
|
362
|
+
{ onExisting: "update" },
|
|
363
|
+
);
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**Consequence:** default keeps operator edits safe across re-seeds; `onExisting: "update"`
|
|
367
|
+
is for seed-owned rows you intentionally overwrite. Schema-wide data fixes still belong in
|
|
368
|
+
migrations, not seed.
|
|
369
|
+
|
|
370
|
+
| Env | Confirm |
|
|
371
|
+
| ----------------- | ----------------------------------------------------- |
|
|
372
|
+
| `local` / `test` | none |
|
|
373
|
+
| `docker` / `prod` | print DB target; type the env name (`--force` for CI) |
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
oke db seed # resolve env like other oke db commands (.oke/mode)
|
|
377
|
+
oke db seed --env docker # explicit override
|
|
378
|
+
oke db seed --env prod --force
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
<Callout title="Seed vs migrate">
|
|
382
|
+
Permanent reference-data corrections go through `oke db generate` → `migrate`, not repeated seed.
|
|
383
|
+
Optional `drizzle-seed` may be used inside a `dev` function body only — never as the seed system
|
|
384
|
+
backbone.
|
|
385
|
+
</Callout>
|
|
386
|
+
|
|
242
387
|
<Callout title="Escape hatch">
|
|
243
388
|
Hand-written Drizzle in `src/schema.ts` stays supported — if there is nothing to emit, the emit
|
|
244
389
|
step is skipped and your file is used as-is. Plugins may contribute **whole new tables**;
|
|
@@ -359,6 +504,8 @@ drivers: {
|
|
|
359
504
|
},
|
|
360
505
|
images: {
|
|
361
506
|
"store.kv": "redis:8-alpine",
|
|
507
|
+
// or: "valkey/valkey:8-alpine"
|
|
508
|
+
// or: "docker.dragonflydb.io/dragonflydb/dragonfly"
|
|
362
509
|
},
|
|
363
510
|
```
|
|
364
511
|
|
|
@@ -374,6 +521,22 @@ images: {
|
|
|
374
521
|
|
|
375
522
|
Missing Redis URL fails boot loudly: `oke boot: redis driver needs REDIS_URL`.
|
|
376
523
|
|
|
524
|
+
Driver id stays `redis` for every image below — same `REDIS_URL`, zero Flow changes.
|
|
525
|
+
Redis is the default because it is the most mature and battle-tested; Valkey and
|
|
526
|
+
Dragonfly are equally legitimate opt-in pins.
|
|
527
|
+
|
|
528
|
+
| Image | Pin | Why pick it | License |
|
|
529
|
+
| ------------- | --------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
530
|
+
| **Redis** | `redis:8-alpine` (default) | Most mature / battle-tested | RSALv2 / SSPLv1 / AGPLv3 (Redis ≥8) — service limits under RSAL/SSPL apply if you offer Redis as a managed service to third parties |
|
|
531
|
+
| **Valkey** | `valkey/valkey:8-alpine` | BSD fork (Linux Foundation) | BSD-3-Clause — permissive; no managed-service restriction |
|
|
532
|
+
| **Dragonfly** | `docker.dragonflydb.io/dragonflydb/dragonfly` | Multi-threaded Redis-wire runtime | BSL 1.1 (converts to Apache 2.0 on a published change date) — free for self-hosting; restricts offering Dragonfly as a commercial managed service |
|
|
533
|
+
|
|
534
|
+
<Callout title="Licenses bite managed-service resellers — read this once" type="warn">
|
|
535
|
+
RSAL, SSPL, and BSL service restrictions apply when you offer that specific datastore **as a
|
|
536
|
+
service** to third parties — not when you run it for your own app. Pick for your situation; OKE
|
|
537
|
+
does not pick a “safer” default for you.
|
|
538
|
+
</Callout>
|
|
539
|
+
|
|
377
540
|
<Callout title="Dry-run refuses KV writes">
|
|
378
541
|
`set` and `delete` throw `DryRunWriteIsolationError` during dry-run — the runtime will not risk a
|
|
379
542
|
double-write against a shared Redis. Reads (`get` / `list`) still run.
|
|
@@ -472,6 +635,8 @@ images: {
|
|
|
472
635
|
|
|
473
636
|
`memory` is the test default. Local `fs` writes under a temp root when no binding `root` is set.
|
|
474
637
|
|
|
638
|
+
**Known limit:** `fs` is single-host. Under horizontal scale each replica sees its own filesystem (silently inconsistent object views). Boot prints a one-shot warning when `drivers.store.files` is `fs`. Use `s3` for docker/prod (create-oke templates already do).
|
|
639
|
+
|
|
475
640
|
<Callout title="Console can browse, not edit bytes">
|
|
476
641
|
The [Console · Store](/docs/console/store) lists keys and can delete them. Direct edit is **KV +
|
|
477
642
|
SQL only** — blob bodies are not patched from the Console.
|
|
@@ -30,4 +30,9 @@ Learn the shape once, install on Bun, then write your first Flow from the standa
|
|
|
30
30
|
description="Health Flow, typed client, Console proof."
|
|
31
31
|
href="/docs/get-started/basic-usage"
|
|
32
32
|
/>
|
|
33
|
+
<Card
|
|
34
|
+
title="Deployment"
|
|
35
|
+
description="Compose, Swarm, Kubernetes, and reverse proxy."
|
|
36
|
+
href="/docs/deployment"
|
|
37
|
+
/>
|
|
33
38
|
</Cards>
|
|
@@ -60,17 +60,18 @@ bunx create-oke@latest my-app --yes
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
Two Notes starters: **standard** (local-first) and **advanced** (docker-ready +
|
|
63
|
-
files/digest/AI).
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
Non-TTY / `--yes` stay scriptable.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
(
|
|
73
|
-
skips the mode prompt unless you delete
|
|
63
|
+
files/digest/AI).
|
|
64
|
+
|
|
65
|
+
On a TTY: pick a template, then **recommended defaults**, **customize** (local
|
|
66
|
+
or docker first; saved to `~/.oke/create-defaults.json`), or **reuse** matching
|
|
67
|
+
saved settings. Project name rejects a non-empty existing path.
|
|
68
|
+
|
|
69
|
+
Non-TTY / `--yes` stay scriptable. Defaults: local SQLite · Docker/prod Postgres.
|
|
70
|
+
`--sql postgres` pins SQL; `--ai` runs the model wizard before install.
|
|
71
|
+
|
|
72
|
+
Scaffold writes `.oke/mode` (standard → `local`, advanced → `docker`) so the
|
|
73
|
+
first `oke dev` skips the mode prompt unless you delete it or pass `--local` /
|
|
74
|
+
`--docker`.
|
|
74
75
|
|
|
75
76
|
</Step>
|
|
76
77
|
|
|
@@ -86,11 +87,12 @@ Four ports come up together (mnemonic: **O·K·E = 6·5·3**):
|
|
|
86
87
|
|
|
87
88
|
<Surfaces />
|
|
88
89
|
|
|
89
|
-
Open `http://localhost:6533`.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
worked — **derived, not
|
|
90
|
+
Open `http://localhost:6533`. Claim the Console with the **claim code** printed
|
|
91
|
+
once in the `oke dev` log, then create the first operator.
|
|
92
|
+
|
|
93
|
+
Password policy matches Gate auth: **≥ 12 characters, with a letter and a
|
|
94
|
+
number.** If the Console lists your flows, the install worked — **derived, not
|
|
95
|
+
configured.**
|
|
94
96
|
|
|
95
97
|
</Step>
|
|
96
98
|
|
|
@@ -32,6 +32,11 @@ on(orderPlaced, sendReceipt);
|
|
|
32
32
|
href="/docs/plugins"
|
|
33
33
|
/>
|
|
34
34
|
<Card title="Console" description="Manifest-derived operator panels." href="/docs/console" />
|
|
35
|
+
<Card
|
|
36
|
+
title="Deployment"
|
|
37
|
+
description="Compose, Swarm, Kubernetes, reverse proxy."
|
|
38
|
+
href="/docs/deployment"
|
|
39
|
+
/>
|
|
35
40
|
<Card
|
|
36
41
|
title="Reference"
|
|
37
42
|
description="Config, fx, env, errors, CLI, security."
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "Documentation",
|
|
3
3
|
"icon": "BookOpen",
|
|
4
|
-
"pages": [
|
|
4
|
+
"pages": [
|
|
5
|
+
"index",
|
|
6
|
+
"get-started",
|
|
7
|
+
"elements",
|
|
8
|
+
"plugins",
|
|
9
|
+
"console",
|
|
10
|
+
"deployment",
|
|
11
|
+
"reference",
|
|
12
|
+
"ai"
|
|
13
|
+
]
|
|
5
14
|
}
|
|
@@ -17,8 +17,7 @@ First-party plugins you `.plug()` onto an app. Each page is one export from `oke
|
|
|
17
17
|
href="/docs/plugins/anonymous"
|
|
18
18
|
/>
|
|
19
19
|
<Card title="Magic link" description="One-time email link." href="/docs/plugins/magic-link" />
|
|
20
|
-
<Card title="
|
|
21
|
-
<Card title="Phone number" description="E.164 SMS OTP." href="/docs/plugins/phone-number" />
|
|
20
|
+
<Card title="OTP" description="SMS, WhatsApp, or email codes." href="/docs/plugins/otp" />
|
|
22
21
|
<Card title="Two-factor" description="TOTP enable / verify." href="/docs/plugins/two-factor" />
|
|
23
22
|
<Card title="Passkey" description="WebAuthn-shaped passkeys." href="/docs/plugins/passkey" />
|
|
24
23
|
</Cards>
|
|
@@ -86,6 +86,48 @@ const { data } = await api.auth.verifyMagicLink({ token });
|
|
|
86
86
|
**Consequence:** the plugin contributes the `auth-magic-link` Channel template and EN/AR
|
|
87
87
|
catalog bodies (`{{link}}`, `{{token}}`). Override copy by merging your own catalog at boot.
|
|
88
88
|
|
|
89
|
+
## Delivery drivers
|
|
90
|
+
|
|
91
|
+
| `drivers.channel.email` | Delivery |
|
|
92
|
+
| ----------------------- | ----------------------------------------------------------------- |
|
|
93
|
+
| `console` | Dev inbox (local/test default) |
|
|
94
|
+
| `smtp` | Any SMTP host — Mailpit under `oke dev --docker` (docker default) |
|
|
95
|
+
| `resend` / `sndr` | Hosted email APIs |
|
|
96
|
+
| `taqnyat-mail` | Taqnyat Mail API (additive option) |
|
|
97
|
+
|
|
98
|
+
### Taqnyat Mail
|
|
99
|
+
|
|
100
|
+
```typescript title="oke.config.ts"
|
|
101
|
+
export default {
|
|
102
|
+
drivers: {
|
|
103
|
+
channel: {
|
|
104
|
+
email: { local: "console", docker: "smtp", test: "console", prod: "taqnyat-mail" },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
| Env | Meaning |
|
|
111
|
+
| -------------------- | ------------------------------------------------ |
|
|
112
|
+
| `TAQNYAT_MAIL_TOKEN` | Taqnyat bearer token enabled for Email |
|
|
113
|
+
| `TAQNYAT_CAMPAIGN` | Campaign name required by Taqnyat `mailSend.php` |
|
|
114
|
+
|
|
115
|
+
The plugin needs no change — delivery stays Channel-mediated via `fx.send`. SMTP/Mailpit
|
|
116
|
+
remains the default docker path; `taqnyat-mail` is strictly additive.
|
|
117
|
+
|
|
118
|
+
## Live tests (opt-in)
|
|
119
|
+
|
|
120
|
+
The Taqnyat live suite sends real email and burns real quota, so it is double-gated: it runs
|
|
121
|
+
only when `OKE_EMAIL_LIVE=1` **and** the real credentials (`TAQNYAT_MAIL_TOKEN`,
|
|
122
|
+
`TAQNYAT_CAMPAIGN`, plus `OKE_TEST_TAQNYAT_MAIL`) are all present.
|
|
123
|
+
|
|
124
|
+
Credentials alone never send; without the flag the suite skips visibly — never a silent pass.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
OKE_EMAIL_LIVE=1 TAQNYAT_MAIL_TOKEN=… TAQNYAT_CAMPAIGN=auth \
|
|
128
|
+
OKE_TEST_TAQNYAT_MAIL=you@example.com bun test src/plugins
|
|
129
|
+
```
|
|
130
|
+
|
|
89
131
|
## Troubleshooting
|
|
90
132
|
|
|
91
133
|
<Accordions>
|
|
@@ -105,14 +147,14 @@ unit tests without SMTP, set `exposeDevToken: true`.
|
|
|
105
147
|
|
|
106
148
|
## Learn more
|
|
107
149
|
|
|
108
|
-
- [
|
|
150
|
+
- [OTP](/docs/plugins/otp) — numeric code instead of a link
|
|
109
151
|
- [Gate](/docs/elements/gate) — `gate.auth`
|
|
110
152
|
- [Channel](/docs/elements/channel) — `fx.send`, Mailpit, consents
|
|
111
153
|
|
|
112
154
|
## Next
|
|
113
155
|
|
|
114
156
|
<Cards>
|
|
115
|
-
<Card title="
|
|
157
|
+
<Card title="OTP" description="SMS, WhatsApp, or email codes." href="/docs/plugins/otp" />
|
|
116
158
|
<Card title="Gate" description="Builtin auth and policies." href="/docs/elements/gate" />
|
|
117
159
|
<Card title="Channel" description="Email delivery and Mailpit." href="/docs/elements/channel" />
|
|
118
160
|
</Cards>
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "OTP"
|
|
3
|
+
description: "Official plugin — one-time codes over SMS, WhatsApp, or email under /auth, with explicit Tier 1 or Tier 2 delivery."
|
|
4
|
+
icon: "KeyRound"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`otp()` signs people in with a one-time code. You must set `tier` — there is no
|
|
9
|
+
auto-detect. Tier 1 is provider-owned SMS Verify; Tier 2 is app-owned delivery
|
|
10
|
+
across the channels you declare.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Enable `gate.auth`, then `.plug(otp({ tier: 1 }))` or
|
|
14
|
+
`.plug(otp({ tier: 2, channels: [...] }))`. Never omit `tier`. Never log raw
|
|
15
|
+
OTPs.
|
|
16
|
+
</Callout>
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
<Steps>
|
|
21
|
+
|
|
22
|
+
<Step>
|
|
23
|
+
### Plug Tier 2 (multi-channel)
|
|
24
|
+
|
|
25
|
+
```typescript title="src/app.ts"
|
|
26
|
+
import { oke } from "okengine";
|
|
27
|
+
import { otp } from "okengine/plugins";
|
|
28
|
+
|
|
29
|
+
export const app = oke({
|
|
30
|
+
name: "shop",
|
|
31
|
+
env: "local",
|
|
32
|
+
gate: { auth: {} },
|
|
33
|
+
}).plug(
|
|
34
|
+
otp({
|
|
35
|
+
tier: 2,
|
|
36
|
+
channels: ["sms", "whatsapp", "email"],
|
|
37
|
+
exposeDevOtp: true, // local DX only
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
</Step>
|
|
43
|
+
|
|
44
|
+
<Step>
|
|
45
|
+
### Request a code
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const { data } = await api.auth.requestOtp({
|
|
49
|
+
phone: "+15551234567",
|
|
50
|
+
email: "ali@example.com",
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`POST /auth/otp/request`. Prior active challenges for that principal are
|
|
55
|
+
invalidated. Delivery follows `channels` order for addresses you pass.
|
|
56
|
+
|
|
57
|
+
</Step>
|
|
58
|
+
|
|
59
|
+
<Step>
|
|
60
|
+
### Resend on another channel (Tier 2 only)
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
const { data } = await api.auth.resendOtp({
|
|
64
|
+
phone: "+15551234567",
|
|
65
|
+
email: "ali@example.com",
|
|
66
|
+
channel: "email",
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Same code, same TTL. Default cooldown is 60 seconds. Tier 1 has no resend
|
|
71
|
+
surface — the provider owns the code.
|
|
72
|
+
|
|
73
|
+
</Step>
|
|
74
|
+
|
|
75
|
+
<Step>
|
|
76
|
+
### Verify
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const { data } = await api.auth.verifyOtp({
|
|
80
|
+
phone: "+15551234567",
|
|
81
|
+
otp,
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`POST /auth/otp/verify` — five failed attempts consume the challenge.
|
|
86
|
+
|
|
87
|
+
</Step>
|
|
88
|
+
|
|
89
|
+
</Steps>
|
|
90
|
+
|
|
91
|
+
## Tiers
|
|
92
|
+
|
|
93
|
+
| | Tier 1 | Tier 2 |
|
|
94
|
+
| -------------------- | ----------------------------- | --------------------------------------------- |
|
|
95
|
+
| Config | `otp({ tier: 1 })` | `otp({ tier: 2, channels: [...] })` |
|
|
96
|
+
| Who owns the code | Provider (Verify API) | Your app |
|
|
97
|
+
| Delivery | `fx.sendOtp` / `fx.verifyOtp` | `fx.deliverOtp` (Channel templates) |
|
|
98
|
+
| Channels | SMS only | `sms` · `whatsapp` · `email` (declared order) |
|
|
99
|
+
| Resend other channel | Impossible | `POST /auth/otp/resend` |
|
|
100
|
+
| `exposeDevOtp` | Forbidden | Optional (default off) |
|
|
101
|
+
|
|
102
|
+
<Callout type="warn" title="Tier 1 limitation">
|
|
103
|
+
Resend-via-different-channel is impossible on Tier 1 — the code value is never visible to OKE. Use
|
|
104
|
+
Tier 2 when you need SMS → email fallback for the same code.
|
|
105
|
+
</Callout>
|
|
106
|
+
|
|
107
|
+
### Tier 1 setup
|
|
108
|
+
|
|
109
|
+
```typescript title="oke.config.ts"
|
|
110
|
+
export default {
|
|
111
|
+
drivers: {
|
|
112
|
+
channel: {
|
|
113
|
+
sms: { local: "console", docker: "taqnyat", test: "console", prod: "taqnyat" },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Boot fails loudly if no SMS driver exposes `sendOtp` / `verifyOtp`. Switch to
|
|
120
|
+
Tier 2, or bind a Verify-capable driver (for example `taqnyat`).
|
|
121
|
+
|
|
122
|
+
### Tier 2 delivery
|
|
123
|
+
|
|
124
|
+
| Concern | Behavior |
|
|
125
|
+
| --------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
126
|
+
| Storage | SHA-256 hash for verify + sealed AES-GCM copy (HKDF `oke-otp-seal-v1`) for redelivery |
|
|
127
|
+
| Seal lifetime | Wiped on verify, lockout, or TTL expiry — never left after the challenge dies |
|
|
128
|
+
| Challenge TTL | Default 10 minutes (`ttlMs`) |
|
|
129
|
+
| Resend cooldown | Default 60 seconds (`resendCooldownMs`) — separate from TTL |
|
|
130
|
+
| Auto failover | On real provider send errors, sently `FallbackTransport` walks remaining media; Taqnyat WhatsApp may use `sendWithFailover` |
|
|
131
|
+
| User resend | Explicit `resend` with `channel` — not automatic |
|
|
132
|
+
|
|
133
|
+
Templates: `auth-otp-email`, `auth-otp-sms`, `auth-otp-whatsapp` (EN/AR,
|
|
134
|
+
`{{otp}}`). SMS here is a plain message — not Taqnyat Verify.
|
|
135
|
+
|
|
136
|
+
## Options
|
|
137
|
+
|
|
138
|
+
| Option | Type | Default | Meaning |
|
|
139
|
+
| ------------------ | -------------------------------- | -------------------------- | --------------------------------- |
|
|
140
|
+
| `tier` | `1 \| 2` | required | Delivery mechanism — no auto |
|
|
141
|
+
| `channels` | `("sms"\|"whatsapp"\|"email")[]` | required on Tier 2 | Build-time preferred order |
|
|
142
|
+
| `ttlMs` | `number` | 10m | Challenge lifetime |
|
|
143
|
+
| `resendCooldownMs` | `number` | 60s | Tier 2 resend spacing |
|
|
144
|
+
| `exposeDevOtp` | `boolean` | `false` | Tier 2 only — raw OTP in response |
|
|
145
|
+
| `from` | `string` | `OKE <no-reply@oke.local>` | Email template From |
|
|
146
|
+
| `secret` | `string` | active\* | Auth secret (\*from `gate.auth`) |
|
|
147
|
+
| `sessions` | `SessionStore` | active\* | Session store |
|
|
148
|
+
| `identities` | `IdentityStore` | new | Email → user |
|
|
149
|
+
| `phones` | `PhoneStore` | new | Phone → user |
|
|
150
|
+
| `verifications` | `VerificationStore` | new | Challenge store |
|
|
151
|
+
|
|
152
|
+
## Surfaces
|
|
153
|
+
|
|
154
|
+
| Flow | Path | Gate | Tier |
|
|
155
|
+
| ----------------- | ------------------------ | ------------------------ | ------ |
|
|
156
|
+
| `auth.requestOtp` | `POST /auth/otp/request` | `gate.public` + otp rate | 1 + 2 |
|
|
157
|
+
| `auth.verifyOtp` | `POST /auth/otp/verify` | `gate.public` + otp rate | 1 + 2 |
|
|
158
|
+
| `auth.resendOtp` | `POST /auth/otp/resend` | `gate.public` + otp rate | 2 only |
|
|
159
|
+
|
|
160
|
+
## Troubleshooting
|
|
161
|
+
|
|
162
|
+
<Accordions>
|
|
163
|
+
<Accordion title='otp(): tier is required'>
|
|
164
|
+
|
|
165
|
+
You omitted `tier`. Set `tier: 1` or `tier: 2` explicitly — OKE never infers
|
|
166
|
+
which mechanism you meant.
|
|
167
|
+
|
|
168
|
+
</Accordion>
|
|
169
|
+
<Accordion title="Boot fails on Tier 1">
|
|
170
|
+
|
|
171
|
+
No Verify-capable SMS driver is bound. Set `drivers.channel.sms` to `taqnyat`
|
|
172
|
+
(or another driver with `sendOtp`/`verifyOtp`), or switch to
|
|
173
|
+
`otp({ tier: 2, channels: [...] })`.
|
|
174
|
+
|
|
175
|
+
</Accordion>
|
|
176
|
+
<Accordion title="resend_cooldown">
|
|
177
|
+
|
|
178
|
+
Wait for `resendCooldownMs` (default 60s). The challenge TTL is unchanged —
|
|
179
|
+
only delivery is rate-limited.
|
|
180
|
+
|
|
181
|
+
</Accordion>
|
|
182
|
+
<Accordion title="No email / SMS with the code (Tier 2)">
|
|
183
|
+
|
|
184
|
+
In `local` / `test` the `console` driver captures messages. Use
|
|
185
|
+
`exposeDevOtp: true` for unit tests without a real provider.
|
|
186
|
+
|
|
187
|
+
</Accordion>
|
|
188
|
+
</Accordions>
|
|
189
|
+
|
|
190
|
+
## Learn more
|
|
191
|
+
|
|
192
|
+
- [Magic link](/docs/plugins/magic-link) — link instead of a code
|
|
193
|
+
- [Channel](/docs/elements/channel) — `fx.send`, drivers, Mailpit
|
|
194
|
+
- [Gate](/docs/elements/gate) — `gate.auth`
|
|
195
|
+
|
|
196
|
+
## Next
|
|
197
|
+
|
|
198
|
+
<Cards>
|
|
199
|
+
<Card title="Magic link" description="Email link sign-in." href="/docs/plugins/magic-link" />
|
|
200
|
+
<Card title="Two-factor" description="TOTP second factor." href="/docs/plugins/two-factor" />
|
|
201
|
+
<Card title="Channel" description="Delivery drivers and Mailpit." href="/docs/elements/channel" />
|
|
202
|
+
</Cards>
|
|
@@ -107,11 +107,12 @@ enable. A recovery code works once, then is consumed.
|
|
|
107
107
|
- [Passkey](/docs/plugins/passkey) — WebAuthn register / authenticate
|
|
108
108
|
- [Gate](/docs/elements/gate) — session + policies
|
|
109
109
|
- [Username](/docs/plugins/username) — first factor to enroll against
|
|
110
|
+
- OTP over SMS or email instead of TOTP? See [OTP](/docs/plugins/otp) / [Magic link](/docs/plugins/magic-link)
|
|
110
111
|
|
|
111
112
|
## Next
|
|
112
113
|
|
|
113
114
|
<Cards>
|
|
114
115
|
<Card title="Passkey" description="WebAuthn register and assert." href="/docs/plugins/passkey" />
|
|
115
116
|
<Card title="Gate" description="Builtin auth and policies." href="/docs/elements/gate" />
|
|
116
|
-
<Card title="
|
|
117
|
+
<Card title="OTP" description="SMS, WhatsApp, or email codes." href="/docs/plugins/otp" />
|
|
117
118
|
</Cards>
|
|
@@ -28,11 +28,14 @@ oke schema generate # core + plugin stubs → schema/oke.ts (--ch
|
|
|
28
28
|
oke db push # domain schema.ts → live local DB (dev; drizzle-kit)
|
|
29
29
|
oke db generate # versioned SQL under drizzle/ (review)
|
|
30
30
|
oke db migrate # apply migrations (explicit; never auto in prod)
|
|
31
|
+
oke db seed # defineSeed (essential + env category); never at boot
|
|
32
|
+
oke db seed --env prod --force # CI: skip docker/prod confirmation prompt
|
|
31
33
|
oke vault set STRIPE_KEY # also: list · import .env · key rotate
|
|
32
34
|
oke client add <url> # types for a separate frontend repo
|
|
33
35
|
|
|
34
|
-
oke docker # docker/Dockerfile +
|
|
35
|
-
|
|
36
|
+
oke docker # docker/Dockerfile + compose.yml + compose.<role>.yml + compose.all.yml · …
|
|
37
|
+
# opt-in images.proxy → Caddyfile (caddy) or Traefik labels + socket-proxy
|
|
38
|
+
oke docker --prod # + compose.prod.yml (HEALTHCHECK /_/ready, deploy, limits); folded into compose.all.yml
|
|
36
39
|
oke docker clean # TTY: pick oke-dev-* stacks → down -v (containers, networks, volumes)
|
|
37
40
|
oke docker clean --yes # non-TTY: current project stack only
|
|
38
41
|
oke docker clean --all --yes # non-TTY: every oke-dev-* stack on this machine
|
|
@@ -26,7 +26,8 @@ drivers: {
|
|
|
26
26
|
files: { local: "fs", docker: "s3", test: "memory", prod: "s3" },
|
|
27
27
|
},
|
|
28
28
|
signal: { local: "memory", docker: "redis", test: "memory", prod: "redis" },
|
|
29
|
-
clock: { local: "memory", docker: "
|
|
29
|
+
clock: { local: "memory", docker: "postgres", test: "frozen", prod: "postgres" },
|
|
30
|
+
journal: { local: "memory", docker: "postgres", test: "memory", prod: "postgres" },
|
|
30
31
|
vault: { local: "env", docker: "openbao", test: "memory", prod: "openbao" },
|
|
31
32
|
channel: {
|
|
32
33
|
email: { local: "console", docker: "smtp", test: "console", prod: "smtp" },
|
|
@@ -42,9 +43,10 @@ drivers: {
|
|
|
42
43
|
| `store.files` | env driver map | `memory` · `fs` · `s3` |
|
|
43
44
|
| `store.index` | env driver map | `memory` · `pgvector` · `libsql` · `meilisearch` |
|
|
44
45
|
| `signal` | env driver map | `memory` · `redis` (boot); `postgres` · `nats` fail loud until clients bind |
|
|
45
|
-
| `clock` | env driver map | `memory` · `file` · `frozen`
|
|
46
|
+
| `clock` | env driver map | `memory` · `postgres` · `file` · `frozen` |
|
|
47
|
+
| `journal` | env driver map | `memory` · `file` · `postgres` |
|
|
46
48
|
| `vault` | env driver map | `env` · `openbao` · `memory` · `managed` |
|
|
47
|
-
| `channel.email` | env driver map | `console` · `smtp` · `resend` · `sndr`
|
|
49
|
+
| `channel.email` | env driver map | `console` · `smtp` · `resend` · `sndr` · `taqnyat-mail` |
|
|
48
50
|
| `channel.sms` | env driver map | `console` · `taqnyat` · `msegat` · `unifonic` |
|
|
49
51
|
| `channel.whatsapp` | env driver map | `console` · `wa-cloud` |
|
|
50
52
|
| `channel.push` | env driver map | `console` · `webpush` · `fcm` |
|
|
@@ -73,16 +75,32 @@ Container image pins keyed by element role — vendor choice lives here, never i
|
|
|
73
75
|
```typescript
|
|
74
76
|
images: {
|
|
75
77
|
"store.sql": "postgres:18",
|
|
78
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
|
|
76
79
|
"store.kv": "redis:8",
|
|
80
|
+
// or: "valkey/valkey:8-alpine"
|
|
81
|
+
// or: "docker.dragonflydb.io/dragonflydb/dragonfly"
|
|
77
82
|
"store.files": "rustfs/rustfs:1.0.0-beta.11",
|
|
78
83
|
"channel.email": "axllent/mailpit:v1.22.3",
|
|
79
84
|
vault: "openbao/openbao:2.6.1",
|
|
80
85
|
ai: "ollama/ollama:latest",
|
|
86
|
+
// opt-in TLS edge — omit for the default (app publishes 6530):
|
|
87
|
+
// proxy: "caddy:2-alpine", // simplest automatic HTTPS
|
|
88
|
+
// proxy: "traefik:v3.3", // label auto-discovery for --scale app=N
|
|
81
89
|
},
|
|
82
90
|
```
|
|
83
91
|
|
|
92
|
+
When both `store.sql` and `pgdog` are pinned, `DATABASE_URL` points at PgDog
|
|
93
|
+
(`6432`) — wire-protocol transparent to Bun.SQL / Drizzle. See
|
|
94
|
+
[Store · Multiple environments](/docs/elements/store#multiple-environments).
|
|
95
|
+
|
|
96
|
+
For `store.kv`, pin Redis (default), Valkey, or Dragonfly — driver id stays
|
|
97
|
+
`redis`, same `REDIS_URL`. Image table + licenses: [Store · KV](/docs/elements/store#kv).
|
|
98
|
+
|
|
84
99
|
For `ai`, the Ollama recipe pulls `OKE_AI_MODEL` (default `qwen3.5:9b` — a balanced local-dev starting point; override freely; on Apple Silicon prefer `qwen3.5:9b-mlx`) and writes `OKE_AI_URL`.
|
|
85
100
|
|
|
101
|
+
For `proxy`, pin **either** Caddy or Traefik — never both. Opt-in only; see
|
|
102
|
+
[Reverse proxy](/docs/deployment/reverse-proxy).
|
|
103
|
+
|
|
86
104
|
## i18n
|
|
87
105
|
|
|
88
106
|
| Option | Type | Default | Meaning |
|