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.
Files changed (38) hide show
  1. package/README.md +148 -13
  2. package/package.json +3 -3
  3. package/site/content/docs/elements/channel.mdx +71 -7
  4. package/site/content/docs/reference/configuration.mdx +5 -4
  5. package/site/content/docs/reference/environment-variables.mdx +32 -8
  6. package/src/cli/openbao-restart.integration.test.ts +106 -97
  7. package/src/docker/dockerfile.integration.test.ts +126 -119
  8. package/src/docker/stack.integration.test.ts +118 -102
  9. package/src/drivers/ai-ollama-tools.integration.test.ts +8 -6
  10. package/src/drivers/ai-ollama.integration.test.ts +3 -19
  11. package/src/drivers/channel-fcm.ts +49 -53
  12. package/src/drivers/channel-msegat.ts +61 -0
  13. package/src/drivers/channel-sently-map.ts +57 -0
  14. package/src/drivers/channel-sently.test.ts +99 -0
  15. package/src/drivers/channel-sndr.ts +28 -0
  16. package/src/drivers/channel-taqnyat.ts +57 -0
  17. package/src/drivers/channel-types.ts +79 -2
  18. package/src/drivers/channel-unifonic.ts +26 -43
  19. package/src/drivers/channel-wa-cloud.ts +33 -47
  20. package/src/drivers/channel-webpush.ts +39 -239
  21. package/src/drivers/index.ts +4 -0
  22. package/src/elements/channel/costs.test.ts +2 -2
  23. package/src/elements/channel/costs.ts +14 -2
  24. package/src/elements/channel/mime.ts +11 -0
  25. package/src/elements/channel/runtime.ts +94 -0
  26. package/src/elements/channel/sndr-webhooks.test.ts +26 -0
  27. package/src/elements/channel.ts +10 -1
  28. package/src/elements/index.ts +9 -0
  29. package/src/kernel/boot-bind/channel.test.ts +68 -3
  30. package/src/kernel/boot-bind/channel.ts +93 -2
  31. package/src/plugins/auth-delivery.mailpit.integration.test.ts +10 -4
  32. package/src/release/exports.test.ts +26 -0
  33. package/src/release/exports.ts +64 -5
  34. package/src/release/index.ts +5 -0
  35. package/src/release/measure.exports.test.ts +13 -1
  36. package/src/release/measure.ts +76 -13
  37. package/src/release/official-plugins.ts +46 -0
  38. 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 media="(prefers-color-scheme: dark)" srcset="site/public/logo/OKE-W.svg" />
4
- <img alt="OKE" src="site/public/logo/OKE-B.svg" width="220" />
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
- # okengine
9
-
10
- _"Stop gluing APIs, jobs, and queues into one backend. One law collapses them — client, Console, and infra come free. Yours to host."_
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
- [![npm](https://img.shields.io/npm/v/okengine.svg)](https://www.npmjs.com/package/okengine)
13
- [![JSR](https://jsr.io/badges/@omqkhafi/okengine)](https://jsr.io/@omqkhafi/okengine)
14
- [![npm](https://img.shields.io/npm/v/create-oke.svg)](https://www.npmjs.com/package/create-oke)
15
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
16
- [![Bun >=1.3](https://img.shields.io/badge/Bun-%3E%3D1.3-black.svg)](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
- **Docs:** [oke.omqkhafi.dev](https://oke.omqkhafi.dev) · **Handbook:** [/docs](https://oke.omqkhafi.dev/docs) · **CLI:** `oke` · **Scaffold:** [`create-oke`](https://www.npmjs.com/package/create-oke)
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 --template hello # scaffold
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 --template hello
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.0",
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": "^0.8.0"
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", // SMTP catcher for the docker stack
125
+ "channel.email": "axllent/mailpit:v1.22.3",
120
126
  },
121
127
  ```
122
128
 
123
- | Driver | Medium | Behavior |
124
- | ---------- | ------ | ---------------------------------------------------- |
125
- | `console` | any | Captures into a readable inbox — local + tests |
126
- | `smtp` | email | Real SMTP — Mailpit in docker, your provider in prod |
127
- | `resend` | email | Resend API |
128
- | `unifonic` | sms | Unifonic SMS API |
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 | medium declared; drivers added per provider |
49
- | `channel.push` | env driver map | medium declared; drivers added per provider |
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
- | Variable | Used for |
67
- | ----------------------- | ----------------------------------------- |
68
- | `SMTP_URL` | Full SMTP connection string (`smtp://…`) |
69
- | `SMTP_USER` | Overrides the user embedded in `SMTP_URL` |
70
- | `SMTP_PASSWORD` | Overrides the password in `SMTP_URL` |
71
- | `OKE_CHANNEL_EMAIL_URL` | OKE-specific alternative to `SMTP_URL` |
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. Skips when no Docker daemon is available.
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
- async function dockerAvailable(): Promise<boolean> {
16
+ function dockerAvailable(): boolean {
17
17
  try {
18
- const proc = Bun.spawn(["docker", "info"], { stdout: "pipe", stderr: "pipe" });
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
- describe("openbao restart persistence (real container)", () => {
35
- test("secret set before restart is readable after restart + unseal", async () => {
36
- if (!(await dockerAvailable())) {
37
- console.warn("skipping: docker daemon not available");
38
- return;
39
- }
40
-
41
- const dir = await mkdtemp(join(tmpdir(), "oke-openbao-it-"));
42
- const dockerDir = join(dir, "docker");
43
- const project = `oke-bao-${Date.now()}`;
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
- const names = ["STRIPE_KEY"];
64
- let first;
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
- first = await ensureOpenBao({ cwd: dir, url, names });
67
- } catch (err) {
68
- const logs = Bun.spawn(
69
- [
70
- "docker",
71
- "compose",
72
- "-p",
73
- project,
74
- ...composeFiles.flatMap((f) => ["-f", f]),
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
- const write = await fetch(`${url}/v1/secret/data/STRIPE_KEY`, {
88
- method: "POST",
89
- headers: { "X-Vault-Token": first.appToken, "content-type": "application/json" },
90
- body: JSON.stringify({ data: { value: "sk_restart_persistent" } }),
91
- });
92
- if (!write.ok) console.error("write failed", write.status, await write.text());
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
- // Kill + start the same volume (a real restart, not a fresh stack).
96
- const restart = await compose(project, dockerDir, [
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
- const probe = await fetch(`${url}/v1/sys/seal-status`);
106
- if (probe.ok) {
107
- apiUp = true;
108
- break;
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
- } catch {
111
- // not up yet
120
+ await Bun.sleep(500);
112
121
  }
113
- await Bun.sleep(500);
114
- }
115
- expect(apiUp).toBe(true);
122
+ expect(apiUp).toBe(true);
116
123
 
117
- const second = await ensureOpenBao({ cwd: dir, url, names });
118
- expect(second.initializedNow).toBe(false);
119
- expect(second.appToken).toBe(first.appToken);
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
- const read = await fetch(`${url}/v1/secret/data/STRIPE_KEY`, {
122
- headers: { "X-Vault-Token": second.appToken },
123
- });
124
- expect(read.ok).toBe(true);
125
- const body = (await read.json()) as { data?: { data?: { value?: string } } };
126
- expect(body.data?.data?.value).toBe("sk_restart_persistent");
127
- } finally {
128
- await compose(project, dockerDir, [
129
- ...composeFiles.flatMap((f) => ["-f", f]),
130
- "down",
131
- "-v",
132
- ]).catch(() => 0);
133
- await rm(dir, { recursive: true, force: true }).catch(() => {});
134
- }
135
- }, 180_000);
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
  });