okengine 0.3.6 → 0.5.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.
Files changed (205) hide show
  1. package/AGENTS.md +2 -0
  2. package/package.json +14 -12
  3. package/site/content/docs/ai/index.mdx +24 -0
  4. package/site/content/docs/ai/llms-txt.mdx +3 -0
  5. package/site/content/docs/ai/meta.json +1 -1
  6. package/site/content/docs/ai/skills.mdx +5 -3
  7. package/site/content/docs/console/gates.mdx +46 -8
  8. package/site/content/docs/console/index.mdx +54 -0
  9. package/site/content/docs/console/meta.json +1 -0
  10. package/site/content/docs/elements/ai.mdx +2 -0
  11. package/site/content/docs/elements/channel.mdx +2 -0
  12. package/site/content/docs/elements/clock.mdx +1 -4
  13. package/site/content/docs/elements/flow.mdx +4 -10
  14. package/site/content/docs/elements/gate.mdx +189 -48
  15. package/site/content/docs/elements/index.mdx +45 -0
  16. package/site/content/docs/elements/meta.json +1 -1
  17. package/site/content/docs/elements/signal.mdx +1 -5
  18. package/site/content/docs/elements/store.mdx +27 -6
  19. package/site/content/docs/elements/vault.mdx +10 -11
  20. package/site/content/docs/get-started/basic-usage.mdx +79 -43
  21. package/site/content/docs/get-started/index.mdx +33 -0
  22. package/site/content/docs/get-started/installation.mdx +95 -43
  23. package/site/content/docs/get-started/introduction.mdx +128 -75
  24. package/site/content/docs/get-started/meta.json +1 -1
  25. package/site/content/docs/get-started/why.mdx +141 -0
  26. package/site/content/docs/index.mdx +9 -31
  27. package/site/content/docs/plugins/anonymous.mdx +95 -0
  28. package/site/content/docs/plugins/compression.mdx +2 -2
  29. package/site/content/docs/plugins/cors.mdx +2 -2
  30. package/site/content/docs/plugins/csrf.mdx +2 -2
  31. package/site/content/docs/plugins/email-otp.mdx +111 -0
  32. package/site/content/docs/plugins/{security-headers.mdx → headers.mdx} +2 -2
  33. package/site/content/docs/plugins/index.mdx +69 -0
  34. package/site/content/docs/plugins/ip-allowlist.mdx +1 -2
  35. package/site/content/docs/plugins/magic-link.mdx +112 -0
  36. package/site/content/docs/plugins/maintenance-mode.mdx +8 -8
  37. package/site/content/docs/plugins/meta.json +10 -1
  38. package/site/content/docs/plugins/passkey.mdx +128 -0
  39. package/site/content/docs/plugins/phone-number.mdx +111 -0
  40. package/site/content/docs/plugins/two-factor.mdx +116 -0
  41. package/site/content/docs/plugins/username.mdx +117 -0
  42. package/site/content/docs/reference/client.mdx +331 -0
  43. package/site/content/docs/reference/configuration.mdx +1 -1
  44. package/site/content/docs/reference/environment-variables.mdx +5 -3
  45. package/site/content/docs/reference/fx.mdx +36 -8
  46. package/site/content/docs/reference/index.mdx +45 -0
  47. package/site/content/docs/reference/meta.json +11 -1
  48. package/site/content/docs/reference/plugins.mdx +25 -14
  49. package/src/auth/auth.test.ts +20 -2
  50. package/src/auth/bindings.ts +439 -0
  51. package/src/auth/breach-check.ts +112 -0
  52. package/src/auth/config.ts +288 -0
  53. package/src/auth/cookies.ts +123 -0
  54. package/src/auth/gate-auth.test.ts +379 -0
  55. package/src/auth/identity.ts +190 -0
  56. package/src/auth/index.ts +117 -1
  57. package/src/auth/method-context.ts +33 -0
  58. package/src/auth/operator.ts +27 -1
  59. package/src/auth/password-policy.test.ts +126 -0
  60. package/src/auth/password-policy.ts +77 -0
  61. package/src/auth/plugin.ts +62 -4
  62. package/src/auth/rate.ts +45 -0
  63. package/src/auth/schema.ts +260 -0
  64. package/src/auth/secondary-storage.ts +37 -0
  65. package/src/auth/sessions.ts +58 -1
  66. package/src/auth/tables.ts +4 -0
  67. package/src/auth/verification.ts +78 -0
  68. package/src/cli/competitor-mention-removal.test.ts +117 -0
  69. package/src/cli/dev.test.ts +3 -3
  70. package/src/cli/dev.ts +20 -0
  71. package/src/cli/meilisearch-local.test.ts +69 -0
  72. package/src/cli/meilisearch-local.ts +188 -0
  73. package/src/cli/schema.ts +95 -23
  74. package/src/client/auth.ts +120 -0
  75. package/src/client-react/index.ts +93 -0
  76. package/src/compiler/aot.test.ts +2 -1
  77. package/src/compiler/extract.ts +19 -0
  78. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +5 -1
  79. package/src/compiler/response.ts +16 -2
  80. package/src/console/server/app.ts +10 -6
  81. package/src/console/server/auth-rate.test.ts +3 -3
  82. package/src/console/server/bind.ts +12 -1
  83. package/src/console/server/channels.test.ts +1 -1
  84. package/src/console/server/console-gates.ts +14 -0
  85. package/src/console/server/console.test.ts +6 -6
  86. package/src/console/server/flows-invoke.test.ts +2 -2
  87. package/src/console/server/flows.ts +2 -0
  88. package/src/console/server/gates.ts +8 -1
  89. package/src/console/server/operator-db.test.ts +4 -4
  90. package/src/console/server/operator-db.ts +22 -4
  91. package/src/console/server/security.gate.test.ts +3 -3
  92. package/src/console/server/store.test.ts +1 -1
  93. package/src/console/server/store.ts +11 -1
  94. package/src/console/ui/dist/assets/index-CjxwRGVv.js +10 -0
  95. package/src/console/ui/dist/assets/panel-access-BGv45snf.js +64 -0
  96. package/src/console/ui/dist/assets/{panel-ai-D_m6WQI8.js → panel-ai-B2S7LEii.js} +1 -1
  97. package/src/console/ui/dist/assets/{panel-architecture-CKnXFyUx.js → panel-architecture-D7UJh91v.js} +1 -1
  98. package/src/console/ui/dist/assets/{panel-channels-DCDd4WAC.js → panel-channels-9T3ybqRu.js} +1 -1
  99. package/src/console/ui/dist/assets/panel-clock-Cb1UXGRQ.js +1 -0
  100. package/src/console/ui/dist/assets/{panel-diff-cdonmH8c.js → panel-diff-DmYbKWmN.js} +1 -1
  101. package/src/console/ui/dist/assets/panel-flows-PiHwT55z.js +48 -0
  102. package/src/console/ui/dist/assets/{panel-gates-B5eTE8XH.js → panel-gates-BQGYXvjT.js} +1 -1
  103. package/src/console/ui/dist/assets/panel-overview-BBnRO18l.js +1 -0
  104. package/src/console/ui/dist/assets/{panel-plugins-Cj7DK1er.js → panel-plugins-D0PsmVw2.js} +1 -1
  105. package/src/console/ui/dist/assets/panel-runs-CWuRDe0r.js +1 -0
  106. package/src/console/ui/dist/assets/{panel-signals-whmDXIg3.js → panel-signals-Bbg4ewpP.js} +1 -1
  107. package/src/console/ui/dist/assets/{panel-store-CEMHLvaw.js → panel-store-CPCbsDRa.js} +1 -1
  108. package/src/console/ui/dist/assets/panel-traces-DVAzuA_S.js +1 -0
  109. package/src/console/ui/dist/assets/{panel-vault-C9wjbki8.js → panel-vault-D1_MvOmo.js} +1 -1
  110. package/src/console/ui/dist/assets/{rolldown-runtime-CNC7AqOf.js → rolldown-runtime-B0Z9INg1.js} +1 -1
  111. package/src/console/ui/dist/index.html +2 -2
  112. package/src/console/ui/gates/fixture.ts +4 -0
  113. package/src/console/ui/gates/types.ts +2 -0
  114. package/src/console/ui/shell/client.ts +1 -0
  115. package/src/docker/compose.ts +5 -0
  116. package/src/docker/docker.test.ts +41 -0
  117. package/src/docker/recipes/index.ts +10 -2
  118. package/src/docker/recipes/meilisearch.ts +31 -0
  119. package/src/drivers/conformance.test.ts +16 -1
  120. package/src/drivers/conformance.ts +40 -3
  121. package/src/drivers/index.ts +14 -2
  122. package/src/drivers/libsql.ts +4 -4
  123. package/src/drivers/meilisearch.integration.test.ts +77 -0
  124. package/src/drivers/meilisearch.test.ts +181 -0
  125. package/src/drivers/meilisearch.ts +208 -0
  126. package/src/drivers/memory.ts +4 -4
  127. package/src/drivers/pgvector.ts +6 -6
  128. package/src/drivers/types.ts +93 -12
  129. package/src/drivers/vault-driver-removal.test.ts +6 -0
  130. package/src/drivers/vault-types.ts +4 -4
  131. package/src/elements/ai/runtime.ts +6 -0
  132. package/src/elements/ai.test.ts +22 -0
  133. package/src/elements/gate/boot.ts +136 -0
  134. package/src/elements/gate/config.ts +69 -0
  135. package/src/elements/gate/declare.ts +51 -1
  136. package/src/elements/gate/runtime.ts +3 -1
  137. package/src/elements/gate.test.ts +77 -0
  138. package/src/elements/gate.ts +20 -1
  139. package/src/elements/index.ts +8 -0
  140. package/src/elements/store/index-boot.test.ts +49 -7
  141. package/src/elements/store/runtime.ts +50 -15
  142. package/src/elements/store.ts +2 -0
  143. package/src/elements/vault.test.ts +27 -4
  144. package/src/elements/vault.ts +1 -1
  145. package/src/index.ts +15 -0
  146. package/src/kernel/app.ts +253 -32
  147. package/src/kernel/boot-bind/store.test.ts +9 -0
  148. package/src/kernel/boot-bind/store.ts +30 -2
  149. package/src/kernel/boot.test.ts +40 -3
  150. package/src/kernel/boot.ts +8 -0
  151. package/src/kernel/call.test.ts +46 -2
  152. package/src/kernel/concurrency.test.ts +58 -0
  153. package/src/kernel/concurrency.ts +48 -0
  154. package/src/kernel/edge.test.ts +3 -3
  155. package/src/kernel/flow.test.ts +2 -2
  156. package/src/kernel/fx.test.ts +12 -2
  157. package/src/kernel/fx.ts +97 -5
  158. package/src/kernel/hooks.test.ts +4 -4
  159. package/src/kernel/index.ts +22 -1
  160. package/src/kernel/pipeline.test.ts +12 -8
  161. package/src/kernel/pipeline.ts +23 -4
  162. package/src/kernel/plugin/decorate.test.ts +3 -3
  163. package/src/kernel/plugin/scoping.test.ts +3 -3
  164. package/src/kernel/plugin-elements.test.ts +51 -0
  165. package/src/kernel/plugin-needs.test.ts +83 -0
  166. package/src/kernel/plugin-needs.ts +129 -0
  167. package/src/kernel/plugin.ts +101 -0
  168. package/src/kernel/redacted.ts +74 -0
  169. package/src/kernel/registry-isolation.test.ts +5 -5
  170. package/src/kernel/registry.ts +102 -3
  171. package/src/kernel/router.ts +3 -3
  172. package/src/manifest/types.ts +2 -0
  173. package/src/plugins/anonymous.ts +58 -0
  174. package/src/plugins/auth/shared.ts +121 -0
  175. package/src/plugins/auth-methods.test.ts +176 -0
  176. package/src/plugins/compression.test.ts +5 -5
  177. package/src/plugins/config-source.test.ts +1 -1
  178. package/src/plugins/cors.test.ts +16 -10
  179. package/src/plugins/csrf.test.ts +1 -1
  180. package/src/plugins/email-otp.ts +161 -0
  181. package/src/plugins/index.ts +31 -0
  182. package/src/plugins/ip-allowlist.test.ts +19 -9
  183. package/src/plugins/magic-link.ts +163 -0
  184. package/src/plugins/maintenance-mode.test.ts +9 -5
  185. package/src/plugins/passkey.ts +216 -0
  186. package/src/plugins/phone-number.ts +149 -0
  187. package/src/plugins/security-headers.test.ts +14 -14
  188. package/src/plugins/two-factor.ts +249 -0
  189. package/src/plugins/username.ts +148 -0
  190. package/src/runs/runs.test.ts +6 -2
  191. package/src/runtime/primitives.ts +37 -4
  192. package/src/runtime/serve.test.ts +3 -2
  193. package/src/runtime/types.ts +25 -2
  194. package/src/test/create-test-app.test.ts +1 -1
  195. package/src/test/create-test-app.ts +4 -1
  196. package/src/test/provisions.integration.test.ts +2 -2
  197. package/site/content/docs/get-started/comparison.mdx +0 -65
  198. package/src/console/ui/dist/assets/index-CrKMmO__.js +0 -10
  199. package/src/console/ui/dist/assets/panel-access-C0J2D-a2.js +0 -64
  200. package/src/console/ui/dist/assets/panel-clock-DjGGFPzr.js +0 -1
  201. package/src/console/ui/dist/assets/panel-flows-DlCU5zjA.js +0 -45
  202. package/src/console/ui/dist/assets/panel-overview-BsFvDdts.js +0 -1
  203. package/src/console/ui/dist/assets/panel-runs-C0gmnoYL.js +0 -1
  204. package/src/console/ui/dist/assets/panel-traces-BDiAuVSK.js +0 -1
  205. package/src/drivers/vault-infisical.ts +0 -57
@@ -1,15 +1,17 @@
1
1
  ---
2
2
  title: "Gate"
3
- description: "Permission to act — auth policies and rate limits attached to the trigger, evaluated before any effect runs, denied as typed errors."
3
+ description: "Permission to act — policies, rate limits, and built-in auth on the HTTP trigger, evaluated before any effect runs."
4
4
  icon: "ShieldCheck"
5
5
  source: "docs/spec/unified-theory.md"
6
6
  ---
7
7
 
8
- Gate is how your app answers **"may this happen?"** before it happens: is this request from a verified member? has this IP exhausted its minute quota? Gates attach to the trigger — the pipeline evaluates them before a single store write, emit, or channel send runs.
8
+ Gate answers **"may this happen?"** before it happens: is this a verified member? has this IP
9
+ burned its minute quota? Attach gates to the HTTP trigger — the pipeline checks them before a
10
+ store write, emit, or channel send runs.
9
11
 
10
12
  <Callout title="The one rule">
11
- Permission sits **at the trigger**, and denial is a typed error value never a thrown exception,
12
- never an effect that runs halfway and rolls back.
13
+ Every HTTP trigger declares posture: attach a real gate, or attach `gate.public`. Omitting both
14
+ fails boot. Denial is a typed error value — never a thrown exception mid-`do`.
13
15
  </Callout>
14
16
 
15
17
  ## Quick start
@@ -17,50 +19,80 @@ Gate is how your app answers **"may this happen?"** before it happens: is this r
17
19
  <Steps>
18
20
 
19
21
  <Step>
20
- ### Declare your gates
22
+ ### Declare policies and register the Gate bag
21
23
 
22
- Policies are named predicates; rate limits are declarative budgets. Both live once, app-wide:
24
+ Policies are named predicates; rate limits are declarative budgets. Put the ones you reuse in
25
+ `oke({ gate: { policies } })`:
23
26
 
24
27
  ```typescript title="src/gates.ts"
25
28
  import { gate } from "okengine";
26
29
 
27
- export const member = gate.policy("member", ({ auth }) => !!auth?.verified);
30
+ export const member = gate.policy("member", ({ auth }) => !!auth.verified);
31
+
32
+ export const canBook = gate.scope("booking:create");
28
33
 
29
34
  export const fair = gate.rate({
30
- strategy: "sliding-window-counter", // the default — near-exact, no boundary bursts
31
35
  max: 60,
32
36
  per: "1m",
33
37
  keyBy: "ip",
34
38
  });
35
39
  ```
36
40
 
41
+ `gate.scope(name)` is sugar for
42
+ `gate.policy(name, ({ auth }) => auth.scopes.has(name))` — one string is both the policy id and
43
+ the scope checked.
44
+
45
+ ```typescript title="src/app.ts"
46
+ import { oke } from "okengine";
47
+ import { member, canBook, fair } from "./gates";
48
+
49
+ export const app = oke({
50
+ name: "shop",
51
+ env: "local",
52
+ gate: { policies: [member, canBook, fair] },
53
+ });
54
+ ```
55
+
37
56
  </Step>
38
57
 
39
58
  <Step>
40
59
  ### Attach them to triggers
41
60
 
42
- `.gate(...)` composes in registration orderevaluated left to right, first denial wins:
61
+ `.gate(...)` composes left to rightfirst denial wins. Public routes must say so explicitly:
43
62
 
44
63
  ```typescript title="src/flows/links/shorten.ts"
64
+ import { on, flow, http, gate } from "okengine";
65
+ import { member, canBook, fair } from "../../gates";
66
+
67
+ export const health = on(
68
+ http.get("/health").gate(gate.public),
69
+ flow({
70
+ out: HealthOut,
71
+ do: () => ({ ok: true as const }),
72
+ }),
73
+ );
74
+
45
75
  export const shorten = on(
46
- http.post("/links").gate(member, fair),
76
+ http.post("/links").gate(member, canBook, fair),
47
77
  flow({
48
78
  in: NewLink,
49
79
  out: LinkCode,
50
80
  errors: { Taken },
51
81
  do: async ({ url, code }, fx) => {
52
- /* runs only if both gates passed */
82
+ /* runs only if every gate passed */
53
83
  },
54
84
  }),
55
85
  );
56
86
  ```
57
87
 
88
+ Do **not** put `gate.public` in `gate.policies` — attach it on the trigger only.
89
+
58
90
  </Step>
59
91
 
60
92
  <Step>
61
- ### Denials are typed failures
93
+ ### See typed denials
62
94
 
63
- A denied request never reaches `do`. It returns one of three typed failures, like any entry in `errors: { … }`:
95
+ A denied request never reaches `do`. It returns one of three typed failures:
64
96
 
65
97
  | Gate result | Typed failure |
66
98
  | -------------------------------- | ------------------------------------- |
@@ -72,32 +104,40 @@ A denied request never reaches `do`. It returns one of three typed failures, lik
72
104
 
73
105
  </Steps>
74
106
 
75
- ## Two kinds of gates
107
+ <GatePipeline />
76
108
 
77
- | Declaration | Question it answers | Evaluated against |
78
- | ------------------------------------- | -------------------------------------- | ---------------------------------- |
79
- | `gate.policy(name, check \| options)` | Is this principal allowed? (ABAC) | auth / operator / request metadata |
80
- | `gate.rate(options)` | Is there budget left for this subject? | an atomic counter on the kv driver |
109
+ ## `oke({ gate })` bag
81
110
 
82
- `gate.policy(name, check)` stays the terse form. Pass
83
- `{ check, description }` when you want a human title in the Console
84
- (falls back to the policy name).
111
+ | Option | Type | Default | Meaning |
112
+ | --------------- | ----------------------- | -------- | --------------------------------------------------------------- |
113
+ | `policies` | `GateDecl[]` | `[]` | Named policy / rate decls for the Gate runtime |
114
+ | `auth` | `GateAuthOptions` | — | Builtin hybrid sessions + `/auth/*` Flows (see below) |
115
+ | `rateLimit` | `{ enabled?: boolean }` | see note | When auth is on, defaults `true` — stricter rates on auth paths |
116
+ | `unguardedHttp` | `"deny"` \| `"allow"` | `"deny"` | `"allow"` skips posture audit **only** when `env === "test"` |
85
117
 
86
- ### The policy context
118
+ ## Declarations
87
119
 
88
- The predicate receives everything it may decide with — nothing else:
120
+ | Declaration | Question it answers | Evaluated against |
121
+ | ------------------------------------- | ------------------------------------------- | ---------------------------------- |
122
+ | `gate.policy(name, check \| options)` | Is this principal allowed? (ABAC) | auth / operator / request metadata |
123
+ | `gate.scope(name)` | Does `auth.scopes` contain this exact name? | `auth.scopes` |
124
+ | `gate.public` | Intentionally unauthenticated surface | always allows |
125
+ | `gate.rate(options)` | Is there budget left for this subject? | an atomic counter on the kv driver |
126
+
127
+ Pass `{ check, description }` on `gate.policy` when you want a human title in the Console (falls
128
+ back to the policy name). The name `"public"` is reserved — use `gate.public`, not
129
+ `gate.policy("public", …)`.
130
+
131
+ ### Policy context
89
132
 
90
133
  | Field | Contents |
91
134
  | ---------- | -------------------------------------------------------- |
92
135
  | `auth` | User principal: `userId`, `scopes` (a `Set`), `verified` |
93
136
  | `operator` | Operator principal (Console plane): `id` |
94
- | `meta` | Request metadata for keying decisions: `ip`, `userId`, … |
95
-
96
- ```typescript
97
- export const admin = gate.policy("admin", ({ auth }) => auth.scopes.has("admin"));
98
- ```
137
+ | `meta` | Request metadata for keying: `ip`, `userId`, … |
99
138
 
100
- A policy name containing `:` is also a `Module:Action` permission the same declaration feeds role-based access reviews in the Console.
139
+ A policy name containing `:` is also a `Module:Action` permission for Console role reviews.
140
+ Fine-grained authorization (`gate.scope`, roles) stays opt-in — attach it only where you need it.
101
141
 
102
142
  ### Rate options
103
143
 
@@ -107,7 +147,7 @@ A policy name containing `:` is also a `Module:Action` permission — the same d
107
147
  | `per` | string | — (**required**) | Window / refill period (`"1m"`, `"60s"`, …) |
108
148
  | `keyBy` | string | — | Subject dimension (`"ip"`, `"user"`, …) |
109
149
  | `strategy` | RateStrategy | `"sliding-window-counter"` | Algorithm (below) |
110
- | `overridable` | boolean | `false` | Allow the Console to retune `max`/`per` live |
150
+ | `overridable` | boolean | `false` | Catalog flag in Console (rate edit UI not shipped) |
111
151
  | `description` | string | — | Human title in the Console (falls back to the rate name) |
112
152
 
113
153
  | Strategy | Behavior |
@@ -118,37 +158,139 @@ A policy name containing `:` is also a `Module:Action` permission — the same d
118
158
  | `token-bucket` | Steady refill; permits saved-up bursts |
119
159
  | `leaky-bucket` | Smooths output to a constant rate |
120
160
 
121
- All five run as atomic Lua on the kv driver — correct under concurrency, identical on the memory driver in tests.
161
+ All five run as atomic Lua on the kv driver — correct under concurrency, identical on the memory
162
+ driver in tests.
163
+
164
+ ## Auth posture at boot
165
+
166
+ Every HTTP trigger must carry a gate or `gate.public`. Omitting both fails boot with
167
+ `GateBootError` — message starts with `gate boot failed —` and lists every gap.
168
+
169
+ ```typescript
170
+ on(http.get("/health").gate(gate.public), flow({/* … */}));
171
+ on(http.post("/bookings").gate(member, canBook), flow({/* … */}));
172
+ ```
173
+
174
+ **Consequence:** first `fetch` / `execute` auto-boots (`autoBoot` defaults to `true`).
175
+ `oke({ autoBoot: false })` is a unit-test escape hatch only. A rate-only chain is declared
176
+ posture but does **not** authenticate.
122
177
 
123
- ## Identity comes from auth
178
+ `gate: { unguardedHttp: "allow" }` skips the audit **only** when `env === "test"`.
179
+ In `local` / `prod` / `docker` migrate with per-trigger `gate.public`.
124
180
 
125
- Gates don't log users in — they decide about principals that already exist. The built-in auth plugin (`okengine/auth`) provides the user plane: hybrid sessions, argon2id password hashing, roles, API keys, and invites, with zero-config defaults. Whatever it establishes lands on `fx.auth`, which is exactly what your policies read.
181
+ ## Built-in auth (`gate.auth`)
126
182
 
127
- ## Every decision is recorded
183
+ Enable hybrid sessions (Bearer access + revocable refresh). Auth HTTP surfaces are ordinary
184
+ Bindings under `/auth` — same posture audit and pipeline as your app Flows:
128
185
 
129
- Pass **and** deny, every evaluation lands on the run's `gates` dimension in the telemetry trace. The Console (`:6533` → Gates) shows which gates fired on a request and their outcomes — an audit trail of permission decisions you never had to build. Rate gates declared `overridable: true` can also be retuned (`max`/`per`) from the Console without a redeploy; anything else rejects edits by design.
186
+ ```typescript title="src/app.ts"
187
+ import { oke } from "okengine";
188
+ import { member, canBook } from "./gates";
189
+
190
+ export const app = oke({
191
+ name: "shop",
192
+ env: "local",
193
+ gate: {
194
+ auth: {
195
+ emailAndPassword: { enabled: true },
196
+ session: { freshAgeMs: 24 * 60 * 60_000 },
197
+ },
198
+ policies: [member, canBook],
199
+ },
200
+ });
201
+ ```
202
+
203
+ | Surface | When |
204
+ | -------------------------- | -------------------------------------- |
205
+ | `POST /auth/sign-up/email` | `emailAndPassword.enabled` |
206
+ | `POST /auth/sign-in/email` | `emailAndPassword.enabled` |
207
+ | `POST /auth/refresh` | when `gate.auth` is set |
208
+ | `POST /auth/revoke` | when `gate.auth` is set |
209
+ | `GET /auth/me` | Bearer session (`auth.session` policy) |
210
+
211
+ | Auth option | Default | Meaning |
212
+ | ------------------------------------------- | --------- | ------------------------------------------------------------------------------ |
213
+ | `secret` | minted\* | HMAC for access tokens (\*`prod` requires you set it) |
214
+ | `basePath` | `"/auth"` | HTTP prefix for auth Flows |
215
+ | `http` | `true` | `false` = secret/tables only (no `/auth/*` Bindings) |
216
+ | `audience` | `oke-app` | Audience stamped on access tokens |
217
+ | `emailAndPassword.enabled` | `false` | Sign-up / sign-in email Flows |
218
+ | `emailAndPassword.requireEmailVerification` | `false` | Block sign-in until email is verified |
219
+ | `passwordPolicy` | secure\* | `\*` minLength 12, letter + number; sign-up → `AuthFailed` / `password_policy` |
220
+ | `breachCheck` | unset | `(password) => Promise<boolean>` — `true` rejects as `password_breached` |
221
+ | `session.freshAgeMs` | 24h | Window for step-up `auth.fresh` policy |
222
+ | `session.accessTtlMs` / `refreshTtlMs` | 14m / 30d | Access / refresh lifetimes |
223
+ | `session.idleTtlMs` | unset | Opt-in: reject refresh when idle too long |
224
+ | `session.absoluteTtlMs` | unset | Opt-in: hard cap from session creation |
225
+ | `session.singleSessionPerUser` | `false` | Opt-in: issuing a session revokes other families for that user |
226
+ | `cookies.enabled` | `false` | Opt-in HttpOnly cookie mirror (Bearer stays default) |
227
+ | `secondaryStorage.enabled` | `false` | Hot auth data in `store.kv` when configured |
228
+ | `user` / `session` / … | defaults | `modelName` / `fields` / `additionalFields` |
229
+
230
+ HIBP via `createHibpBreachCheck` from `okengine/auth` (k-anonymity range API; requires
231
+ `userAgent`; `onError` defaults to `"reject"` — fail closed while the check is enabled):
232
+
233
+ ```typescript
234
+ import { createHibpBreachCheck } from "okengine/auth";
235
+
236
+ gate: {
237
+ auth: {
238
+ emailAndPassword: { enabled: true },
239
+ breachCheck: createHibpBreachCheck({ userAgent: "shop/1.0" }),
240
+ },
241
+ }
242
+ ```
243
+
244
+ Customize tables, then run `oke schema generate` (supports `--check` in CI). Extra sign-in
245
+ shapes: [Plugins](/docs/plugins) (username, magic link, OTP, TOTP, passkeys). Call auth from
246
+ [createClient](/docs/reference/client) — one client, helpers in `okengine/client/auth`.
247
+
248
+ ## Identity and decisions
249
+
250
+ Gates decide on principals that already exist. `gate.auth` issues hybrid sessions onto
251
+ `fx.auth` (Console: `fx.operator`). Audit across `fx.call` uses read-only `fx.principal`
252
+ (see [fx](/docs/reference/fx)).
253
+
254
+ Pass **and** deny land on the run's `gates` dimension. Console (`:6533` → Gates) shows the
255
+ chain. Rate `overridable` is a catalog flag only — Clock schedules are what the Console can
256
+ edit live today.
130
257
 
131
258
  ## Troubleshooting
132
259
 
133
260
  <Accordions>
261
+ <Accordion title="Boot: gate boot failed — N HTTP trigger(s) missing auth posture">
262
+
263
+ Empty `.gate(...)` chain. Attach a policy/rate gate, or `.gate(gate.public)`. First `fetch`
264
+ auto-boots — do not rely on `gate.unguardedHttp: "allow"` outside `env: "test"`.
265
+
266
+ </Accordion>
134
267
  <Accordion title="Can a policy read the request body?">
135
268
 
136
- No — and that is deliberate. Gates run before the flow, so they see only the policy context (`auth`, `operator`, `meta`). Body-dependent decisions belong inside `do`, expressed as typed `errors`.
269
+ No. Gates run before the flow, so they see only `auth`, `operator`, and `meta`. Body-dependent
270
+ decisions belong inside `do` as typed `errors`.
137
271
 
138
272
  </Accordion>
139
273
  <Accordion title="Logged-in users still get Forbidden">
140
274
 
141
- `Forbidden` means the request _was_ authenticated but a policy said no. The failure includes the gate name and reason — check whether your predicate requires `verified` or a scope the user lacks.
275
+ `Forbidden` means authenticated but a policy said no. Check `verified` or a missing scope
276
+ the failure includes the gate name and reason.
142
277
 
143
278
  </Accordion>
144
- <Accordion title="Which keyBy should I use?">
279
+ <Accordion title="Prod boot: gate.auth: secret is required">
145
280
 
146
- `"ip"` for public unauthenticated surfaces (sign-up, password reset), `"user"` for authenticated quotas. Keying an authenticated endpoint by IP punishes users behind shared NAT; keying a public endpoint by user is impossible.
281
+ Set `gate.auth.secret` (or provide it before boot). Non-prod may mint a development secret.
147
282
 
148
283
  </Accordion>
149
- <Accordion title="The Console won't retune my rate limit">
284
+ <Accordion title="Sign-up returns AuthFailed password_policy">
150
285
 
151
- The gate was declared without `overridable: true`. Add it and redeploy — live retuning is opt-in per gate so a mistyped change cannot silently open a floodgate.
286
+ Default policy requires length 12 plus a letter and a number. Short passwords like `"123"`
287
+ are rejected — not accepted silently. Tighten or loosen via `gate.auth.passwordPolicy`.
288
+
289
+ </Accordion>
290
+ <Accordion title="Which keyBy should I use?">
291
+
292
+ `"ip"` for public surfaces (sign-up, password reset), `"user"` for authenticated quotas.
293
+ Keying an authenticated endpoint by IP punishes shared NAT.
152
294
 
153
295
  </Accordion>
154
296
  </Accordions>
@@ -156,18 +298,17 @@ The gate was declared without `overridable: true`. Add it and redeploy — live
156
298
  ## Learn more
157
299
 
158
300
  - [Flow](/docs/elements/flow) — the trigger pipeline gates plug into
159
- - [Console · Gates](/docs/console/gates) — decision audit and live retuning
160
- - [Vault](/docs/elements/vault) — the credentials your policies protect
301
+ - [fx](/docs/reference/fx) — `fx.auth`, `fx.operator`, `fx.principal`
302
+ - [Client](/docs/reference/client) — Bearer `createClient` + `okengine/client/auth`
303
+ - [Plugins](/docs/plugins) — username, magic link, OTP, TOTP, passkeys
304
+ - [Console · Gates](/docs/console/gates) — decision audit and posture
305
+ - [Vault](/docs/elements/vault) — credentials your policies protect
161
306
 
162
307
  ## Next
163
308
 
164
309
  <Cards>
165
310
  <Card title="Vault" description="Continue to Vault." href="/docs/elements/vault" />
166
- <Card
167
- title="Introduction"
168
- description="Eight elements overview."
169
- href="/docs/get-started/introduction"
170
- />
311
+ <Card title="Plugins" description="Sign-in method plugins under /auth." href="/docs/plugins" />
171
312
  <Card
172
313
  title="Console"
173
314
  description="Panels derived from the Manifest."
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: "Elements"
3
+ description: "The eight irreducible parts of an OKE backend — Flow through AI, one page each."
4
+ icon: "Boxes"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ Every backend concern collapses to one of these eight. Pick the element you need.
9
+
10
+ ## Pages
11
+
12
+ <Cards>
13
+ <Card title="Flow" description="Behavior — on(Trigger) → Effects." href="/docs/elements/flow" />
14
+ <Card
15
+ title="Signal"
16
+ description="Data in motion — queues, pub/sub, streams."
17
+ href="/docs/elements/signal"
18
+ />
19
+ <Card
20
+ title="Store"
21
+ description="Data at rest — sql, kv, files, index."
22
+ href="/docs/elements/store"
23
+ />
24
+ <Card
25
+ title="Clock"
26
+ description="Time — schedules, intervals, durable sleeps."
27
+ href="/docs/elements/clock"
28
+ />
29
+ <Card
30
+ title="Gate"
31
+ description="Permission to act — auth and rate limits."
32
+ href="/docs/elements/gate"
33
+ />
34
+ <Card
35
+ title="Vault"
36
+ description="Protected knowledge — secrets and keys."
37
+ href="/docs/elements/vault"
38
+ />
39
+ <Card
40
+ title="Channel"
41
+ description="Reaching humans — email, SMS, push."
42
+ href="/docs/elements/channel"
43
+ />
44
+ <Card title="AI" description="Reaching machine intelligence." href="/docs/elements/ai" />
45
+ </Cards>
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "title": "Elements",
3
3
  "icon": "Boxes",
4
- "pages": ["flow", "signal", "store", "clock", "gate", "vault", "channel", "ai"]
4
+ "pages": ["index", "flow", "signal", "store", "clock", "gate", "vault", "channel", "ai"]
5
5
  }
@@ -76,11 +76,7 @@ That's the loop: declare → `fx.emit` → `on(signal, flow)`. The runtime handl
76
76
 
77
77
  ## The three delivery physics
78
78
 
79
- | `delivery` | Semantics | Use for |
80
- | ------------- | ------------------------------------------------------ | ---------------------------------------------------------- |
81
- | `"once"` | Queue: competing consumers, retries, dead-letter queue | Jobs that must happen exactly once — emails, payments sync |
82
- | `"broadcast"` | Pub/sub: **every** subscriber receives a copy | Cache invalidation, cross-service events |
83
- | `"live"` | Stream: client-subscribable and replayable | Live feeds, dashboards, progress updates |
79
+ <SignalDelivery />
84
80
 
85
81
  The declaration is identical in shape for all three — switching physics later is a one-word change, not a migration to another library.
86
82
 
@@ -90,12 +90,7 @@ export const createNote = on(
90
90
 
91
91
  Pick the facet that matches the physics of your data. Each declaration is one line; the runtime handle shows what flows can do with it.
92
92
 
93
- | Facet | Declares | Best for | `fx.store(…)` handle |
94
- | -------------------------- | ----------------- | ------------------------------------- | ------------------------------------------------------ |
95
- | `store.sql(name, opts)` | a SQL database | domain tables, relations, constraints | `select` · `insert` · `update` · `delete` · `findById` |
96
- | `store.kv(name, opts?)` | a key-value space | cache, sessions, rate limits | `get` · `set(key, value, ttl?)` · `delete` · `list` |
97
- | `store.files(name, opts?)` | a blob bucket | uploads, exports, attachments | `put` · `get` · `delete` · `list(prefix?)` |
98
- | `store.index(name, opts)` | a vector index | semantic search / RAG (`dims`) | `upsert` · `search(vector, topK?)` · `delete` |
93
+ <StoreFacets />
99
94
 
100
95
  Every facet accepts optional `description` — a human title in the Console
101
96
  (falls back to the store name). `store.index` also takes `dims`.
@@ -282,6 +277,32 @@ drivers: {
282
277
 
283
278
  **Consequence:** a configured SQL-backed index that cannot reach its engine — missing peer, missing `vector` extension, wrong sql driver — fails loudly at first use. It never silently falls back to `memory`.
284
279
 
280
+ ### Full-text search — `meilisearch` (opt-in)
281
+
282
+ `meilisearch` is a fourth `store.index` backend, opt-in only — `memory` stays the default. It is a genuinely different capability from the vector ANN drivers: typo-tolerant, faceted full-text relevance, not cosine similarity. The two models are kept apart by a **discriminated union** on `driverId`, so TypeScript rejects a vector query against a text index (and vice versa) at compile time — never at runtime:
283
+
284
+ | `driverId` | `upsert` | `search` | `score` means |
285
+ | -------------------------------- | --------------------- | ---------------------------------------- | --------------------------- |
286
+ | `memory` / `pgvector` / `libsql` | `(id, vector, meta?)` | `search(vector, topK?)` → `IndexHit[]` | cosine similarity |
287
+ | `meilisearch` | `(id, document)` | `search(q, opts?)` → `{ hits, facets? }` | relevance (`_rankingScore`) |
288
+
289
+ ```typescript title="oke.config.ts"
290
+ drivers: {
291
+ store: {
292
+ index: { local: "meilisearch", docker: "meilisearch", prod: "meilisearch" },
293
+ },
294
+ },
295
+ images: {
296
+ "store.index": "getmeili/meilisearch:v1.37",
297
+ },
298
+ ```
299
+
300
+ - **Local mode** needs the `meilisearch` binary on `PATH` (a documented prerequisite, like Docker for `--docker` — OKE never auto-downloads binaries). `oke dev` spawns it with a generated master key persisted under `.oke/meilisearch/` (`0700`/`0600`).
301
+ - **Docker / prod** uses the image recipe; the app gets `OKE_STORE_INDEX_URL` + `OKE_STORE_INDEX_KEY` from Compose — a standalone HTTP service, never the shared `sqlUrl`.
302
+ - **Fail-loud:** a configured meilisearch that is unreachable or unhealthy throws `MeilisearchUnavailableError` — never a silent memory fallback.
303
+ - **Not for vectors:** `ai.embed` / `fx.search` stay vector-only; pointing an embed `into` a meilisearch index fails loud (embeddings don't apply).
304
+ - **Alpine caveat:** the raw glibc binary fails on Alpine/musl _hosts_; the official image is musl-clean. Install the binary for your platform (e.g. Homebrew / install script), or use `--docker`.
305
+
285
306
  ## Privacy built in
286
307
 
287
308
  Columns tagged `.pii()` or `.sensitive()` are masked at the store boundary — flows, logs, and the Console see a mask, not the value. Revealing cleartext PII requires an explicit `pii:reveal` gate on the flow, so access is a permission, not a convention.
@@ -55,7 +55,9 @@ oke vault set STRIPE_KEY sk_test_your_key_here
55
55
  <Step>
56
56
  ### Read it in a Flow
57
57
 
58
- Flows read secrets through `fx.vault` — never through `process.env`:
58
+ Flows read secrets through `fx.vault` — never through `process.env`. `fx.vault` returns a
59
+ `Redacted<string>`: printing, logging, or serializing it yields a placeholder, never the value.
60
+ Call `.reveal()` once at the boundary that needs the real credential:
59
61
 
60
62
  ```typescript title="src/flows/billing/charge.ts"
61
63
  export const charge = on(
@@ -64,14 +66,17 @@ export const charge = on(
64
66
  in: ChargeInput,
65
67
  out: z.object({ id: z.string() }),
66
68
  do: async (input, fx) => {
67
- const key = fx.vault(stripeKey); // cleartext, inside this flow only
68
- const intent = await stripe(key).create(input);
69
+ const key = fx.vault(stripeKey); // Redacted safe to pass to fx.log
70
+ const intent = await stripe(key.reveal()).create(input);
69
71
  return { id: intent.id };
70
72
  },
71
73
  }),
72
74
  );
73
75
  ```
74
76
 
77
+ `fx.log` masks any `Redacted` found in the logged data (nested included). A revealed string that
78
+ still reaches `fx.log` is scrubbed by the boot redactor as before.
79
+
75
80
  </Step>
76
81
 
77
82
  </Steps>
@@ -111,13 +116,7 @@ export const dbUrl = vault.secret("DATABASE_URL", {
111
116
 
112
117
  At boot, each contract is resolved through this chain — **first hit wins**:
113
118
 
114
- | # | Source | Typical content |
115
- | --- | -------------------- | ------------------------------------------------- |
116
- | 1 | `process.env` | Real environment (CI, hosting platform) |
117
- | 2 | `.env.local` | Your machine's local overrides (gitignored) |
118
- | 3 | `docker/.env.docker` | Generated compose stack credentials |
119
- | 4 | vault driver | OpenBao in docker/prod mode |
120
- | 5 | `dev` fallback | The `dev:` option on the contract (never in prod) |
119
+ <VaultResolution />
121
120
 
122
121
  If every layer misses a contract, boot **fails** before any request is served:
123
122
 
@@ -167,7 +166,7 @@ vault: {
167
166
  | `openbao` | OpenBao (KV v2) | Docker + prod — durable, access-controlled |
168
167
  | `memory` | in-process map | Tests |
169
168
 
170
- Other drivers (`infisical`, `managed`) implement the same `VaultDriver` interface — adding one is a driver exercise, not an architecture change.
169
+ Other drivers (`managed`) implement the same `VaultDriver` interface — adding one is a driver exercise, not an architecture change.
171
170
 
172
171
  ## OpenBao in docker and prod
173
172