okengine 0.6.1 → 0.8.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/AGENTS.md +2 -2
- package/README.md +73 -181
- package/package.json +3 -3
- package/site/content/docs/console/clock.mdx +25 -5
- package/site/content/docs/console/signals.mdx +13 -4
- package/site/content/docs/elements/ai.mdx +29 -2
- package/site/content/docs/elements/channel.mdx +42 -25
- package/site/content/docs/elements/clock.mdx +59 -25
- package/site/content/docs/elements/flow.mdx +8 -2
- package/site/content/docs/elements/gate.mdx +8 -1
- package/site/content/docs/elements/signal.mdx +173 -29
- package/site/content/docs/elements/store.mdx +595 -73
- package/site/content/docs/elements/vault.mdx +15 -5
- package/site/content/docs/get-started/basic-usage.mdx +13 -6
- package/site/content/docs/get-started/installation.mdx +52 -14
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/plugins/magic-link.mdx +42 -0
- package/site/content/docs/plugins/phone-number.mdx +78 -17
- package/site/content/docs/plugins/two-factor.mdx +1 -0
- package/site/content/docs/reference/cli.md +32 -2
- package/site/content/docs/reference/configuration.mdx +15 -13
- package/site/content/docs/reference/environment-variables.mdx +32 -18
- package/site/content/docs/reference/errors.mdx +12 -11
- package/site/content/docs/reference/fx.mdx +15 -13
- package/site/content/docs/reference/security.md +3 -1
- package/src/cli/ai-setup/ai-setup.test.ts +144 -0
- package/src/cli/ai-setup/apply.ts +214 -0
- package/src/cli/ai-setup/catalog.ts +263 -0
- package/src/cli/ai-setup/detect-ollama.ts +166 -0
- package/src/cli/ai-setup/index.ts +228 -0
- package/src/cli/ai-setup/prompts.ts +649 -0
- package/src/cli/ai-setup/recommend.test.ts +100 -0
- package/src/cli/ai-setup/recommend.ts +203 -0
- package/src/cli/ai.ts +38 -0
- package/src/cli/db-auto-push.test.ts +6 -1
- package/src/cli/db-auto-push.ts +8 -3
- 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-db-push.test.ts +59 -0
- package/src/cli/dev.test.ts +40 -0
- package/src/cli/dev.ts +55 -1
- package/src/cli/docker-clean.test.ts +179 -0
- package/src/cli/docker-clean.ts +406 -0
- package/src/cli/docker.ts +16 -4
- package/src/cli/ensure-drizzle-config.ts +1 -1
- package/src/cli/hero-meta.test.ts +6 -5
- package/src/cli/hero-meta.ts +7 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +22 -0
- package/src/cli/load-config.ts +7 -2
- package/src/cli/openbao-bootstrap.test.ts +1 -0
- package/src/cli/openbao-bootstrap.ts +9 -2
- package/src/cli/registry.ts +116 -2
- package/src/client/live-gap.test.ts +35 -0
- package/src/client/transport.test.ts +21 -0
- package/src/client/transport.ts +27 -5
- package/src/compiler/effects-infer.ts +1 -0
- package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
- package/src/config/define-config.test.ts +6 -6
- package/src/config/index.ts +5 -1
- package/src/console/server/console.test.ts +45 -0
- package/src/console/server/flows.ts +39 -9
- package/src/console/server/i18n.ts +41 -0
- package/src/console/server/vault.ts +12 -38
- package/src/console/ui/dist/assets/index-ClX0ZCe5.js +10 -0
- package/src/console/ui/dist/assets/{panel-access-BGv45snf.js → panel-access-ri1X9Otj.js} +1 -1
- package/src/console/ui/dist/assets/{panel-ai-B2S7LEii.js → panel-ai-DLHjRoJk.js} +1 -1
- package/src/console/ui/dist/assets/{panel-architecture-D7UJh91v.js → panel-architecture-DI3bQF3J.js} +1 -1
- package/src/console/ui/dist/assets/{panel-channels-9T3ybqRu.js → panel-channels-BDYWX29p.js} +1 -1
- package/src/console/ui/dist/assets/{panel-clock-Cb1UXGRQ.js → panel-clock-i-JpTjVB.js} +1 -1
- package/src/console/ui/dist/assets/{panel-diff-DmYbKWmN.js → panel-diff-CFuWxiXy.js} +1 -1
- package/src/console/ui/dist/assets/{panel-flows-PiHwT55z.js → panel-flows-B8TRhcoe.js} +1 -1
- package/src/console/ui/dist/assets/{panel-gates-BQGYXvjT.js → panel-gates-CfkBgco7.js} +1 -1
- package/src/console/ui/dist/assets/{panel-overview-BBnRO18l.js → panel-overview-DONDzBd2.js} +1 -1
- package/src/console/ui/dist/assets/{panel-plugins-D0PsmVw2.js → panel-plugins-DDJwTehL.js} +1 -1
- package/src/console/ui/dist/assets/{panel-runs-CWuRDe0r.js → panel-runs-D5zf-D9c.js} +1 -1
- package/src/console/ui/dist/assets/{panel-signals-Bbg4ewpP.js → panel-signals-Dsxdu_AR.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-CPCbsDRa.js → panel-store-hy7O8HOs.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-DVAzuA_S.js → panel-traces-CBiAe5go.js} +1 -1
- package/src/console/ui/dist/assets/{panel-vault-D1_MvOmo.js → panel-vault-Za1GyJfM.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/shell/components/ui.tsx +5 -1
- package/src/console/ui/shell/setup/Wizard.tsx +26 -5
- package/src/docker/cleanup.test.ts +193 -0
- package/src/docker/cleanup.ts +355 -0
- package/src/docker/docker.test.ts +4 -0
- package/src/docker/index.ts +19 -0
- package/src/docker/recipes/ollama.ts +10 -5
- package/src/drivers/ai-ollama.ts +45 -2
- package/src/drivers/ai-providers.test.ts +31 -0
- package/src/drivers/channel-sently.test.ts +8 -0
- package/src/drivers/channel-taqnyat-mail.ts +34 -0
- package/src/drivers/channel-types.ts +71 -0
- package/src/drivers/clock-postgres.test.ts +258 -0
- package/src/drivers/clock-postgres.ts +410 -0
- package/src/drivers/index.ts +21 -0
- package/src/drivers/journal-postgres.test.ts +175 -0
- package/src/drivers/journal-postgres.ts +492 -0
- package/src/drivers/signal-engine.ts +98 -25
- package/src/drivers/signal-nats.ts +4 -4
- package/src/drivers/signal-postgres.ts +158 -64
- package/src/drivers/signal-redis.ts +4 -4
- package/src/drivers/signal-types.ts +61 -2
- package/src/drivers/vault-driver-removal.test.ts +5 -1
- package/src/elements/channel/runtime.ts +51 -0
- package/src/elements/channel.test.ts +71 -0
- package/src/elements/clock/chaos-child.ts +389 -0
- package/src/elements/clock/chaos.test.ts +204 -0
- package/src/elements/clock/durable.ts +7 -0
- package/src/elements/clock/reconcile.ts +110 -0
- package/src/elements/clock/runtime.ts +13 -0
- package/src/elements/clock.test.ts +164 -2
- package/src/elements/clock.ts +2 -1
- package/src/elements/gate/runtime.ts +6 -0
- package/src/elements/signal/chaos-child.ts +41 -4
- package/src/elements/signal/declare.ts +4 -1
- package/src/elements/signal/delivery-modes.test.ts +179 -0
- package/src/elements/signal/key-ordering.test.ts +306 -0
- package/src/elements/signal/lease-reclaim.test.ts +123 -0
- package/src/elements/signal/optional-emit.test.ts +76 -0
- package/src/elements/signal/order-lifecycle.test.ts +130 -0
- package/src/elements/signal/orphan-messages.test.ts +152 -0
- package/src/elements/signal/runtime.ts +11 -4
- package/src/elements/signal/schema-emit.test.ts +107 -0
- package/src/elements/signal.test.ts +89 -5
- package/src/elements/store/files-fx.ts +104 -0
- package/src/elements/store/files-image.test.ts +251 -0
- package/src/elements/store/files-image.ts +494 -0
- package/src/elements/store/runtime.ts +60 -0
- 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 +29 -0
- package/src/elements/vault/boot-chain.ts +150 -0
- package/src/elements/vault/runtime.ts +8 -0
- package/src/index.ts +15 -0
- package/src/kernel/app.ts +168 -15
- package/src/kernel/boot-bind/channel.test.ts +16 -0
- package/src/kernel/boot-bind/channel.ts +13 -0
- package/src/kernel/boot-bind/clock.ts +63 -7
- package/src/kernel/boot-bind/gate.ts +71 -3
- package/src/kernel/boot-bind/honor-config.test.ts +348 -0
- package/src/kernel/boot-bind/journal.ts +89 -0
- package/src/kernel/boot-bind/signal.ts +64 -7
- package/src/kernel/boot-bind/vault.ts +27 -8
- package/src/kernel/boot.test.ts +6 -4
- package/src/kernel/boot.ts +63 -15
- package/src/kernel/concurrency.ts +1 -1
- package/src/kernel/errors.ts +8 -2
- package/src/kernel/fx.test.ts +19 -0
- package/src/kernel/fx.ts +153 -9
- package/src/kernel/index.ts +6 -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/plugins/auth-methods.security.test.ts +10 -7
- package/src/plugins/phone-number.ts +67 -10
- package/src/plugins/taqnyat.live.test.ts +174 -0
- package/src/runtime/dev-request-log.test.ts +20 -1
- package/src/runtime/dev-request-log.ts +38 -0
- package/src/term.test.ts +15 -0
- package/src/term.ts +8 -3
- package/src/console/ui/dist/assets/index-CjxwRGVv.js +0 -10
|
@@ -83,6 +83,14 @@ still reaches `fx.log` is scrubbed by the boot redactor as before.
|
|
|
83
83
|
|
|
84
84
|
That's the whole loop: declare → set → read. Everything below is what OKE guarantees on top of it.
|
|
85
85
|
|
|
86
|
+
## Redacted until you reveal
|
|
87
|
+
|
|
88
|
+
<VaultRedacted />
|
|
89
|
+
|
|
90
|
+
Pass the wrapper freely — `fx.log`, `String()`, and `JSON.stringify` all yield
|
|
91
|
+
`[redacted]`. Call `.reveal()` once at the provider boundary that needs the
|
|
92
|
+
credential.
|
|
93
|
+
|
|
86
94
|
## Two kinds of contracts
|
|
87
95
|
|
|
88
96
|
| Declaration | Sensitive? | Console shows | Use for |
|
|
@@ -143,17 +151,19 @@ In docker mode, `oke vault set` writes straight to the running OpenBao — no gi
|
|
|
143
151
|
The **Console** (`:6533`) can also set and rotate values, but it is **write-only**: it shows a salted fingerprint (`sha256:…`) per secret, never the cleartext. When you rotate a key there, the panel shows the **blast radius** — which in-flight durable runs will wake up with the new value.
|
|
144
152
|
|
|
145
153
|
<Callout title="Secrets never reach logs">
|
|
146
|
-
|
|
147
|
-
|
|
154
|
+
`fx.vault` returns `Redacted` — `fx.log` shows `[redacted]`. A revealed string that still reaches
|
|
155
|
+
a log is scrubbed by the boot redactor to `[redacted:secret]`. Traces and the Console get
|
|
156
|
+
fingerprints, never cleartext.
|
|
148
157
|
</Callout>
|
|
149
158
|
|
|
150
159
|
## Per-environment drivers
|
|
151
160
|
|
|
152
|
-
Which backend holds your values depends on the mode — configured once in `oke.config.ts
|
|
161
|
+
Which backend holds your values depends on the mode — configured once in `oke.config.ts`.
|
|
162
|
+
App boot and the Console share the same resolution-chain builder (`env` layers → backend):
|
|
153
163
|
|
|
154
164
|
```typescript title="oke.config.ts"
|
|
155
165
|
vault: {
|
|
156
|
-
local: "
|
|
166
|
+
local: "env", // .env.local — zero ceremony
|
|
157
167
|
docker: "openbao", // real vault in the compose stack
|
|
158
168
|
test: "memory", // seeded map — deterministic tests
|
|
159
169
|
prod: "openbao", // real vault in production
|
|
@@ -162,7 +172,7 @@ vault: {
|
|
|
162
172
|
|
|
163
173
|
| Driver | Backend | Best for |
|
|
164
174
|
| --------- | --------------- | ------------------------------------------ |
|
|
165
|
-
| `
|
|
175
|
+
| `env` | `.env.local` | Local loop — no infra at all |
|
|
166
176
|
| `openbao` | OpenBao (KV v2) | Docker + prod — durable, access-controlled |
|
|
167
177
|
| `memory` | in-process map | Tests |
|
|
168
178
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Basic Usage
|
|
3
|
-
description: Scaffold the standard starter, call
|
|
4
|
-
source: packages/create-oke/
|
|
3
|
+
description: Scaffold the standard Notes starter, call health and notes Flows, and inspect them in the Console.
|
|
4
|
+
source: packages/create-oke/templates/standard
|
|
5
5
|
icon: SquareTerminal
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
Write your first Flow, call it from a typed client, and prove it with
|
|
9
|
-
`bun:test` — using the same standard starter `create-oke` scaffolds.
|
|
9
|
+
`bun:test` — using the same standard Notes starter `create-oke` scaffolds.
|
|
10
10
|
|
|
11
11
|
<Callout title="The one rule">
|
|
12
12
|
All reads, writes, emits, sends, and external calls belong behind `fx`. That is what makes effects
|
|
@@ -26,6 +26,9 @@ cd my-app
|
|
|
26
26
|
oke dev
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
Use `--template advanced` for the same Notes domain with files attach, a daily
|
|
30
|
+
digest Clock, and an AI summarize stub.
|
|
31
|
+
|
|
29
32
|
<Surfaces />
|
|
30
33
|
|
|
31
34
|
</Step>
|
|
@@ -33,7 +36,7 @@ oke dev
|
|
|
33
36
|
<Step>
|
|
34
37
|
### Read the health Flow
|
|
35
38
|
|
|
36
|
-
The standard starter already exports a health Flow:
|
|
39
|
+
The standard starter already exports a named health Flow:
|
|
37
40
|
|
|
38
41
|
```typescript title="flows/main"
|
|
39
42
|
import { on, flow, http, gate } from "okengine";
|
|
@@ -42,13 +45,16 @@ import { z } from "zod";
|
|
|
42
45
|
export const health = on(
|
|
43
46
|
http.get("/health").gate(gate.public),
|
|
44
47
|
flow({
|
|
48
|
+
name: "main.health",
|
|
49
|
+
unit: "main",
|
|
45
50
|
out: z.object({ ok: z.literal(true) }),
|
|
46
51
|
do: () => ({ ok: true as const }),
|
|
47
52
|
}),
|
|
48
53
|
);
|
|
49
54
|
```
|
|
50
55
|
|
|
51
|
-
|
|
56
|
+
Named flows show up as `main.health` (not `flow_1`) in `oke dev` logs.
|
|
57
|
+
Change the route or output and save — App and Console update together from the
|
|
52
58
|
same Manifest.
|
|
53
59
|
|
|
54
60
|
</Step>
|
|
@@ -61,8 +67,9 @@ Adopted modules become namespaces on the client:
|
|
|
61
67
|
```typescript title="app"
|
|
62
68
|
import { oke } from "okengine";
|
|
63
69
|
import * as main from "./flows/main";
|
|
70
|
+
import * as notes from "./flows/notes";
|
|
64
71
|
|
|
65
|
-
export const app = oke({ name: "
|
|
72
|
+
export const app = oke({ name: "notes" }).adopt({ main, notes });
|
|
66
73
|
export type App = typeof app;
|
|
67
74
|
```
|
|
68
75
|
|
|
@@ -6,11 +6,11 @@ icon: Download
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
This page gets you from zero to a running app on Bun. The engine targets
|
|
9
|
-
**Bun ≥ 1.3** — prefer it for install, scaffold, and `oke dev`.
|
|
9
|
+
**Bun ≥ 1.3.14** — prefer it for install, scaffold, and `oke dev`.
|
|
10
10
|
|
|
11
11
|
<Callout title="The one rule">
|
|
12
|
-
|
|
13
|
-
the library API only.
|
|
12
|
+
The npm package is `okengine`; the CLI binary is `oke`. Use `bun add` in a project or `bun install
|
|
13
|
+
-g okengine` for a global `oke` on PATH. JSR is the library API only.
|
|
14
14
|
</Callout>
|
|
15
15
|
|
|
16
16
|
## Quick start
|
|
@@ -20,7 +20,7 @@ This page gets you from zero to a running app on Bun. The engine targets
|
|
|
20
20
|
<Step>
|
|
21
21
|
### Prerequisites
|
|
22
22
|
|
|
23
|
-
- [Bun](https://bun.sh) ≥ 1.3 (`bun --version`)
|
|
23
|
+
- [Bun](https://bun.sh) ≥ 1.3.14 (`bun --version`)
|
|
24
24
|
- A terminal and a code editor
|
|
25
25
|
|
|
26
26
|
</Step>
|
|
@@ -28,10 +28,18 @@ This page gets you from zero to a running app on Bun. The engine targets
|
|
|
28
28
|
<Step>
|
|
29
29
|
### Install the package
|
|
30
30
|
|
|
31
|
+
Project dependency (framework + local `oke`):
|
|
32
|
+
|
|
31
33
|
```bash title="Terminal"
|
|
32
34
|
bun add okengine
|
|
33
35
|
```
|
|
34
36
|
|
|
37
|
+
Global CLI (`oke` on PATH):
|
|
38
|
+
|
|
39
|
+
```bash title="Terminal"
|
|
40
|
+
bun install -g okengine
|
|
41
|
+
```
|
|
42
|
+
|
|
35
43
|
Library-only via JSR:
|
|
36
44
|
|
|
37
45
|
```bash title="Terminal"
|
|
@@ -46,12 +54,24 @@ bunx jsr add @omqkhafi/okengine
|
|
|
46
54
|
```bash title="Terminal"
|
|
47
55
|
bunx create-oke@latest my-app
|
|
48
56
|
bunx create-oke@latest my-app --template standard
|
|
57
|
+
bunx create-oke@latest my-app --template advanced
|
|
49
58
|
bunx create-oke@latest my-app --sql postgres
|
|
59
|
+
bunx create-oke@latest my-app --yes
|
|
50
60
|
```
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
Two Notes starters: **standard** (local-first) and **advanced** (docker-ready +
|
|
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`.
|
|
55
75
|
|
|
56
76
|
</Step>
|
|
57
77
|
|
|
@@ -63,12 +83,16 @@ cd my-app
|
|
|
63
83
|
oke dev
|
|
64
84
|
```
|
|
65
85
|
|
|
66
|
-
|
|
86
|
+
Four ports come up together (mnemonic: **O·K·E = 6·5·3**):
|
|
67
87
|
|
|
68
88
|
<Surfaces />
|
|
69
89
|
|
|
70
|
-
Open `http://localhost:6533`.
|
|
71
|
-
|
|
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.**
|
|
72
96
|
|
|
73
97
|
</Step>
|
|
74
98
|
|
|
@@ -103,8 +127,9 @@ Uses the `docker` driver profile in `oke.config.ts` (filled from `prod` when
|
|
|
103
127
|
omitted). Compose and credentials land under `docker/` (`.env.docker` beside
|
|
104
128
|
compose); host ports are unique per project.
|
|
105
129
|
|
|
106
|
-
Bare `oke dev`
|
|
107
|
-
to
|
|
130
|
+
Bare `oke dev` uses `.oke/mode` when present (seeded by create-oke). On a TTY
|
|
131
|
+
with no saved mode it prompts once and writes the file; non-TTY defaults to
|
|
132
|
+
`local`.
|
|
108
133
|
|
|
109
134
|
| Protocol | Env shape (generated) |
|
|
110
135
|
| ------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
@@ -122,8 +147,9 @@ controls are preserved when `oke dev --docker` regenerates the file.
|
|
|
122
147
|
<Accordions>
|
|
123
148
|
|
|
124
149
|
<Accordion title="oke: command not found after install">
|
|
125
|
-
|
|
126
|
-
invoke via `bunx oke dev
|
|
150
|
+
Install the npm package `okengine` (not JSR alone): `bun add okengine` in the project, or `bun
|
|
151
|
+
install -g okengine` for a global binary. Or invoke via `bunx oke dev`. There is no separate `oke`
|
|
152
|
+
package name — only `okengine`.
|
|
127
153
|
</Accordion>
|
|
128
154
|
|
|
129
155
|
<Accordion title="Console is empty / no flows listed">
|
|
@@ -131,11 +157,23 @@ controls are preserved when `oke dev --docker` regenerates the file.
|
|
|
131
157
|
boots and the Manifest is extracted — fix TypeScript errors in the terminal first.
|
|
132
158
|
</Accordion>
|
|
133
159
|
|
|
160
|
+
<Accordion title="Create first operator fails (password / claim code)">
|
|
161
|
+
Use a password with at least 12 characters, including a letter and a number. Copy the claim code
|
|
162
|
+
from the current `oke dev` boot log (it expires in 30 minutes; restart mint a new one). The form
|
|
163
|
+
and the request log now show the reason — not a bare `TransportError` / `500`.
|
|
164
|
+
</Accordion>
|
|
165
|
+
|
|
134
166
|
<Accordion title="Database out of sync after editing schema">
|
|
135
167
|
Local `oke dev` auto-pushes by default. If you passed `--no-db-push` or set `db.autoPush: false`,
|
|
136
168
|
run `oke db push` yourself, or re-enable auto-push.
|
|
137
169
|
</Accordion>
|
|
138
170
|
|
|
171
|
+
<Accordion title="Docker containers left after deleting a project">
|
|
172
|
+
Each project gets its own `oke-dev-<id>` compose stack; `oke dev` stop keeps volumes. Run
|
|
173
|
+
`oke docker clean` (TTY picker) or `oke docker clean --all --yes` from anywhere to remove
|
|
174
|
+
leftover containers, networks, and volumes.
|
|
175
|
+
</Accordion>
|
|
176
|
+
|
|
139
177
|
</Accordions>
|
|
140
178
|
|
|
141
179
|
## Learn more
|
|
@@ -60,7 +60,7 @@ on(db.table(users).changed("email"), reverify);
|
|
|
60
60
|
<Step>
|
|
61
61
|
### See what was derived
|
|
62
62
|
|
|
63
|
-
Run `oke dev`.
|
|
63
|
+
Run `oke dev`. Four surfaces come up together — nothing configured by hand in
|
|
64
64
|
a separate dashboard.
|
|
65
65
|
|
|
66
66
|
<Surfaces />
|
|
@@ -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>
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Phone number"
|
|
3
|
-
description: "Official plugin — E.164 phone OTP request and verify under /auth."
|
|
3
|
+
description: "Official plugin — E.164 phone OTP request and verify under /auth, with provider-managed delivery via Taqnyat Verify."
|
|
4
4
|
icon: "Smartphone"
|
|
5
5
|
source: "docs/spec/unified-theory.md"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
`phoneNumber()` signs people in with a phone number in E.164 form (`+15551234567`).
|
|
9
|
-
|
|
8
|
+
`phoneNumber()` signs people in with a phone number in E.164 form (`+15551234567`). Delivery
|
|
9
|
+
has two shapes: provider-managed OTP through Taqnyat Verify when an SMS driver is bound, or a
|
|
10
|
+
local hashed code (10 minutes, 5 attempts) you deliver yourself.
|
|
10
11
|
|
|
11
12
|
<Callout title="The one rule">
|
|
12
|
-
Enable `gate.auth`, then `.plug(phoneNumber())`.
|
|
13
|
-
|
|
13
|
+
Enable `gate.auth`, then `.plug(phoneNumber())`. For real SMS set `drivers.channel.sms` to
|
|
14
|
+
`taqnyat` — the plugin uses Taqnyat's Verify API, never a self-generated code. Use `exposeDevOtp`
|
|
15
|
+
only when no SMS driver is bound.
|
|
14
16
|
</Callout>
|
|
15
17
|
|
|
16
18
|
## Quick start
|
|
@@ -40,7 +42,8 @@ export const app = oke({
|
|
|
40
42
|
const { data, error } = await api.auth.requestPhoneOtp({ phone: "+15551234567" });
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
`POST /auth/phone/request`. Invalid E.164 → `AuthFailed` / `invalid_phone`.
|
|
45
|
+
`POST /auth/phone/request`. Invalid E.164 → `AuthFailed` / `invalid_phone`. Optional `lang:
|
|
46
|
+
"en" | "ar"` picks the OTP message language on the Taqnyat path.
|
|
44
47
|
|
|
45
48
|
</Step>
|
|
46
49
|
|
|
@@ -60,16 +63,51 @@ const { data } = await api.auth.verifyPhoneOtp({
|
|
|
60
63
|
|
|
61
64
|
</Steps>
|
|
62
65
|
|
|
66
|
+
## Delivery
|
|
67
|
+
|
|
68
|
+
| Bound SMS driver | Request behavior | Verify behavior |
|
|
69
|
+
| -------------------- | ---------------------------------------------------------- | ------------------------------ |
|
|
70
|
+
| `taqnyat` | Taqnyat Verify `sendOtp` (provider generates the code) | Taqnyat Verify `verifyOtp` |
|
|
71
|
+
| none | Local 6-digit code, hashed at rest (`exposeDevOtp` for DX) | Local hash compare, 5 attempts |
|
|
72
|
+
| any other SMS driver | Fails loudly — provider-managed OTP unsupported | — |
|
|
73
|
+
|
|
74
|
+
**Consequence:** the request response never contains `devOtp` on the Taqnyat path — the code
|
|
75
|
+
exists only inside the SMS Taqnyat sends.
|
|
76
|
+
|
|
77
|
+
### Taqnyat Verify
|
|
78
|
+
|
|
79
|
+
```typescript title="oke.config.ts"
|
|
80
|
+
export default {
|
|
81
|
+
drivers: {
|
|
82
|
+
channel: {
|
|
83
|
+
sms: { local: "console", docker: "taqnyat", test: "console", prod: "taqnyat" },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| Env | Meaning |
|
|
90
|
+
| ---------------------- | -------------------------------------------- |
|
|
91
|
+
| `TAQNYAT_BEARER_TOKEN` | Taqnyat bearer token (`TAQNYAT_TOKEN` alias) |
|
|
92
|
+
| `TAQNYAT_SENDER` | Pre-approved sender id |
|
|
93
|
+
|
|
94
|
+
Request stores a provider challenge (`taqnyat:<requestId>`, 10m TTL, 5 attempts) and calls
|
|
95
|
+
`fx.sendOtp`; verify replays the same `requestId` with the code through `fx.verifyOtp`. Both
|
|
96
|
+
are capability-gated `send` effects — dry-run records them without contacting Taqnyat.
|
|
97
|
+
|
|
98
|
+
An SMS driver without Verify support (for example a generic SMS sender) makes the request fail
|
|
99
|
+
with a clear error: use `exposeDevOtp` locally or switch `drivers.channel.sms` to `taqnyat`.
|
|
100
|
+
|
|
63
101
|
## Options
|
|
64
102
|
|
|
65
|
-
| Option | Type | Default | Meaning
|
|
66
|
-
| --------------- | ------------------- | -------- |
|
|
67
|
-
| `secret` | `string` | active\* | HMAC secret (\*from `gate.auth`)
|
|
68
|
-
| `sessions` | `SessionStore` | active\* | Session store
|
|
69
|
-
| `ttlMs` | `number` | 10m | Challenge lifetime
|
|
70
|
-
| `exposeDevOtp` | `boolean` | `false` | Include raw OTP in the request response |
|
|
71
|
-
| `phones` | `PhoneStore` | new | Phone → userId map
|
|
72
|
-
| `verifications` | `VerificationStore` | new | Challenge store
|
|
103
|
+
| Option | Type | Default | Meaning |
|
|
104
|
+
| --------------- | ------------------- | -------- | --------------------------------------------------------- |
|
|
105
|
+
| `secret` | `string` | active\* | HMAC secret (\*from `gate.auth`) |
|
|
106
|
+
| `sessions` | `SessionStore` | active\* | Session store |
|
|
107
|
+
| `ttlMs` | `number` | 10m | Challenge lifetime |
|
|
108
|
+
| `exposeDevOtp` | `boolean` | `false` | Include raw OTP in the request response (local path only) |
|
|
109
|
+
| `phones` | `PhoneStore` | new | Phone → userId map |
|
|
110
|
+
| `verifications` | `VerificationStore` | new | Challenge store |
|
|
73
111
|
|
|
74
112
|
## Surfaces
|
|
75
113
|
|
|
@@ -80,6 +118,21 @@ const { data } = await api.auth.verifyPhoneOtp({
|
|
|
80
118
|
|
|
81
119
|
E.164 pattern: `^\+[1-9]\d{7,14}$`.
|
|
82
120
|
|
|
121
|
+
## Live tests (opt-in)
|
|
122
|
+
|
|
123
|
+
The Taqnyat live suite sends real SMS and burns real quota, so it is double-gated: it runs only when
|
|
124
|
+
`OKE_SMS_LIVE=1` **and** the real credentials (`TAQNYAT_TOKEN`, `TAQNYAT_SENDER`, plus
|
|
125
|
+
`OKE_TEST_TAQNYAT_PHONE` — `TAQNYAT_TO` also accepted) are all present. Credentials alone never send.
|
|
126
|
+
|
|
127
|
+
The suite sends exactly one SMS and never asks for the code.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
OKE_SMS_LIVE=1 TAQNYAT_TOKEN=… TAQNYAT_SENDER=YourBrand \
|
|
131
|
+
OKE_TEST_TAQNYAT_PHONE=+9665xxxxxxxx bun test src/plugins
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Without the flag the suite skips visibly — never a silent pass.
|
|
135
|
+
|
|
83
136
|
## Troubleshooting
|
|
84
137
|
|
|
85
138
|
<Accordions>
|
|
@@ -88,10 +141,18 @@ E.164 pattern: `^\+[1-9]\d{7,14}$`.
|
|
|
88
141
|
Number must start with `+` and a non-zero country digit, then 7–14 more digits. No spaces or
|
|
89
142
|
dashes.
|
|
90
143
|
|
|
144
|
+
</Accordion>
|
|
145
|
+
<Accordion title="SMS driver does not support provider-managed OTP">
|
|
146
|
+
|
|
147
|
+
The configured `drivers.channel.sms` driver has no Verify API. Either switch to `taqnyat` or
|
|
148
|
+
remove the SMS driver so the local hashed path (`exposeDevOtp`) is used.
|
|
149
|
+
|
|
91
150
|
</Accordion>
|
|
92
151
|
<Accordion title="No SMS arrived">
|
|
93
152
|
|
|
94
|
-
|
|
153
|
+
On the local path no SMS is sent by design — use `exposeDevOtp`, or bind `taqnyat` with valid
|
|
154
|
+
`TAQNYAT_*` credentials. On the Taqnyat path check the sender id is pre-approved and the
|
|
155
|
+
number is E.164.
|
|
95
156
|
|
|
96
157
|
</Accordion>
|
|
97
158
|
</Accordions>
|
|
@@ -99,8 +160,8 @@ v1 does not send SMS. Use `exposeDevOtp` in local/test, or deliver via your own
|
|
|
99
160
|
## Learn more
|
|
100
161
|
|
|
101
162
|
- [Email OTP](/docs/plugins/email-otp) — email variant of the same OTP rules
|
|
102
|
-
- [
|
|
103
|
-
- [Channel](/docs/elements/channel) —
|
|
163
|
+
- [Magic link](/docs/plugins/magic-link) — one-time email link sign-in
|
|
164
|
+
- [Channel](/docs/elements/channel) — SMS / email drivers and delivery
|
|
104
165
|
|
|
105
166
|
## Next
|
|
106
167
|
|
|
@@ -107,6 +107,7 @@ 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 [Phone number](/docs/plugins/phone-number) / [Magic link](/docs/plugins/magic-link)
|
|
110
111
|
|
|
111
112
|
## Next
|
|
112
113
|
|
|
@@ -8,13 +8,15 @@ source: "docs/cli.md"
|
|
|
8
8
|
### Commands
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
bun add okengine # ONE package
|
|
11
|
+
bun add okengine # ONE package (project)
|
|
12
|
+
bun install -g okengine # global `oke` on PATH
|
|
12
13
|
|
|
13
14
|
oke dev # watch · hot reload · Console :6533 · app :6530 · MCP :6535 · docs MCP :6536
|
|
14
15
|
# → client types + domain schema push on save (local)
|
|
15
16
|
oke dev --local # -l session-only local (in-memory; never writes .oke/mode)
|
|
16
|
-
oke dev --no-db-push # opt out of auto oke db push on schema
|
|
17
|
+
oke dev --no-db-push # opt out of auto oke db push on schema input save
|
|
17
18
|
oke dev --docker # -d infra compose under docker/ (no app container; host Bun)
|
|
19
|
+
# Ctrl-C / close terminal → docker compose stop (volumes kept)
|
|
18
20
|
oke dev -d store.sql,signal # partial: only these roles get real backends
|
|
19
21
|
oke mode local|docker # get/set default oke dev mode (saved in .oke/mode)
|
|
20
22
|
|
|
@@ -26,15 +28,21 @@ oke schema generate # core + plugin stubs → schema/oke.ts (--ch
|
|
|
26
28
|
oke db push # domain schema.ts → live local DB (dev; drizzle-kit)
|
|
27
29
|
oke db generate # versioned SQL under drizzle/ (review)
|
|
28
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
|
|
29
33
|
oke vault set STRIPE_KEY # also: list · import .env · key rotate
|
|
30
34
|
oke client add <url> # types for a separate frontend repo
|
|
31
35
|
|
|
32
36
|
oke docker # docker/Dockerfile + docker/compose.<role>.yml · …
|
|
33
37
|
oke docker --prod # healthchecks, volumes, limits, secret refs, deploy.replicas
|
|
38
|
+
oke docker clean # TTY: pick oke-dev-* stacks → down -v (containers, networks, volumes)
|
|
39
|
+
oke docker clean --yes # non-TTY: current project stack only
|
|
40
|
+
oke docker clean --all --yes # non-TTY: every oke-dev-* stack on this machine
|
|
34
41
|
oke images pin # tags → digests in oke.images.lock
|
|
35
42
|
|
|
36
43
|
oke build --target edge # < 15 kB kernel profile
|
|
37
44
|
oke eval # run prompt eval sets; fails CI on regression
|
|
45
|
+
oke ai setup # configure AI driver + models (TTY wizard or flags)
|
|
38
46
|
oke branch prod --at "yesterday" # fork journaled state into a sandbox
|
|
39
47
|
oke privacy erase --subject <id> # crypto-shredding: deletes the key, not the terabytes
|
|
40
48
|
oke upgrade # run codemods for a breaking change, print the diff
|
|
@@ -44,6 +52,24 @@ In development, app, Console, MCP, and docs MCP prefer the canonical ports and m
|
|
|
44
52
|
when occupied. Docker infrastructure uses stable per-project offsets in disjoint ranges for
|
|
45
53
|
each built-in service, preventing one service's offset port from overlapping another's.
|
|
46
54
|
|
|
55
|
+
### create-oke
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
bunx create-oke@latest my-app # TTY: template → recommended / reuse / customize
|
|
59
|
+
bunx create-oke@latest my-app --template advanced
|
|
60
|
+
bunx create-oke@latest my-app --yes # non-interactive recommended defaults
|
|
61
|
+
bunx create-oke@latest my-app --sql postgres
|
|
62
|
+
bunx create-oke@latest my-app --ai # AI model wizard before install
|
|
63
|
+
bunx create-oke@latest my-app --no-ai
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
On a TTY: pick **standard** or **advanced**, then recommended defaults or
|
|
67
|
+
customize. Customize asks **local** or **docker** first, walks facets for that
|
|
68
|
+
side only, then whether to customize the other (defaults if no). Writes
|
|
69
|
+
user-global `~/.oke/create-defaults.json` (reuse only when `template` matches).
|
|
70
|
+
Scaffold writes `.oke/mode` from the primary side. Non-TTY / `--yes` / explicit
|
|
71
|
+
`--template` never prompt.
|
|
72
|
+
|
|
47
73
|
### Additional commands
|
|
48
74
|
|
|
49
75
|
```bash
|
|
@@ -52,6 +78,8 @@ oke doctor --json # -j JSON on stdout; hints on stderr (agents /
|
|
|
52
78
|
oke stack --json # -j
|
|
53
79
|
oke images list # recipe · image · tag · digest · size (--json|-j)
|
|
54
80
|
oke gates list # Module:Action catalogue (--json|-j)
|
|
81
|
+
oke ai setup --provider ollama --yes # non-interactive AI config
|
|
82
|
+
# TTY Ollama: needs quiz → RAM-tier panel → recommend / installed / manual (+ ← Back)
|
|
55
83
|
```
|
|
56
84
|
|
|
57
85
|
### Shell completion
|
|
@@ -74,6 +102,8 @@ Long form is canonical in docs; short form is convenience only. Shared letters f
|
|
|
74
102
|
| `--docker` | `-d` | `dev` |
|
|
75
103
|
| `--no-db-push` | | `dev` |
|
|
76
104
|
| `--prod` | `-p` | `docker` |
|
|
105
|
+
| `--yes` | `-y` | `docker clean` |
|
|
106
|
+
| `--all` | | `docker clean` |
|
|
77
107
|
| `--port` | `-p` | `start` |
|
|
78
108
|
| `--check` | `-c` | `schema generate` |
|
|
79
109
|
| `--config` | `-c` | `stack`, `docker`, `images`, `db *` (drizzle) |
|
|
@@ -25,9 +25,10 @@ drivers: {
|
|
|
25
25
|
kv: { local: "memory", docker: "redis", test: "memory", prod: "redis" },
|
|
26
26
|
files: { local: "fs", docker: "s3", test: "memory", prod: "s3" },
|
|
27
27
|
},
|
|
28
|
-
signal: { local: "memory", docker: "
|
|
28
|
+
signal: { local: "memory", docker: "redis", test: "memory", prod: "redis" },
|
|
29
29
|
clock: { local: "memory", docker: "postgres", test: "frozen", prod: "postgres" },
|
|
30
|
-
|
|
30
|
+
journal: { local: "memory", docker: "postgres", test: "memory", prod: "postgres" },
|
|
31
|
+
vault: { local: "env", docker: "openbao", test: "memory", prod: "openbao" },
|
|
31
32
|
channel: {
|
|
32
33
|
email: { local: "console", docker: "smtp", test: "console", prod: "smtp" },
|
|
33
34
|
sms: { local: "console", docker: "taqnyat", test: "console", prod: "taqnyat" },
|
|
@@ -41,10 +42,11 @@ drivers: {
|
|
|
41
42
|
| `store.kv` | env driver map | `memory` · `redis` |
|
|
42
43
|
| `store.files` | env driver map | `memory` · `fs` · `s3` |
|
|
43
44
|
| `store.index` | env driver map | `memory` · `pgvector` · `libsql` · `meilisearch` |
|
|
44
|
-
| `signal` | env driver map | `memory` · `
|
|
45
|
-
| `clock` | env driver map | `memory` · `postgres` · `frozen`
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
45
|
+
| `signal` | env driver map | `memory` · `redis` (boot); `postgres` · `nats` fail loud until clients bind |
|
|
46
|
+
| `clock` | env driver map | `memory` · `postgres` · `file` · `frozen` |
|
|
47
|
+
| `journal` | env driver map | `memory` · `file` · `postgres` |
|
|
48
|
+
| `vault` | env driver map | `env` · `openbao` · `memory` · `managed` |
|
|
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` |
|
|
@@ -128,13 +130,13 @@ Runs retention and redaction at the config surface — **not** the same key as `
|
|
|
128
130
|
|
|
129
131
|
Domain schema sync for `oke db push | generate | migrate` (Drizzle). Unrelated to `oke schema generate`.
|
|
130
132
|
|
|
131
|
-
| Option | Default | Meaning
|
|
132
|
-
| ----------- | --------------------------- |
|
|
133
|
-
| `autoPush` | `true` (local only) | Auto-run `db push`
|
|
134
|
-
| `config` | `"drizzle.config.ts"` | Path to the drizzle-kit config
|
|
135
|
-
| `declare` | `"src/schema.decl.ts"` | Abstract schema module (`store.schema.table` exports)
|
|
136
|
-
| `generated` | `"src/schema.generated.ts"` | Where `oke db` emits dialect Drizzle
|
|
137
|
-
| `entry` | `src/app.ts` | App entry for collecting plugin table contributions
|
|
133
|
+
| Option | Default | Meaning |
|
|
134
|
+
| ----------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
135
|
+
| `autoPush` | `true` (local only) | Auto-run `db push` when schema inputs change in `oke dev` (not on emit of `generated`); forced off in docker/prod |
|
|
136
|
+
| `config` | `"drizzle.config.ts"` | Path to the drizzle-kit config |
|
|
137
|
+
| `declare` | `"src/schema.decl.ts"` | Abstract schema module (`store.schema.table` exports) |
|
|
138
|
+
| `generated` | `"src/schema.generated.ts"` | Where `oke db` emits dialect Drizzle |
|
|
139
|
+
| `entry` | `src/app.ts` | App entry for collecting plugin table contributions |
|
|
138
140
|
|
|
139
141
|
## topology
|
|
140
142
|
|