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
@@ -0,0 +1,116 @@
1
+ ---
2
+ title: "Two-factor"
3
+ description: "Official plugin — TOTP enable, verify, and disable for an authenticated session."
4
+ icon: "LockKeyhole"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ `twoFactor()` adds RFC 6238 TOTP (HMAC-SHA1, 6 digits, 30s) after someone already has a session.
9
+ Enable returns a secret, `otpauth://` URL, and recovery codes; verify mints a new session.
10
+
11
+ <Callout title="The one rule">
12
+ Enable `gate.auth`, then `.plug(twoFactor())`. Enable and disable need a Bearer session —
13
+ verify is public and takes `{ userId, code }` after password (or other) sign-in.
14
+ </Callout>
15
+
16
+ ## Quick start
17
+
18
+ <Steps>
19
+
20
+ <Step>
21
+ ### Plug it
22
+
23
+ ```typescript title="src/app.ts"
24
+ import { oke } from "okengine";
25
+ import { twoFactor } from "okengine/plugins";
26
+
27
+ export const app = oke({
28
+ name: "shop",
29
+ env: "local",
30
+ gate: { auth: {} },
31
+ }).plug(twoFactor());
32
+ ```
33
+
34
+ </Step>
35
+
36
+ <Step>
37
+ ### Enable (session required)
38
+
39
+ Wire Bearer on `createClient` (`auth.getToken` / `memorySession`) — calls take input only.
40
+
41
+ ```typescript
42
+ const { data } = await api.auth.twoFactorEnable({});
43
+ // data.secret, data.otpauthUrl, data.recoveryCodes
44
+ ```
45
+
46
+ `POST /auth/two-factor/enable` — writes `oke_two_factor`. Store recovery codes once; they are
47
+ shown only here.
48
+
49
+ </Step>
50
+
51
+ <Step>
52
+ ### Verify a code
53
+
54
+ ```typescript
55
+ const { data } = await api.auth.twoFactorVerify({ userId, code: "123456" });
56
+ // hybrid session tokens — recovery codes also accepted once each
57
+ ```
58
+
59
+ `POST /auth/two-factor/verify`. Missing / disabled factor or bad code → `AuthFailed` /
60
+ `invalid_credentials`. Disable: `api.auth.twoFactorDisable` (Bearer via `auth.getToken`).
61
+
62
+ </Step>
63
+
64
+ </Steps>
65
+
66
+ ## Options
67
+
68
+ | Option | Type | Default | Meaning |
69
+ | ---------- | ---------------- | ---------- | -------------------------------- |
70
+ | `secret` | `string` | active\* | HMAC secret (\*from `gate.auth`) |
71
+ | `sessions` | `SessionStore` | active\* | Session store |
72
+ | `now` | `() => number` | `Date.now` | Injectable clock |
73
+ | `factors` | `TwoFactorStore` | new | Per-user TOTP + recovery hashes |
74
+ | `issuer` | `string` | `"oke"` | Label in `otpauth://` URLs |
75
+
76
+ ## Surfaces
77
+
78
+ | Flow | Path | Gate |
79
+ | ----------------------- | ------------------------------- | ------------------------ |
80
+ | `auth.twoFactorEnable` | `POST /auth/two-factor/enable` | session + bearer |
81
+ | `auth.twoFactorVerify` | `POST /auth/two-factor/verify` | `gate.public` + otp rate |
82
+ | `auth.twoFactorDisable` | `POST /auth/two-factor/disable` | session + bearer |
83
+
84
+ **Consequence:** verify issues a full session for `userId` — use it as the step after first-factor
85
+ sign-in when the account has 2FA enabled.
86
+
87
+ ## Troubleshooting
88
+
89
+ <Accordions>
90
+ <Accordion title="twoFactorEnable returns AuthFailed unauthenticated">
91
+
92
+ Wire a session into `createClient` (`auth.getToken` / `memorySession`) from email/password
93
+ (or another method) first — enable is gated on Bearer.
94
+
95
+ </Accordion>
96
+ <Accordion title="verify always fails">
97
+
98
+ Code must be six digits. Check clock skew (±1 window), and that you enrolled the `secret` from
99
+ enable. A recovery code works once, then is consumed.
100
+
101
+ </Accordion>
102
+ </Accordions>
103
+
104
+ ## Learn more
105
+
106
+ - [Passkey](/docs/plugins/passkey) — WebAuthn-shaped register / authenticate
107
+ - [Gate](/docs/elements/gate) — session + policies
108
+ - [Username](/docs/plugins/username) — first factor to enroll against
109
+
110
+ ## Next
111
+
112
+ <Cards>
113
+ <Card title="Passkey" description="WebAuthn register and assert." href="/docs/plugins/passkey" />
114
+ <Card title="Gate" description="Builtin auth and policies." href="/docs/elements/gate" />
115
+ <Card title="Email OTP" description="Passwordless email codes." href="/docs/plugins/email-otp" />
116
+ </Cards>
@@ -0,0 +1,117 @@
1
+ ---
2
+ title: "Username"
3
+ description: "Official plugin — username + password sign-up and sign-in under /auth, plugged onto gate.auth."
4
+ icon: "UserRound"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ `username()` lets people register and sign in with a username instead of email. It adds two public
9
+ Flows under `/auth` and contributes the `oke_usernames` table.
10
+
11
+ <Callout title="The one rule">
12
+ Enable `gate.auth` first, then `.plug(username())`. The plugin `.needs("auth")` and joins the HTTP
13
+ router via Bindings — not registry metadata alone.
14
+ </Callout>
15
+
16
+ ## Quick start
17
+
18
+ <Steps>
19
+
20
+ <Step>
21
+ ### Plug it
22
+
23
+ ```typescript title="src/app.ts"
24
+ import { oke } from "okengine";
25
+ import { username } from "okengine/plugins";
26
+
27
+ export const app = oke({
28
+ name: "shop",
29
+ env: "local",
30
+ gate: { auth: {} },
31
+ }).plug(username());
32
+ ```
33
+
34
+ </Step>
35
+
36
+ <Step>
37
+ ### Sign up
38
+
39
+ ```typescript
40
+ const { data, error } = await api.auth.signUpUsername({
41
+ username: "ali",
42
+ password: "CorrectHorse1",
43
+ });
44
+ ```
45
+
46
+ `POST /auth/sign-up/username` — usernames are normalized to lowercase; allowed pattern
47
+ `[a-z0-9._-]{3,64}`.
48
+
49
+ </Step>
50
+
51
+ <Step>
52
+ ### Sign in
53
+
54
+ ```typescript
55
+ const { data } = await api.auth.signInUsername({
56
+ username: "ali",
57
+ password: "CorrectHorse1",
58
+ });
59
+ // data: accessToken, refreshToken, accessExpiresAt, userId
60
+ ```
61
+
62
+ Unknown username and bad password both return `AuthFailed` with
63
+ `reason: "invalid_credentials"` (enumeration-safe).
64
+
65
+ </Step>
66
+
67
+ </Steps>
68
+
69
+ ## Options
70
+
71
+ | Option | Type | Default | Meaning |
72
+ | ----------- | --------------- | ---------- | ----------------------------------------------------------- |
73
+ | `secret` | `string` | active\* | HMAC secret (\*from `gate.auth` when plugged after `oke()`) |
74
+ | `sessions` | `SessionStore` | active\* | Session store shared with Gate auth |
75
+ | `now` | `() => number` | `Date.now` | Injectable clock |
76
+ | `usernames` | `UsernameStore` | new map | Shared in-memory credential store |
77
+
78
+ ## Surfaces
79
+
80
+ | Flow | Path | Gate |
81
+ | --------------------- | ----------------------------- | ---------------------------- |
82
+ | `auth.signUpUsername` | `POST /auth/sign-up/username` | `gate.public` + sign-up rate |
83
+ | `auth.signInUsername` | `POST /auth/sign-in/username` | `gate.public` + sign-in rate |
84
+
85
+ ## Troubleshooting
86
+
87
+ <Accordions>
88
+ <Accordion title="plugin boot failed — needs &quot;auth&quot;">
89
+
90
+ Set `oke({ gate: { auth: { … } } })` before `.plug(username())`.
91
+
92
+ </Accordion>
93
+ <Accordion title="AuthFailed invalid_credentials on sign-up">
94
+
95
+ Username taken, or it fails the `[a-z0-9._-]{3,64}` pattern after lowercasing. Same error shape
96
+ on purpose — do not treat it as "exists" in the UI.
97
+
98
+ </Accordion>
99
+ </Accordions>
100
+
101
+ ## Learn more
102
+
103
+ - [Gate](/docs/elements/gate) — `gate.auth` and posture
104
+ - [Plugins](/docs/plugins) — all auth method plugins
105
+ - [Client](/docs/reference/client) — `createClient` + `memorySession`
106
+
107
+ ## Next
108
+
109
+ <Cards>
110
+ <Card
111
+ title="Anonymous"
112
+ description="Session without a password."
113
+ href="/docs/plugins/anonymous"
114
+ />
115
+ <Card title="Gate" description="Builtin auth and policies." href="/docs/elements/gate" />
116
+ <Card title="Magic link" description="Email link sign-in." href="/docs/plugins/magic-link" />
117
+ </Cards>
@@ -0,0 +1,331 @@
1
+ ---
2
+ title: "Client"
3
+ description: "Typed caller for your flows — createClient from okengine/client, zero codegen, errors as values."
4
+ icon: "MonitorSmartphone"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ `okengine/client` is how a browser, CLI, or another service calls your app's flows. Adopt the unit, take `typeof app`, and `api.notes.get({ id })` is fully typed — same contracts the server already has, no separate schema project.
9
+
10
+ <Callout title="The one rule">
11
+ Treat every call as a result envelope: `{ data, error }`. Flow failures are values you switch on
12
+ (`error.code`); they are never thrown. Only transport / protocol problems use
13
+ `code: "TransportError"`.
14
+ </Callout>
15
+
16
+ ## Quick start
17
+
18
+ <Steps>
19
+
20
+ <Step>
21
+ ### Adopt flows and export `App`
22
+
23
+ ```typescript title="src/app.ts"
24
+ import { oke } from "okengine";
25
+ import * as main from "./flows/main";
26
+
27
+ export const app = oke({ name: "standard" }).adopt({ main });
28
+ export type App = typeof app;
29
+ ```
30
+
31
+ </Step>
32
+
33
+ <Step>
34
+ ### Create the client
35
+
36
+ Same repo — pass the app value so HTTP triggers hit REST (method + path from adopt):
37
+
38
+ ```typescript title="client"
39
+ import { createClient } from "okengine/client";
40
+ import { app } from "./app";
41
+
42
+ const api = createClient(app, "http://localhost:6530");
43
+ ```
44
+
45
+ Or type-only with `createClient<App>(url)` and pass `$routes: app.$routes` when you want REST
46
+ instead of RPC.
47
+
48
+ </Step>
49
+
50
+ <Step>
51
+ ### Call a flow and narrow the result
52
+
53
+ ```typescript
54
+ const { data, error } = await api.main.health();
55
+
56
+ if (error) {
57
+ // TransportError or a declared flow code
58
+ return;
59
+ }
60
+
61
+ // data inferred from the flow's `out`
62
+ console.log(data.ok);
63
+ ```
64
+
65
+ With the starter, that is `GET /health` on port **6530** when `$routes` are wired.
66
+
67
+ </Step>
68
+
69
+ </Steps>
70
+
71
+ ## `createClient` forms
72
+
73
+ | Form | Types from | Wire |
74
+ | ------------------------------- | -------------------------------------- | ------------------------------------------------ |
75
+ | `createClient(app, url, opts?)` | `typeof app` | REST from `app.$routes`; untriggered flows → RPC |
76
+ | `createClient<App>(url, opts?)` | Explicit `App` type | RPC unless `opts.$routes` or `opts.routes` |
77
+ | `createClient(url, opts?)` | Ambient `Register` (`oke-client.d.ts`) | Same — pass routes for REST |
78
+
79
+ `oke dev` regenerates `oke-client.d.ts` from `GET /_oke/client.json`. A separate frontend repo
80
+ runs `oke client add <url>` (default out: `oke-client.d.ts`).
81
+
82
+ ```bash
83
+ oke client add http://localhost:6530
84
+ oke client add https://api.example.com --out ./types/oke-client.d.ts
85
+ ```
86
+
87
+ ## Options
88
+
89
+ | Option | Type | Default | Meaning |
90
+ | --------------- | ---------------------------------------------- | ------------------ | -------------------------------------------------------------- |
91
+ | `fetch` | `(input, init?) => Promise<Response>` | `globalThis.fetch` | Inject a fetch implementation |
92
+ | `headers` | `Record<string, string>` \| pairs \| `() => …` | — | Static headers, or a getter per request |
93
+ | `timeout` | `number` (ms) | — | Abort after this many milliseconds |
94
+ | `retry.retries` | `number` | `0` | Extra attempts after the first (network / 5xx) |
95
+ | `retry.delay` | `number` (ms) | `50` | Initial backoff delay |
96
+ | `retry.backoff` | `number` | `2` | Multiplier after each retry |
97
+ | `auth.getToken` | `() => string \| null \| …` | — | Bearer access token (or null) |
98
+ | `auth.refresh` | `() => Promise<string \| null \| …>` | — | Runs once on HTTP 401, then the request retries |
99
+ | `$routes` | `ClientRouteMap` | — | Runtime map from `app.$routes` (REST when method+path present) |
100
+ | `routes` | `Record<"unit.flow", { method, path }>` | — | Flat REST table; wins over flattening `$routes` |
101
+
102
+ **Consequence:** `createClient<App>(url)` alone types the proxy but still posts
103
+ `POST /_oke/{unit}/{flow}` until you pass the app value, `$routes`, or `routes`.
104
+
105
+ ## REST vs RPC
106
+
107
+ | Situation | Request |
108
+ | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
109
+ | HTTP trigger with method + path on `$routes` | That method and path (`:id` filled from input; leftover fields → query on GET/HEAD, JSON body otherwise) |
110
+ | Adopted flow with no HTTP trigger | `POST {base}/_oke/{unit}/{flow}` with JSON body |
111
+ | Incomplete proxy path (`api.notes()` with no flow) | Result error: `Incomplete path: api.notes(…)` |
112
+
113
+ ```typescript
114
+ // REST — createClient(app, url) saw method/path from on(http.get("/notes/:id"), …)
115
+ await api.notes.get({ id: "n_1" }); // GET /notes/n_1
116
+
117
+ // RPC — untriggered flow named notes.stats
118
+ await api.notes.stats({ id: "n_1" }); // POST /_oke/notes/stats
119
+ ```
120
+
121
+ ## Result envelope and helpers
122
+
123
+ Success may include optional top-level `meta` (for example pagination). Declared flow errors and
124
+ transport failures share the failure arm:
125
+
126
+ ```typescript
127
+ import { isOk, isFail, isErrorCode, isTransportError } from "okengine/client";
128
+
129
+ const result = await api.bookings.create({ flightId: "SK1", seats: 9 });
130
+
131
+ if (isOk(result)) {
132
+ result.data.id;
133
+ } else if (result.error.code === "FlightFull") {
134
+ result.error.data.seatsLeft; // narrowed
135
+ } else if (isTransportError(result.error)) {
136
+ result.error.data.message;
137
+ }
138
+
139
+ isFail(result); // true when error !== null
140
+ isErrorCode(result.error, "FlightFull"); // type predicate helper
141
+ ```
142
+
143
+ Prefer `error?.code === "FlightFull"` for inference; use `isErrorCode` in shared helpers.
144
+
145
+ ## Auth on the client
146
+
147
+ One client: `createClient`. With `gate.auth`, the app exposes `/auth/*` Flows
148
+ (sign-in, refresh, me). Helpers under `okengine/client/auth` store tokens — they are
149
+ **not** a second factory.
150
+
151
+ ```typescript
152
+ import { createClient } from "okengine/client";
153
+ import { memorySession } from "okengine/client/auth";
154
+ import { app } from "./app";
155
+
156
+ const session = memorySession();
157
+
158
+ const api = createClient(app, "http://localhost:6530", {
159
+ auth: {
160
+ getToken: () => session.getToken(),
161
+ refresh: () => session.refresh(api),
162
+ },
163
+ });
164
+
165
+ const { data } = await api.auth.signInEmail({ email, password });
166
+ if (data) session.set(data);
167
+ ```
168
+
169
+ React: `useSession(api, session)` from `okengine/client-react`.
170
+
171
+ | Step | What happens |
172
+ | ---------------------- | ---------------------------------------------------------------- |
173
+ | Every request | `getToken()` → `Authorization: Bearer …` when a token is present |
174
+ | HTTP **401** | `refresh()` runs **once**, then the same call retries |
175
+ | HTTP **403** / **429** | No refresh — decode the failure envelope as usual |
176
+
177
+ **Consequence:** `refresh` must mutate whatever `getToken` reads. Returning a new string alone does
178
+ nothing if storage was not updated.
179
+
180
+ After a gated call, switch on the denial codes (values, not throws):
181
+
182
+ | Code | HTTP | `error.data` | Typical fix |
183
+ | -------------- | ---- | ------------------ | ------------------------------------------------------------ |
184
+ | `Unauthorized` | 401 | `{}` | Sign in, or let `auth.refresh` run; re-login if still denied |
185
+ | `Forbidden` | 403 | `{ gate, reason }` | Wrong scopes / policy — show denied |
186
+ | `RateLimited` | 429 | `{ retryAfterMs }` | Wait `retryAfterMs` before retrying |
187
+
188
+ These gate codes are **not** listed in each Flow’s `errors` map — they can appear on any gated
189
+ route.
190
+
191
+ A 401 with no `{ data, error }` body becomes `TransportError` with `data.status: 401`.
192
+
193
+ | Helper | Package | Role |
194
+ | --------------------------- | ----------------------- | ------------------------------------------------ |
195
+ | `memorySession` | `okengine/client/auth` | In-memory access/refresh bag for `auth.getToken` |
196
+ | `AUTH_ERROR_CODES` | `okengine/client/auth` | Common auth Flow / gate codes |
197
+ | `useSession(api, session?)` | `okengine/client-react` | React status + `auth.me` |
198
+
199
+ Core `okengine/client` stays under the size budget — helpers are separate exports. Not in
200
+ core today: cookie jars or plugin `.client()` decorations. Browser apps: also see
201
+ [CORS](/docs/plugins/cors) and [CSRF](/docs/plugins/csrf).
202
+
203
+ ## Elements from the client
204
+
205
+ <Callout title="Flows only">
206
+ The client calls **Flows**. Every other element runs on the server through `fx`. You reach its
207
+ outcome by calling a Flow that uses it — or by handling a gate denial on that call.
208
+ </Callout>
209
+
210
+ | Element | On the client | How |
211
+ | --------------------------------- | ------------- | --------------------------------------------------------------------------------------- |
212
+ | [Flow](/docs/elements/flow) | Direct | `api.unit.flow(input)` — the only public surface |
213
+ | [Gate](/docs/elements/gate) | Indirect | Bearer via `auth`; denials as `Unauthorized` / `Forbidden` / `RateLimited` |
214
+ | [Store](/docs/elements/store) | Via Flows | `fx.store` inside Flows; `store.resource` + `on(http.resource…)` → five Flows you adopt |
215
+ | [Signal](/docs/elements/signal) | Via Flows | Emit/consume server-side; no subscribe API on `okengine/client` yet |
216
+ | [Clock](/docs/elements/clock) | Via Flows | Schedules fire on the server — the client never ticks a clock |
217
+ | [Vault](/docs/elements/vault) | Via Flows | Secrets stay server-side; never ship them to the browser package |
218
+ | [Channel](/docs/elements/channel) | Via Flows | `fx.send` in a Flow — the client does not send email/SMS/push |
219
+ | [AI](/docs/elements/ai) | Via Flows | `fx.ask` / `fx.run` inside a Flow; the client gets that Flow’s `out` |
220
+
221
+ ### Store resources
222
+
223
+ Mount a resource, adopt the returned ops, then call the five Flows like any other:
224
+
225
+ ```typescript
226
+ const notesR = store.resource(db, notes, {/* in, out, list, unit: "notes" */});
227
+ const mounted = on(http.resource("/notes", notesR.all()));
228
+ // .adopt({ notes: mounted }) →
229
+ await api.notes.list({ limit: 20 }); // GET /notes?limit=20 — meta may carry nextCursor
230
+ await api.notes.get({ id }); // GET /notes/:id — NotFound when missing
231
+ await api.notes.remove({ id }); // DELETE → 204, data undefined
232
+ ```
233
+
234
+ See [Store](/docs/elements/store) for the list query language and schemas. Auth posture for HTTP
235
+ triggers is covered under [Gate](/docs/elements/gate).
236
+
237
+ ### Signal and live queries
238
+
239
+ `delivery: "live"` and `http.get(…).live()` are Manifest / driver flags today. `okengine/client`
240
+ does **not** expose WebSocket, SSE, or `api.*.subscribe`. Until that ships, poll or call an HTTP
241
+ Flow that returns the current state.
242
+
243
+ ## Exports
244
+
245
+ | Export | Kind | Role |
246
+ | -------------------------------------------- | --------- | ------------------------------------------------- |
247
+ | `createClient` | function | Typed proxy `api.unit.flow(input?)` |
248
+ | `flattenRoutes` | function | `$routes` → flat `unit.flow` REST table |
249
+ | `createTransport` | function | Low-level HTTP transport (timeout / retry / auth) |
250
+ | `isOk` / `isFail` | function | Envelope predicates |
251
+ | `isErrorCode` / `isTransportError` | function | Error narrowing |
252
+ | `Client`, `ClientOptions`, `ClientResult`, … | types | Contracts and options |
253
+ | `Register` | interface | Module-augmentation slot for ambient App types |
254
+ | `AppOf` | type | Brand a bare route map as an App |
255
+
256
+ Budget: the `./client` export stays under the measured client-runtime cap (hard gate in CI).
257
+
258
+ ## Troubleshooting
259
+
260
+ <Accordions>
261
+
262
+ <Accordion title="api.main.health is not a function / type error">
263
+
264
+ Confirm the flow is `export`ed from the module you `.adopt({ main })`, and that `createClient` is
265
+ typed with that `App` (or ambient `Register` after `oke-client.d.ts` regenerates). Restart
266
+ `oke dev` after renaming exports.
267
+
268
+ </Accordion>
269
+
270
+ <Accordion title="Calls hit /_oke/… instead of my HTTP path">
271
+
272
+ Types alone do not choose REST. Pass `createClient(app, url)`, or
273
+ `createClient(url, { $routes: app.$routes })`, or an explicit `routes` map.
274
+
275
+ </Accordion>
276
+
277
+ <Accordion title='error.code is "TransportError"'>
278
+
279
+ Network failure, abort (`timeout`), non-JSON body, empty error response, or HTTP status without a
280
+ `{ data, error }` envelope. Declared flow codes (`NotFound`, `FlightFull`, …) never use this code.
281
+ Message text lives in `error.data.message`; HTTP status may appear as `error.data.status`.
282
+
283
+ </Accordion>
284
+
285
+ <Accordion title="401 loops or refresh never sticks">
286
+
287
+ `auth.refresh` runs once per call on HTTP 401. It must update the store `getToken` reads — the
288
+ return value is ignored. With `gate.auth`, `POST /auth/refresh` is built in; `memorySession.refresh(api)`
289
+ calls `api.auth.refresh({ refreshToken })`. Re-login when rotation fails or no refresh token remains.
290
+
291
+ </Accordion>
292
+
293
+ <Accordion title="Failed to fetch …/_oke/client.json">
294
+
295
+ `oke client add` needs a running app that serves the descriptor. Start the app (`oke dev` /
296
+ `oke start`), check the URL, then retry. Usage when the URL is missing:
297
+ `Usage: oke client add <url> [--out oke-client.d.ts]`.
298
+
299
+ </Accordion>
300
+
301
+ </Accordions>
302
+
303
+ ## Learn more
304
+
305
+ - [Basic usage](/docs/get-started/basic-usage) — adopt → client → test loop
306
+ - [Gate](/docs/elements/gate) — policies, `gate.public`, denials
307
+ - [Store](/docs/elements/store) — `store.resource` and list query language
308
+ - [Flow](/docs/elements/flow) — `in` / `out` / `errors` and `fx.fail`
309
+ - [Errors](/docs/reference/errors) — framework codes vs failure values
310
+ - [CORS](/docs/plugins/cors) · [CSRF](/docs/plugins/csrf) — browser callers
311
+ - [CLI Reference](/docs/reference/cli) — `oke client add`, `oke dev`
312
+
313
+ ## Next
314
+
315
+ <Cards>
316
+ <Card
317
+ title="Gate"
318
+ description="Auth policies and rate limits before any effect."
319
+ href="/docs/elements/gate"
320
+ />
321
+ <Card
322
+ title="Store"
323
+ description="Resources that become five typed client Flows."
324
+ href="/docs/elements/store"
325
+ />
326
+ <Card
327
+ title="Errors"
328
+ description="OKE codes, denials, and failure values."
329
+ href="/docs/reference/errors"
330
+ />
331
+ </Cards>
@@ -39,7 +39,7 @@ drivers: {
39
39
  | `store.sql` | env driver map | `sqlite` · `postgres` · `libsql` · `pglite` · `memory` |
40
40
  | `store.kv` | env driver map | `memory` · `redis` |
41
41
  | `store.files` | env driver map | `memory` · `fs` · `s3` |
42
- | `store.index` | env driver map | `memory` · `pgvector` · `libsql` |
42
+ | `store.index` | env driver map | `memory` · `pgvector` · `libsql` · `meilisearch` |
43
43
  | `signal` | env driver map | `memory` · `postgres` · `redis` · `nats` |
44
44
  | `clock` | env driver map | `memory` · `postgres` · `frozen` |
45
45
  | `vault` | env driver map | `dotenv` · `openbao` · `memory` |
@@ -24,9 +24,11 @@ OKE reads environment variables at boot for connection detail and secrets — ne
24
24
 
25
25
  ## Index store
26
26
 
27
- | Variable | Used for | Default when unset |
28
- | ------------------ | ------------------------- | ------------------ |
29
- | `OKE_INDEX_DRIVER` | Force the index driver id | config map |
27
+ | Variable | Used for | Default when unset |
28
+ | --------------------- | --------------------------------------------------- | ------------------ |
29
+ | `OKE_INDEX_DRIVER` | Force the index driver id | config map |
30
+ | `OKE_STORE_INDEX_URL` | Meilisearch base URL (`meilisearch` driver) | — |
31
+ | `OKE_STORE_INDEX_KEY` | Meilisearch API / master key (`meilisearch` driver) | `MEILI_MASTER_KEY` |
30
32
 
31
33
  ## KV store
32
34
 
@@ -44,9 +44,14 @@ See [Store](/docs/elements/store) for the query-builder surface.
44
44
  | `fx.all([...thunks])` | — | Parallel; first rejection aborts siblings |
45
45
  | `fx.race([...thunks])` | — | First settle wins; losers aborted |
46
46
  | `fx.retry(fn, opts?)` | — | Exponential backoff + jitter (plain Promise) |
47
+ | `fx.using(acq, rel, use)` | — | `release` runs once on settle or ambient abort |
47
48
  | `fx.signal` | — | Ambient `AbortSignal` for the current branch |
48
49
  | `fx.fail(code, data, opts?)` | — | Typed failure value (`opts.message` overrides) |
49
50
 
51
+ `fx.call` starts the callee with an **empty** `fx.auth` (fail-closed for authorization). For
52
+ audit/attribution only, read `fx.principal` — it propagates the originating identity without
53
+ copying into `fx.auth`. Gates never consult `fx.principal`.
54
+
50
55
  ## Concurrency and retry
51
56
 
52
57
  Pass **thunks** to `all` / `race` — not already-started Promises — so each branch gets an abort scope before work begins.
@@ -81,6 +86,18 @@ const charge = await fx.step("charge", () =>
81
86
  `Promise.all`.
82
87
  </Callout>
83
88
 
89
+ `fx.using(acquire, release, use)` scopes a process-local resource to one attempt: `release` runs
90
+ exactly once when `use` settles **or** when the ambient signal aborts (a sibling `fx.race` winner,
91
+ a failing `fx.all` sibling). It is not journaled — do not hold handles across durable park/resume.
92
+
93
+ ```typescript
94
+ const rows = await fx.using(
95
+ () => pool.acquire(),
96
+ (conn) => conn.release(),
97
+ (conn) => conn.query("select …"),
98
+ );
99
+ ```
100
+
84
101
  **Consequence:** put `fx.retry` inside `fx.step` on durable flows so a completed charge never re-runs on resume. Coarse whole-body retry is `flow({ retry: { … } })` on the same journal session.
85
102
 
86
103
  ## Channel
@@ -102,9 +119,9 @@ AI calls are nondeterministic: journaling is forced on and auto-cache disabled a
102
119
 
103
120
  ## Vault
104
121
 
105
- | Signature | Records | Returns / notes |
106
- | -------------------- | ------- | ----------------------------------------------------------------------- |
107
- | `fx.vault(contract)` | `read` | Cleartext, inside this flow only registered with the redactor at boot |
122
+ | Signature | Records | Returns / notes |
123
+ | -------------------- | ------- | ----------------------------------------------------------------------------------------- |
124
+ | `fx.vault(contract)` | `read` | `Redacted<string>` prints/logs as a placeholder; `.reveal()` at the credential boundary |
108
125
 
109
126
  ## Clock
110
127
 
@@ -144,11 +161,22 @@ Returning a plain value instead answers 200 with `{ data: value, error: null }`
144
161
 
145
162
  ## Principals
146
163
 
147
- | Property | Shape |
148
- | ------------- | --------------------------------------------------------------------- |
149
- | `fx.auth` | `{ userId: string \| null, scopes: Set<string>, verified?: boolean }` |
150
- | `fx.operator` | `{ id: string \| null }` — Console plane |
151
- | `fx.tenant` | `{ id: string \| null }` active tenant |
164
+ | Property | Shape |
165
+ | -------------- | ------------------------------------------------------------------------- |
166
+ | `fx.auth` | `{ userId: string \| null, scopes: Set<string>, verified?: boolean }` |
167
+ | `fx.operator` | `{ id: string \| null }` — Console plane |
168
+ | `fx.principal` | Read-only origin: `userId`, `operatorId`, `scopes`, `verified?`, `plane?` |
169
+ | `fx.tenant` | `{ id: string \| null }` — active tenant |
170
+
171
+ **Consequence:** use `fx.auth` / gates for authorization; use `fx.principal` only when a callee
172
+ must log who started the call chain.
173
+
174
+ ## Not on `fx`
175
+
176
+ <Callout title="No fx.metric">
177
+ Investigated and declined. [Runs](/docs/console/runs) already own per-invocation observability as
178
+ wide events; a parallel counter/gauge API would be a second instrumentation path without a sink.
179
+ </Callout>
152
180
 
153
181
  ## Learn more
154
182