okengine 0.19.8 → 0.20.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 +1 -1
- package/package.json +1 -1
- package/site/content/docs/client/auth.mdx +1 -2
- package/site/content/docs/client/calling.mdx +8 -8
- package/site/content/docs/client/index.mdx +4 -4
- package/site/content/docs/client/react.mdx +5 -0
- package/site/content/docs/elements/channel/email.mdx +25 -36
- package/site/content/docs/elements/channel/index.mdx +88 -46
- package/site/content/docs/elements/channel/push.mdx +7 -9
- package/site/content/docs/elements/channel/sms.mdx +6 -4
- package/site/content/docs/elements/channel/whatsapp.mdx +11 -9
- package/site/content/docs/elements/clock/index.mdx +14 -25
- package/site/content/docs/elements/flow/http.mdx +24 -8
- package/site/content/docs/elements/flow/index.mdx +15 -12
- package/site/content/docs/elements/flow/routing.mdx +166 -122
- package/site/content/docs/elements/gate/rls.mdx +2 -2
- package/site/content/docs/elements/store/index.mdx +11 -3
- package/site/content/docs/elements/store/search.mdx +5 -5
- package/site/content/docs/elements/store/sql.mdx +91 -33
- package/site/content/docs/elements/vault/index.mdx +3 -5
- package/site/content/docs/plugins/magic-link.mdx +4 -3
- package/site/content/docs/plugins/otp.mdx +4 -3
- package/site/content/docs/plugins/two-factor.mdx +4 -0
- package/site/content/docs/providers/index.mdx +1 -1
- package/site/content/docs/recipes/index.mdx +1 -1
- package/site/content/docs/reference/cli.mdx +13 -4
- package/site/content/docs/reference/configuration.mdx +7 -7
- package/site/content/docs/reference/errors.mdx +30 -30
- package/site/content/docs/reference/fx.mdx +16 -8
- package/site/content/docs/reference/i18n.mdx +4 -4
- package/site/content/docs/reference/plugins.mdx +4 -4
- package/site/content/docs/understand/try-it.mdx +2 -0
- package/src/cli/ai-setup/ai-setup.test.ts +40 -0
- package/src/cli/ai-setup/apply.ts +28 -46
- package/src/cli/ask-vault-gaps.test.ts +60 -4
- package/src/cli/ask-vault-gaps.ts +59 -3
- package/src/cli/build.test.ts +3 -3
- package/src/cli/build.ts +5 -5
- package/src/cli/db-auto-push.test.ts +11 -0
- package/src/cli/db-auto-push.ts +6 -2
- package/src/cli/db.test.ts +1 -1
- package/src/cli/db.ts +6 -6
- package/src/cli/dev-app-runner.ts +12 -4
- package/src/cli/dev-db-push.test.ts +6 -2
- package/src/cli/dev-schema-sync.ts +1 -1
- package/src/cli/dev.test.ts +10 -7
- package/src/cli/dev.ts +18 -12
- package/src/cli/ensure-drizzle-config.ts +4 -3
- package/src/client-react/browser.test.ts +23 -0
- package/src/client-react/use-live-query.ts +1 -1
- package/src/compiler/flow-path.test.ts +1 -0
- package/src/compiler/flow-path.ts +1 -1
- package/src/compiler/generate-adopt.test.ts +55 -1
- package/src/compiler/generate-adopt.ts +111 -21
- package/src/config/index.ts +6 -4
- package/src/console/ui-next/dist/assets/{access-page-C5yG4aS2.js → access-page-DFLu0wTA.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-D2ToVK86.js → agent-disclosure-DGscxaF5.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-7HUR0kCz.js → cache-glyph-BGmRZk7d.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-D0ky3aXt.js → call-pii-button--feUYxvG.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-BLUHH2dB.js → collapsible-JWvpaiGY.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-BgHEFMtm.js → duration-tone-D9yCJG4n.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-DDybqshQ.js → flows-page-Bs6MD9GB.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-BFiJKYV4.js → highlighted-json-xH8MrEnv.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BVAcB0bI.js → http-method-C4vB6ZIw.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-BID6LSYI.js → index-yTCY4AcS.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CANo_mMK.js → observability-page-BxJ3R6dU.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-CqQGCshp.js → replica-lag-QRKB_IE8.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-DyUDkEt4.js → request-meta-DqZ-fMu5.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CA0rJ_Ow.js → store-page-Dixb6L7a.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-DjH_fMzI.js → trace-detail-sheet-CazhjtiU.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CWSomIeQ.js → tree-expand-toggle-DlnqYKfr.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-Cz9TIViw.js → units-page-BXTLjU2-.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-Bb1o1O9p.js → vault-page-39KR__bc.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/drivers/clock-postgres.test.ts +10 -2
- package/src/drivers/clock-postgres.ts +18 -2
- package/src/drivers/vault-driver-removal.test.ts +2 -2
- package/src/elements/channel/declare.ts +66 -3
- package/src/elements/channel/runtime.ts +9 -11
- package/src/elements/channel.test.ts +42 -0
- package/src/elements/channel.ts +4 -2
- package/src/elements/clock/reconcile.ts +45 -24
- package/src/elements/clock.test.ts +33 -0
- package/src/elements/store/emit-drizzle.ts +285 -65
- package/src/elements/store/load-plugin-tables.ts +1 -1
- package/src/elements/store/prepare-row.test.ts +57 -4
- package/src/elements/store/schema-decl.test.ts +178 -0
- package/src/elements/store/sql-session.ts +44 -2
- package/src/elements/store/table.ts +8 -6
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/app.ts +26 -32
- package/src/kernel/auto-registry.test.ts +26 -1
- package/src/kernel/boot.ts +2 -2
- package/src/kernel/boundary-contract.ts +6 -1
- package/src/kernel/errors.ts +3 -3
- package/src/kernel/flow-units.ts +3 -3
- package/src/kernel/fx.ts +12 -2
- package/src/kernel/mutation-id.ts +8 -0
- package/src/kernel/plugin.ts +4 -3
- package/src/kernel/project-out.test.ts +176 -0
- package/src/kernel/project-out.ts +91 -0
- package/src/kernel/realtime-bind.ts +2 -3
- package/src/kernel/router/linear.ts +12 -6
- package/src/kernel/router.test.ts +13 -0
- package/src/plugins/magic-link.ts +25 -24
- package/src/plugins/otp.ts +35 -24
- package/src/plugins/two-factor.ts +15 -0
- package/src/runs/duckdb.test.ts +2 -2
package/AGENTS.md
CHANGED
|
@@ -84,7 +84,7 @@ After changing `src/kernel/`, `src/client/`, `src/compiler/`, `src/validation/`,
|
|
|
84
84
|
Published packages:
|
|
85
85
|
|
|
86
86
|
- `okengine` — framework. Subpath exports: `.`, `./client`, `./test`, `./config`, `./auth`, `./plugins`, `./drivers/*`. `"sideEffects": false`. CLI binary: `oke`.
|
|
87
|
-
- `create-oke` — scaffold CLI (`bunx create-oke@latest <name>`). Lives in `packages/create-oke` and ships
|
|
87
|
+
- `create-oke` — scaffold CLI (`bunx create-oke@latest <name>`). Lives in `packages/create-oke` and ships starters from `packages/create-oke/templates/{blank,shorter}`.
|
|
88
88
|
|
|
89
89
|
Engine: Bun `>=1.4.2`.
|
|
90
90
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "okengine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "One law. Eight elements. One contract. The backend model stays small; operational surfaces are derived from it instead of maintained separately.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -171,8 +171,7 @@ auth.bind(api);
|
|
|
171
171
|
| `Forbidden` | 403 | Wrong scopes / CSRF — read `error.data.reason` |
|
|
172
172
|
| `RateLimited` | 429 | Wait `retryAfterMs` |
|
|
173
173
|
|
|
174
|
-
Also see [CORS](/docs/plugins/cors) and [CSRF](/docs/plugins/csrf).
|
|
175
|
-
demos cookie + passkey + `<Can>`.
|
|
174
|
+
Also see [CORS](/docs/plugins/cors) and [CSRF](/docs/plugins/csrf).
|
|
176
175
|
|
|
177
176
|
## Troubleshooting
|
|
178
177
|
|
|
@@ -81,11 +81,11 @@ export const publicApiUrl = vault.config("PUBLIC_API_URL", {
|
|
|
81
81
|
});
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
| Runtime | Base URL for `createClient`
|
|
85
|
-
| ------------------------------------------ |
|
|
86
|
-
| Browser / Bun
|
|
87
|
-
| Node / worker / CLI | `vault.env.required("PUBLIC_API_URL")`
|
|
88
|
-
| Separate storefront after `oke client add` | Same `vault.env("PUBLIC_API_URL") ?? ""`
|
|
84
|
+
| Runtime | Base URL for `createClient` |
|
|
85
|
+
| ------------------------------------------ | --------------------------------------------------------- |
|
|
86
|
+
| Browser / Bun | `vault.env("PUBLIC_API_URL") ?? ""` — empty = same origin |
|
|
87
|
+
| Node / worker / CLI | `vault.env.required("PUBLIC_API_URL")` |
|
|
88
|
+
| Separate storefront after `oke client add` | Same `vault.env("PUBLIC_API_URL") ?? ""` |
|
|
89
89
|
|
|
90
90
|
Set `PUBLIC_API_URL` via environment or `oke vault set`. Prefer `vault.env` over raw
|
|
91
91
|
`process.env` — same helpers as the rest of the measure. See
|
|
@@ -362,8 +362,8 @@ const api = createClient(import.meta.env.PUBLIC_API_URL ?? "", { $routes: routes
|
|
|
362
362
|
|
|
363
363
|
Ambient `.d.ts` types `in` / `out` / live / stream stamps. The routes module supplies wire REST.
|
|
364
364
|
|
|
365
|
-
|
|
366
|
-
|
|
365
|
+
Point `createClient` at your app origin (`PUBLIC_API_URL`), or leave it empty when the
|
|
366
|
+
browser page is same-origin with the API.
|
|
367
367
|
|
|
368
368
|
CLI details: [CLI Reference](/docs/reference/cli).
|
|
369
369
|
|
|
@@ -387,7 +387,7 @@ Budget: the `./client` export stays under the measured client-runtime cap (hard
|
|
|
387
387
|
<Accordions>
|
|
388
388
|
|
|
389
389
|
<Accordion title="api.bookings.get is not a function / type error">
|
|
390
|
-
Confirm the Flow is `export`ed from a generated unit (`import "@/flows
|
|
390
|
+
Confirm the Flow is `export`ed from a generated unit (`import "@/flows"` then
|
|
391
391
|
`oke({ name })`), or from a module you still `.adopt({ bookings })`.
|
|
392
392
|
|
|
393
393
|
Type `createClient` with that `App` (or ambient `Register` after `oke-client.d.ts` regenerates).
|
|
@@ -27,7 +27,7 @@ the envelope.
|
|
|
27
27
|
|
|
28
28
|
```typescript title="src/app.ts"
|
|
29
29
|
import "@/core";
|
|
30
|
-
import "@/flows
|
|
30
|
+
import "@/flows";
|
|
31
31
|
import { oke } from "okengine/http";
|
|
32
32
|
|
|
33
33
|
export const app = oke({ name: "commerce" });
|
|
@@ -46,7 +46,7 @@ import { createClient } from "okengine/client";
|
|
|
46
46
|
import { vault } from "okengine/vault";
|
|
47
47
|
import { app } from "../../src/app";
|
|
48
48
|
|
|
49
|
-
// Base URL via vault.env (empty = same
|
|
49
|
+
// Base URL via vault.env (empty = same origin as the page)
|
|
50
50
|
const api = createClient(app, vault.env("PUBLIC_API_URL") ?? "");
|
|
51
51
|
const { data, error } = await api.bookings.get({ id: "bkg_7f3a" });
|
|
52
52
|
|
|
@@ -128,8 +128,8 @@ const api = createClient(vault.env("PUBLIC_API_URL") ?? "");
|
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
Declare the origin on the server with [Vault · Config](/docs/elements/vault/config)
|
|
131
|
-
(`PUBLIC_API_URL`), then set it via env or `oke vault set`. Leave it unset in local
|
|
132
|
-
dev so `createClient("")` stays same-origin behind
|
|
131
|
+
(`PUBLIC_API_URL`), then set it via env or `oke vault set`. Leave it unset in local frontend
|
|
132
|
+
dev so `createClient("")` stays same-origin behind your proxy.
|
|
133
133
|
|
|
134
134
|
</Tab>
|
|
135
135
|
|
|
@@ -221,6 +221,11 @@ Prefer [`createAuthClient`](/docs/client/auth) for cookie/Bearer sessions and me
|
|
|
221
221
|
Without a token, status becomes `"unauthenticated"`.
|
|
222
222
|
</Accordion>
|
|
223
223
|
|
|
224
|
+
<Accordion title="Vite SPA is a blank page">
|
|
225
|
+
The console shows `node:async_hooks` / `AsyncLocalStorage` from `okengine/client-react`. Upgrade
|
|
226
|
+
okengine — `Can` / `useLiveQuery` must not import the server realtime binder.
|
|
227
|
+
</Accordion>
|
|
228
|
+
|
|
224
229
|
<Accordion title="useLiveQuery never connects">
|
|
225
230
|
Pass `live: { method, path }` from `app.$routes` for the resource’s `/live` route. Set
|
|
226
231
|
`enabled: true` (or omit). See [Live](/docs/client/live) for exposure and resume errors.
|
|
@@ -6,14 +6,14 @@ source: "docs/spec/unified-theory.md"
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
Email (`channel.email`) is the default Channel medium. Declare a binder with a From address,
|
|
9
|
-
|
|
9
|
+
put `{{field}}` bodies on `.template({ catalog })`, and send with `fx.send`.
|
|
10
10
|
|
|
11
11
|
For developers who need local catchers and production HTTP MTAs — same `fx.send` path, swap the
|
|
12
12
|
driver.
|
|
13
13
|
|
|
14
14
|
<Callout title="The one rule">
|
|
15
|
-
Use `channel.email({ from }).template(name, { schema, locales })
|
|
16
|
-
|
|
15
|
+
Use `channel.email({ from }).template(name, { schema, locales, catalog })`. Subject and body
|
|
16
|
+
live on the template — not on `oke()`.
|
|
17
17
|
</Callout>
|
|
18
18
|
|
|
19
19
|
## Smallest Example
|
|
@@ -33,32 +33,20 @@ export const passwordReset = mail.template("auth.resetPassword", {
|
|
|
33
33
|
description: "Password reset link",
|
|
34
34
|
locales: ["en"],
|
|
35
35
|
schema: z.object({ resetLink: z.string().url() }),
|
|
36
|
+
catalog: {
|
|
37
|
+
en: {
|
|
38
|
+
subject: "Reset your password",
|
|
39
|
+
text: "Open {{resetLink}} to choose a new password.",
|
|
40
|
+
html: '<p><a href="{{resetLink}}">Reset your password</a></p>',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
36
43
|
});
|
|
37
44
|
```
|
|
38
45
|
|
|
39
46
|
</Step>
|
|
40
47
|
|
|
41
48
|
<Step>
|
|
42
|
-
###
|
|
43
|
-
|
|
44
|
-
```typescript title="src/app.ts"
|
|
45
|
-
import { oke } from "okengine";
|
|
46
|
-
|
|
47
|
-
oke({
|
|
48
|
-
name: "app",
|
|
49
|
-
channel: {
|
|
50
|
-
catalog: {
|
|
51
|
-
"auth.resetPassword": {
|
|
52
|
-
en: {
|
|
53
|
-
subject: "Reset your password",
|
|
54
|
-
text: "Open {{resetLink}} to choose a new password.",
|
|
55
|
-
html: '<p><a href="{{resetLink}}">Reset your password</a></p>',
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
```
|
|
49
|
+
### Send from a Flow
|
|
62
50
|
|
|
63
51
|
```typescript title="src/flows/auth/reset.ts"
|
|
64
52
|
import { on, flow, http } from "okengine";
|
|
@@ -118,13 +106,13 @@ Per-locale `subject` / `text` / `html` with `{{field}}` interpolation from `data
|
|
|
118
106
|
|
|
119
107
|
```typescript
|
|
120
108
|
catalog: {
|
|
121
|
-
"
|
|
122
|
-
|
|
123
|
-
ar: { subject: "إعادة التعيين", text: "{{resetLink}}" },
|
|
124
|
-
},
|
|
109
|
+
en: { subject: "Reset", text: "{{resetLink}}", html: "<a href=\"{{resetLink}}\">Reset</a>" },
|
|
110
|
+
ar: { subject: "إعادة التعيين", text: "{{resetLink}}" },
|
|
125
111
|
}
|
|
126
112
|
```
|
|
127
113
|
|
|
114
|
+
Put that object on `.template({ catalog })`. Do not nest the template name again.
|
|
115
|
+
|
|
128
116
|
</Tab>
|
|
129
117
|
|
|
130
118
|
<Tab value="Failover">
|
|
@@ -146,8 +134,8 @@ Receipt status `fallback` means an earlier attempt failed and a later one succee
|
|
|
146
134
|
|
|
147
135
|
<Tab value="Plugin catalog">
|
|
148
136
|
|
|
149
|
-
Official plugins
|
|
150
|
-
`channel.catalog`
|
|
137
|
+
Official plugins put copy on `.template({ catalog })` (OTP, magic link, two-factor). App
|
|
138
|
+
`oke({ channel.catalog })` and plugin `.channelCatalog(…)` overlay those bodies at boot.
|
|
151
139
|
|
|
152
140
|
</Tab>
|
|
153
141
|
|
|
@@ -164,11 +152,12 @@ Official plugins contribute catalogs with `.channelCatalog(…)` (e.g. OTP, magi
|
|
|
164
152
|
|
|
165
153
|
### `.template(name, options?)`
|
|
166
154
|
|
|
167
|
-
| Option | Type | Meaning
|
|
168
|
-
| ------------- | ---------- |
|
|
169
|
-
| `description` | `string` | Console label
|
|
170
|
-
| `locales` | `string[]` | Declared locales
|
|
171
|
-
| `schema` | Schema | Payload contract for `data`
|
|
155
|
+
| Option | Type | Meaning |
|
|
156
|
+
| ------------- | ---------- | -------------------------------------- |
|
|
157
|
+
| `description` | `string` | Console label |
|
|
158
|
+
| `locales` | `string[]` | Declared locales |
|
|
159
|
+
| `schema` | Schema | Payload contract for `data` |
|
|
160
|
+
| `catalog` | object | Per-locale `subject` / `text` / `html` |
|
|
172
161
|
|
|
173
162
|
Default From when neither binder nor template sets one: `"oke@localhost.test"`.
|
|
174
163
|
|
|
@@ -229,8 +218,8 @@ Channel catalogs are **not** ICU — do not use `fx.t` for email bodies ([i18n](
|
|
|
229
218
|
</Accordion>
|
|
230
219
|
|
|
231
220
|
<Accordion title="Message in Mailpit has JSON body / template name as subject">
|
|
232
|
-
No catalog entry for that template + locale. Add `
|
|
233
|
-
`subject` / `text` / `html`.
|
|
221
|
+
No catalog entry for that template + locale. Add `catalog` on `.template(…)` (or overlay with
|
|
222
|
+
`oke({channel.catalog})` / plugin `.channelCatalog`) with `subject` / `text` / `html`.
|
|
234
223
|
</Accordion>
|
|
235
224
|
|
|
236
225
|
<Accordion title="suppressed/opted-out or prior-bounce — no provider call">
|
|
@@ -6,16 +6,16 @@ source: "docs/spec/unified-theory.md"
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
Channel is how your backend **reaches a person** — the order-confirmation email, the SMS
|
|
9
|
-
sign-in code, a WhatsApp notice, or a device push. You declare a template on a medium
|
|
10
|
-
`{{field}}`
|
|
9
|
+
sign-in code, a WhatsApp notice, or a device push. You declare a template on a medium with
|
|
10
|
+
per-locale `{{field}}` bodies, then send from a Flow with `fx.send`.
|
|
11
11
|
|
|
12
12
|
For developers wiring Mailpit locally and Resend / Taqnyat / FCM in production — templates and
|
|
13
13
|
drivers first, never vendor SDKs inside `do`.
|
|
14
14
|
|
|
15
15
|
<Callout title="The one rule">
|
|
16
|
-
Declare a template (`channel.email(…).template(
|
|
17
|
-
Bodies live
|
|
18
|
-
|
|
16
|
+
Declare a template (`channel.email(…).template({ catalog })`), then `fx.send(template, { to, data })`.
|
|
17
|
+
Bodies live on the declare (`subject` / `text` / `html` per locale). Consent and prior bounces
|
|
18
|
+
suppress before any driver runs.
|
|
19
19
|
</Callout>
|
|
20
20
|
|
|
21
21
|
<ChannelPhysics />
|
|
@@ -27,18 +27,28 @@ drivers first, never vendor SDKs inside `do`.
|
|
|
27
27
|
<Step>
|
|
28
28
|
### Declare an email template
|
|
29
29
|
|
|
30
|
-
```typescript title="src/core/
|
|
30
|
+
```typescript title="src/core/email.ts"
|
|
31
31
|
import { channel } from "okengine";
|
|
32
32
|
import { z } from "zod";
|
|
33
33
|
|
|
34
|
-
const mail = channel.email({ from: "
|
|
34
|
+
const mail = channel.email({ from: "Shorter <shorter@localhost>" });
|
|
35
35
|
|
|
36
|
-
export const
|
|
36
|
+
export const linkCreatedMail = mail.template("link-created", {
|
|
37
|
+
description: "Short URL delivered to the demo inbox",
|
|
37
38
|
locales: ["en"],
|
|
38
39
|
schema: z.object({
|
|
39
40
|
id: z.string(),
|
|
40
|
-
|
|
41
|
+
code: z.string(),
|
|
42
|
+
url: z.string(),
|
|
43
|
+
shortUrl: z.string(),
|
|
41
44
|
}),
|
|
45
|
+
catalog: {
|
|
46
|
+
en: {
|
|
47
|
+
subject: "Short link {{code}}",
|
|
48
|
+
text: "{{shortUrl}} → {{url}}",
|
|
49
|
+
html: '<p><a href="{{shortUrl}}">{{code}}</a> → {{url}}</p>',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
42
52
|
});
|
|
43
53
|
```
|
|
44
54
|
|
|
@@ -50,26 +60,42 @@ Import this module before `oke()` so auto-registry adopts the template (or pass
|
|
|
50
60
|
<Step>
|
|
51
61
|
### Send from a Flow
|
|
52
62
|
|
|
53
|
-
```typescript title="src/flows/
|
|
54
|
-
import { on, flow } from "okengine";
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
63
|
+
```typescript title="src/flows/links/signals.ts"
|
|
64
|
+
import { on, flow, signal } from "okengine";
|
|
65
|
+
import { z } from "zod";
|
|
66
|
+
import { linkCreatedMail } from "@/core";
|
|
67
|
+
|
|
68
|
+
export const linkCreated = signal.once("link-created", {
|
|
69
|
+
retries: 3,
|
|
70
|
+
deadLetter: true,
|
|
71
|
+
schema: z.object({
|
|
72
|
+
id: z.string(),
|
|
73
|
+
code: z.string(),
|
|
74
|
+
url: z.string(),
|
|
75
|
+
shortUrl: z.string(),
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
57
78
|
|
|
58
79
|
export const onCreated = on(
|
|
59
|
-
|
|
60
|
-
flow("
|
|
80
|
+
linkCreated,
|
|
81
|
+
flow("links.onCreated", {
|
|
61
82
|
do: async (payload, fx) => {
|
|
62
|
-
await fx.send(
|
|
83
|
+
await fx.send(linkCreatedMail, {
|
|
63
84
|
to: "you@localhost",
|
|
64
|
-
data: {
|
|
85
|
+
data: {
|
|
86
|
+
id: payload.id,
|
|
87
|
+
code: payload.code,
|
|
88
|
+
url: payload.url,
|
|
89
|
+
shortUrl: payload.shortUrl,
|
|
90
|
+
},
|
|
65
91
|
});
|
|
66
92
|
},
|
|
67
93
|
}),
|
|
68
94
|
);
|
|
69
95
|
```
|
|
70
96
|
|
|
71
|
-
The compiler stamps `sends: ["
|
|
72
|
-
`email:
|
|
97
|
+
The compiler stamps `sends: ["link-created"]` on the Flow’s effects (template name — not
|
|
98
|
+
`email:link-created`). Declaration and subscriber share `signals.ts`.
|
|
73
99
|
|
|
74
100
|
</Step>
|
|
75
101
|
|
|
@@ -77,8 +103,8 @@ The compiler stamps `sends: ["note-created"]` on the Flow’s effects (template
|
|
|
77
103
|
### See it locally
|
|
78
104
|
|
|
79
105
|
With `drivers.channel.email.dev: "smtp"` and Mailpit pinned, open the Mailpit UI
|
|
80
|
-
(`MAILPIT_UI_URL`).
|
|
81
|
-
`text: JSON.stringify(data)`.
|
|
106
|
+
(`MAILPIT_UI_URL`). Subject and HTML come from the template `catalog`. Omit `catalog` and the
|
|
107
|
+
runtime falls back to `subject: link-created` and `text: JSON.stringify(data)`.
|
|
82
108
|
|
|
83
109
|
</Step>
|
|
84
110
|
|
|
@@ -95,9 +121,14 @@ From a bare send to catalog bodies, locale, and same-medium failover:
|
|
|
95
121
|
Template handle + recipient — catalog optional for local smoke tests:
|
|
96
122
|
|
|
97
123
|
```typescript
|
|
98
|
-
await fx.send(
|
|
124
|
+
await fx.send(linkCreatedMail, {
|
|
99
125
|
to: "alice@example.com",
|
|
100
|
-
data: {
|
|
126
|
+
data: {
|
|
127
|
+
id: "lnk_1",
|
|
128
|
+
code: "ok",
|
|
129
|
+
url: "https://example.com",
|
|
130
|
+
shortUrl: "http://localhost:6530/ok",
|
|
131
|
+
},
|
|
101
132
|
});
|
|
102
133
|
```
|
|
103
134
|
|
|
@@ -105,23 +136,22 @@ await fx.send(noteCreatedMail, {
|
|
|
105
136
|
|
|
106
137
|
<Tab value="Catalog">
|
|
107
138
|
|
|
108
|
-
Bodies are `{{field}}` strings per locale — not ICU, not React.
|
|
109
|
-
plugin `.channelCatalog(…)
|
|
110
|
-
|
|
111
|
-
```typescript title="src/
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
},
|
|
139
|
+
Bodies are `{{field}}` strings per locale — not ICU, not React. Put them on the template.
|
|
140
|
+
`oke({ channel.catalog })` and plugin `.channelCatalog(…)` are overlays (later locale wins):
|
|
141
|
+
|
|
142
|
+
```typescript title="src/core/email.ts"
|
|
143
|
+
export const linkCreatedMail = mail.template("link-created", {
|
|
144
|
+
locales: ["en"],
|
|
145
|
+
schema: z.object({
|
|
146
|
+
code: z.string(),
|
|
147
|
+
url: z.string(),
|
|
148
|
+
shortUrl: z.string(),
|
|
149
|
+
}),
|
|
150
|
+
catalog: {
|
|
151
|
+
en: {
|
|
152
|
+
subject: "Short link {{code}}",
|
|
153
|
+
text: "{{shortUrl}} → {{url}}",
|
|
154
|
+
html: '<p><a href="{{shortUrl}}">{{code}}</a> → {{url}}</p>',
|
|
125
155
|
},
|
|
126
156
|
},
|
|
127
157
|
});
|
|
@@ -136,9 +166,14 @@ Precedence: explicit `locale` → `profileLocale` → `Accept-Language` →
|
|
|
136
166
|
`fx.locale`.
|
|
137
167
|
|
|
138
168
|
```typescript
|
|
139
|
-
await fx.send(
|
|
169
|
+
await fx.send(linkCreatedMail, {
|
|
140
170
|
to: "alice@example.com",
|
|
141
|
-
data: {
|
|
171
|
+
data: {
|
|
172
|
+
id: "lnk_1",
|
|
173
|
+
code: "ok",
|
|
174
|
+
url: "https://example.com",
|
|
175
|
+
shortUrl: "http://localhost:6530/ok",
|
|
176
|
+
},
|
|
142
177
|
locale: "ar",
|
|
143
178
|
});
|
|
144
179
|
```
|
|
@@ -154,9 +189,14 @@ Order same-medium drivers for failover. Provider / 5xx errors advance; permanent
|
|
|
154
189
|
(400, invalid address) do **not**:
|
|
155
190
|
|
|
156
191
|
```typescript
|
|
157
|
-
await fx.send(
|
|
192
|
+
await fx.send(linkCreatedMail, {
|
|
158
193
|
to: "alice@example.com",
|
|
159
|
-
data: {
|
|
194
|
+
data: {
|
|
195
|
+
id: "lnk_1",
|
|
196
|
+
code: "ok",
|
|
197
|
+
url: "https://example.com",
|
|
198
|
+
shortUrl: "http://localhost:6530/ok",
|
|
199
|
+
},
|
|
160
200
|
via: ["smtp", "resend"],
|
|
161
201
|
});
|
|
162
202
|
```
|
|
@@ -193,12 +233,14 @@ succeeded — every attempt is kept on the receipt.
|
|
|
193
233
|
| `description` | `string` | name | Console / docs label |
|
|
194
234
|
| `locales` | `string[]` | — | Declared locale tags for the template |
|
|
195
235
|
| `schema` | Schema | — | Payload shape (Zod / Standard Schema) |
|
|
236
|
+
| `catalog` | object | — | Per-locale `subject` / `text` / `html` |
|
|
196
237
|
| `from` | `string` | binder’s | Override sender on agnostic `channel.template` only |
|
|
197
238
|
| `medium` | medium | `"email"` | Only on `channel.template()` |
|
|
198
239
|
|
|
199
240
|
Empty name throws `TypeError: channel.template: name is required`.
|
|
200
241
|
|
|
201
|
-
|
|
242
|
+
`oke({ channel.catalog })` overlays bodies after template catalogs (tests / plugin copy
|
|
243
|
+
overrides). Manifest stays free of copy.
|
|
202
244
|
|
|
203
245
|
## `fx` surface
|
|
204
246
|
|
|
@@ -295,7 +337,7 @@ Env knobs: [Environment variables](/docs/reference/environment-variables). Local
|
|
|
295
337
|
|
|
296
338
|
<Accordion title="OKE1004 — UNDECLARED_SEND">
|
|
297
339
|
Cause: `Flow "{flow}" sends "{resource}" without declaring it.` Touch the template handle
|
|
298
|
-
inside `do` (inference) or list `effects: { sends: ["
|
|
340
|
+
inside `do` (inference) or list `effects: { sends: ["link-created"] }`.
|
|
299
341
|
</Accordion>
|
|
300
342
|
|
|
301
343
|
<Accordion title="channel: no email transport in driver chain">
|
|
@@ -37,6 +37,9 @@ export const orderPush = push.template("order.status", {
|
|
|
37
37
|
title: z.string(),
|
|
38
38
|
body: z.string(),
|
|
39
39
|
}),
|
|
40
|
+
catalog: {
|
|
41
|
+
en: { subject: "{{title}}", text: "{{body}}" },
|
|
42
|
+
},
|
|
40
43
|
});
|
|
41
44
|
```
|
|
42
45
|
|
|
@@ -59,11 +62,6 @@ export const app = oke({
|
|
|
59
62
|
privateKey: process.env.FCM_PRIVATE_KEY,
|
|
60
63
|
}),
|
|
61
64
|
],
|
|
62
|
-
catalog: {
|
|
63
|
-
"order.status": {
|
|
64
|
-
en: { subject: "{{title}}", text: "{{body}}" },
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
65
|
},
|
|
68
66
|
});
|
|
69
67
|
```
|
|
@@ -128,13 +126,13 @@ Web Push where the subscription is supplied on the runtime path.
|
|
|
128
126
|
<Tab value="Catalog">
|
|
129
127
|
|
|
130
128
|
```typescript
|
|
131
|
-
|
|
129
|
+
catalog: {
|
|
132
130
|
en: { subject: "{{title}}", text: "{{body}}" },
|
|
133
131
|
}
|
|
134
132
|
```
|
|
135
133
|
|
|
136
|
-
`subject` becomes the notification title on FCM; `text`
|
|
137
|
-
through as FCM data payload when present.
|
|
134
|
+
Put that on `push.template({ catalog })`. `subject` becomes the notification title on FCM; `text`
|
|
135
|
+
is the body. Extra `data` fields pass through as FCM data payload when present.
|
|
138
136
|
|
|
139
137
|
</Tab>
|
|
140
138
|
|
|
@@ -163,7 +161,7 @@ Config key `drivers.channel.push` exists for Manifest / tooling ids (`console`
|
|
|
163
161
|
|
|
164
162
|
### `.template(name, options?)`
|
|
165
163
|
|
|
166
|
-
`description` · `locales` · `schema` — same as other mediums.
|
|
164
|
+
`description` · `locales` · `schema` · `catalog` — same as other mediums.
|
|
167
165
|
|
|
168
166
|
## Troubleshooting
|
|
169
167
|
|
|
@@ -52,6 +52,7 @@ const sms = channel.sms({ sender: "ACME" });
|
|
|
52
52
|
export const orderShippedSms = sms.template("order.shipped", {
|
|
53
53
|
locales: ["en"],
|
|
54
54
|
schema: z.object({ tracking: z.string() }),
|
|
55
|
+
catalog: { en: { text: "Shipped — track {{tracking}}" } },
|
|
55
56
|
});
|
|
56
57
|
```
|
|
57
58
|
|
|
@@ -62,7 +63,7 @@ await fx.send(orderShippedSms, {
|
|
|
62
63
|
});
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
Missing `catalog` falls back to `subject: templateName` and `text: JSON.stringify(data)`.
|
|
66
67
|
|
|
67
68
|
</Step>
|
|
68
69
|
|
|
@@ -102,6 +103,7 @@ Transactional SMS through the same `fx.send` path as email:
|
|
|
102
103
|
const sms = channel.sms({ from: "ACME" });
|
|
103
104
|
export const alertSms = sms.template("ops.alert", {
|
|
104
105
|
schema: z.object({ message: z.string() }),
|
|
106
|
+
catalog: { en: { text: "{{message}}" } },
|
|
105
107
|
});
|
|
106
108
|
|
|
107
109
|
await fx.send(alertSms, { to: phone, data: { message: "Disk 90%" } });
|
|
@@ -202,7 +204,7 @@ Same medium options as email: `from` / `sender`.
|
|
|
202
204
|
|
|
203
205
|
### `.template(name, options?)`
|
|
204
206
|
|
|
205
|
-
`description` · `locales` · `schema`
|
|
207
|
+
`description` · `locales` · `schema` · `catalog` — `text` is typical for SMS.
|
|
206
208
|
|
|
207
209
|
## Troubleshooting
|
|
208
210
|
|
|
@@ -229,8 +231,8 @@ Same medium options as email: `from` / `sender`.
|
|
|
229
231
|
</Accordion>
|
|
230
232
|
|
|
231
233
|
<Accordion title="channel: otp delivery failed on all channels">
|
|
232
|
-
Every medium in the failover list failed. Check driver credentials, suppression, and
|
|
233
|
-
|
|
234
|
+
Every medium in the failover list failed. Check driver credentials, suppression, and `catalog` on
|
|
235
|
+
each `.template()`.
|
|
234
236
|
</Accordion>
|
|
235
237
|
|
|
236
238
|
</Accordions>
|
|
@@ -53,6 +53,10 @@ const wa = channel.whatsapp();
|
|
|
53
53
|
export const bookingReminder = wa.template("booking.reminder", {
|
|
54
54
|
locales: ["en", "ar"],
|
|
55
55
|
schema: z.object({ when: z.string(), place: z.string() }),
|
|
56
|
+
catalog: {
|
|
57
|
+
en: { text: "Reminder: {{when}} at {{place}}" },
|
|
58
|
+
ar: { text: "تذكير: {{when}} — {{place}}" },
|
|
59
|
+
},
|
|
56
60
|
});
|
|
57
61
|
```
|
|
58
62
|
|
|
@@ -85,14 +89,12 @@ and SMS.
|
|
|
85
89
|
Catalog typically supplies `text` (and optional `subject` where the transport uses it):
|
|
86
90
|
|
|
87
91
|
```typescript
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
},
|
|
95
|
-
},
|
|
92
|
+
export const bookingReminder = wa.template("booking.reminder", {
|
|
93
|
+
locales: ["en", "ar"],
|
|
94
|
+
schema: z.object({ when: z.string(), place: z.string() }),
|
|
95
|
+
catalog: {
|
|
96
|
+
en: { text: "Reminder: {{when}} at {{place}}" },
|
|
97
|
+
ar: { text: "تذكير: {{when}} — {{place}}" },
|
|
96
98
|
},
|
|
97
99
|
});
|
|
98
100
|
```
|
|
@@ -165,7 +167,7 @@ oke boot: unknown whatsapp channel driver "…"
|
|
|
165
167
|
|
|
166
168
|
### `.template(name, options?)`
|
|
167
169
|
|
|
168
|
-
`description` · `locales` · `schema` — same as other mediums.
|
|
170
|
+
`description` · `locales` · `schema` · `catalog` — same as other mediums.
|
|
169
171
|
|
|
170
172
|
## Troubleshooting
|
|
171
173
|
|
|
@@ -21,32 +21,19 @@ For developers scheduling work on okengine — one handle shape; drivers swap by
|
|
|
21
21
|
<Steps>
|
|
22
22
|
|
|
23
23
|
<Step>
|
|
24
|
-
### Declare
|
|
24
|
+
### Declare and bind in the unit
|
|
25
25
|
|
|
26
|
-
```typescript title="src/
|
|
27
|
-
import { clock } from "okengine";
|
|
28
|
-
|
|
29
|
-
export const digestClock = clock.every("notes.digest", "1d");
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
</Step>
|
|
33
|
-
|
|
34
|
-
<Step>
|
|
35
|
-
### Bind a Flow
|
|
26
|
+
```typescript title="src/flows/links/expire.ts"
|
|
27
|
+
import { on, flow, clock } from "okengine/http";
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
import { on, flow } from "okengine";
|
|
39
|
-
import { digestClock } from "@/clocks/digest";
|
|
40
|
-
import { db, notes } from "@/schema";
|
|
41
|
-
import { isNull } from "drizzle-orm";
|
|
29
|
+
const expireClock = clock.every("links.expire", "1h");
|
|
42
30
|
|
|
43
|
-
export const
|
|
44
|
-
|
|
45
|
-
flow(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return { active: rows.length, at: new Date(fx.clock.now()).toISOString() };
|
|
31
|
+
export const expire = on(
|
|
32
|
+
expireClock,
|
|
33
|
+
flow({
|
|
34
|
+
do: async (_input, fx) => {
|
|
35
|
+
const now = fx.clock.now();
|
|
36
|
+
return { archived: 0, at: new Date(now).toISOString() };
|
|
50
37
|
},
|
|
51
38
|
}),
|
|
52
39
|
);
|
|
@@ -57,9 +44,11 @@ export const digest = on(
|
|
|
57
44
|
<Step>
|
|
58
45
|
### See it tick
|
|
59
46
|
|
|
60
|
-
With `oke dev`, the scheduler reconciles `
|
|
47
|
+
With `oke dev`, the scheduler reconciles `links.expire` into the Store and leader-elects
|
|
61
48
|
before each fire. `do` receives no payload — read time with `fx.clock.now()` (epoch-ms).
|
|
62
|
-
|
|
49
|
+
|
|
50
|
+
Pass that number into SQL `timestamp` columns as-is — insert, update, and WHERE binds
|
|
51
|
+
coerce to `Date`. For a hand-built ISO reply, use `new Date(fx.clock.now()).toISOString()`.
|
|
63
52
|
|
|
64
53
|
Under `drivers.clock.test = "frozen"`, advance time in tests instead of waiting a day.
|
|
65
54
|
|