okengine 0.2.8 → 0.3.4

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 +59 -31
  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 +98 -0
  44. package/site/content/docs/plugins/csrf.mdx +96 -0
  45. package/site/content/docs/plugins/ip-allowlist.mdx +93 -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 +149 -0
  75. package/src/cli/openbao-bootstrap.ts +314 -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 +172 -0
  159. package/src/plugins/cors.ts +153 -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 +139 -0
  165. package/src/plugins/ip-allowlist.ts +129 -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,98 @@
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`; requires an explicit origin list |
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
+ <Callout type="error">
48
+ `cors({ origin: "*", credentials: true })` throws at construction. Browsers reject that literal
49
+ pair; reflecting the request origin would grant **any** site credentialed access. List exact
50
+ origins for cookies/`Authorization` — no any-origin + credentials shortcut.
51
+ </Callout>
52
+
53
+ ## Notes
54
+
55
+ | Behavior | Detail |
56
+ | ----------------- | ------------------------------------------------------------------------------------------ |
57
+ | Preflight | Answered by the plugin's **edge handler** — runs even when no flow matches the path/method |
58
+ | Denied preflight | `204` with no CORS headers — the correct, quiet failure; the browser blocks it |
59
+ | Credentials + `*` | Construction throws — enumerate origins; never reflect `*` into credentialed access |
60
+ | `Vary` | `Origin` (plus request-method/headers on preflight) is appended, never duplicated |
61
+ | Non-HTTP triggers | No-op |
62
+
63
+ ## Runtime configuration
64
+
65
+ 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:
66
+
67
+ ```typescript
68
+ const origins = configSource({
69
+ plugin: "cors",
70
+ code: { origin: "https://app.example.com" },
71
+ db: { store: db },
72
+ kv: cache,
73
+ });
74
+ on(every("30s"), origins.sync());
75
+ export const app = oke({ name: "shop", env: "dev" }).plug(cors(origins));
76
+ ```
77
+
78
+ See [Plugins → Runtime configuration](/docs/reference/plugins#runtime-configuration-code-or-db) for the full contract.
79
+
80
+ ## Next
81
+
82
+ <Cards>
83
+ <Card
84
+ title="CSRF"
85
+ description="Block cross-site state changes with fetch metadata."
86
+ href="/docs/plugins/csrf"
87
+ />
88
+ <Card
89
+ title="Security Headers"
90
+ description="The full secure-headers set on every response."
91
+ href="/docs/plugins/security-headers"
92
+ />
93
+ <Card
94
+ title="Plugin API"
95
+ description="Edge handlers and runtime configuration."
96
+ href="/docs/reference/plugins"
97
+ />
98
+ </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,93 @@
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 |
40
+ | `trustedProxyDepth` | `number` | `1` | Trusted proxies that append XFF; client IP is that many from the right |
41
+
42
+ ```typescript
43
+ .plug(ipAllowlist({ deny: ["198.51.100.9"], trustedProxyDepth: 1 }))
44
+ ```
45
+
46
+ <Callout type="error">
47
+ Standard reverse proxies **append** to `X-Forwarded-For` — left-side hops are attacker-controlled.
48
+ The plugin trusts the hop `trustedProxyDepth` from the **right** (default `1` = last hop). Set
49
+ this to your real proxy count; wrong depth bypasses the allowlist — topology-dependent, not
50
+ drop-in.
51
+ </Callout>
52
+
53
+ ## Notes
54
+
55
+ | Behavior | Detail |
56
+ | ----------------- | ---------------------------------------------------------------------------------- |
57
+ | XFF parsing | Last hop (depth `1`) is the client; left-side spoofed entries are ignored |
58
+ | Missing header | Denied when `allow` is set (unknown is not allowed); permitted for deny-only rules |
59
+ | Deny wins | An IP in both lists is blocked |
60
+ | Non-HTTP triggers | No-op — there is no client IP outside HTTP |
61
+
62
+ ## Runtime configuration
63
+
64
+ Block an abusive IP from the database and every instance picks it up on the next sync:
65
+
66
+ ```typescript
67
+ const rules = configSource({
68
+ plugin: "ip-allowlist",
69
+ code: { deny: [] },
70
+ db: { store: db },
71
+ kv: cache,
72
+ });
73
+ on(every("30s"), rules.sync());
74
+ export const app = oke({ name: "shop", env: "dev" }).plug(ipAllowlist(rules));
75
+ ```
76
+
77
+ See [Plugins → Runtime configuration](/docs/reference/plugins#runtime-configuration-code-or-db) for the full contract.
78
+
79
+ ## Next
80
+
81
+ <Cards>
82
+ <Card
83
+ title="Maintenance Mode"
84
+ description="Drain traffic with one flag."
85
+ href="/docs/plugins/maintenance-mode"
86
+ />
87
+ <Card
88
+ title="Gate"
89
+ description="Policies and rate limits after the IP edge."
90
+ href="/docs/elements/gate"
91
+ />
92
+ <Card title="Plugin API" description="Build your own plugin." href="/docs/reference/plugins" />
93
+ </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
+ }