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.
- package/AGENTS.md +2 -0
- package/package.json +14 -12
- package/site/content/docs/ai/index.mdx +24 -0
- package/site/content/docs/ai/llms-txt.mdx +3 -0
- package/site/content/docs/ai/meta.json +1 -1
- package/site/content/docs/ai/skills.mdx +5 -3
- package/site/content/docs/console/gates.mdx +46 -8
- package/site/content/docs/console/index.mdx +54 -0
- package/site/content/docs/console/meta.json +1 -0
- package/site/content/docs/elements/ai.mdx +2 -0
- package/site/content/docs/elements/channel.mdx +2 -0
- package/site/content/docs/elements/clock.mdx +1 -4
- package/site/content/docs/elements/flow.mdx +4 -10
- package/site/content/docs/elements/gate.mdx +189 -48
- package/site/content/docs/elements/index.mdx +45 -0
- package/site/content/docs/elements/meta.json +1 -1
- package/site/content/docs/elements/signal.mdx +1 -5
- package/site/content/docs/elements/store.mdx +27 -6
- package/site/content/docs/elements/vault.mdx +10 -11
- package/site/content/docs/get-started/basic-usage.mdx +79 -43
- package/site/content/docs/get-started/index.mdx +33 -0
- package/site/content/docs/get-started/installation.mdx +95 -43
- package/site/content/docs/get-started/introduction.mdx +128 -75
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/why.mdx +141 -0
- package/site/content/docs/index.mdx +9 -31
- package/site/content/docs/plugins/anonymous.mdx +95 -0
- package/site/content/docs/plugins/compression.mdx +2 -2
- package/site/content/docs/plugins/cors.mdx +2 -2
- package/site/content/docs/plugins/csrf.mdx +2 -2
- package/site/content/docs/plugins/email-otp.mdx +111 -0
- package/site/content/docs/plugins/{security-headers.mdx → headers.mdx} +2 -2
- package/site/content/docs/plugins/index.mdx +69 -0
- package/site/content/docs/plugins/ip-allowlist.mdx +1 -2
- package/site/content/docs/plugins/magic-link.mdx +112 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +8 -8
- package/site/content/docs/plugins/meta.json +10 -1
- package/site/content/docs/plugins/passkey.mdx +128 -0
- package/site/content/docs/plugins/phone-number.mdx +111 -0
- package/site/content/docs/plugins/two-factor.mdx +116 -0
- package/site/content/docs/plugins/username.mdx +117 -0
- package/site/content/docs/reference/client.mdx +331 -0
- package/site/content/docs/reference/configuration.mdx +1 -1
- package/site/content/docs/reference/environment-variables.mdx +5 -3
- package/site/content/docs/reference/fx.mdx +36 -8
- package/site/content/docs/reference/index.mdx +45 -0
- package/site/content/docs/reference/meta.json +11 -1
- package/site/content/docs/reference/plugins.mdx +25 -14
- package/src/auth/auth.test.ts +20 -2
- package/src/auth/bindings.ts +439 -0
- package/src/auth/breach-check.ts +112 -0
- package/src/auth/config.ts +288 -0
- package/src/auth/cookies.ts +123 -0
- package/src/auth/gate-auth.test.ts +379 -0
- package/src/auth/identity.ts +190 -0
- package/src/auth/index.ts +117 -1
- package/src/auth/method-context.ts +33 -0
- package/src/auth/operator.ts +27 -1
- package/src/auth/password-policy.test.ts +126 -0
- package/src/auth/password-policy.ts +77 -0
- package/src/auth/plugin.ts +62 -4
- package/src/auth/rate.ts +45 -0
- package/src/auth/schema.ts +260 -0
- package/src/auth/secondary-storage.ts +37 -0
- package/src/auth/sessions.ts +58 -1
- package/src/auth/tables.ts +4 -0
- package/src/auth/verification.ts +78 -0
- package/src/cli/competitor-mention-removal.test.ts +117 -0
- package/src/cli/dev.test.ts +3 -3
- package/src/cli/dev.ts +20 -0
- package/src/cli/meilisearch-local.test.ts +69 -0
- package/src/cli/meilisearch-local.ts +188 -0
- package/src/cli/schema.ts +95 -23
- package/src/client/auth.ts +120 -0
- package/src/client-react/index.ts +93 -0
- package/src/compiler/aot.test.ts +2 -1
- package/src/compiler/extract.ts +19 -0
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +5 -1
- package/src/compiler/response.ts +16 -2
- package/src/console/server/app.ts +10 -6
- package/src/console/server/auth-rate.test.ts +3 -3
- package/src/console/server/bind.ts +12 -1
- package/src/console/server/channels.test.ts +1 -1
- package/src/console/server/console-gates.ts +14 -0
- package/src/console/server/console.test.ts +6 -6
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/server/flows.ts +2 -0
- package/src/console/server/gates.ts +8 -1
- package/src/console/server/operator-db.test.ts +4 -4
- package/src/console/server/operator-db.ts +22 -4
- package/src/console/server/security.gate.test.ts +3 -3
- package/src/console/server/store.test.ts +1 -1
- package/src/console/server/store.ts +11 -1
- package/src/console/ui/dist/assets/index-CjxwRGVv.js +10 -0
- package/src/console/ui/dist/assets/panel-access-BGv45snf.js +64 -0
- package/src/console/ui/dist/assets/{panel-ai-D_m6WQI8.js → panel-ai-B2S7LEii.js} +1 -1
- package/src/console/ui/dist/assets/{panel-architecture-CKnXFyUx.js → panel-architecture-D7UJh91v.js} +1 -1
- package/src/console/ui/dist/assets/{panel-channels-DCDd4WAC.js → panel-channels-9T3ybqRu.js} +1 -1
- package/src/console/ui/dist/assets/panel-clock-Cb1UXGRQ.js +1 -0
- package/src/console/ui/dist/assets/{panel-diff-cdonmH8c.js → panel-diff-DmYbKWmN.js} +1 -1
- package/src/console/ui/dist/assets/panel-flows-PiHwT55z.js +48 -0
- package/src/console/ui/dist/assets/{panel-gates-B5eTE8XH.js → panel-gates-BQGYXvjT.js} +1 -1
- package/src/console/ui/dist/assets/panel-overview-BBnRO18l.js +1 -0
- package/src/console/ui/dist/assets/{panel-plugins-Cj7DK1er.js → panel-plugins-D0PsmVw2.js} +1 -1
- package/src/console/ui/dist/assets/panel-runs-CWuRDe0r.js +1 -0
- package/src/console/ui/dist/assets/{panel-signals-whmDXIg3.js → panel-signals-Bbg4ewpP.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-CEMHLvaw.js → panel-store-CPCbsDRa.js} +1 -1
- package/src/console/ui/dist/assets/panel-traces-DVAzuA_S.js +1 -0
- package/src/console/ui/dist/assets/{panel-vault-C9wjbki8.js → panel-vault-D1_MvOmo.js} +1 -1
- package/src/console/ui/dist/assets/{rolldown-runtime-CNC7AqOf.js → rolldown-runtime-B0Z9INg1.js} +1 -1
- package/src/console/ui/dist/index.html +2 -2
- package/src/console/ui/gates/fixture.ts +4 -0
- package/src/console/ui/gates/types.ts +2 -0
- package/src/console/ui/shell/client.ts +1 -0
- package/src/docker/compose.ts +5 -0
- package/src/docker/docker.test.ts +41 -0
- package/src/docker/recipes/index.ts +10 -2
- package/src/docker/recipes/meilisearch.ts +31 -0
- package/src/drivers/conformance.test.ts +16 -1
- package/src/drivers/conformance.ts +40 -3
- package/src/drivers/index.ts +14 -2
- package/src/drivers/libsql.ts +4 -4
- package/src/drivers/meilisearch.integration.test.ts +77 -0
- package/src/drivers/meilisearch.test.ts +181 -0
- package/src/drivers/meilisearch.ts +208 -0
- package/src/drivers/memory.ts +4 -4
- package/src/drivers/pgvector.ts +6 -6
- package/src/drivers/types.ts +93 -12
- package/src/drivers/vault-driver-removal.test.ts +6 -0
- package/src/drivers/vault-types.ts +4 -4
- package/src/elements/ai/runtime.ts +6 -0
- package/src/elements/ai.test.ts +22 -0
- package/src/elements/gate/boot.ts +136 -0
- package/src/elements/gate/config.ts +69 -0
- package/src/elements/gate/declare.ts +51 -1
- package/src/elements/gate/runtime.ts +3 -1
- package/src/elements/gate.test.ts +77 -0
- package/src/elements/gate.ts +20 -1
- package/src/elements/index.ts +8 -0
- package/src/elements/store/index-boot.test.ts +49 -7
- package/src/elements/store/runtime.ts +50 -15
- package/src/elements/store.ts +2 -0
- package/src/elements/vault.test.ts +27 -4
- package/src/elements/vault.ts +1 -1
- package/src/index.ts +15 -0
- package/src/kernel/app.ts +253 -32
- package/src/kernel/boot-bind/store.test.ts +9 -0
- package/src/kernel/boot-bind/store.ts +30 -2
- package/src/kernel/boot.test.ts +40 -3
- package/src/kernel/boot.ts +8 -0
- package/src/kernel/call.test.ts +46 -2
- package/src/kernel/concurrency.test.ts +58 -0
- package/src/kernel/concurrency.ts +48 -0
- package/src/kernel/edge.test.ts +3 -3
- package/src/kernel/flow.test.ts +2 -2
- package/src/kernel/fx.test.ts +12 -2
- package/src/kernel/fx.ts +97 -5
- package/src/kernel/hooks.test.ts +4 -4
- package/src/kernel/index.ts +22 -1
- package/src/kernel/pipeline.test.ts +12 -8
- package/src/kernel/pipeline.ts +23 -4
- package/src/kernel/plugin/decorate.test.ts +3 -3
- package/src/kernel/plugin/scoping.test.ts +3 -3
- package/src/kernel/plugin-elements.test.ts +51 -0
- package/src/kernel/plugin-needs.test.ts +83 -0
- package/src/kernel/plugin-needs.ts +129 -0
- package/src/kernel/plugin.ts +101 -0
- package/src/kernel/redacted.ts +74 -0
- package/src/kernel/registry-isolation.test.ts +5 -5
- package/src/kernel/registry.ts +102 -3
- package/src/kernel/router.ts +3 -3
- package/src/manifest/types.ts +2 -0
- package/src/plugins/anonymous.ts +58 -0
- package/src/plugins/auth/shared.ts +121 -0
- package/src/plugins/auth-methods.test.ts +176 -0
- package/src/plugins/compression.test.ts +5 -5
- package/src/plugins/config-source.test.ts +1 -1
- package/src/plugins/cors.test.ts +16 -10
- package/src/plugins/csrf.test.ts +1 -1
- package/src/plugins/email-otp.ts +161 -0
- package/src/plugins/index.ts +31 -0
- package/src/plugins/ip-allowlist.test.ts +19 -9
- package/src/plugins/magic-link.ts +163 -0
- package/src/plugins/maintenance-mode.test.ts +9 -5
- package/src/plugins/passkey.ts +216 -0
- package/src/plugins/phone-number.ts +149 -0
- package/src/plugins/security-headers.test.ts +14 -14
- package/src/plugins/two-factor.ts +249 -0
- package/src/plugins/username.ts +148 -0
- package/src/runs/runs.test.ts +6 -2
- package/src/runtime/primitives.ts +37 -4
- package/src/runtime/serve.test.ts +3 -2
- package/src/runtime/types.ts +25 -2
- package/src/test/create-test-app.test.ts +1 -1
- package/src/test/create-test-app.ts +4 -1
- package/src/test/provisions.integration.test.ts +2 -2
- package/site/content/docs/get-started/comparison.mdx +0 -65
- package/src/console/ui/dist/assets/index-CrKMmO__.js +0 -10
- package/src/console/ui/dist/assets/panel-access-C0J2D-a2.js +0 -64
- package/src/console/ui/dist/assets/panel-clock-DjGGFPzr.js +0 -1
- package/src/console/ui/dist/assets/panel-flows-DlCU5zjA.js +0 -45
- package/src/console/ui/dist/assets/panel-overview-BsFvDdts.js +0 -1
- package/src/console/ui/dist/assets/panel-runs-C0gmnoYL.js +0 -1
- package/src/console/ui/dist/assets/panel-traces-BDiAuVSK.js +0 -1
- 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 "auth"">
|
|
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
|
|
28
|
-
|
|
|
29
|
-
| `OKE_INDEX_DRIVER`
|
|
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` |
|
|
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
|
|
148
|
-
|
|
|
149
|
-
| `fx.auth`
|
|
150
|
-
| `fx.operator`
|
|
151
|
-
| `fx.
|
|
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
|
|