okengine 0.2.8 → 0.3.2

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 (180) hide show
  1. package/AGENTS.md +17 -15
  2. package/README.md +57 -29
  3. package/package.json +11 -16
  4. package/site/content/docs/ai/llms-txt.mdx +54 -0
  5. package/site/content/docs/ai/mcp.mdx +123 -0
  6. package/site/content/docs/ai/meta.json +5 -0
  7. package/site/content/docs/ai/skills.mdx +53 -0
  8. package/site/content/docs/console/access.mdx +29 -0
  9. package/site/content/docs/console/ai.mdx +35 -0
  10. package/site/content/docs/console/architecture.mdx +35 -0
  11. package/site/content/docs/console/channels.mdx +37 -0
  12. package/site/content/docs/console/clock.mdx +31 -0
  13. package/site/content/docs/console/flows.mdx +31 -0
  14. package/site/content/docs/console/gates.mdx +35 -0
  15. package/site/content/docs/console/manifest-diff.mdx +34 -0
  16. package/site/content/docs/console/meta.json +23 -0
  17. package/site/content/docs/console/overview.mdx +40 -0
  18. package/site/content/docs/console/plugins.mdx +41 -0
  19. package/site/content/docs/console/privacy.mdx +32 -0
  20. package/site/content/docs/console/runs.mdx +40 -0
  21. package/site/content/docs/console/signals.mdx +31 -0
  22. package/site/content/docs/console/store.mdx +32 -0
  23. package/site/content/docs/console/tenancy.mdx +32 -0
  24. package/site/content/docs/console/traces.mdx +34 -0
  25. package/site/content/docs/console/vault.mdx +37 -0
  26. package/site/content/docs/elements/ai.mdx +180 -0
  27. package/site/content/docs/elements/channel.mdx +167 -0
  28. package/site/content/docs/elements/clock.mdx +182 -0
  29. package/site/content/docs/elements/flow.mdx +288 -0
  30. package/site/content/docs/elements/gate.mdx +171 -0
  31. package/site/content/docs/elements/meta.json +5 -0
  32. package/site/content/docs/elements/signal.mdx +171 -0
  33. package/site/content/docs/elements/store.mdx +320 -0
  34. package/site/content/docs/elements/vault.mdx +263 -0
  35. package/site/content/docs/get-started/basic-usage.mdx +124 -0
  36. package/site/content/docs/get-started/comparison.mdx +65 -0
  37. package/site/content/docs/get-started/installation.mdx +113 -0
  38. package/site/content/docs/get-started/introduction.mdx +123 -0
  39. package/site/content/docs/get-started/meta.json +5 -0
  40. package/site/content/docs/index.mdx +63 -0
  41. package/site/content/docs/meta.json +5 -0
  42. package/site/content/docs/plugins/compression.mdx +60 -0
  43. package/site/content/docs/plugins/cors.mdx +92 -0
  44. package/site/content/docs/plugins/csrf.mdx +96 -0
  45. package/site/content/docs/plugins/ip-allowlist.mdx +92 -0
  46. package/site/content/docs/plugins/maintenance-mode.mdx +101 -0
  47. package/site/content/docs/plugins/meta.json +15 -0
  48. package/site/content/docs/plugins/security-headers.mdx +136 -0
  49. package/site/content/docs/reference/cli.md +101 -0
  50. package/site/content/docs/reference/configuration.mdx +159 -0
  51. package/site/content/docs/reference/environment-variables.mdx +87 -0
  52. package/site/content/docs/reference/errors.mdx +80 -0
  53. package/site/content/docs/reference/fx.mdx +117 -0
  54. package/site/content/docs/reference/meta.json +5 -0
  55. package/site/content/docs/reference/plugins.mdx +249 -0
  56. package/site/content/docs/reference/security.md +63 -0
  57. package/src/auth/auth.test.ts +3 -0
  58. package/src/cli/ask-dev-mode.ts +1 -1
  59. package/src/cli/db.ts +87 -17
  60. package/src/cli/dev-db-push.test.ts +32 -2
  61. package/src/cli/dev-schema-sync.test.ts +66 -0
  62. package/src/cli/dev-schema-sync.ts +139 -0
  63. package/src/cli/dev.test.ts +123 -1
  64. package/src/cli/dev.ts +150 -8
  65. package/src/cli/doc-staleness.test.ts +4 -4
  66. package/src/cli/docker-cli.test.ts +20 -0
  67. package/src/cli/docker.ts +10 -0
  68. package/src/cli/drizzle-env.test.ts +67 -0
  69. package/src/cli/drizzle-env.ts +78 -0
  70. package/src/cli/ensure-drizzle-config.ts +50 -0
  71. package/src/cli/hero-meta.test.ts +1 -1
  72. package/src/cli/load-config.ts +6 -0
  73. package/src/cli/mode.ts +24 -4
  74. package/src/cli/openbao-bootstrap.test.ts +147 -0
  75. package/src/cli/openbao-bootstrap.ts +280 -0
  76. package/src/cli/openbao-restart.integration.test.ts +136 -0
  77. package/src/cli/ports.test.ts +7 -5
  78. package/src/cli/ports.ts +6 -2
  79. package/src/cli/resolve-dev-sql-env.test.ts +48 -0
  80. package/src/cli/resolve-dev-sql-env.ts +42 -0
  81. package/src/cli/stack.ts +7 -4
  82. package/src/cli/vault-cmd.ts +63 -0
  83. package/src/client/types.ts +7 -1
  84. package/src/compiler/extract.test.ts +40 -0
  85. package/src/compiler/extract.ts +123 -1
  86. package/src/compiler/fixtures/skyport/oke.config.ts +2 -2
  87. package/src/compiler/fixtures/skyport.expected.json +1 -1
  88. package/src/compiler/response.ts +12 -0
  89. package/src/config/define-config.test.ts +4 -6
  90. package/src/config/index.ts +2 -15
  91. package/src/console/server/app.ts +2 -0
  92. package/src/console/server/vault.ts +21 -6
  93. package/src/docker/compose.ts +112 -16
  94. package/src/docker/derive.ts +7 -1
  95. package/src/docker/docker.test.ts +103 -0
  96. package/src/docker/index.ts +11 -1
  97. package/src/docker/recipes/index.ts +3 -2
  98. package/src/docker/recipes/openbao.ts +47 -0
  99. package/src/docker/recipes/redis.ts +5 -1
  100. package/src/docker/recipes/rustfs.ts +2 -3
  101. package/src/docker/stack-id.test.ts +43 -8
  102. package/src/docker/stack-id.ts +99 -20
  103. package/src/docker/stack.ts +36 -4
  104. package/src/docker/types.ts +3 -0
  105. package/src/docs-origin.ts +4 -4
  106. package/src/drivers/drizzle-dialect.test.ts +20 -0
  107. package/src/drivers/drizzle-dialect.ts +37 -0
  108. package/src/drivers/index.ts +1 -2
  109. package/src/drivers/memory.ts +278 -39
  110. package/src/drivers/s3.ts +10 -1
  111. package/src/drivers/vault-driver-removal.test.ts +55 -0
  112. package/src/drivers/vault-openbao.test.ts +97 -0
  113. package/src/drivers/vault-openbao.ts +102 -35
  114. package/src/drivers/vault-types.ts +3 -10
  115. package/src/elements/store/declare.ts +4 -1
  116. package/src/elements/store/resource-list-docs.fixture.ts +56 -0
  117. package/src/elements/store/resource-list-docs.test.ts +79 -0
  118. package/src/elements/store/resource.test.ts +253 -0
  119. package/src/elements/store/resource.ts +786 -0
  120. package/src/elements/store/sql-condition.test.ts +132 -0
  121. package/src/elements/store/sql-condition.ts +284 -46
  122. package/src/elements/store/sql-session.test.ts +86 -1
  123. package/src/elements/store/sql-session.ts +187 -27
  124. package/src/elements/store/table.ts +34 -4
  125. package/src/elements/store.ts +16 -0
  126. package/src/elements/vault/runtime.ts +1 -1
  127. package/src/elements/vault.test.ts +1 -28
  128. package/src/elements/vault.ts +1 -1
  129. package/src/kernel/app.ts +59 -25
  130. package/src/kernel/boot-bind/channel.test.ts +60 -0
  131. package/src/kernel/boot-bind/channel.ts +64 -2
  132. package/src/kernel/boot-bind/store.test.ts +10 -1
  133. package/src/kernel/boot-bind/store.ts +49 -2
  134. package/src/kernel/boot.test.ts +0 -1
  135. package/src/kernel/boot.ts +1 -1
  136. package/src/kernel/edge.test.ts +68 -0
  137. package/src/kernel/errors.registry.test.ts +1 -1
  138. package/src/kernel/flow.ts +8 -0
  139. package/src/kernel/fx.test.ts +23 -3
  140. package/src/kernel/fx.ts +115 -18
  141. package/src/kernel/hooks.test.ts +33 -0
  142. package/src/kernel/hooks.ts +22 -0
  143. package/src/kernel/index.ts +7 -0
  144. package/src/kernel/on.ts +44 -3
  145. package/src/kernel/plugin.ts +33 -3
  146. package/src/kernel/registry-isolation.test.ts +74 -0
  147. package/src/kernel/registry.ts +22 -1
  148. package/src/kernel/triggers.ts +59 -0
  149. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  150. package/src/manifest/fixtures/skyport.manifest.json +1 -1
  151. package/src/manifest/index.ts +1 -1
  152. package/src/manifest/types.ts +1 -1
  153. package/src/manifest/validate.ts +2 -2
  154. package/src/plugins/compression.test.ts +127 -0
  155. package/src/plugins/compression.ts +94 -0
  156. package/src/plugins/config-source.test.ts +204 -0
  157. package/src/plugins/config-source.ts +209 -0
  158. package/src/plugins/cors.test.ts +138 -0
  159. package/src/plugins/cors.ts +129 -0
  160. package/src/plugins/csrf.test.ts +102 -0
  161. package/src/plugins/csrf.ts +86 -0
  162. package/src/plugins/headers.ts +54 -0
  163. package/src/plugins/index.ts +26 -0
  164. package/src/plugins/ip-allowlist.test.ts +105 -0
  165. package/src/plugins/ip-allowlist.ts +76 -0
  166. package/src/plugins/maintenance-mode.test.ts +91 -0
  167. package/src/plugins/maintenance-mode.ts +85 -0
  168. package/src/plugins/security-headers.test.ts +243 -0
  169. package/src/plugins/security-headers.ts +255 -0
  170. package/src/release/measure.ts +1 -2
  171. package/src/test/create-test-app.ts +14 -2
  172. package/docs/spec/console.md +0 -762
  173. package/docs/spec/example.md +0 -1374
  174. package/docs/spec/four-applications.md +0 -1376
  175. package/docs/spec/unified-theory.md +0 -498
  176. package/src/cli/doc-drift.test.ts +0 -147
  177. package/src/cli/doc-drift.ts +0 -401
  178. package/src/cli/doctor-diff-examples.ts +0 -90
  179. package/src/drivers/vault-sops.ts +0 -246
  180. /package/{spec/manifest.v1.schema.json → manifest.v1.schema.json} +0 -0
@@ -0,0 +1,5 @@
1
+ {
2
+ "title": "Get Started",
3
+ "icon": "Rocket",
4
+ "pages": ["introduction", "comparison", "installation", "basic-usage"]
5
+ }
@@ -0,0 +1,63 @@
1
+ ---
2
+ title: "Documentation"
3
+ description: "One law. Eight elements. Ten exports."
4
+ icon: "BookOpen"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ Welcome to the okengine handbook. Pick a section below — or start with Get Started if you are new.
9
+
10
+ <Callout title="The one law">
11
+ Every backend behavior is a Flow: `on(Trigger) → Effects`. One species; triggers are typed values.
12
+ </Callout>
13
+
14
+ ```ts
15
+ on(http.post("/bookings"), createBooking);
16
+ on(every("10m"), expireStale);
17
+ on(orderPlaced, sendReceipt);
18
+ ```
19
+
20
+ ## Browse by section
21
+
22
+ <Cards>
23
+ <Card
24
+ title="Get Started"
25
+ description="One law → install → first flows."
26
+ href="/docs/get-started/introduction"
27
+ />
28
+ <Card
29
+ title="Basic Usage"
30
+ description="Build from the standard starter."
31
+ href="/docs/get-started/basic-usage"
32
+ />
33
+ <Card
34
+ title="Elements"
35
+ description="Flow → AI reference, one page each."
36
+ href="/docs/elements/flow"
37
+ />
38
+ <Card
39
+ title="Console"
40
+ description="Seventeen Manifest-derived panels."
41
+ href="/docs/console/overview"
42
+ />
43
+ <Card
44
+ title="Plugins"
45
+ description="Official extensions — security headers, CORS, CSRF, compression, maintenance, IP rules."
46
+ href="/docs/plugins/security-headers"
47
+ />
48
+ <Card
49
+ title="CLI Reference"
50
+ description="`oke` and `create-oke` everyday commands."
51
+ href="/docs/reference/cli"
52
+ />
53
+ <Card
54
+ title="Security"
55
+ description="Console security posture — Host, Origin, MCP."
56
+ href="/docs/reference/security"
57
+ />
58
+ <Card
59
+ title="AI Resources"
60
+ description="AGENTS.md, MCP `:6535`, `/llms.txt`."
61
+ href="/docs/ai/mcp"
62
+ />
63
+ </Cards>
@@ -0,0 +1,5 @@
1
+ {
2
+ "title": "Documentation",
3
+ "icon": "BookOpen",
4
+ "pages": ["index", "get-started", "elements", "plugins", "console", "reference", "ai"]
5
+ }
@@ -0,0 +1,60 @@
1
+ ---
2
+ title: "Compression"
3
+ description: "Official plugin — gzip response bodies for clients that send Accept-Encoding: gzip, with a size threshold and content-type matcher. Bun.serve does not compress on its own."
4
+ icon: "Shrink"
5
+ source: "src/plugins/compression.ts"
6
+ ---
7
+
8
+ `compression()` gzips HTTP response bodies with the native `Bun.gzipSync` binding when the client advertises `Accept-Encoding: gzip`. `Bun.serve` never compresses on its own — without this plugin every byte goes over the wire raw, no matter how large the JSON.
9
+
10
+ ## Quick start
11
+
12
+ ```typescript title="src/app.ts"
13
+ import { oke } from "okengine";
14
+ import { compression } from "okengine/plugins";
15
+
16
+ export const app = oke({ name: "shop", env: "dev" }).plug(compression());
17
+ ```
18
+
19
+ A client sending `Accept-Encoding: gzip` now receives `Content-Encoding: gzip` with a `Vary: Accept-Encoding` marker; a client that does not ask gets the untouched body.
20
+
21
+ ## Options
22
+
23
+ | Option | Type | Default | Does |
24
+ | --------- | -------- | -------------------------------------------- | ----------------------------------------------------------------------- |
25
+ | `minSize` | `number` | `1024` | Bodies smaller than this pass through raw (gzip can grow tiny payloads) |
26
+ | `match` | `RegExp` | JSON · `+json` · javascript · xml · `text/*` | Which `Content-Type`s are compressible |
27
+
28
+ ```typescript
29
+ .plug(compression({ minSize: 0 })) // compress even tiny bodies (tests, debugging)
30
+ ```
31
+
32
+ ## Notes
33
+
34
+ | Behavior | Detail |
35
+ | ----------------- | ---------------------------------------------------------------------- |
36
+ | Negotiation | Runs only when `Accept-Encoding` allows gzip — `gzip;q=0` is respected |
37
+ | Already encoded | Skips responses that already carry `Content-Encoding` |
38
+ | `no-transform` | Skips responses whose `Cache-Control` forbids transformation |
39
+ | Content-Length | Deleted after compression — stale lengths would corrupt the response |
40
+ | Non-HTTP triggers | No-op — nothing to compress outside HTTP |
41
+
42
+ ## Runtime configuration
43
+
44
+ Thresholds and matchers can follow the database like every other official plugin — pass a `configSource()` as the options. See [Plugins → Runtime configuration](/docs/reference/plugins#runtime-configuration-code-or-db) for the full contract.
45
+
46
+ ## Next
47
+
48
+ <Cards>
49
+ <Card
50
+ title="Security Headers"
51
+ description="The full secure-headers set on every response."
52
+ href="/docs/plugins/security-headers"
53
+ />
54
+ <Card
55
+ title="IP Allowlist"
56
+ description="Allow/deny rules by client IP."
57
+ href="/docs/plugins/ip-allowlist"
58
+ />
59
+ <Card title="Plugin API" description="Build your own plugin." href="/docs/reference/plugins" />
60
+ </Cards>
@@ -0,0 +1,92 @@
1
+ ---
2
+ title: "CORS"
3
+ description: "Official plugin — cross-origin rules at the edge: preflight OPTIONS answered even for paths bound to other methods, plus CORS headers on matched responses. Closed by default."
4
+ icon: "Globe"
5
+ source: "src/plugins/cors.ts"
6
+ ---
7
+
8
+ `cors()` decides which websites may call your app from a browser. It answers preflight `OPTIONS` requests itself — even for paths bound to other methods, which would otherwise 404 before any middleware could run — and stamps `Access-Control-*` headers on matched responses.
9
+
10
+ ## Quick start
11
+
12
+ ```typescript title="src/app.ts"
13
+ import { oke } from "okengine";
14
+ import { cors } from "okengine/plugins";
15
+
16
+ export const app = oke({ name: "shop", env: "dev" }).plug(
17
+ cors({ origin: "https://app.example.com" }),
18
+ );
19
+ ```
20
+
21
+ Browsers on `https://app.example.com` can now call every flow; every other origin gets a quiet `204` on preflight with **no** CORS headers, so the browser blocks it. Same-origin traffic never needs this plugin — browsers only enforce CORS across origins.
22
+
23
+ <Callout title="Closed by default">
24
+ `cors()` with no `origin` opens nothing. Cross-origin access is a deliberate decision — pass
25
+ `"*"`, one origin, or an exact-match list when you mean it.
26
+ </Callout>
27
+
28
+ ## Options
29
+
30
+ | Option | Type | Default | Does |
31
+ | ---------------- | ----------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------- |
32
+ | `origin` | `"*"` · `string` · `string[]` | none (closed) | Origins allowed cross-origin; lists are exact matches |
33
+ | `methods` | `string[]` | GET · HEAD · POST · PUT · PATCH · DELETE · OPTIONS | Methods answered on preflight |
34
+ | `allowedHeaders` | `string[]` | reflect the request's `Access-Control-Request-Headers` | `Access-Control-Allow-Headers` on preflight |
35
+ | `exposedHeaders` | `string[]` | omit | `Access-Control-Expose-Headers` on actual responses |
36
+ | `credentials` | `boolean` | `false` | Send `Access-Control-Allow-Credentials`; `"*"` is then reflected per-origin |
37
+ | `maxAge` | `number` | omit | `Access-Control-Max-Age` seconds on preflight |
38
+
39
+ ```typescript
40
+ .plug(cors({
41
+ origin: ["https://app.example.com", "https://admin.example.com"],
42
+ credentials: true,
43
+ maxAge: 600,
44
+ }))
45
+ ```
46
+
47
+ ## Notes
48
+
49
+ | Behavior | Detail |
50
+ | ----------------- | ------------------------------------------------------------------------------------------ |
51
+ | Preflight | Answered by the plugin's **edge handler** — runs even when no flow matches the path/method |
52
+ | Denied preflight | `204` with no CORS headers — the correct, quiet failure; the browser blocks it |
53
+ | Credentials + `*` | Browsers reject that pair, so the plugin reflects the request origin instead |
54
+ | `Vary` | `Origin` (plus request-method/headers on preflight) is appended, never duplicated |
55
+ | Non-HTTP triggers | No-op |
56
+
57
+ ## Runtime configuration
58
+
59
+ Origin lists belong to the class of config you want to change without a redeploy — an emergency integration, a partner cutover. Pass a `configSource()` instead of static options and the origin rule follows the database:
60
+
61
+ ```typescript
62
+ const origins = configSource({
63
+ plugin: "cors",
64
+ code: { origin: "https://app.example.com" },
65
+ db: { store: db },
66
+ kv: cache,
67
+ });
68
+ on(every("30s"), origins.sync());
69
+ export const app = oke({ name: "shop", env: "dev" }).plug(cors(origins));
70
+ ```
71
+
72
+ See [Plugins → Runtime configuration](/docs/reference/plugins#runtime-configuration-code-or-db) for the full contract.
73
+
74
+ ## Next
75
+
76
+ <Cards>
77
+ <Card
78
+ title="CSRF"
79
+ description="Block cross-site state changes with fetch metadata."
80
+ href="/docs/plugins/csrf"
81
+ />
82
+ <Card
83
+ title="Security Headers"
84
+ description="The full secure-headers set on every response."
85
+ href="/docs/plugins/security-headers"
86
+ />
87
+ <Card
88
+ title="Plugin API"
89
+ description="Edge handlers and runtime configuration."
90
+ href="/docs/reference/plugins"
91
+ />
92
+ </Cards>
@@ -0,0 +1,96 @@
1
+ ---
2
+ title: "CSRF"
3
+ description: "Official plugin — cross-site request forgery defense using fetch metadata (Sec-Fetch-Site) with an Origin fallback. Stateless: no tokens, no cookies, no session reads."
4
+ icon: "ShieldAlert"
5
+ source: "src/plugins/csrf.ts"
6
+ ---
7
+
8
+ `csrf()` blocks browsers on other sites from mutating your state. It runs at `onAuth` — before gate policies and the flow body — using the fetch-metadata headers every modern browser sends, with an `Origin` check as the fallback. No tokens to mint, no cookies to double-submit, no session reads: the defense is stateless.
9
+
10
+ ## Quick start
11
+
12
+ ```typescript title="src/app.ts"
13
+ import { oke } from "okengine";
14
+ import { csrf } from "okengine/plugins";
15
+
16
+ export const app = oke({ name: "shop", env: "dev" }).plug(csrf());
17
+ ```
18
+
19
+ `GET`/`HEAD`/`OPTIONS` always pass. A `POST` with `Sec-Fetch-Site: cross-site` now gets the gate element's typed `Forbidden` — unless its `Origin` is yours or allow-listed.
20
+
21
+ ## How it decides
22
+
23
+ <Steps>
24
+
25
+ <Step>
26
+ ### Fetch metadata first
27
+
28
+ `Sec-Fetch-Site: same-origin` and `none` pass. `same-site` passes by default (your subdomains) — set `allowSameSite: false` when subdomains host untrusted content. `cross-site` falls through to the Origin check.
29
+
30
+ </Step>
31
+
32
+ <Step>
33
+ ### Origin fallback
34
+
35
+ With `cross-site` (or no metadata at all — older browsers), the `Origin` header must be the app's own origin or an entry in `allowOrigins`; anything else is `Forbidden`.
36
+
37
+ </Step>
38
+
39
+ <Step>
40
+ ### Headerless clients
41
+
42
+ Requests carrying neither header — curl, server-to-server calls, webhooks — are not browser CSRF vectors, so they pass by default. Set `allowNoHeader: false` to fail closed when your auth is purely cookie-based.
43
+
44
+ </Step>
45
+
46
+ </Steps>
47
+
48
+ ## Options
49
+
50
+ | Option | Type | Default | Does |
51
+ | --------------- | ---------- | ------- | ------------------------------------------------------------------------------ |
52
+ | `allowOrigins` | `string[]` | none | Absolute origins allowed to mutate cross-site (e.g. a separately-hosted admin) |
53
+ | `allowSameSite` | `boolean` | `true` | Allow `Sec-Fetch-Site: same-site` (your subdomains) |
54
+ | `allowNoHeader` | `boolean` | `true` | Allow mutating requests with neither metadata nor `Origin` |
55
+
56
+ ## Notes
57
+
58
+ | Behavior | Detail |
59
+ | ----------------- | ---------------------------------------------------------------------------------------------------------- |
60
+ | Denial shape | The gate element's typed `Forbidden` (`error.data.reason: "csrf"`) — same as any gate denial |
61
+ | Legacy browsers | No fetch metadata → the `Origin` check carries the defense |
62
+ | Token patterns | Double-submit tokens can layer on later for defense-in-depth; fetch metadata alone covers current browsers |
63
+ | Non-HTTP triggers | No-op — clock and signal flows are never browser-driven |
64
+
65
+ ## Runtime configuration
66
+
67
+ Allow-listed origins are exactly the config you want to change live. Pass a `configSource()` and the rules follow the database within one sync interval:
68
+
69
+ ```typescript
70
+ const rules = configSource({
71
+ plugin: "csrf",
72
+ code: { allowOrigins: ["https://admin.example.com"] },
73
+ db: { store: db },
74
+ kv: cache,
75
+ });
76
+ on(every("30s"), rules.sync());
77
+ export const app = oke({ name: "shop", env: "dev" }).plug(csrf(rules));
78
+ ```
79
+
80
+ See [Plugins → Runtime configuration](/docs/reference/plugins#runtime-configuration-code-or-db) for the full contract.
81
+
82
+ ## Next
83
+
84
+ <Cards>
85
+ <Card title="CORS" description="Cross-origin rules at the edge." href="/docs/plugins/cors" />
86
+ <Card
87
+ title="Gate"
88
+ description="The element behind the typed denials."
89
+ href="/docs/elements/gate"
90
+ />
91
+ <Card
92
+ title="Security Headers"
93
+ description="The full secure-headers set on every response."
94
+ href="/docs/plugins/security-headers"
95
+ />
96
+ </Cards>
@@ -0,0 +1,92 @@
1
+ ---
2
+ title: "IP Allowlist"
3
+ description: "Official plugin — allow/deny rules by client IP at the edge of the pipeline, with the same typed Forbidden denial the gate element produces."
4
+ icon: "ListChecks"
5
+ source: "src/plugins/ip-allowlist.ts"
6
+ ---
7
+
8
+ `ipAllowlist()` enforces IP rules at `onAuth`, before any gate policy or flow body runs. Internal admin surfaces, staging environments, and webhook endpoints stop unknown clients at the edge with the same typed `Forbidden` denial the [gate element](/docs/elements/gate) produces.
9
+
10
+ ## Quick start
11
+
12
+ ```typescript title="src/app.ts"
13
+ import { oke } from "okengine";
14
+ import { ipAllowlist } from "okengine/plugins";
15
+
16
+ export const app = oke({ name: "shop", env: "dev" }).plug(
17
+ ipAllowlist({ allow: ["203.0.113.7", "2001:db8::42"] }),
18
+ );
19
+ ```
20
+
21
+ A client whose IP is not on the list receives `403` with a typed denial:
22
+
23
+ ```json
24
+ {
25
+ "data": null,
26
+ "error": {
27
+ "code": "Forbidden",
28
+ "data": { "reason": "ip_not_allowed", "ip": "198.51.100.9" }
29
+ }
30
+ }
31
+ ```
32
+
33
+ ## Options
34
+
35
+ | Option | Type | Default | Does |
36
+ | -------- | ---------- | ------------------- | ---------------------------------------------------------- |
37
+ | `allow` | `string[]` | — (everyone passes) | Exact IPs permitted — every other client is denied |
38
+ | `deny` | `string[]` | — (nobody blocked) | Exact IPs blocked — checked first, so deny wins on overlap |
39
+ | `header` | `string` | `"x-forwarded-for"` | Header carrying the client IP (first hop wins) |
40
+
41
+ ```typescript
42
+ .plug(ipAllowlist({ deny: ["198.51.100.9"], header: "x-real-ip" }))
43
+ ```
44
+
45
+ <Callout type="warn">
46
+ The IP header is only trustworthy behind a proxy that **sets or overwrites** it (your load
47
+ balancer, reverse proxy, or platform edge). A client connecting directly can send any
48
+ `x-forwarded-for` value it likes — an allowlist is a strong boundary only when the proxy owns the
49
+ header.
50
+ </Callout>
51
+
52
+ ## Notes
53
+
54
+ | Behavior | Detail |
55
+ | ----------------- | ---------------------------------------------------------------------------------- |
56
+ | XFF parsing | First comma-separated hop is the client; proxy-appended tails are ignored |
57
+ | Missing header | Denied when `allow` is set (unknown is not allowed); permitted for deny-only rules |
58
+ | Deny wins | An IP in both lists is blocked |
59
+ | Non-HTTP triggers | No-op — there is no client IP outside HTTP |
60
+
61
+ ## Runtime configuration
62
+
63
+ Block an abusive IP from the database and every instance picks it up on the next sync:
64
+
65
+ ```typescript
66
+ const rules = configSource({
67
+ plugin: "ip-allowlist",
68
+ code: { deny: [] },
69
+ db: { store: db },
70
+ kv: cache,
71
+ });
72
+ on(every("30s"), rules.sync());
73
+ export const app = oke({ name: "shop", env: "dev" }).plug(ipAllowlist(rules));
74
+ ```
75
+
76
+ See [Plugins → Runtime configuration](/docs/reference/plugins#runtime-configuration-code-or-db) for the full contract.
77
+
78
+ ## Next
79
+
80
+ <Cards>
81
+ <Card
82
+ title="Maintenance Mode"
83
+ description="Drain traffic with one flag."
84
+ href="/docs/plugins/maintenance-mode"
85
+ />
86
+ <Card
87
+ title="Gate"
88
+ description="Policies and rate limits after the IP edge."
89
+ href="/docs/elements/gate"
90
+ />
91
+ <Card title="Plugin API" description="Build your own plugin." href="/docs/reference/plugins" />
92
+ </Cards>
@@ -0,0 +1,101 @@
1
+ ---
2
+ title: "Maintenance Mode"
3
+ description: "Official plugin — drain HTTP traffic with one flag: every flow returns a 503 ServiceUnavailable envelope with an optional Retry-After, while allow-listed paths and an operator bypass header stay alive."
4
+ icon: "Construction"
5
+ source: "src/plugins/maintenance-mode.ts"
6
+ ---
7
+
8
+ `maintenanceMode()` short-circuits every HTTP invocation at `onRequest` — the earliest pipeline stage — with a typed `503 ServiceUnavailable` envelope. Deploys, migrations, and incidents stop traffic with one flag instead of a deploy-time firewall rule.
9
+
10
+ ## Quick start
11
+
12
+ ```typescript title="src/app.ts"
13
+ import { oke } from "okengine";
14
+ import { maintenanceMode } from "okengine/plugins";
15
+
16
+ export const app = oke({ name: "shop", env: "dev" }).plug(
17
+ maintenanceMode({ enabled: process.env.MAINTENANCE_MODE === "1" }),
18
+ );
19
+ ```
20
+
21
+ With `MAINTENANCE_MODE=1` in the environment, every HTTP flow answers:
22
+
23
+ ```json
24
+ {
25
+ "data": null,
26
+ "error": {
27
+ "code": "ServiceUnavailable",
28
+ "data": { "retryAfter": 300 },
29
+ "message": "Service is under maintenance."
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## Options
35
+
36
+ | Option | Type | Default | Does |
37
+ | -------------- | ---------- | --------------------------------- | ---------------------------------------------------- |
38
+ | `enabled` | `boolean` | `true` | Master switch — drive it from the environment |
39
+ | `retryAfter` | `number` | — (omitted) | `Retry-After` seconds on the 503 |
40
+ | `allowPaths` | `string[]` | — (none) | Path prefixes that keep serving (e.g. `["/health"]`) |
41
+ | `bypassHeader` | `string` | — (none) | Header whose non-empty value lets a request through |
42
+ | `message` | `string` | `"Service is under maintenance."` | Failure message in the envelope |
43
+
44
+ ```typescript
45
+ .plug(
46
+ maintenanceMode({
47
+ enabled: process.env.MAINTENANCE_MODE === "1",
48
+ retryAfter: 300,
49
+ allowPaths: ["/health"], // load-balancer checks stay green
50
+ bypassHeader: "x-ops-token", // operators keep working
51
+ }),
52
+ )
53
+ ```
54
+
55
+ <Callout type="info">
56
+ The bypass header is presence-based — an ops convenience, not authentication. Anyone who learns
57
+ the header name passes through, so treat its value as a light secret and never as a security
58
+ boundary.
59
+ </Callout>
60
+
61
+ ## Notes
62
+
63
+ | Behavior | Detail |
64
+ | ----------------- | ----------------------------------------------------------------------------------- |
65
+ | Pipeline position | `onRequest` — flows never parse, authenticate, or execute |
66
+ | Still shaped | The 503 flows through `onResponse`, so Security Headers and Compression apply to it |
67
+ | Non-HTTP triggers | No-op — clock flows and signal subscribers keep running |
68
+ | Infra routes | `/_oke/*` bypass the pipeline entirely and stay up |
69
+
70
+ ## Runtime configuration
71
+
72
+ The flagship `configSource()` use case — flip `enabled` from the database and drain traffic without a redeploy:
73
+
74
+ ```typescript
75
+ const maintenance = configSource({
76
+ plugin: "maintenance-mode",
77
+ code: { enabled: false },
78
+ db: { store: db },
79
+ kv: cache,
80
+ });
81
+ on(every("30s"), maintenance.sync());
82
+ export const app = oke({ name: "shop", env: "dev" }).plug(maintenanceMode(maintenance));
83
+ ```
84
+
85
+ See [Plugins → Runtime configuration](/docs/reference/plugins#runtime-configuration-code-or-db) for the full contract.
86
+
87
+ ## Next
88
+
89
+ <Cards>
90
+ <Card
91
+ title="IP Allowlist"
92
+ description="Allow/deny rules by client IP."
93
+ href="/docs/plugins/ip-allowlist"
94
+ />
95
+ <Card
96
+ title="Security Headers"
97
+ description="The full secure-headers set on every response."
98
+ href="/docs/plugins/security-headers"
99
+ />
100
+ <Card title="Plugin API" description="Build your own plugin." href="/docs/reference/plugins" />
101
+ </Cards>
@@ -0,0 +1,15 @@
1
+ {
2
+ "title": "Plugins",
3
+ "icon": "Puzzle",
4
+ "pages": [
5
+ "---Security---",
6
+ "security-headers",
7
+ "cors",
8
+ "csrf",
9
+ "ip-allowlist",
10
+ "---Operations---",
11
+ "maintenance-mode",
12
+ "---Performance---",
13
+ "compression"
14
+ ]
15
+ }