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
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Gate"
|
|
3
|
-
description: "Permission to act —
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
22
|
+
### Declare policies and register the Gate bag
|
|
21
23
|
|
|
22
|
-
Policies are named predicates; rate limits are declarative budgets.
|
|
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
|
|
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
|
|
61
|
+
`.gate(...)` composes left to right — first 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
|
|
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
|
-
###
|
|
93
|
+
### See typed denials
|
|
62
94
|
|
|
63
|
-
A denied request never reaches `do`. It returns one of three typed failures
|
|
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
|
-
|
|
107
|
+
<GatePipeline />
|
|
76
108
|
|
|
77
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
118
|
+
## Declarations
|
|
87
119
|
|
|
88
|
-
|
|
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
|
|
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
|
|
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` |
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
181
|
+
## Built-in auth (`gate.auth`)
|
|
126
182
|
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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="
|
|
279
|
+
<Accordion title="Prod boot: gate.auth: secret is required">
|
|
145
280
|
|
|
146
|
-
`
|
|
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="
|
|
284
|
+
<Accordion title="Sign-up returns AuthFailed password_policy">
|
|
150
285
|
|
|
151
|
-
|
|
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
|
-
- [
|
|
160
|
-
- [
|
|
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>
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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); //
|
|
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
|
-
|
|
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 (`
|
|
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
|
|