okengine 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/site/content/docs/elements/ai.mdx +82 -1
- package/site/content/docs/elements/channel.mdx +6 -1
- package/site/content/docs/elements/flow.mdx +20 -17
- package/site/content/docs/get-started/why.mdx +10 -10
- package/site/content/docs/plugins/email-otp.mdx +25 -19
- package/site/content/docs/plugins/headers.mdx +10 -10
- package/site/content/docs/plugins/magic-link.mdx +27 -21
- package/site/content/docs/plugins/passkey.mdx +36 -24
- package/site/content/docs/plugins/two-factor.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +7 -0
- package/site/content/docs/reference/environment-variables.mdx +10 -5
- package/site/content/docs/reference/errors.mdx +14 -0
- package/site/content/docs/reference/fx.mdx +68 -16
- package/site/content/docs/reference/i18n.mdx +313 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +1 -0
- package/site/content/docs/reference/plugins.mdx +1 -0
- package/src/auth/auth.test.ts +3 -0
- package/src/auth/bindings.ts +1 -1
- package/src/auth/constant-time.ts +22 -0
- package/src/auth/index.ts +2 -0
- package/src/auth/method-context.ts +12 -2
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/compiler/aot.test.ts +16 -13
- package/src/compiler/effects-infer.ts +46 -0
- package/src/console/server/ai.test.ts +34 -5
- package/src/docker/compose.ts +9 -0
- package/src/docker/docker.test.ts +39 -0
- package/src/docker/index.ts +11 -1
- package/src/docker/recipes/index.ts +3 -1
- package/src/docker/recipes/ollama.ts +43 -0
- package/src/docker/stack-id.ts +2 -0
- package/src/drivers/ai-mock.ts +60 -0
- package/src/drivers/ai-ollama-tools.integration.test.ts +107 -0
- package/src/drivers/ai-ollama.integration.test.ts +197 -0
- package/src/drivers/ai-ollama.ts +327 -0
- package/src/drivers/ai-openai-compatible.ts +211 -21
- package/src/drivers/ai-providers.test.ts +179 -2
- package/src/drivers/ai-stream.test.ts +195 -0
- package/src/drivers/ai-types.ts +42 -1
- package/src/drivers/channel-smtp.ts +8 -2
- package/src/drivers/index.ts +21 -1
- package/src/drivers/ollama.ts +14 -0
- package/src/elements/ai/rate.test.ts +53 -0
- package/src/elements/ai/rate.ts +66 -0
- package/src/elements/ai/redacted-prompt.test.ts +90 -0
- package/src/elements/ai/runtime.ts +330 -100
- package/src/elements/ai/tools.test.ts +99 -0
- package/src/elements/ai.test.ts +26 -2
- package/src/elements/ai.ts +10 -1
- package/src/i18n/catalogs/ar.ts +67 -0
- package/src/i18n/catalogs/en.ts +68 -0
- package/src/i18n/failure-message.test.ts +56 -0
- package/src/i18n/failure-message.ts +93 -0
- package/src/i18n/format.ts +67 -0
- package/src/i18n/index.ts +57 -0
- package/src/i18n/locale-context.ts +48 -0
- package/src/i18n/messages.test.ts +173 -0
- package/src/i18n/messages.ts +169 -0
- package/src/i18n/types.ts +90 -0
- package/src/index.ts +26 -0
- package/src/kernel/app.ts +92 -2
- package/src/kernel/boot-bind/ai.test.ts +60 -0
- package/src/kernel/boot-bind/ai.ts +125 -2
- package/src/kernel/boot.test.ts +4 -3
- package/src/kernel/boot.ts +1 -1
- package/src/kernel/errors.ts +56 -5
- package/src/kernel/fx.test.ts +27 -0
- package/src/kernel/fx.ts +74 -18
- package/src/kernel/pipeline.test.ts +4 -0
- package/src/kernel/pipeline.ts +1 -1
- package/src/kernel/plugin.ts +16 -0
- package/src/kernel/registry.ts +15 -0
- package/src/plugins/auth/shared.ts +5 -1
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +330 -0
- package/src/plugins/auth-methods.security.test.ts +764 -0
- package/src/plugins/compression.ts +1 -1
- package/src/plugins/config-source.test.ts +11 -11
- package/src/plugins/config-source.ts +2 -2
- package/src/plugins/cors.ts +1 -1
- package/src/plugins/email-otp.ts +54 -1
- package/src/plugins/{security-headers.test.ts → headers.test.ts} +18 -18
- package/src/plugins/headers.ts +240 -41
- package/src/plugins/index.ts +27 -5
- package/src/plugins/magic-link.ts +63 -3
- package/src/plugins/passkey-webauthn.ts +217 -0
- package/src/plugins/passkey.ts +99 -33
- package/src/plugins/response-headers.ts +54 -0
- package/src/plugins/two-factor.ts +6 -2
- package/src/plugins/username-policy.test.ts +302 -0
- package/src/plugins/username.ts +290 -9
- package/src/release/measure.ts +8 -1
- package/src/plugins/security-headers.ts +0 -255
|
@@ -7,6 +7,7 @@ source: "docs/spec/unified-theory.md"
|
|
|
7
7
|
|
|
8
8
|
`twoFactor()` adds RFC 6238 TOTP (HMAC-SHA1, 6 digits, 30s) after someone already has a session.
|
|
9
9
|
Enable returns a secret, `otpauth://` URL, and recovery codes; verify mints a new session.
|
|
10
|
+
TOTP codes are compared in constant time (±1 step window).
|
|
10
11
|
|
|
11
12
|
<Callout title="The one rule">
|
|
12
13
|
Enable `gate.auth`, then `.plug(twoFactor())`. Enable and disable need a Bearer session —
|
|
@@ -103,7 +104,7 @@ enable. A recovery code works once, then is consumed.
|
|
|
103
104
|
|
|
104
105
|
## Learn more
|
|
105
106
|
|
|
106
|
-
- [Passkey](/docs/plugins/passkey) — WebAuthn
|
|
107
|
+
- [Passkey](/docs/plugins/passkey) — WebAuthn register / authenticate
|
|
107
108
|
- [Gate](/docs/elements/gate) — session + policies
|
|
108
109
|
- [Username](/docs/plugins/username) — first factor to enroll against
|
|
109
110
|
|
|
@@ -76,9 +76,12 @@ images: {
|
|
|
76
76
|
"store.files": "rustfs/rustfs:1.0.0-beta.11",
|
|
77
77
|
"channel.email": "axllent/mailpit:v1.22.3",
|
|
78
78
|
vault: "openbao/openbao:2.6.1",
|
|
79
|
+
ai: "ollama/ollama:latest",
|
|
79
80
|
},
|
|
80
81
|
```
|
|
81
82
|
|
|
83
|
+
For `ai`, the Ollama recipe pulls `OKE_AI_MODEL` (default `qwen3.5:9b` — a balanced local-dev starting point; override freely; on Apple Silicon prefer `qwen3.5:9b-mlx`) and writes `OKE_AI_URL`.
|
|
84
|
+
|
|
82
85
|
## i18n
|
|
83
86
|
|
|
84
87
|
| Option | Type | Default | Meaning |
|
|
@@ -91,6 +94,10 @@ images: {
|
|
|
91
94
|
i18n: { locales: ["en", "ar"], default: "en", dir: { ar: "rtl" } },
|
|
92
95
|
```
|
|
93
96
|
|
|
97
|
+
`fx.t` catalogs are ICU strings from `defineLocale` / `defineMessages` — see
|
|
98
|
+
[i18n](/docs/reference/i18n). Built-ins also cover failure `error.message` and
|
|
99
|
+
OKE cause/fix; Channel templates keep separate `locales` + `{{field}}` bodies.
|
|
100
|
+
|
|
94
101
|
## tenancy
|
|
95
102
|
|
|
96
103
|
Multi-tenant isolation policy.
|
|
@@ -72,11 +72,16 @@ Written by the first-boot bootstrap; you normally only set these when pointing a
|
|
|
72
72
|
|
|
73
73
|
## AI providers
|
|
74
74
|
|
|
75
|
-
| Variable | Used for
|
|
76
|
-
| ------------------- |
|
|
77
|
-
| `ANTHROPIC_API_KEY` | `anthropic` driver credential
|
|
78
|
-
| `ANTHROPIC_MODEL` | Model override for the anthropic driver |
|
|
79
|
-
| `OPENAI_API_KEY` | `openai-compatible` driver credential
|
|
75
|
+
| Variable | Used for | Default when unset |
|
|
76
|
+
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
|
|
77
|
+
| `ANTHROPIC_API_KEY` | `anthropic` driver credential | — |
|
|
78
|
+
| `ANTHROPIC_MODEL` | Model override for the anthropic driver | — |
|
|
79
|
+
| `OPENAI_API_KEY` | `openai-compatible` driver credential | — |
|
|
80
|
+
| `OPENAI_BASE_URL` | `openai-compatible` base URL | OpenAI cloud |
|
|
81
|
+
| `OKE_AI_DRIVER` | Force the AI driver id (honoured in docker mode) | config map |
|
|
82
|
+
| `OKE_AI_URL` | Ollama (or openai-compatible) base URL | `http://127.0.0.1:11434` (ollama) |
|
|
83
|
+
| `OKE_AI_MODEL` | Default model for `ollama` (any pulled model; `qwen3.5:9b` is a balanced local-dev starting point; on Apple Silicon try `qwen3.5:9b-mlx`) | `qwen3.5:9b` |
|
|
84
|
+
| `OLLAMA_HOST` | Alternate Ollama host (`host:port` or URL) when `OKE_AI_URL` is unset | — |
|
|
80
85
|
|
|
81
86
|
## Framework behavior
|
|
82
87
|
|
|
@@ -14,6 +14,19 @@ OKE1101 domain table not found — migrations have not been applied.
|
|
|
14
14
|
|
|
15
15
|
Codes are permanent once published — you can match on them safely across upgrades.
|
|
16
16
|
|
|
17
|
+
## Localized messages
|
|
18
|
+
|
|
19
|
+
Typed failures and OKE codes ship English and Arabic ICU catalogs. Locale comes
|
|
20
|
+
from `Accept-Language` (matched to `i18n.locales`); fallback is `i18n.default`.
|
|
21
|
+
|
|
22
|
+
| Surface | Keys | How it appears |
|
|
23
|
+
| ------------------ | ------------------------------------------ | ---------------------------------------- |
|
|
24
|
+
| `fx.fail` / `fail` | `errors.{code}` · `errors.{code}.{reason}` | Optional `error.message` on the envelope |
|
|
25
|
+
| Thrown `OkeError` | `oke.{code}.cause` · `oke.{code}.fix` | Cause + fix lines in the thrown message |
|
|
26
|
+
|
|
27
|
+
Override via `defineLocale`. Pass `fail(code, data, { message })` for a custom
|
|
28
|
+
string. Custom app codes stay message-less until registered.
|
|
29
|
+
|
|
17
30
|
## OKE numeric codes
|
|
18
31
|
|
|
19
32
|
| Code | Name | Cause | Fix |
|
|
@@ -75,6 +88,7 @@ Thrown by specific subsystems — each names its own cause:
|
|
|
75
88
|
|
|
76
89
|
## Learn more
|
|
77
90
|
|
|
91
|
+
- [i18n](/docs/reference/i18n) — catalogs, `fx.t`, locale matching
|
|
78
92
|
- [Flow](/docs/elements/flow) — `fx.fail` and the response envelope
|
|
79
93
|
- [Gate](/docs/elements/gate) — where the three denials come from
|
|
80
94
|
- [CLI Reference](/docs/reference/cli) — `oke db migrate` and friends
|
|
@@ -102,20 +102,23 @@ const rows = await fx.using(
|
|
|
102
102
|
|
|
103
103
|
## Channel
|
|
104
104
|
|
|
105
|
-
| Signature
|
|
106
|
-
|
|
|
107
|
-
| `fx.send(template, { to?, data?, via
|
|
105
|
+
| Signature | Records | Notes |
|
|
106
|
+
| -------------------------------------------- | ------- | ------------------------------------------------------------------------------------------ |
|
|
107
|
+
| `fx.send(template, { to?, data?, via?, … })` | `send` | `via` orders fallback; `locale` / `profileLocale` / `acceptLanguage` feed the locale chain |
|
|
108
|
+
|
|
109
|
+
Omit locale opts and the send uses `fx.locale`. Dry runs record _would have fired_ and never
|
|
110
|
+
contact a provider. Channel bodies use `{{field}}` catalogs — not ICU (see [Channel](/docs/elements/channel)).
|
|
108
111
|
|
|
109
112
|
## AI
|
|
110
113
|
|
|
111
|
-
| Signature
|
|
112
|
-
|
|
|
113
|
-
| `fx.ask(prompt, input?, { via? })`
|
|
114
|
-
| `fx.run(agent, input?)`
|
|
115
|
-
| `fx.stream(model, { prompt?, data? })`
|
|
116
|
-
| `fx.search(embed, query, { topK? })`
|
|
114
|
+
| Signature | Records | Returns |
|
|
115
|
+
| ----------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------- |
|
|
116
|
+
| `fx.ask(prompt, input?, { via?, tools?, maxSteps? })` | `ask` (+ `call` per tool) | Object validated against the prompt's `out` |
|
|
117
|
+
| `fx.run(agent, input?)` | `ask` | Agent result |
|
|
118
|
+
| `fx.stream(model, { prompt?, data? })` | `ask` | `AsyncIterable<string>` — real driver stream; cancels via ambient `fx.signal` |
|
|
119
|
+
| `fx.search(embed, query, { topK? })` | `read` | Matches from the index/embed |
|
|
117
120
|
|
|
118
|
-
AI calls are nondeterministic: journaling is forced on and auto-cache disabled around them.
|
|
121
|
+
AI calls are nondeterministic: journaling is forced on and auto-cache disabled around them. `tools` are Flow refs — each model tool call goes through `fx.call` (same capability and Runs path).
|
|
119
122
|
|
|
120
123
|
## Vault
|
|
121
124
|
|
|
@@ -153,11 +156,59 @@ Returning a plain value instead answers 200 with `{ data: value, error: null }`
|
|
|
153
156
|
|
|
154
157
|
## Logging, i18n, ids
|
|
155
158
|
|
|
156
|
-
| Signature | Notes
|
|
157
|
-
| ------------------------------------------ |
|
|
158
|
-
| `fx.log.debug/info/warn/error(msg, data?)` | Redacting — secrets print as `***`
|
|
159
|
-
| `fx.t(key,
|
|
160
|
-
| `fx.
|
|
159
|
+
| Signature | Notes |
|
|
160
|
+
| ------------------------------------------ | ----------------------------------------------------------- |
|
|
161
|
+
| `fx.log.debug/info/warn/error(msg, data?)` | Redacting — secrets print as `***` |
|
|
162
|
+
| `fx.t(key, values?)` | ICU MessageFormat — active locale → `i18n.default` → key |
|
|
163
|
+
| `fx.locale` | Active locale (`Accept-Language` matched to `i18n.locales`) |
|
|
164
|
+
| `fx.id()` | UUID — the only legal id generator |
|
|
165
|
+
|
|
166
|
+
`fx.t` uses [ICU MessageFormat](https://unicode-org.github.io/icu/userguide/format_parse/messages/)
|
|
167
|
+
(FormatJS): `{name}`, plurals, `select` / `selectordinal`, rich-text tags.
|
|
168
|
+
Full syntax and escaping: [i18n · ICU](/docs/reference/i18n#icu-messageformat).
|
|
169
|
+
|
|
170
|
+
Register catalogs with `defineLocale` before boot (nested trees → dot keys). Augment
|
|
171
|
+
`Register` so keys autocomplete and typos fail at compile time:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { defineLocale, defineMessages, type MessagesFor } from "okengine";
|
|
175
|
+
|
|
176
|
+
export const en = defineMessages({
|
|
177
|
+
greeting: "Hello, {name}",
|
|
178
|
+
items: "{count, plural, one {# item} other {# items}}",
|
|
179
|
+
place: "You finished {place, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}!",
|
|
180
|
+
status: "{status, select, online {Online} offline {Offline} other {Unknown}}",
|
|
181
|
+
cta: "Read <docs>the docs</docs>",
|
|
182
|
+
});
|
|
183
|
+
defineLocale("en", en);
|
|
184
|
+
|
|
185
|
+
defineLocale("ar", {
|
|
186
|
+
greeting: "مرحباً، {name}",
|
|
187
|
+
items: "{count, plural, zero {لا عناصر} one {عنصر واحد} other {# عناصر}}",
|
|
188
|
+
place: "أنهيت في المرتبة {place, selectordinal, other {#}}!",
|
|
189
|
+
status: "{status, select, online {متصل} offline {غير متصل} other {غير معروف}}",
|
|
190
|
+
cta: "اقرأ <docs>التوثيق</docs>",
|
|
191
|
+
} satisfies MessagesFor<typeof en>);
|
|
192
|
+
|
|
193
|
+
declare module "okengine" {
|
|
194
|
+
interface Register {
|
|
195
|
+
messages: typeof en;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
fx.t("greeting", { name: input.name });
|
|
202
|
+
fx.t("items", { count: 3 });
|
|
203
|
+
fx.t("cta", { docs: (chunks) => `<a href="/docs">${chunks.join("")}</a>` });
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Supported locales and the fallback come from `oke.config.ts` `i18n`.
|
|
207
|
+
`create-oke` scaffolds English and Arabic locale modules already wired this way.
|
|
208
|
+
|
|
209
|
+
Built-in `errors.*` / `oke.*` catalogs localize `fx.fail` messages and thrown
|
|
210
|
+
`OkeError` cause/fix the same way — full i18n guide:
|
|
211
|
+
[i18n](/docs/reference/i18n), [Errors](/docs/reference/errors).
|
|
161
212
|
|
|
162
213
|
## Principals
|
|
163
214
|
|
|
@@ -181,5 +232,6 @@ must log who started the call chain.
|
|
|
181
232
|
## Learn more
|
|
182
233
|
|
|
183
234
|
- [Flow](/docs/elements/flow) — why `fx` is the only door
|
|
235
|
+
- [Channel](/docs/elements/channel) — `fx.send`, consent, locale chain, `{{field}}` catalogs
|
|
184
236
|
- [Errors](/docs/reference/errors) — what `fx.fail` produces
|
|
185
|
-
- [Configuration](/docs/reference/configuration) —
|
|
237
|
+
- [Configuration](/docs/reference/configuration) — drivers and the `i18n` block
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "i18n"
|
|
3
|
+
description: "ICU message catalogs for fx.t, typed keys, request locale, and how failures and channels pick a language."
|
|
4
|
+
icon: "Languages"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
App copy lives in message catalogs — greetings, plurals, and status lines you
|
|
9
|
+
format inside a Flow with `fx.t`. Configure supported locales once in
|
|
10
|
+
`oke.config.ts`; the request's `Accept-Language` picks the active tag.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Register catalogs with `defineLocale` before boot, list every locale in `i18n.locales`, and call
|
|
14
|
+
`fx.t(key, values?)` for Flow copy. Channel emails use a separate `{{ field }}` catalog — not ICU.
|
|
15
|
+
</Callout>
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
<Steps>
|
|
20
|
+
|
|
21
|
+
<Step>
|
|
22
|
+
### Configure locales
|
|
23
|
+
|
|
24
|
+
```typescript title="oke.config.ts"
|
|
25
|
+
i18n: { locales: ["en", "ar"], default: "en", dir: { ar: "rtl" } },
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If `i18n` is omitted, boot still defaults to `locales: ["en", "ar"]` and
|
|
29
|
+
`default: "en"`.
|
|
30
|
+
|
|
31
|
+
</Step>
|
|
32
|
+
|
|
33
|
+
<Step>
|
|
34
|
+
### Register catalogs
|
|
35
|
+
|
|
36
|
+
Side-effect import locale modules before boot (the starter already does this
|
|
37
|
+
from `src/app.ts`):
|
|
38
|
+
|
|
39
|
+
```typescript title="src/locales/en.ts"
|
|
40
|
+
import { defineMessages, defineLocale } from "okengine";
|
|
41
|
+
|
|
42
|
+
export const en = defineMessages({
|
|
43
|
+
greeting: "Hello, {name}",
|
|
44
|
+
items: "{count, plural, one {# item} other {# items}}",
|
|
45
|
+
errors: { notFound: "Not found" },
|
|
46
|
+
});
|
|
47
|
+
defineLocale("en", en);
|
|
48
|
+
|
|
49
|
+
declare module "okengine" {
|
|
50
|
+
interface Register {
|
|
51
|
+
messages: typeof en;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```typescript title="src/locales/ar.ts"
|
|
57
|
+
import { defineLocale, type MessagesFor } from "okengine";
|
|
58
|
+
import { en } from "./en";
|
|
59
|
+
|
|
60
|
+
defineLocale("ar", {
|
|
61
|
+
greeting: "مرحباً، {name}",
|
|
62
|
+
items: "{count, plural, zero {لا عناصر} one {عنصر واحد} other {# عناصر}}",
|
|
63
|
+
errors: { notFound: "غير موجود" },
|
|
64
|
+
} satisfies MessagesFor<typeof en>);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
</Step>
|
|
68
|
+
|
|
69
|
+
<Step>
|
|
70
|
+
### Use `fx.t` in a Flow
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
do: async (input, fx) => {
|
|
74
|
+
return {
|
|
75
|
+
text: fx.t("greeting", { name: input.name }),
|
|
76
|
+
countLabel: fx.t("items", { count: input.count }),
|
|
77
|
+
locale: fx.locale,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Send `Accept-Language: ar` (or `ar-SA`) against `locales: ["en", "ar"]` and
|
|
83
|
+
`fx.locale` is `"ar"`. Missing keys fall back through `i18n.default`, then the
|
|
84
|
+
key string itself.
|
|
85
|
+
|
|
86
|
+
</Step>
|
|
87
|
+
|
|
88
|
+
</Steps>
|
|
89
|
+
|
|
90
|
+
## Config (`i18n`)
|
|
91
|
+
|
|
92
|
+
| Option | Type | Default (when omitted) | Meaning |
|
|
93
|
+
| --------- | ---------- | ---------------------- | ------------------------------------------- |
|
|
94
|
+
| `locales` | `string[]` | `["en", "ar"]` | Tags matched against `Accept-Language` |
|
|
95
|
+
| `default` | string | `"en"` | Fallback for `fx.t`, Channel, fail messages |
|
|
96
|
+
| `dir` | record | — | Per-locale direction: `"ltr"` \| `"rtl"` |
|
|
97
|
+
|
|
98
|
+
Matching: exact tag → language subtag (`ar-SA` → `ar`) → `default`.
|
|
99
|
+
|
|
100
|
+
## `fx.t` and `fx.locale`
|
|
101
|
+
|
|
102
|
+
| Signature | Notes |
|
|
103
|
+
| -------------------- | -------------------------------------------------------- |
|
|
104
|
+
| `fx.t(key, values?)` | ICU MessageFormat — active locale → `i18n.default` → key |
|
|
105
|
+
| `fx.locale` | Active BCP 47 tag for this run |
|
|
106
|
+
|
|
107
|
+
Nested trees flatten to dot keys (`errors.notFound`). App overlays win over
|
|
108
|
+
built-in keys for the same locale.
|
|
109
|
+
|
|
110
|
+
## ICU MessageFormat
|
|
111
|
+
|
|
112
|
+
`fx.t` formats catalog strings with
|
|
113
|
+
[ICU MessageFormat](https://unicode-org.github.io/icu/userguide/format_parse/messages/)
|
|
114
|
+
(FormatJS). The active locale drives plural/select rules — English `one`/`other` vs Arabic `zero`/`two`/`few`/`many` on the same key.
|
|
115
|
+
|
|
116
|
+
| Feature | Syntax sketch | `values` |
|
|
117
|
+
| --------------- | --------------------------------------------------------- | --------------------------- |
|
|
118
|
+
| Interpolation | `Hello, {name}` | `{ name: "Ada" }` |
|
|
119
|
+
| Exact plural | `{count, plural, =0 {none} one {# item} other {# items}}` | `{ count: 0 }` |
|
|
120
|
+
| Cardinal plural | `{count, plural, one {…} other {…}}` | `{ count: number }` |
|
|
121
|
+
| Ordinal | `{place, selectordinal, one {#st} two {#nd} other {#th}}` | `{ place: number }` |
|
|
122
|
+
| Select | `{status, select, online {…} offline {…} other {…}}` | `{ status: "online" }` |
|
|
123
|
+
| Rich-text tag | `Read <docs>the docs</docs>` | `{ docs: (chunks) => "…" }` |
|
|
124
|
+
|
|
125
|
+
`#` inside a plural/ordinal branch is the numeric argument. Always include an
|
|
126
|
+
`other` (or `=N`) branch — ICU requires a fallback.
|
|
127
|
+
|
|
128
|
+
### Interpolation
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// catalog: "Hello, {name}"
|
|
132
|
+
fx.t("greeting", { name: "Ada" }); // → "Hello, Ada"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Values may be `string`, `number`, `boolean`, `Date`, `null` / `undefined`, or a
|
|
136
|
+
rich-text function (below). Missing args leave the source string unformatted.
|
|
137
|
+
|
|
138
|
+
### Plurals (cardinal)
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// en: "{count, plural, =0 {no items} one {# item} other {# items}}"
|
|
142
|
+
fx.t("items", { count: 0 }); // → "no items"
|
|
143
|
+
fx.t("items", { count: 1 }); // → "1 item"
|
|
144
|
+
fx.t("items", { count: 5 }); // → "5 items"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### Arabic cardinals
|
|
148
|
+
|
|
149
|
+
Arabic (`ar`) uses six [CLDR](https://cldr.unicode.org/index/cldr-spec/plural-rules)
|
|
150
|
+
cardinal categories. FormatJS picks the branch from `fx.locale` — an English `one`/`other` skeleton on `ar` misfires for dual, paucal, and hundreds.
|
|
151
|
+
|
|
152
|
+
| Category | When (integers) | Typical form |
|
|
153
|
+
| -------- | -------------------------------------- | ---------------------------------- |
|
|
154
|
+
| `zero` | `n = 0` | No items / special zero phrasing |
|
|
155
|
+
| `one` | `n = 1` | Singular |
|
|
156
|
+
| `two` | `n = 2` | Dual |
|
|
157
|
+
| `few` | `n % 100` in `3…10` (also `103…110` …) | Paucal — often sound plural |
|
|
158
|
+
| `many` | `n % 100` in `11…99` | Accusative / “tamyīz” style counts |
|
|
159
|
+
| `other` | `100…102`, `200…202`, … and fractions | General plural / leftover integers |
|
|
160
|
+
|
|
161
|
+
Write every branch on the Arabic catalog (starter `items` key):
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// ar catalog
|
|
165
|
+
items: "{count, plural, zero {لا عناصر} one {عنصر واحد} two {عنصران} few {# عناصر} many {# عنصراً} other {# عنصر}}";
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// fx.locale === "ar"
|
|
170
|
+
fx.t("items", { count: 0 }); // → "لا عناصر" (zero)
|
|
171
|
+
fx.t("items", { count: 1 }); // → "عنصر واحد" (one)
|
|
172
|
+
fx.t("items", { count: 2 }); // → "عنصران" (two)
|
|
173
|
+
fx.t("items", { count: 5 }); // → "5 عناصر" (few)
|
|
174
|
+
fx.t("items", { count: 11 }); // → "11 عنصراً" (many)
|
|
175
|
+
fx.t("items", { count: 100 }); // → "100 عنصر" (other)
|
|
176
|
+
fx.t("items", { count: 103 }); // → "103 عناصر" (few — 103 % 100 = 3)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Consequence:** copy the six-way shape for Arabic noun counts; do not reuse an
|
|
180
|
+
English `one`/`other` skeleton. `#` still inserts the number inside a branch.
|
|
181
|
+
|
|
182
|
+
### Ordinals (`selectordinal`)
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// "You finished {place, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}!"
|
|
186
|
+
fx.t("place", { place: 1 }); // → "You finished 1st!"
|
|
187
|
+
fx.t("place", { place: 11 }); // → "You finished 11th!"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Select (enums)
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// "{status, select, online {Online} offline {Offline} other {Unknown}}"
|
|
194
|
+
fx.t("status", { status: "online" }); // → "Online"
|
|
195
|
+
fx.t("status", { status: "away" }); // → "Unknown"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Rich-text tags
|
|
199
|
+
|
|
200
|
+
Tags in the message become function values. The function receives the formatted
|
|
201
|
+
inner chunks and returns a string (HTML, Markdown, plain wrappers):
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// catalog: "Read <docs>the docs</docs>"
|
|
205
|
+
fx.t("cta", {
|
|
206
|
+
docs: (chunks) => `<a href="/docs">${chunks.join("")}</a>`,
|
|
207
|
+
});
|
|
208
|
+
// → 'Read <a href="/docs">the docs</a>'
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Escaping
|
|
212
|
+
|
|
213
|
+
| Need | Write |
|
|
214
|
+
| --------------------------- | ------------------------------------------ |
|
|
215
|
+
| Apostrophe in copy | Double it: `this flow''s effects` |
|
|
216
|
+
| Literal `{` / `}` in output | Quote the braces: `'{'optional: true'}'` |
|
|
217
|
+
| Channel-style `{{field}}` | Not ICU — use Channel catalogs, not `fx.t` |
|
|
218
|
+
|
|
219
|
+
Malformed ICU falls back to the raw catalog string (no throw from `fx.t`).
|
|
220
|
+
|
|
221
|
+
## Typed keys
|
|
222
|
+
|
|
223
|
+
Augment `Register` with your English tree so `fx.t` autocompletes and rejects
|
|
224
|
+
typos. Keep other locales aligned with `satisfies MessagesFor<typeof en>`.
|
|
225
|
+
|
|
226
|
+
| Helper | Role |
|
|
227
|
+
| ---------------- | ------------------------------------------------ |
|
|
228
|
+
| `defineMessages` | Preserve a `const` English (or canonical) tree |
|
|
229
|
+
| `defineLocale` | Register / replace a locale's flat catalog |
|
|
230
|
+
| `MessagesFor<T>` | Same key shape as `T`; leaf values are strings |
|
|
231
|
+
| `AppMessageKey` | Flattened key union once `Register` is augmented |
|
|
232
|
+
|
|
233
|
+
## Built-in failure catalogs
|
|
234
|
+
|
|
235
|
+
English and Arabic ship for typed failures and OKE codes — no app registration
|
|
236
|
+
required:
|
|
237
|
+
|
|
238
|
+
| Surface | Keys | Appears as |
|
|
239
|
+
| ------------------ | ------------------------------------------ | -------------------------------- |
|
|
240
|
+
| `fx.fail` / `fail` | `errors.{code}` · `errors.{code}.{reason}` | Optional `error.message` |
|
|
241
|
+
| Thrown `OkeError` | `oke.{code}.cause` · `oke.{code}.fix` | Cause + fix lines in the message |
|
|
242
|
+
|
|
243
|
+
Override any key with `defineLocale`. Pass `fail(code, data, { message })` (or
|
|
244
|
+
`fx.t(...)`) when you need a one-off string. Custom app codes stay message-less
|
|
245
|
+
until registered. Full tables: [Errors](/docs/reference/errors).
|
|
246
|
+
|
|
247
|
+
## Channel catalogs are separate
|
|
248
|
+
|
|
249
|
+
`fx.send` templates use `{{field}}` bodies and their own `locales` list — not
|
|
250
|
+
ICU. Omit `locale` / `profileLocale` / `acceptLanguage` on `fx.send` and the
|
|
251
|
+
send uses `fx.locale`. Details: [Channel](/docs/elements/channel).
|
|
252
|
+
|
|
253
|
+
## Troubleshooting
|
|
254
|
+
|
|
255
|
+
<Accordions>
|
|
256
|
+
<Accordion title="fx.t returns the key string unchanged">
|
|
257
|
+
|
|
258
|
+
No catalog entry for that key in the active locale or `i18n.default`. Register
|
|
259
|
+
it with `defineLocale`, import the locale module before boot, and check the
|
|
260
|
+
flattened key (`errors.notFound`, not `errors: { notFound }`).
|
|
261
|
+
|
|
262
|
+
</Accordion>
|
|
263
|
+
<Accordion title="Response is English despite Accept-Language: ar">
|
|
264
|
+
|
|
265
|
+
The tag must match `i18n.locales` (exact or base language). A request for `fr`
|
|
266
|
+
with only `["en", "ar"]` falls back to `i18n.default`. Confirm the header reaches
|
|
267
|
+
the app (proxies sometimes strip it).
|
|
268
|
+
|
|
269
|
+
</Accordion>
|
|
270
|
+
<Accordion title="Email body is still English while fx.t is Arabic">
|
|
271
|
+
|
|
272
|
+
Channel catalogs are separate `{{field}}` strings. Add an `ar` body on the
|
|
273
|
+
template / plugin catalog; `fx.t` does not translate Channel templates.
|
|
274
|
+
|
|
275
|
+
</Accordion>
|
|
276
|
+
<Accordion title="TypeScript rejects a key that exists at runtime">
|
|
277
|
+
|
|
278
|
+
Augment `Register` with `messages: typeof en` in the English locale module.
|
|
279
|
+
Without that, `fx.t` accepts any `string` and loses autocomplete.
|
|
280
|
+
|
|
281
|
+
</Accordion>
|
|
282
|
+
<Accordion title="Plural message looks wrong or returns the raw template">
|
|
283
|
+
|
|
284
|
+
Missing `other` (or `=N`), a typo in a branch name, or an unescaped `{` / `'`
|
|
285
|
+
makes FormatJS reject the message — `fx.t` then returns the catalog source.
|
|
286
|
+
Keep `#` inside plural/ordinal branches only; double apostrophes (`''`).
|
|
287
|
+
|
|
288
|
+
</Accordion>
|
|
289
|
+
</Accordions>
|
|
290
|
+
|
|
291
|
+
## Learn more
|
|
292
|
+
|
|
293
|
+
- [fx](/docs/reference/fx) — full `fx` surface including `fx.t` / `fx.locale`
|
|
294
|
+
- [Errors](/docs/reference/errors) — localized failure messages and OKE codes
|
|
295
|
+
- [Configuration](/docs/reference/configuration) — `i18n` block next to drivers
|
|
296
|
+
- [Channel](/docs/elements/channel) — `{{field}}` templates and locale chain
|
|
297
|
+
- [Flow](/docs/elements/flow) — envelope shape with optional `error.message`
|
|
298
|
+
|
|
299
|
+
## Next
|
|
300
|
+
|
|
301
|
+
<Cards>
|
|
302
|
+
<Card
|
|
303
|
+
title="Errors"
|
|
304
|
+
description="OKE codes, denials, and localized messages."
|
|
305
|
+
href="/docs/reference/errors"
|
|
306
|
+
/>
|
|
307
|
+
<Card
|
|
308
|
+
title="Channel"
|
|
309
|
+
description="Human reach — templates, consent, locale chain."
|
|
310
|
+
href="/docs/elements/channel"
|
|
311
|
+
/>
|
|
312
|
+
<Card title="fx" description="The complete fx surface and effects." href="/docs/reference/fx" />
|
|
313
|
+
</Cards>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Reference"
|
|
3
|
-
description: "Lookup pages — config, fx, client, env vars, errors, plugins API, CLI, and Console security."
|
|
3
|
+
description: "Lookup pages — config, fx, i18n, client, env vars, errors, plugins API, CLI, and Console security."
|
|
4
4
|
icon: "BookMarked"
|
|
5
5
|
source: "docs/spec/unified-theory.md"
|
|
6
6
|
---
|
|
@@ -16,6 +16,11 @@ Dense tables and command lists. Reach for these when you already know what you a
|
|
|
16
16
|
href="/docs/reference/configuration"
|
|
17
17
|
/>
|
|
18
18
|
<Card title="fx" description="The complete fx surface and effects." href="/docs/reference/fx" />
|
|
19
|
+
<Card
|
|
20
|
+
title="i18n"
|
|
21
|
+
description="ICU catalogs, fx.t, typed keys, locale matching."
|
|
22
|
+
href="/docs/reference/i18n"
|
|
23
|
+
/>
|
|
19
24
|
<Card
|
|
20
25
|
title="Client"
|
|
21
26
|
description="Typed createClient — REST, RPC, envelope helpers."
|
|
@@ -77,6 +77,7 @@ Every method below exists on both the fluent definition and the boot-time builde
|
|
|
77
77
|
| `.signal(decl)` | A signal declaration — merged into boot signals |
|
|
78
78
|
| `.gate(decl)` | A gate declaration — merged into boot gates |
|
|
79
79
|
| `.channelTemplate(decl)` | A channel template — merged into boot channel templates |
|
|
80
|
+
| `.channelCatalog(catalog)` | Template body catalog entries — merged into boot channel catalog (`{{field}}` interpolation) |
|
|
80
81
|
| `.driver(id, impl)` | A protocol-named driver for an existing element |
|
|
81
82
|
| `.image(role, recipe)` | An image recipe for a docker role |
|
|
82
83
|
| `.table(name, columns, options)` | A whole DB table, merged into the generated schema (`options.description` / `plane` optional) |
|
package/src/auth/auth.test.ts
CHANGED
package/src/auth/bindings.ts
CHANGED
|
@@ -305,7 +305,7 @@ export function createAuthHttpBindings(
|
|
|
305
305
|
}
|
|
306
306
|
if (touchRateLimit(bag, ctx.now()) === "rate_limited") {
|
|
307
307
|
return fail("AuthRateLimited", {
|
|
308
|
-
reason: "
|
|
308
|
+
reason: "rate_limited",
|
|
309
309
|
});
|
|
310
310
|
}
|
|
311
311
|
const user = await authenticateUser(identities, input.email, input.password);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constant-time equality for auth secrets (OTP, claim codes, challenges).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Constant-time string equality (UTF-16 code units).
|
|
7
|
+
* Length mismatches still scan to `max(len)` so timing does not leak length
|
|
8
|
+
* beyond the longer operand.
|
|
9
|
+
*
|
|
10
|
+
* @param a - Expected
|
|
11
|
+
* @param b - Candidate
|
|
12
|
+
*/
|
|
13
|
+
export function constantTimeEqual(a: string, b: string): boolean {
|
|
14
|
+
const len = Math.max(a.length, b.length);
|
|
15
|
+
let mismatch = a.length === b.length ? 0 : 1;
|
|
16
|
+
for (let i = 0; i < len; i++) {
|
|
17
|
+
const ac = a.charCodeAt(i) || 0;
|
|
18
|
+
const bc = b.charCodeAt(i) || 0;
|
|
19
|
+
mismatch |= ac ^ bc;
|
|
20
|
+
}
|
|
21
|
+
return mismatch === 0;
|
|
22
|
+
}
|
package/src/auth/index.ts
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
* Active Gate auth context for method plugins plugged on the same app.
|
|
3
3
|
*
|
|
4
4
|
* Set during `oke({ gate: { auth } })` so `.plug(username())` etc. share
|
|
5
|
-
* the app session store
|
|
5
|
+
* the app session store, HMAC secret, password policy, hash knobs, and
|
|
6
|
+
* breach check without callers re-passing them.
|
|
6
7
|
*/
|
|
7
8
|
|
|
9
|
+
import type { PasswordHashOptions } from "../runtime/types.ts";
|
|
10
|
+
import type { BreachCheckFn } from "./breach-check.ts";
|
|
11
|
+
import type { PasswordPolicyOptions } from "./password-policy.ts";
|
|
8
12
|
import type { SessionStore } from "./sessions.ts";
|
|
9
13
|
|
|
10
14
|
/** Shared context for auth method plugins. */
|
|
@@ -12,6 +16,12 @@ export interface ActiveGateAuthContext {
|
|
|
12
16
|
readonly secret: string;
|
|
13
17
|
readonly sessions: SessionStore;
|
|
14
18
|
readonly now?: () => number;
|
|
19
|
+
/** From `gate.auth.passwordPolicy` — shared by credential method plugins. */
|
|
20
|
+
readonly passwordPolicy?: PasswordPolicyOptions;
|
|
21
|
+
/** From `gate.auth.password` — Bun.password cost knobs. */
|
|
22
|
+
readonly password?: PasswordHashOptions;
|
|
23
|
+
/** From `gate.auth.breachCheck` — optional breach checker. */
|
|
24
|
+
readonly breachCheck?: BreachCheckFn;
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
let active: ActiveGateAuthContext | undefined;
|
|
@@ -19,7 +29,7 @@ let active: ActiveGateAuthContext | undefined;
|
|
|
19
29
|
/**
|
|
20
30
|
* Publish the app's Gate auth binding for subsequent `.plug()` method plugins.
|
|
21
31
|
*
|
|
22
|
-
* @param ctx -
|
|
32
|
+
* @param ctx - Shared auth material, or undefined to clear
|
|
23
33
|
*/
|
|
24
34
|
export function setActiveGateAuthContext(ctx: ActiveGateAuthContext | undefined): void {
|
|
25
35
|
active = ctx;
|