okengine 0.4.3 → 0.5.1

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 (138) hide show
  1. package/package.json +3 -1
  2. package/site/content/docs/ai/index.mdx +24 -0
  3. package/site/content/docs/ai/llms-txt.mdx +3 -0
  4. package/site/content/docs/ai/meta.json +1 -1
  5. package/site/content/docs/console/gates.mdx +46 -8
  6. package/site/content/docs/console/index.mdx +54 -0
  7. package/site/content/docs/console/meta.json +1 -0
  8. package/site/content/docs/elements/gate.mdx +187 -48
  9. package/site/content/docs/elements/index.mdx +45 -0
  10. package/site/content/docs/elements/meta.json +1 -1
  11. package/site/content/docs/get-started/basic-usage.mdx +4 -3
  12. package/site/content/docs/get-started/index.mdx +33 -0
  13. package/site/content/docs/get-started/meta.json +1 -1
  14. package/site/content/docs/get-started/why.mdx +10 -10
  15. package/site/content/docs/index.mdx +9 -31
  16. package/site/content/docs/plugins/anonymous.mdx +95 -0
  17. package/site/content/docs/plugins/compression.mdx +2 -2
  18. package/site/content/docs/plugins/cors.mdx +2 -2
  19. package/site/content/docs/plugins/csrf.mdx +2 -2
  20. package/site/content/docs/plugins/email-otp.mdx +111 -0
  21. package/site/content/docs/plugins/{security-headers.mdx → headers.mdx} +11 -11
  22. package/site/content/docs/plugins/index.mdx +69 -0
  23. package/site/content/docs/plugins/magic-link.mdx +112 -0
  24. package/site/content/docs/plugins/maintenance-mode.mdx +8 -8
  25. package/site/content/docs/plugins/meta.json +10 -1
  26. package/site/content/docs/plugins/passkey.mdx +140 -0
  27. package/site/content/docs/plugins/phone-number.mdx +111 -0
  28. package/site/content/docs/plugins/two-factor.mdx +117 -0
  29. package/site/content/docs/plugins/username.mdx +117 -0
  30. package/site/content/docs/reference/client.mdx +331 -0
  31. package/site/content/docs/reference/fx.mdx +20 -5
  32. package/site/content/docs/reference/index.mdx +45 -0
  33. package/site/content/docs/reference/meta.json +11 -1
  34. package/site/content/docs/reference/plugins.mdx +25 -14
  35. package/src/auth/auth.test.ts +20 -2
  36. package/src/auth/bindings.ts +439 -0
  37. package/src/auth/breach-check.ts +112 -0
  38. package/src/auth/config.ts +288 -0
  39. package/src/auth/constant-time.ts +22 -0
  40. package/src/auth/cookies.ts +123 -0
  41. package/src/auth/gate-auth.test.ts +379 -0
  42. package/src/auth/identity.ts +190 -0
  43. package/src/auth/index.ts +119 -1
  44. package/src/auth/method-context.ts +33 -0
  45. package/src/auth/operator.ts +27 -1
  46. package/src/auth/password-policy.test.ts +126 -0
  47. package/src/auth/password-policy.ts +77 -0
  48. package/src/auth/plugin.ts +62 -4
  49. package/src/auth/rate.ts +45 -0
  50. package/src/auth/schema.ts +260 -0
  51. package/src/auth/secondary-storage.ts +37 -0
  52. package/src/auth/sessions.ts +58 -1
  53. package/src/auth/tables.ts +4 -0
  54. package/src/auth/verification.ts +78 -0
  55. package/src/cli/competitor-mention-removal.test.ts +3 -3
  56. package/src/cli/dev.test.ts +3 -3
  57. package/src/cli/schema.ts +95 -23
  58. package/src/client/auth.ts +120 -0
  59. package/src/client-react/index.ts +93 -0
  60. package/src/compiler/aot.test.ts +2 -1
  61. package/src/compiler/extract.ts +19 -0
  62. package/src/compiler/response.ts +16 -2
  63. package/src/console/server/app.ts +10 -6
  64. package/src/console/server/auth-rate.test.ts +3 -3
  65. package/src/console/server/bind.ts +12 -1
  66. package/src/console/server/channels.test.ts +1 -1
  67. package/src/console/server/console-gates.ts +14 -0
  68. package/src/console/server/console.test.ts +6 -6
  69. package/src/console/server/flows-invoke.test.ts +2 -2
  70. package/src/console/server/flows.ts +2 -0
  71. package/src/console/server/gates.ts +8 -1
  72. package/src/console/server/operator-db.test.ts +4 -4
  73. package/src/console/server/operator-db.ts +22 -4
  74. package/src/console/server/security.gate.test.ts +3 -3
  75. package/src/console/ui/gates/fixture.ts +4 -0
  76. package/src/console/ui/gates/types.ts +2 -0
  77. package/src/console/ui/shell/client.ts +1 -0
  78. package/src/elements/gate/boot.ts +136 -0
  79. package/src/elements/gate/config.ts +69 -0
  80. package/src/elements/gate/declare.ts +51 -1
  81. package/src/elements/gate/runtime.ts +3 -1
  82. package/src/elements/gate.test.ts +77 -0
  83. package/src/elements/gate.ts +20 -1
  84. package/src/elements/index.ts +8 -0
  85. package/src/index.ts +11 -0
  86. package/src/kernel/app.ts +253 -32
  87. package/src/kernel/boot.test.ts +40 -3
  88. package/src/kernel/boot.ts +8 -0
  89. package/src/kernel/call.test.ts +46 -2
  90. package/src/kernel/edge.test.ts +3 -3
  91. package/src/kernel/flow.test.ts +2 -2
  92. package/src/kernel/fx.test.ts +1 -0
  93. package/src/kernel/fx.ts +60 -0
  94. package/src/kernel/hooks.test.ts +4 -4
  95. package/src/kernel/index.ts +12 -0
  96. package/src/kernel/pipeline.test.ts +12 -8
  97. package/src/kernel/pipeline.ts +23 -4
  98. package/src/kernel/plugin/decorate.test.ts +3 -3
  99. package/src/kernel/plugin/scoping.test.ts +3 -3
  100. package/src/kernel/plugin-elements.test.ts +51 -0
  101. package/src/kernel/plugin-needs.test.ts +83 -0
  102. package/src/kernel/plugin-needs.ts +129 -0
  103. package/src/kernel/plugin.ts +101 -0
  104. package/src/kernel/registry-isolation.test.ts +5 -5
  105. package/src/kernel/registry.ts +102 -3
  106. package/src/manifest/types.ts +2 -0
  107. package/src/plugins/anonymous.ts +58 -0
  108. package/src/plugins/auth/shared.ts +121 -0
  109. package/src/plugins/auth-methods.security.test.ts +762 -0
  110. package/src/plugins/auth-methods.test.ts +176 -0
  111. package/src/plugins/compression.test.ts +5 -5
  112. package/src/plugins/compression.ts +1 -1
  113. package/src/plugins/config-source.test.ts +12 -12
  114. package/src/plugins/config-source.ts +2 -2
  115. package/src/plugins/cors.test.ts +16 -10
  116. package/src/plugins/cors.ts +1 -1
  117. package/src/plugins/csrf.test.ts +1 -1
  118. package/src/plugins/email-otp.ts +161 -0
  119. package/src/plugins/{security-headers.test.ts → headers.test.ts} +26 -26
  120. package/src/plugins/headers.ts +240 -41
  121. package/src/plugins/index.ts +42 -3
  122. package/src/plugins/ip-allowlist.test.ts +19 -9
  123. package/src/plugins/magic-link.ts +163 -0
  124. package/src/plugins/maintenance-mode.test.ts +9 -5
  125. package/src/plugins/passkey-webauthn.ts +217 -0
  126. package/src/plugins/passkey.ts +282 -0
  127. package/src/plugins/phone-number.ts +149 -0
  128. package/src/plugins/response-headers.ts +54 -0
  129. package/src/plugins/two-factor.ts +253 -0
  130. package/src/plugins/username.ts +148 -0
  131. package/src/runs/runs.test.ts +6 -2
  132. package/src/runtime/primitives.ts +37 -4
  133. package/src/runtime/serve.test.ts +3 -2
  134. package/src/runtime/types.ts +25 -2
  135. package/src/test/create-test-app.test.ts +1 -1
  136. package/src/test/create-test-app.ts +4 -1
  137. package/src/test/provisions.integration.test.ts +1 -1
  138. package/src/plugins/security-headers.ts +0 -255
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okengine",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
4
4
  "description": "One law. Eight elements. Ten exports. One package. One manifest. Every backend need is derived, never added.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,6 +26,8 @@
26
26
  "exports": {
27
27
  ".": "./src/index.ts",
28
28
  "./client": "./src/client/index.ts",
29
+ "./client/auth": "./src/client/auth.ts",
30
+ "./client-react": "./src/client-react/index.ts",
29
31
  "./test": "./src/test/index.ts",
30
32
  "./config": "./src/config/index.ts",
31
33
  "./auth": "./src/auth/index.ts",
@@ -0,0 +1,24 @@
1
+ ---
2
+ title: "AI Resources"
3
+ description: "Machine-facing surfaces — MCP on :6535/:6536, agent skills, and /llms.txt."
4
+ icon: "Bot"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ Resources for AI agents that read or operate an OKE app — not the `ai` element itself.
9
+
10
+ ## Pages
11
+
12
+ <Cards>
13
+ <Card title="MCP" description="Runtime MCP on :6535; docs MCP on :6536." href="/docs/ai/mcp" />
14
+ <Card
15
+ title="Skills"
16
+ description="AGENTS.md contract and installable skills."
17
+ href="/docs/ai/skills"
18
+ />
19
+ <Card
20
+ title="llms.txt"
21
+ description="Machine-readable docs index and pages."
22
+ href="/docs/ai/llms-txt"
23
+ />
24
+ </Cards>
@@ -31,6 +31,9 @@ curl https://oke.omqkhafi.dev/llms.mdx/docs/elements/vault
31
31
  only when breadth matters more than tokens; per-page `/llms.mdx` is the precise instrument.
32
32
  </Callout>
33
33
 
34
+ The homepage **Onboard AI** button copies a one-line prompt that points an agent at
35
+ `/llms.txt` plus the repo-root `AGENTS.md` — project-wide context, not a single docs page.
36
+
34
37
  ## How it pairs with the docs MCP
35
38
 
36
39
  These endpoints serve the _published_ docs of the framework. During `oke dev`, the [docs MCP server](/docs/ai/mcp) also runs on :6536 (`oke.docs.search` / `oke.docs.get`), answering from the same content over the MCP protocol — pick the plain HTTP endpoints for one-shot context, the MCP tools when the agent is already connected as a client.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "title": "AI Resources",
3
3
  "icon": "Bot",
4
- "pages": ["mcp", "skills", "llms-txt"]
4
+ "pages": ["index", "mcp", "skills", "llms-txt"]
5
5
  }
@@ -1,13 +1,15 @@
1
1
  ---
2
2
  title: "Gates"
3
- description: "Permission matrix, rate counters, MFA map."
3
+ description: "Permission inquiry, rate counters, and continuous audit of unguarded vs explicitly public flows."
4
4
  icon: "Shield"
5
5
  source: "docs/spec/console.md"
6
6
  ---
7
7
 
8
- Answers: **permission matrix, rate counters, MFA map**
8
+ The Gates panel answers **who may do what** without dumping a giant roles×permissions matrix as
9
+ the home screen. Inquire from a principal or from a flow; the simulator shows the chain in
10
+ evaluation order.
9
11
 
10
- <Callout title="Governing rule">
12
+ <Callout title="The one rule">
11
13
  Refuse the giant roles×permissions matrix as the entry point — inquire from principal or from
12
14
  flow.
13
15
  </Callout>
@@ -15,10 +17,10 @@ Answers: **permission matrix, rate counters, MFA map**
15
17
  ## What this panel shows
16
18
 
17
19
  <Cards>
18
- <Card title="Simulator" description="Gate chain in evaluation order — where a 403 stopped." />
20
+ <Card title="Simulator" description="Gate chain in evaluation order — where a denial stopped." />
19
21
  <Card
20
- title="Unguarded flows"
21
- description="Continuous audit: public user-plane flows after deploy."
22
+ title="Unguarded vs public"
23
+ description="Empty chain (missing posture) vs gate.public (intentional)."
22
24
  />
23
25
  <Card title="Deploy diff" description="Permission widening caught from Manifest Diff." />
24
26
  <Card
@@ -27,9 +29,45 @@ Answers: **permission matrix, rate counters, MFA map**
27
29
  />
28
30
  </Cards>
29
31
 
32
+ ## Unguarded vs explicit public
33
+
34
+ | Flag | Meaning |
35
+ | --------------- | ---------------------------------------------------------------------------- |
36
+ | Unguarded | User-plane flow with an **empty** gate chain — missing auth posture |
37
+ | Explicit public | Chain includes the reserved `public` sentinel (`gate.public` on the trigger) |
38
+
39
+ App boot fails on unguarded HTTP triggers (`GateBootError`) unless `env === "test"` and
40
+ `gate: { unguardedHttp: "allow" }`. Prefer fixing the flow: attach a policy, or attach
41
+ `gate.public` when the surface is intentionally open.
42
+
43
+ App authors configure Gate under `oke({ gate: { policies, auth, unguardedHttp } })` — see
44
+ [Gate](/docs/elements/gate). Builtin `/auth/*` Flows from `gate.auth` are audited the same way.
45
+
30
46
  ## Catalog
31
47
 
48
+ Policies, rates, and `/auth/*` Bindings from the Manifest — inquire and audit, not a live
49
+ rate editor. Rate `overridable` is a catalog flag; schedule edits live under
50
+ [Clock](/docs/console/clock).
51
+
32
52
  <Cards>
33
- <Card title="Dev" description="full" />
34
- <Card title="Prod" description="limits if `overridable`" />
53
+ <Card title="Dev" description="full catalog + simulator" />
54
+ <Card title="Prod" description="same inquire surface; no silent ungated HTTP" />
55
+ </Cards>
56
+
57
+ ## Learn more
58
+
59
+ - [Gate](/docs/elements/gate) — policies, rate limits, `gate.auth`, posture
60
+ - [Plugins](/docs/plugins) — plugged sign-in method plugins
61
+ - [Access](/docs/console/access) — roles and API keys
62
+
63
+ ## Next
64
+
65
+ <Cards>
66
+ <Card title="Gate" description="Declare policies and auth." href="/docs/elements/gate" />
67
+ <Card title="Access" description="Roles and API keys." href="/docs/console/access" />
68
+ <Card
69
+ title="Overview"
70
+ description="Console panels from the Manifest."
71
+ href="/docs/console/overview"
72
+ />
35
73
  </Cards>
@@ -0,0 +1,54 @@
1
+ ---
2
+ title: "Console"
3
+ description: "Manifest-derived operator panels on :6533 — health, elements, traces, access, and more."
4
+ icon: "LayoutDashboard"
5
+ source: "docs/spec/console.md"
6
+ ---
7
+
8
+ The Console is derived from your Manifest. Each panel answers one operator question.
9
+
10
+ ## Pages
11
+
12
+ <Cards>
13
+ <Card
14
+ title="Overview"
15
+ description="Is the system healthy right now?"
16
+ href="/docs/console/overview"
17
+ />
18
+ <Card
19
+ title="Flows"
20
+ description="What exists; call it; read its contract."
21
+ href="/docs/console/flows"
22
+ />
23
+ <Card title="Signals" description="Queue depth, in-flight, DLQ." href="/docs/console/signals" />
24
+ <Card title="Store" description="Browse sql/kv/files/index." href="/docs/console/store" />
25
+ <Card title="Clock" description="Crons, sleeps, journal." href="/docs/console/clock" />
26
+ <Card
27
+ title="Gates"
28
+ description="Permission matrix and rate counters."
29
+ href="/docs/console/gates"
30
+ />
31
+ <Card title="Vault" description="Secret contracts and rotation." href="/docs/console/vault" />
32
+ <Card
33
+ title="Channels"
34
+ description="Templates, receipts, opt-outs."
35
+ href="/docs/console/channels"
36
+ />
37
+ <Card title="AI" description="Prompts, evals, cost, agents." href="/docs/console/ai" />
38
+ <Card title="Architecture" description="How it all connects." href="/docs/console/architecture" />
39
+ <Card title="Traces" description="One timeline across effects." href="/docs/console/traces" />
40
+ <Card title="Runs" description="Wide events per execution." href="/docs/console/runs" />
41
+ <Card
42
+ title="Manifest Diff"
43
+ description="Blast radius of a deploy."
44
+ href="/docs/console/manifest-diff"
45
+ />
46
+ <Card title="Access" description="Identities, roles, API keys." href="/docs/console/access" />
47
+ <Card title="Plugins" description="Installed plugins and panels." href="/docs/console/plugins" />
48
+ <Card title="Privacy" description="Where PII lives (conditional)." href="/docs/console/privacy" />
49
+ <Card
50
+ title="Tenancy"
51
+ description="Per-tenant usage (conditional)."
52
+ href="/docs/console/tenancy"
53
+ />
54
+ </Cards>
@@ -2,6 +2,7 @@
2
2
  "title": "Console",
3
3
  "icon": "LayoutDashboard",
4
4
  "pages": [
5
+ "index",
5
6
  "overview",
6
7
  "flows",
7
8
  "signals",
@@ -1,15 +1,17 @@
1
1
  ---
2
2
  title: "Gate"
3
- description: "Permission to act — auth policies and rate limits attached to the trigger, evaluated before any effect runs, denied as typed errors."
3
+ description: "Permission to act — policies, rate limits, and built-in auth on the HTTP trigger, evaluated before any effect runs."
4
4
  icon: "ShieldCheck"
5
5
  source: "docs/spec/unified-theory.md"
6
6
  ---
7
7
 
8
- Gate is how your app answers **"may this happen?"** before it happens: is this request from a verified member? has this IP exhausted its minute quota? Gates attach to the trigger — the pipeline evaluates them before a single store write, emit, or channel send runs.
8
+ Gate answers **"may this happen?"** before it happens: is this a verified member? has this IP
9
+ burned its minute quota? Attach gates to the HTTP trigger — the pipeline checks them before a
10
+ store write, emit, or channel send runs.
9
11
 
10
12
  <Callout title="The one rule">
11
- Permission sits **at the trigger**, and denial is a typed error value never a thrown exception,
12
- never an effect that runs halfway and rolls back.
13
+ Every HTTP trigger declares posture: attach a real gate, or attach `gate.public`. Omitting both
14
+ fails boot. Denial is a typed error value — never a thrown exception mid-`do`.
13
15
  </Callout>
14
16
 
15
17
  ## Quick start
@@ -17,50 +19,80 @@ Gate is how your app answers **"may this happen?"** before it happens: is this r
17
19
  <Steps>
18
20
 
19
21
  <Step>
20
- ### Declare your gates
22
+ ### Declare policies and register the Gate bag
21
23
 
22
- Policies are named predicates; rate limits are declarative budgets. Both live once, app-wide:
24
+ Policies are named predicates; rate limits are declarative budgets. Put the ones you reuse in
25
+ `oke({ gate: { policies } })`:
23
26
 
24
27
  ```typescript title="src/gates.ts"
25
28
  import { gate } from "okengine";
26
29
 
27
- export const member = gate.policy("member", ({ auth }) => !!auth?.verified);
30
+ export const member = gate.policy("member", ({ auth }) => !!auth.verified);
31
+
32
+ export const canBook = gate.scope("booking:create");
28
33
 
29
34
  export const fair = gate.rate({
30
- strategy: "sliding-window-counter", // the default — near-exact, no boundary bursts
31
35
  max: 60,
32
36
  per: "1m",
33
37
  keyBy: "ip",
34
38
  });
35
39
  ```
36
40
 
41
+ `gate.scope(name)` is sugar for
42
+ `gate.policy(name, ({ auth }) => auth.scopes.has(name))` — one string is both the policy id and
43
+ the scope checked.
44
+
45
+ ```typescript title="src/app.ts"
46
+ import { oke } from "okengine";
47
+ import { member, canBook, fair } from "./gates";
48
+
49
+ export const app = oke({
50
+ name: "shop",
51
+ env: "local",
52
+ gate: { policies: [member, canBook, fair] },
53
+ });
54
+ ```
55
+
37
56
  </Step>
38
57
 
39
58
  <Step>
40
59
  ### Attach them to triggers
41
60
 
42
- `.gate(...)` composes in registration orderevaluated left to right, first denial wins:
61
+ `.gate(...)` composes left to rightfirst denial wins. Public routes must say so explicitly:
43
62
 
44
63
  ```typescript title="src/flows/links/shorten.ts"
64
+ import { on, flow, http, gate } from "okengine";
65
+ import { member, canBook, fair } from "../../gates";
66
+
67
+ export const health = on(
68
+ http.get("/health").gate(gate.public),
69
+ flow({
70
+ out: HealthOut,
71
+ do: () => ({ ok: true as const }),
72
+ }),
73
+ );
74
+
45
75
  export const shorten = on(
46
- http.post("/links").gate(member, fair),
76
+ http.post("/links").gate(member, canBook, fair),
47
77
  flow({
48
78
  in: NewLink,
49
79
  out: LinkCode,
50
80
  errors: { Taken },
51
81
  do: async ({ url, code }, fx) => {
52
- /* runs only if both gates passed */
82
+ /* runs only if every gate passed */
53
83
  },
54
84
  }),
55
85
  );
56
86
  ```
57
87
 
88
+ Do **not** put `gate.public` in `gate.policies` — attach it on the trigger only.
89
+
58
90
  </Step>
59
91
 
60
92
  <Step>
61
- ### Denials are typed failures
93
+ ### See typed denials
62
94
 
63
- A denied request never reaches `do`. It returns one of three typed failures, like any entry in `errors: { … }`:
95
+ A denied request never reaches `do`. It returns one of three typed failures:
64
96
 
65
97
  | Gate result | Typed failure |
66
98
  | -------------------------------- | ------------------------------------- |
@@ -74,32 +106,38 @@ A denied request never reaches `do`. It returns one of three typed failures, lik
74
106
 
75
107
  <GatePipeline />
76
108
 
77
- ## Two kinds of gates
109
+ ## `oke({ gate })` bag
110
+
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"` |
78
117
 
79
- | Declaration | Question it answers | Evaluated against |
80
- | ------------------------------------- | -------------------------------------- | ---------------------------------- |
81
- | `gate.policy(name, check \| options)` | Is this principal allowed? (ABAC) | auth / operator / request metadata |
82
- | `gate.rate(options)` | Is there budget left for this subject? | an atomic counter on the kv driver |
118
+ ## Declarations
83
119
 
84
- `gate.policy(name, check)` stays the terse form. Pass
85
- `{ check, description }` when you want a human title in the Console
86
- (falls back to the policy name).
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 |
87
126
 
88
- ### The policy context
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", …)`.
89
130
 
90
- The predicate receives everything it may decide with — nothing else:
131
+ ### Policy context
91
132
 
92
133
  | Field | Contents |
93
134
  | ---------- | -------------------------------------------------------- |
94
135
  | `auth` | User principal: `userId`, `scopes` (a `Set`), `verified` |
95
136
  | `operator` | Operator principal (Console plane): `id` |
96
- | `meta` | Request metadata for keying decisions: `ip`, `userId`, … |
97
-
98
- ```typescript
99
- export const admin = gate.policy("admin", ({ auth }) => auth.scopes.has("admin"));
100
- ```
137
+ | `meta` | Request metadata for keying: `ip`, `userId`, … |
101
138
 
102
- A policy name containing `:` is also a `Module:Action` permission the same declaration feeds role-based access reviews in the Console.
139
+ A policy name containing `:` is also a `Module:Action` permission for Console role reviews.
140
+ Fine-grained authorization (`gate.scope`, roles) stays opt-in — attach it only where you need it.
103
141
 
104
142
  ### Rate options
105
143
 
@@ -109,7 +147,7 @@ A policy name containing `:` is also a `Module:Action` permission — the same d
109
147
  | `per` | string | — (**required**) | Window / refill period (`"1m"`, `"60s"`, …) |
110
148
  | `keyBy` | string | — | Subject dimension (`"ip"`, `"user"`, …) |
111
149
  | `strategy` | RateStrategy | `"sliding-window-counter"` | Algorithm (below) |
112
- | `overridable` | boolean | `false` | Allow the Console to retune `max`/`per` live |
150
+ | `overridable` | boolean | `false` | Catalog flag in Console (rate edit UI not shipped) |
113
151
  | `description` | string | — | Human title in the Console (falls back to the rate name) |
114
152
 
115
153
  | Strategy | Behavior |
@@ -120,37 +158,139 @@ A policy name containing `:` is also a `Module:Action` permission — the same d
120
158
  | `token-bucket` | Steady refill; permits saved-up bursts |
121
159
  | `leaky-bucket` | Smooths output to a constant rate |
122
160
 
123
- All five run as atomic Lua on the kv driver — correct under concurrency, identical on the memory driver in tests.
161
+ All five run as atomic Lua on the kv driver — correct under concurrency, identical on the memory
162
+ driver in tests.
163
+
164
+ ## Auth posture at boot
165
+
166
+ Every HTTP trigger must carry a gate or `gate.public`. Omitting both fails boot with
167
+ `GateBootError` — message starts with `gate boot failed —` and lists every gap.
168
+
169
+ ```typescript
170
+ on(http.get("/health").gate(gate.public), flow({/* … */}));
171
+ on(http.post("/bookings").gate(member, canBook), flow({/* … */}));
172
+ ```
173
+
174
+ **Consequence:** first `fetch` / `execute` auto-boots (`autoBoot` defaults to `true`).
175
+ `oke({ autoBoot: false })` is a unit-test escape hatch only. A rate-only chain is declared
176
+ posture but does **not** authenticate.
124
177
 
125
- ## Identity comes from auth
178
+ `gate: { unguardedHttp: "allow" }` skips the audit **only** when `env === "test"`.
179
+ In `local` / `prod` / `docker` migrate with per-trigger `gate.public`.
126
180
 
127
- Gates don't log users in — they decide about principals that already exist. The built-in auth plugin (`okengine/auth`) provides the user plane: hybrid sessions, argon2id password hashing, roles, API keys, and invites, with zero-config defaults. Whatever it establishes lands on `fx.auth`, which is exactly what your policies read.
181
+ ## Built-in auth (`gate.auth`)
128
182
 
129
- ## Every decision is recorded
183
+ Enable hybrid sessions (Bearer access + revocable refresh). Auth HTTP surfaces are ordinary
184
+ Bindings under `/auth` — same posture audit and pipeline as your app Flows:
130
185
 
131
- Pass **and** deny, every evaluation lands on the run's `gates` dimension in the telemetry trace. The Console (`:6533` → Gates) shows which gates fired on a request and their outcomes — an audit trail of permission decisions you never had to build. Rate gates declared `overridable: true` can also be retuned (`max`/`per`) from the Console without a redeploy; anything else rejects edits by design.
186
+ ```typescript title="src/app.ts"
187
+ import { oke } from "okengine";
188
+ import { member, canBook } from "./gates";
189
+
190
+ export const app = oke({
191
+ name: "shop",
192
+ env: "local",
193
+ gate: {
194
+ auth: {
195
+ emailAndPassword: { enabled: true },
196
+ session: { freshAgeMs: 24 * 60 * 60_000 },
197
+ },
198
+ policies: [member, canBook],
199
+ },
200
+ });
201
+ ```
202
+
203
+ | Surface | When |
204
+ | -------------------------- | -------------------------------------- |
205
+ | `POST /auth/sign-up/email` | `emailAndPassword.enabled` |
206
+ | `POST /auth/sign-in/email` | `emailAndPassword.enabled` |
207
+ | `POST /auth/refresh` | when `gate.auth` is set |
208
+ | `POST /auth/revoke` | when `gate.auth` is set |
209
+ | `GET /auth/me` | Bearer session (`auth.session` policy) |
210
+
211
+ | Auth option | Default | Meaning |
212
+ | ------------------------------------------- | --------- | ------------------------------------------------------------------------------ |
213
+ | `secret` | minted\* | HMAC for access tokens (\*`prod` requires you set it) |
214
+ | `basePath` | `"/auth"` | HTTP prefix for auth Flows |
215
+ | `http` | `true` | `false` = secret/tables only (no `/auth/*` Bindings) |
216
+ | `audience` | `oke-app` | Audience stamped on access tokens |
217
+ | `emailAndPassword.enabled` | `false` | Sign-up / sign-in email Flows |
218
+ | `emailAndPassword.requireEmailVerification` | `false` | Block sign-in until email is verified |
219
+ | `passwordPolicy` | secure\* | `\*` minLength 12, letter + number; sign-up → `AuthFailed` / `password_policy` |
220
+ | `breachCheck` | unset | `(password) => Promise<boolean>` — `true` rejects as `password_breached` |
221
+ | `session.freshAgeMs` | 24h | Window for step-up `auth.fresh` policy |
222
+ | `session.accessTtlMs` / `refreshTtlMs` | 14m / 30d | Access / refresh lifetimes |
223
+ | `session.idleTtlMs` | unset | Opt-in: reject refresh when idle too long |
224
+ | `session.absoluteTtlMs` | unset | Opt-in: hard cap from session creation |
225
+ | `session.singleSessionPerUser` | `false` | Opt-in: issuing a session revokes other families for that user |
226
+ | `cookies.enabled` | `false` | Opt-in HttpOnly cookie mirror (Bearer stays default) |
227
+ | `secondaryStorage.enabled` | `false` | Hot auth data in `store.kv` when configured |
228
+ | `user` / `session` / … | defaults | `modelName` / `fields` / `additionalFields` |
229
+
230
+ HIBP via `createHibpBreachCheck` from `okengine/auth` (k-anonymity range API; requires
231
+ `userAgent`; `onError` defaults to `"reject"` — fail closed while the check is enabled):
232
+
233
+ ```typescript
234
+ import { createHibpBreachCheck } from "okengine/auth";
235
+
236
+ gate: {
237
+ auth: {
238
+ emailAndPassword: { enabled: true },
239
+ breachCheck: createHibpBreachCheck({ userAgent: "shop/1.0" }),
240
+ },
241
+ }
242
+ ```
243
+
244
+ Customize tables, then run `oke schema generate` (supports `--check` in CI). Extra sign-in
245
+ shapes: [Plugins](/docs/plugins) (username, magic link, OTP, TOTP, passkeys). Call auth from
246
+ [createClient](/docs/reference/client) — one client, helpers in `okengine/client/auth`.
247
+
248
+ ## Identity and decisions
249
+
250
+ Gates decide on principals that already exist. `gate.auth` issues hybrid sessions onto
251
+ `fx.auth` (Console: `fx.operator`). Audit across `fx.call` uses read-only `fx.principal`
252
+ (see [fx](/docs/reference/fx)).
253
+
254
+ Pass **and** deny land on the run's `gates` dimension. Console (`:6533` → Gates) shows the
255
+ chain. Rate `overridable` is a catalog flag only — Clock schedules are what the Console can
256
+ edit live today.
132
257
 
133
258
  ## Troubleshooting
134
259
 
135
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>
136
267
  <Accordion title="Can a policy read the request body?">
137
268
 
138
- No — and that is deliberate. Gates run before the flow, so they see only the policy context (`auth`, `operator`, `meta`). Body-dependent decisions belong inside `do`, expressed as typed `errors`.
269
+ No. Gates run before the flow, so they see only `auth`, `operator`, and `meta`. Body-dependent
270
+ decisions belong inside `do` as typed `errors`.
139
271
 
140
272
  </Accordion>
141
273
  <Accordion title="Logged-in users still get Forbidden">
142
274
 
143
- `Forbidden` means the request _was_ authenticated but a policy said no. The failure includes the gate name and reason — check whether your predicate requires `verified` or a scope the user lacks.
275
+ `Forbidden` means authenticated but a policy said no. Check `verified` or a missing scope
276
+ the failure includes the gate name and reason.
144
277
 
145
278
  </Accordion>
146
- <Accordion title="Which keyBy should I use?">
279
+ <Accordion title="Prod boot: gate.auth: secret is required">
147
280
 
148
- `"ip"` for public unauthenticated surfaces (sign-up, password reset), `"user"` for authenticated quotas. Keying an authenticated endpoint by IP punishes users behind shared NAT; keying a public endpoint by user is impossible.
281
+ Set `gate.auth.secret` (or provide it before boot). Non-prod may mint a development secret.
149
282
 
150
283
  </Accordion>
151
- <Accordion title="The Console won't retune my rate limit">
284
+ <Accordion title="Sign-up returns AuthFailed password_policy">
152
285
 
153
- The gate was declared without `overridable: true`. Add it and redeploy — live retuning is opt-in per gate so a mistyped change cannot silently open a floodgate.
286
+ Default policy requires length 12 plus a letter and a number. Short passwords like `"123"`
287
+ are rejected — not accepted silently. Tighten or loosen via `gate.auth.passwordPolicy`.
288
+
289
+ </Accordion>
290
+ <Accordion title="Which keyBy should I use?">
291
+
292
+ `"ip"` for public surfaces (sign-up, password reset), `"user"` for authenticated quotas.
293
+ Keying an authenticated endpoint by IP punishes shared NAT.
154
294
 
155
295
  </Accordion>
156
296
  </Accordions>
@@ -158,18 +298,17 @@ The gate was declared without `overridable: true`. Add it and redeploy — live
158
298
  ## Learn more
159
299
 
160
300
  - [Flow](/docs/elements/flow) — the trigger pipeline gates plug into
161
- - [Console · Gates](/docs/console/gates) — decision audit and live retuning
162
- - [Vault](/docs/elements/vault) — the credentials your policies protect
301
+ - [fx](/docs/reference/fx) — `fx.auth`, `fx.operator`, `fx.principal`
302
+ - [Client](/docs/reference/client) — Bearer `createClient` + `okengine/client/auth`
303
+ - [Plugins](/docs/plugins) — username, magic link, OTP, TOTP, passkeys
304
+ - [Console · Gates](/docs/console/gates) — decision audit and posture
305
+ - [Vault](/docs/elements/vault) — credentials your policies protect
163
306
 
164
307
  ## Next
165
308
 
166
309
  <Cards>
167
310
  <Card title="Vault" description="Continue to Vault." href="/docs/elements/vault" />
168
- <Card
169
- title="Introduction"
170
- description="Eight elements overview."
171
- href="/docs/get-started/introduction"
172
- />
311
+ <Card title="Plugins" description="Sign-in method plugins under /auth." href="/docs/plugins" />
173
312
  <Card
174
313
  title="Console"
175
314
  description="Panels derived from the Manifest."
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: "Elements"
3
+ description: "The eight irreducible parts of an OKE backend — Flow through AI, one page each."
4
+ icon: "Boxes"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ Every backend concern collapses to one of these eight. Pick the element you need.
9
+
10
+ ## Pages
11
+
12
+ <Cards>
13
+ <Card title="Flow" description="Behavior — on(Trigger) → Effects." href="/docs/elements/flow" />
14
+ <Card
15
+ title="Signal"
16
+ description="Data in motion — queues, pub/sub, streams."
17
+ href="/docs/elements/signal"
18
+ />
19
+ <Card
20
+ title="Store"
21
+ description="Data at rest — sql, kv, files, index."
22
+ href="/docs/elements/store"
23
+ />
24
+ <Card
25
+ title="Clock"
26
+ description="Time — schedules, intervals, durable sleeps."
27
+ href="/docs/elements/clock"
28
+ />
29
+ <Card
30
+ title="Gate"
31
+ description="Permission to act — auth and rate limits."
32
+ href="/docs/elements/gate"
33
+ />
34
+ <Card
35
+ title="Vault"
36
+ description="Protected knowledge — secrets and keys."
37
+ href="/docs/elements/vault"
38
+ />
39
+ <Card
40
+ title="Channel"
41
+ description="Reaching humans — email, SMS, push."
42
+ href="/docs/elements/channel"
43
+ />
44
+ <Card title="AI" description="Reaching machine intelligence." href="/docs/elements/ai" />
45
+ </Cards>
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "title": "Elements",
3
3
  "icon": "Boxes",
4
- "pages": ["flow", "signal", "store", "clock", "gate", "vault", "channel", "ai"]
4
+ "pages": ["index", "flow", "signal", "store", "clock", "gate", "vault", "channel", "ai"]
5
5
  }
@@ -36,11 +36,11 @@ oke dev
36
36
  The standard starter already exports a health Flow:
37
37
 
38
38
  ```typescript title="flows/main"
39
- import { on, flow, http } from "okengine";
39
+ import { on, flow, http, gate } from "okengine";
40
40
  import { z } from "zod";
41
41
 
42
42
  export const health = on(
43
- http.get("/health"),
43
+ http.get("/health").gate(gate.public),
44
44
  flow({
45
45
  out: z.object({ ok: z.literal(true) }),
46
46
  do: () => ({ ok: true as const }),
@@ -75,7 +75,8 @@ const { data, error } = await api.main.health({});
75
75
  ```
76
76
 
77
77
  `data` and `error` are inferred from the Flow contracts — no separate client
78
- schema or codegen project.
78
+ schema. Bearer: `auth: { getToken, refresh }` (`okengine/client/auth` helpers;
79
+ `useSession` in `okengine/client-react`). Full options: [Client](/docs/reference/client).
79
80
 
80
81
  </Step>
81
82