okengine 0.6.0 → 0.6.1
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 +148 -13
- package/package.json +3 -3
- package/site/content/docs/elements/channel.mdx +71 -7
- package/site/content/docs/reference/configuration.mdx +5 -4
- package/site/content/docs/reference/environment-variables.mdx +32 -8
- package/src/cli/openbao-restart.integration.test.ts +106 -97
- package/src/docker/dockerfile.integration.test.ts +126 -119
- package/src/docker/stack.integration.test.ts +118 -102
- package/src/drivers/ai-ollama-tools.integration.test.ts +8 -6
- package/src/drivers/ai-ollama.integration.test.ts +3 -19
- package/src/drivers/channel-fcm.ts +49 -53
- package/src/drivers/channel-msegat.ts +61 -0
- package/src/drivers/channel-sently-map.ts +57 -0
- package/src/drivers/channel-sently.test.ts +99 -0
- package/src/drivers/channel-sndr.ts +28 -0
- package/src/drivers/channel-taqnyat.ts +57 -0
- package/src/drivers/channel-types.ts +79 -2
- package/src/drivers/channel-unifonic.ts +26 -43
- package/src/drivers/channel-wa-cloud.ts +33 -47
- package/src/drivers/channel-webpush.ts +39 -239
- package/src/drivers/index.ts +4 -0
- package/src/elements/channel/costs.test.ts +2 -2
- package/src/elements/channel/costs.ts +14 -2
- package/src/elements/channel/mime.ts +11 -0
- package/src/elements/channel/runtime.ts +94 -0
- package/src/elements/channel/sndr-webhooks.test.ts +26 -0
- package/src/elements/channel.ts +10 -1
- package/src/elements/index.ts +9 -0
- package/src/kernel/boot-bind/channel.test.ts +68 -3
- package/src/kernel/boot-bind/channel.ts +93 -2
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +10 -4
- package/src/release/exports.test.ts +26 -0
- package/src/release/exports.ts +64 -5
- package/src/release/index.ts +5 -0
- package/src/release/measure.exports.test.ts +13 -1
- package/src/release/measure.ts +76 -13
- package/src/release/official-plugins.ts +46 -0
- package/src/release/readme.test.ts +30 -2
package/README.md
CHANGED
|
@@ -1,21 +1,90 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<picture>
|
|
3
|
-
<source
|
|
4
|
-
|
|
3
|
+
<source
|
|
4
|
+
media="(prefers-color-scheme: dark)"
|
|
5
|
+
srcset="https://shieldcn.dev/header/grid.svg?title=okengine&subtitle=One+law.+Eight+elements.+Ten+exports.&logo=https://raw.githubusercontent.com/omqkhafi/okengine/main/site/public/logo/OKE-W.svg&theme=zinc&size=banner&mode=dark&font=geist&align=left"
|
|
6
|
+
/>
|
|
7
|
+
<img
|
|
8
|
+
alt="okengine — One law. Eight elements. Ten exports."
|
|
9
|
+
src="https://shieldcn.dev/header/grid.svg?title=okengine&subtitle=One+law.+Eight+elements.+Ten+exports.&logo=https://raw.githubusercontent.com/omqkhafi/okengine/main/site/public/logo/OKE-B.svg&theme=zinc&size=banner&mode=light&font=geist&align=left"
|
|
10
|
+
width="750"
|
|
11
|
+
/>
|
|
5
12
|
</picture>
|
|
6
13
|
</p>
|
|
7
14
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://www.npmjs.com/package/okengine">
|
|
17
|
+
<picture>
|
|
18
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/npm/okengine.svg?variant=outline&mode=dark&size=sm&theme=zinc&font=geist" />
|
|
19
|
+
<img alt="npm" src="https://shieldcn.dev/npm/okengine.svg?variant=outline&mode=light&size=sm&theme=zinc&font=geist" />
|
|
20
|
+
</picture>
|
|
21
|
+
</a>
|
|
22
|
+
<a href="https://jsr.io/@omqkhafi/okengine">
|
|
23
|
+
<picture>
|
|
24
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/jsr/@omqkhafi/okengine.svg?variant=outline&mode=dark&size=sm&theme=zinc&font=geist" />
|
|
25
|
+
<img alt="JSR" src="https://shieldcn.dev/jsr/@omqkhafi/okengine.svg?variant=outline&mode=light&size=sm&theme=zinc&font=geist" />
|
|
26
|
+
</picture>
|
|
27
|
+
</a>
|
|
28
|
+
<a href="https://www.npmjs.com/package/create-oke">
|
|
29
|
+
<picture>
|
|
30
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/npm/create-oke.svg?variant=outline&mode=dark&size=sm&theme=zinc&font=geist" />
|
|
31
|
+
<img alt="create-oke" src="https://shieldcn.dev/npm/create-oke.svg?variant=outline&mode=light&size=sm&theme=zinc&font=geist" />
|
|
32
|
+
</picture>
|
|
33
|
+
</a>
|
|
34
|
+
<a href="https://www.npmjs.com/package/okengine">
|
|
35
|
+
<picture>
|
|
36
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/npm/types/okengine.svg?variant=outline&mode=dark&size=sm&theme=zinc&font=geist" />
|
|
37
|
+
<img alt="types" src="https://shieldcn.dev/npm/types/okengine.svg?variant=outline&mode=light&size=sm&theme=zinc&font=geist" />
|
|
38
|
+
</picture>
|
|
39
|
+
</a>
|
|
40
|
+
<a href="https://opensource.org/licenses/MIT">
|
|
41
|
+
<picture>
|
|
42
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/github/license/omqkhafi/okengine.svg?variant=outline&mode=dark&size=sm&theme=zinc&font=geist" />
|
|
43
|
+
<img alt="MIT" src="https://shieldcn.dev/github/license/omqkhafi/okengine.svg?variant=outline&mode=light&size=sm&theme=zinc&font=geist" />
|
|
44
|
+
</picture>
|
|
45
|
+
</a>
|
|
46
|
+
<a href="https://bun.sh">
|
|
47
|
+
<picture>
|
|
48
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/badge/Bun-%3E%3D1.3.svg?logo=bun&variant=outline&mode=dark&size=sm&theme=zinc&font=geist" />
|
|
49
|
+
<img alt="Bun >=1.3" src="https://shieldcn.dev/badge/Bun-%3E%3D1.3.svg?logo=bun&variant=outline&mode=light&size=sm&theme=zinc&font=geist" />
|
|
50
|
+
</picture>
|
|
51
|
+
</a>
|
|
52
|
+
<a href="https://github.com/omqkhafi/okengine/releases">
|
|
53
|
+
<picture>
|
|
54
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/github/release/omqkhafi/okengine.svg?variant=outline&mode=dark&size=sm&theme=zinc&font=geist" />
|
|
55
|
+
<img alt="release" src="https://shieldcn.dev/github/release/omqkhafi/okengine.svg?variant=outline&mode=light&size=sm&theme=zinc&font=geist" />
|
|
56
|
+
</picture>
|
|
57
|
+
</a>
|
|
58
|
+
<a href="https://github.com/omqkhafi/okengine/stargazers">
|
|
59
|
+
<picture>
|
|
60
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/github/stars/omqkhafi/okengine.svg?variant=outline&mode=dark&size=sm&theme=zinc&font=geist" />
|
|
61
|
+
<img alt="stars" src="https://shieldcn.dev/github/stars/omqkhafi/okengine.svg?variant=outline&mode=light&size=sm&theme=zinc&font=geist" />
|
|
62
|
+
</picture>
|
|
63
|
+
</a>
|
|
64
|
+
<a href="https://github.com/omqkhafi/okengine/actions">
|
|
65
|
+
<picture>
|
|
66
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/github/ci/omqkhafi/okengine.svg?variant=outline&mode=dark&size=sm&theme=zinc&font=geist" />
|
|
67
|
+
<img alt="CI" src="https://shieldcn.dev/github/ci/omqkhafi/okengine.svg?variant=outline&mode=light&size=sm&theme=zinc&font=geist" />
|
|
68
|
+
</picture>
|
|
69
|
+
</a>
|
|
70
|
+
<a href="https://github.com/omqkhafi/okengine/commits">
|
|
71
|
+
<picture>
|
|
72
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/github/last-commit/omqkhafi/okengine.svg?variant=outline&mode=dark&size=sm&theme=zinc&font=geist" />
|
|
73
|
+
<img alt="last commit" src="https://shieldcn.dev/github/last-commit/omqkhafi/okengine.svg?variant=outline&mode=light&size=sm&theme=zinc&font=geist" />
|
|
74
|
+
</picture>
|
|
75
|
+
</a>
|
|
76
|
+
</p>
|
|
11
77
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
[](https://opensource.org/licenses/MIT)
|
|
16
|
-
[](https://bun.sh)
|
|
78
|
+
<p align="center">
|
|
79
|
+
<em>Stop gluing APIs, jobs, and queues into one backend. One law collapses them — client, Console, and infra come free. Yours to host.</em>
|
|
80
|
+
</p>
|
|
17
81
|
|
|
18
|
-
|
|
82
|
+
<p align="center">
|
|
83
|
+
<a href="https://oke.omqkhafi.dev"><strong>Docs</strong></a> ·
|
|
84
|
+
<a href="https://oke.omqkhafi.dev/docs"><strong>Handbook</strong></a> ·
|
|
85
|
+
<code>oke</code> CLI ·
|
|
86
|
+
<a href="https://www.npmjs.com/package/create-oke"><code>create-oke</code></a>
|
|
87
|
+
</p>
|
|
19
88
|
|
|
20
89
|
> [!WARNING]
|
|
21
90
|
> **Early development (`v0.x`) — not production-ready.**
|
|
@@ -27,13 +96,13 @@ _"Stop gluing APIs, jobs, and queues into one backend. One law collapses them
|
|
|
27
96
|
```bash
|
|
28
97
|
bun add okengine # framework + `oke` CLI
|
|
29
98
|
bunx jsr add @omqkhafi/okengine # JSR — library API only
|
|
30
|
-
bunx create-oke@latest my-app
|
|
99
|
+
bunx create-oke@latest my-app # scaffold
|
|
31
100
|
```
|
|
32
101
|
|
|
33
102
|
## Quick start
|
|
34
103
|
|
|
35
104
|
```bash
|
|
36
|
-
bunx create-oke@latest my-app
|
|
105
|
+
bunx create-oke@latest my-app
|
|
37
106
|
cd my-app
|
|
38
107
|
oke dev
|
|
39
108
|
# or: oke mode docker && oke dev # compose infra · app on host Bun
|
|
@@ -102,6 +171,72 @@ export const remove = mounted.remove;
|
|
|
102
171
|
|
|
103
172
|
Full walkthrough: [Basic usage](https://oke.omqkhafi.dev/docs/get-started/basic-usage).
|
|
104
173
|
|
|
174
|
+
## Charts
|
|
175
|
+
|
|
176
|
+
<p align="center">
|
|
177
|
+
<a href="https://github.com/omqkhafi/okengine/issues">
|
|
178
|
+
<picture>
|
|
179
|
+
<source
|
|
180
|
+
media="(prefers-color-scheme: dark)"
|
|
181
|
+
srcset="https://shieldcn.dev/chart/github/issues/omqkhafi/okengine.svg?theme=zinc&font=geist&mode=dark"
|
|
182
|
+
/>
|
|
183
|
+
<img
|
|
184
|
+
alt="GitHub issues over time"
|
|
185
|
+
src="https://shieldcn.dev/chart/github/issues/omqkhafi/okengine.svg?theme=zinc&font=geist&mode=light"
|
|
186
|
+
width="750"
|
|
187
|
+
/>
|
|
188
|
+
</picture>
|
|
189
|
+
</a>
|
|
190
|
+
</p>
|
|
191
|
+
|
|
192
|
+
<p align="center">
|
|
193
|
+
<a href="https://github.com/omqkhafi">
|
|
194
|
+
<picture>
|
|
195
|
+
<source
|
|
196
|
+
media="(prefers-color-scheme: dark)"
|
|
197
|
+
srcset="https://shieldcn.dev/chart/github/commits/omqkhafi.svg?theme=zinc&font=geist&mode=dark"
|
|
198
|
+
/>
|
|
199
|
+
<img
|
|
200
|
+
alt="Lifetime commit history"
|
|
201
|
+
src="https://shieldcn.dev/chart/github/commits/omqkhafi.svg?theme=zinc&font=geist&mode=light"
|
|
202
|
+
width="750"
|
|
203
|
+
/>
|
|
204
|
+
</picture>
|
|
205
|
+
</a>
|
|
206
|
+
</p>
|
|
207
|
+
|
|
208
|
+
<p align="center">
|
|
209
|
+
<a href="https://www.npmjs.com/package/okengine">
|
|
210
|
+
<picture>
|
|
211
|
+
<source
|
|
212
|
+
media="(prefers-color-scheme: dark)"
|
|
213
|
+
srcset="https://shieldcn.dev/chart/npm/okengine.svg?theme=zinc&font=geist&mode=dark"
|
|
214
|
+
/>
|
|
215
|
+
<img
|
|
216
|
+
alt="okengine npm downloads"
|
|
217
|
+
src="https://shieldcn.dev/chart/npm/okengine.svg?theme=zinc&font=geist&mode=light"
|
|
218
|
+
width="750"
|
|
219
|
+
/>
|
|
220
|
+
</picture>
|
|
221
|
+
</a>
|
|
222
|
+
</p>
|
|
223
|
+
|
|
224
|
+
<p align="center">
|
|
225
|
+
<a href="https://www.npmjs.com/package/create-oke">
|
|
226
|
+
<picture>
|
|
227
|
+
<source
|
|
228
|
+
media="(prefers-color-scheme: dark)"
|
|
229
|
+
srcset="https://shieldcn.dev/chart/npm/create-oke.svg?theme=zinc&font=geist&mode=dark"
|
|
230
|
+
/>
|
|
231
|
+
<img
|
|
232
|
+
alt="create-oke npm downloads"
|
|
233
|
+
src="https://shieldcn.dev/chart/npm/create-oke.svg?theme=zinc&font=geist&mode=light"
|
|
234
|
+
width="750"
|
|
235
|
+
/>
|
|
236
|
+
</picture>
|
|
237
|
+
</a>
|
|
238
|
+
</p>
|
|
239
|
+
|
|
105
240
|
## Documentation
|
|
106
241
|
|
|
107
242
|
Everything beyond this page lives in the site handbook:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "okengine",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "One law. Eight elements. Ten exports. One package. One manifest. Every backend need is derived, never added.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"prepack": "bun run build",
|
|
59
59
|
"bench": "bun test ./src/runtime/cold-start.bench.ts src/client/budget.test.ts src/console/budget.test.ts src/kernel/budget.test.ts src/kernel/routing-budget.test.ts src/runs/bench.test.ts",
|
|
60
60
|
"budgets": "bun src/release/publish.ts",
|
|
61
|
-
"gate": "bun test src/cli/doc-staleness.test.ts src/cli/competitor-mention-removal.test.ts src/drivers/vault-driver-removal.test.ts src/kernel/errors.registry.test.ts src/upgrade/codemods.test.ts",
|
|
61
|
+
"gate": "PUBLISH_GATE=1 bun test src/cli/doc-staleness.test.ts src/cli/competitor-mention-removal.test.ts src/drivers/vault-driver-removal.test.ts src/kernel/errors.registry.test.ts src/upgrade/codemods.test.ts scripts/publish.gate.test.ts",
|
|
62
62
|
"dev": "bun run --cwd site dev",
|
|
63
63
|
"site:build": "bun run --cwd site build",
|
|
64
64
|
"ci": "bun scripts/ci.ts",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"ajv-formats": "^3.0.1",
|
|
74
74
|
"intl-messageformat": "^11.2.13",
|
|
75
75
|
"oxc-parser": "^0.142.0",
|
|
76
|
-
"sently": "
|
|
76
|
+
"sently": "1.0.0"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@base-ui/react": "^1.6.0",
|
|
@@ -109,23 +109,52 @@ Each send records its attempts — driver, ok/error, timestamp, message id — s
|
|
|
109
109
|
|
|
110
110
|
## Per-environment drivers
|
|
111
111
|
|
|
112
|
+
Boot opens the email driver from `drivers.channel.email`, and appends an SMS
|
|
113
|
+
driver when `drivers.channel.sms` is set (other than `console`). WhatsApp and
|
|
114
|
+
push drivers are passed via `BootOptions.channel.drivers` when you need them.
|
|
115
|
+
|
|
112
116
|
```typescript title="oke.config.ts"
|
|
113
117
|
drivers: {
|
|
114
118
|
channel: {
|
|
115
119
|
email: { local: "console", docker: "smtp", test: "console", prod: "smtp" },
|
|
120
|
+
// optional — omit until you need SMS at boot
|
|
121
|
+
sms: { local: "console", docker: "taqnyat", test: "console", prod: "taqnyat" },
|
|
116
122
|
},
|
|
117
123
|
},
|
|
118
124
|
images: {
|
|
119
|
-
"channel.email": "axllent/mailpit:v1.22.3",
|
|
125
|
+
"channel.email": "axllent/mailpit:v1.22.3",
|
|
120
126
|
},
|
|
121
127
|
```
|
|
122
128
|
|
|
123
|
-
| Driver | Medium
|
|
124
|
-
| ---------- |
|
|
125
|
-
| `console` | any
|
|
126
|
-
| `smtp` | email
|
|
127
|
-
| `resend` | email
|
|
128
|
-
| `
|
|
129
|
+
| Driver | Medium | Behavior |
|
|
130
|
+
| ---------- | -------- | ------------------------------------------------------------------- |
|
|
131
|
+
| `console` | any | Captures into a readable inbox — local + tests |
|
|
132
|
+
| `smtp` | email | Real SMTP — Mailpit in docker, your provider in prod |
|
|
133
|
+
| `resend` | email | Resend HTTP API (`RESEND_API_KEY`) |
|
|
134
|
+
| `sndr` | email | SNDR HTTP API (`SNDR_API_KEY`) |
|
|
135
|
+
| `taqnyat` | sms | Taqnyat SMS (`TAQNYAT_BEARER_TOKEN` + `TAQNYAT_SENDER`) |
|
|
136
|
+
| `msegat` | sms | Msegat SMS (`MSEGAT_USERNAME` + `MSEGAT_API_KEY` + `MSEGAT_SENDER`) |
|
|
137
|
+
| `unifonic` | sms | Unifonic el.cloud SMS (`UNIFONIC_APPSID`) |
|
|
138
|
+
| `wa-cloud` | whatsapp | Meta WhatsApp Cloud API |
|
|
139
|
+
| `webpush` | push | Web Push + VAPID (`createPushSender` → `WebPushTransport`) |
|
|
140
|
+
| `fcm` | push | Firebase Cloud Messaging HTTP v1 |
|
|
141
|
+
|
|
142
|
+
Email fallback uses ordered email transports; SMS with multiple SMS drivers uses
|
|
143
|
+
the same failover path. Credentials for boot-opened drivers are listed under
|
|
144
|
+
[Environment Variables](/docs/reference/environment-variables).
|
|
145
|
+
|
|
146
|
+
`webpush` needs VAPID keys — open it yourself and include it in
|
|
147
|
+
`BootOptions.channel.drivers` (boot does not open push from env):
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { openWebPushChannel } from "okengine/drivers/channel-webpush";
|
|
151
|
+
|
|
152
|
+
openWebPushChannel({
|
|
153
|
+
vapidPublicKey: process.env.VAPID_PUBLIC_KEY!,
|
|
154
|
+
vapidPrivateKey: process.env.VAPID_PRIVATE_KEY!,
|
|
155
|
+
vapidSubject: process.env.VAPID_SUBJECT,
|
|
156
|
+
});
|
|
157
|
+
```
|
|
129
158
|
|
|
130
159
|
## Troubleshooting
|
|
131
160
|
|
|
@@ -144,11 +173,44 @@ Check consent first: if they opted out, sends to them are suppressed by design.
|
|
|
144
173
|
|
|
145
174
|
Locale resolution falls back through the chain to your default locale when a translation is missing — the send still succeeds with the fallback body. Check that the template declares `locales: ["en", "ar"]` and that the Arabic body exists in the catalog.
|
|
146
175
|
|
|
176
|
+
</Accordion>
|
|
177
|
+
<Accordion title="How do I ingest SNDR / Unifonic delivery webhooks?">
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import {
|
|
181
|
+
parseSndrWebhook,
|
|
182
|
+
verifySndrSignature,
|
|
183
|
+
parseUnifonicWebhook,
|
|
184
|
+
toDeliveryEvent,
|
|
185
|
+
} from "okengine/channel";
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Verify the signature when the provider sends one, parse the body, then call
|
|
189
|
+
`ingestOutcome` on your channel runtime with the bounce/complaint fields.
|
|
190
|
+
Console reads the receipt ledger — never raw webhook JSON.
|
|
191
|
+
|
|
147
192
|
</Accordion>
|
|
148
193
|
<Accordion title="How do I know which medium finally delivered?">
|
|
149
194
|
|
|
150
195
|
The receipt keeps every attempt in order with its outcome. A send that succeeded on a later medium reports status `fallback` — you can see the full chain in Console → Channels.
|
|
151
196
|
|
|
197
|
+
</Accordion>
|
|
198
|
+
<Accordion title="How do I verify Web Push against a real browser?">
|
|
199
|
+
|
|
200
|
+
Subscribe in a real browser with sently’s `examples/webpush-interop` page
|
|
201
|
+
(paste the same `VAPID_PUBLIC_KEY`), copy the `PushSubscription` JSON, then:
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
export VAPID_PUBLIC_KEY='…'
|
|
205
|
+
export VAPID_PRIVATE_KEY='…'
|
|
206
|
+
bun scripts/webpush-interop.ts /tmp/push-sub.json
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
That path is Channel → `webpush` → sently `createPushSender` →
|
|
210
|
+
`WebPushTransport`, and it prints the push-service HTTP response.
|
|
211
|
+
|
|
212
|
+
Confirm a visible OS notification titled `okengine webpush interop`.
|
|
213
|
+
|
|
152
214
|
</Accordion>
|
|
153
215
|
</Accordions>
|
|
154
216
|
|
|
@@ -156,6 +218,8 @@ The receipt keeps every attempt in order with its outcome. A send that succeeded
|
|
|
156
218
|
|
|
157
219
|
- [Flow](/docs/elements/flow) — `fx.send` inside `do`
|
|
158
220
|
- [Console · Channels](/docs/console/channels) — receipts, attempts, suppression
|
|
221
|
+
- [Environment Variables](/docs/reference/environment-variables) — Channel boot credentials
|
|
222
|
+
- [Configuration](/docs/reference/configuration) — `drivers.channel.*` maps
|
|
159
223
|
- [Signal](/docs/elements/signal) — machine-to-machine messaging, the other side of the line
|
|
160
224
|
|
|
161
225
|
## Next
|
|
@@ -30,6 +30,7 @@ drivers: {
|
|
|
30
30
|
vault: { local: "dotenv", docker: "openbao", test: "memory", prod: "openbao" },
|
|
31
31
|
channel: {
|
|
32
32
|
email: { local: "console", docker: "smtp", test: "console", prod: "smtp" },
|
|
33
|
+
sms: { local: "console", docker: "taqnyat", test: "console", prod: "taqnyat" },
|
|
33
34
|
},
|
|
34
35
|
},
|
|
35
36
|
```
|
|
@@ -43,10 +44,10 @@ drivers: {
|
|
|
43
44
|
| `signal` | env driver map | `memory` · `postgres` · `redis` · `nats` |
|
|
44
45
|
| `clock` | env driver map | `memory` · `postgres` · `frozen` |
|
|
45
46
|
| `vault` | env driver map | `dotenv` · `openbao` · `memory` |
|
|
46
|
-
| `channel.email` | env driver map | `console` · `smtp` · `resend`
|
|
47
|
-
| `channel.sms` | env driver map | `console` · `unifonic`
|
|
48
|
-
| `channel.whatsapp` | env driver map |
|
|
49
|
-
| `channel.push` | env driver map |
|
|
47
|
+
| `channel.email` | env driver map | `console` · `smtp` · `resend` · `sndr` |
|
|
48
|
+
| `channel.sms` | env driver map | `console` · `taqnyat` · `msegat` · `unifonic` |
|
|
49
|
+
| `channel.whatsapp` | env driver map | `console` · `wa-cloud` |
|
|
50
|
+
| `channel.push` | env driver map | `console` · `webpush` · `fcm` |
|
|
50
51
|
| `ai` | env driver map | `mock` · `anthropic` · `openai-compatible` · `bedrock` · `vertex` · `ollama` |
|
|
51
52
|
| `runs` | env driver map | runs-store driver (distinct from top-level `runs`) |
|
|
52
53
|
| `prod` | `string[]` | flat protocol list for the Manifest — nested maps are preferred |
|
|
@@ -61,14 +61,38 @@ Written by the first-boot bootstrap; you normally only set these when pointing a
|
|
|
61
61
|
| `OKE_VAULT_TOKEN` | App token (least-privilege) | — |
|
|
62
62
|
| `OKE_VAULT_MOUNT` | KV v2 mount path | `"secret"` |
|
|
63
63
|
|
|
64
|
-
## Channel (email)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
64
|
+
## Channel (email) — boot binder
|
|
65
|
+
|
|
66
|
+
Read when `drivers.channel.email` resolves to that driver id.
|
|
67
|
+
|
|
68
|
+
| Variable | Used for |
|
|
69
|
+
| ----------------------- | ------------------------------------------------ |
|
|
70
|
+
| `SMTP_URL` | `smtp` — full SMTP URL (`smtp://…`) |
|
|
71
|
+
| `SMTP_USER` | Overrides the user embedded in `SMTP_URL` |
|
|
72
|
+
| `SMTP_PASSWORD` | Overrides the password in `SMTP_URL` |
|
|
73
|
+
| `OKE_CHANNEL_EMAIL_URL` | Alternative to `SMTP_URL` |
|
|
74
|
+
| `RESEND_API_KEY` | `resend` API key |
|
|
75
|
+
| `SNDR_API_KEY` | `sndr` API key |
|
|
76
|
+
| `SNDR_BASE_URL` | Optional SNDR API origin (default `api.sndr.sh`) |
|
|
77
|
+
|
|
78
|
+
## Channel (SMS) — boot binder
|
|
79
|
+
|
|
80
|
+
Read when `drivers.channel.sms` resolves to that driver id (`console` opens nothing).
|
|
81
|
+
|
|
82
|
+
| Variable | Used for |
|
|
83
|
+
| ---------------------- | -------------------------------- |
|
|
84
|
+
| `TAQNYAT_BEARER_TOKEN` | `taqnyat` bearer token |
|
|
85
|
+
| `TAQNYAT_TOKEN` | Alias for `TAQNYAT_BEARER_TOKEN` |
|
|
86
|
+
| `TAQNYAT_SENDER` | Taqnyat pre-approved sender id |
|
|
87
|
+
| `MSEGAT_USERNAME` | `msegat` account username |
|
|
88
|
+
| `MSEGAT_API_KEY` | `msegat` API key |
|
|
89
|
+
| `MSEGAT_SENDER` | Msegat pre-approved sender id |
|
|
90
|
+
| `UNIFONIC_APPSID` | `unifonic` AppSid |
|
|
91
|
+
| `UNIFONIC_APP_SID` | Alias for `UNIFONIC_APPSID` |
|
|
92
|
+
| `UNIFONIC_SENDER` | Unifonic SenderID (optional) |
|
|
93
|
+
|
|
94
|
+
WhatsApp (`wa-cloud`) and push (`webpush` / `fcm`) are not opened from env at
|
|
95
|
+
boot — pass them on `BootOptions.channel.drivers` with their open options.
|
|
72
96
|
|
|
73
97
|
## AI providers
|
|
74
98
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Proves the acceptance criterion — set a secret, kill/restart the container
|
|
5
5
|
* on the same Raft volume, re-run the bootstrap unseal, and read the secret
|
|
6
|
-
* back.
|
|
6
|
+
* back. Opt-in via `OKE_TEST_DOCKER=1` plus a live Docker daemon.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, expect, test } from "bun:test";
|
|
@@ -13,10 +13,9 @@ import { join } from "node:path";
|
|
|
13
13
|
import { deriveInfrastructure, writeDerivedFiles } from "../docker/index.ts";
|
|
14
14
|
import { ensureOpenBao } from "./openbao-bootstrap.ts";
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
function dockerAvailable(): boolean {
|
|
17
17
|
try {
|
|
18
|
-
|
|
19
|
-
return (await proc.exited) === 0;
|
|
18
|
+
return Bun.spawnSync(["docker", "info"], { stdout: "pipe", stderr: "pipe" }).exitCode === 0;
|
|
20
19
|
} catch {
|
|
21
20
|
return false;
|
|
22
21
|
}
|
|
@@ -31,106 +30,116 @@ async function compose(project: string, dir: string, args: readonly string[]): P
|
|
|
31
30
|
return proc.exited;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const composeFiles = ["compose.yml", "compose.vault.yml"];
|
|
45
|
-
try {
|
|
46
|
-
const derived = deriveInfrastructure({
|
|
47
|
-
images: { vault: "openbao/openbao:2.6.1" },
|
|
48
|
-
app: "baotest",
|
|
49
|
-
includeApp: false,
|
|
50
|
-
composeDir: "docker",
|
|
51
|
-
});
|
|
52
|
-
await writeDerivedFiles(derived, dockerDir, { writeStackEnv: true });
|
|
53
|
-
const spec = derived.specs.find((s) => s.role === "vault")!;
|
|
54
|
-
const url = `http://127.0.0.1:${spec.hostPort}`;
|
|
55
|
-
|
|
56
|
-
const up = await compose(project, dockerDir, [
|
|
57
|
-
...composeFiles.flatMap((f) => ["-f", f]),
|
|
58
|
-
"up",
|
|
59
|
-
"-d",
|
|
60
|
-
]);
|
|
61
|
-
expect(up).toBe(0);
|
|
33
|
+
const WANT = process.env.OKE_TEST_DOCKER === "1";
|
|
34
|
+
const DOCKER = WANT && dockerAvailable();
|
|
35
|
+
if (!DOCKER) {
|
|
36
|
+
console.log(
|
|
37
|
+
WANT
|
|
38
|
+
? "skip: openbao restart e2e (docker daemon not available)"
|
|
39
|
+
: "skip: openbao restart e2e (OKE_TEST_DOCKER≠1)",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const live = DOCKER ? test : test.skip;
|
|
62
43
|
|
|
63
|
-
|
|
64
|
-
|
|
44
|
+
describe("openbao restart persistence (real container)", () => {
|
|
45
|
+
live(
|
|
46
|
+
"secret set before restart is readable after restart + unseal",
|
|
47
|
+
async () => {
|
|
48
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-openbao-it-"));
|
|
49
|
+
const dockerDir = join(dir, "docker");
|
|
50
|
+
const project = `oke-bao-${Date.now()}`;
|
|
51
|
+
const composeFiles = ["compose.yml", "compose.vault.yml"];
|
|
65
52
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"logs",
|
|
76
|
-
"vault",
|
|
77
|
-
],
|
|
78
|
-
{ cwd: dockerDir, stdout: "pipe", stderr: "pipe" },
|
|
79
|
-
);
|
|
80
|
-
console.error(await new Response(logs.stdout).text());
|
|
81
|
-
console.error(await new Response(logs.stderr).text());
|
|
82
|
-
await logs.exited;
|
|
83
|
-
throw err;
|
|
84
|
-
}
|
|
85
|
-
expect(first.appToken.length).toBeGreaterThan(0);
|
|
53
|
+
const derived = deriveInfrastructure({
|
|
54
|
+
images: { vault: "openbao/openbao:2.6.1" },
|
|
55
|
+
app: "baotest",
|
|
56
|
+
includeApp: false,
|
|
57
|
+
composeDir: "docker",
|
|
58
|
+
});
|
|
59
|
+
await writeDerivedFiles(derived, dockerDir, { writeStackEnv: true });
|
|
60
|
+
const spec = derived.specs.find((s) => s.role === "vault")!;
|
|
61
|
+
const url = `http://127.0.0.1:${spec.hostPort}`;
|
|
86
62
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
expect(write.ok).toBe(true);
|
|
63
|
+
const up = await compose(project, dockerDir, [
|
|
64
|
+
...composeFiles.flatMap((f) => ["-f", f]),
|
|
65
|
+
"up",
|
|
66
|
+
"-d",
|
|
67
|
+
]);
|
|
68
|
+
expect(up).toBe(0);
|
|
94
69
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
...composeFiles.flatMap((f) => ["-f", f]),
|
|
98
|
-
"restart",
|
|
99
|
-
]);
|
|
100
|
-
expect(restart).toBe(0);
|
|
101
|
-
// Poll the API until the server is back (sealed or unsealed).
|
|
102
|
-
let apiUp = false;
|
|
103
|
-
for (let i = 0; i < 60; i++) {
|
|
70
|
+
const names = ["STRIPE_KEY"];
|
|
71
|
+
let first;
|
|
104
72
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
73
|
+
first = await ensureOpenBao({ cwd: dir, url, names });
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const logs = Bun.spawn(
|
|
76
|
+
[
|
|
77
|
+
"docker",
|
|
78
|
+
"compose",
|
|
79
|
+
"-p",
|
|
80
|
+
project,
|
|
81
|
+
...composeFiles.flatMap((f) => ["-f", f]),
|
|
82
|
+
"logs",
|
|
83
|
+
"vault",
|
|
84
|
+
],
|
|
85
|
+
{ cwd: dockerDir, stdout: "pipe", stderr: "pipe" },
|
|
86
|
+
);
|
|
87
|
+
console.error(await new Response(logs.stdout).text());
|
|
88
|
+
console.error(await new Response(logs.stderr).text());
|
|
89
|
+
await logs.exited;
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
expect(first.appToken.length).toBeGreaterThan(0);
|
|
93
|
+
|
|
94
|
+
const write = await fetch(`${url}/v1/secret/data/STRIPE_KEY`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: { "X-Vault-Token": first.appToken, "content-type": "application/json" },
|
|
97
|
+
body: JSON.stringify({ data: { value: "sk_restart_persistent" } }),
|
|
98
|
+
});
|
|
99
|
+
if (!write.ok) console.error("write failed", write.status, await write.text());
|
|
100
|
+
expect(write.ok).toBe(true);
|
|
101
|
+
|
|
102
|
+
// Kill + start the same volume (a real restart, not a fresh stack).
|
|
103
|
+
const restart = await compose(project, dockerDir, [
|
|
104
|
+
...composeFiles.flatMap((f) => ["-f", f]),
|
|
105
|
+
"restart",
|
|
106
|
+
]);
|
|
107
|
+
expect(restart).toBe(0);
|
|
108
|
+
// Poll the API until the server is back (sealed or unsealed).
|
|
109
|
+
let apiUp = false;
|
|
110
|
+
for (let i = 0; i < 60; i++) {
|
|
111
|
+
try {
|
|
112
|
+
const probe = await fetch(`${url}/v1/sys/seal-status`);
|
|
113
|
+
if (probe.ok) {
|
|
114
|
+
apiUp = true;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// not up yet
|
|
109
119
|
}
|
|
110
|
-
|
|
111
|
-
// not up yet
|
|
120
|
+
await Bun.sleep(500);
|
|
112
121
|
}
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
expect(apiUp).toBe(true);
|
|
122
|
+
expect(apiUp).toBe(true);
|
|
116
123
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
const second = await ensureOpenBao({ cwd: dir, url, names });
|
|
125
|
+
expect(second.initializedNow).toBe(false);
|
|
126
|
+
expect(second.appToken).toBe(first.appToken);
|
|
120
127
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
const read = await fetch(`${url}/v1/secret/data/STRIPE_KEY`, {
|
|
129
|
+
headers: { "X-Vault-Token": second.appToken },
|
|
130
|
+
});
|
|
131
|
+
expect(read.ok).toBe(true);
|
|
132
|
+
const body = (await read.json()) as { data?: { data?: { value?: string } } };
|
|
133
|
+
expect(body.data?.data?.value).toBe("sk_restart_persistent");
|
|
134
|
+
} finally {
|
|
135
|
+
await compose(project, dockerDir, [
|
|
136
|
+
...composeFiles.flatMap((f) => ["-f", f]),
|
|
137
|
+
"down",
|
|
138
|
+
"-v",
|
|
139
|
+
]).catch(() => 0);
|
|
140
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
180_000,
|
|
144
|
+
);
|
|
136
145
|
});
|