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,85 @@
1
+ /**
2
+ * Official `maintenance-mode` plugin — drain HTTP traffic with one flag.
3
+ * Uses only the public plugin API (unified-theory §14).
4
+ */
5
+
6
+ import { plugin, type PluginDef } from "../kernel/plugin.ts";
7
+ import {
8
+ isConfigSource,
9
+ pluginConfigSnapshot,
10
+ resolvePluginOptions,
11
+ withConfigTable,
12
+ type ConfigSource,
13
+ } from "./config-source.ts";
14
+
15
+ /** Options for {@link maintenanceMode}. */
16
+ export interface MaintenanceModeOptions {
17
+ /**
18
+ * Whether the mode is active. Default `true` — plugging the plugin turns
19
+ * it on. Drive it from the environment to flip without a code change:
20
+ * `enabled: process.env.MAINTENANCE_MODE === "1"`.
21
+ */
22
+ readonly enabled?: boolean;
23
+ /** `Retry-After` value in seconds on the 503. Omitted unless provided. */
24
+ readonly retryAfter?: number;
25
+ /**
26
+ * Path prefixes that keep serving while the mode is active (e.g.
27
+ * `["/health"]` so load-balancer checks stay green).
28
+ */
29
+ readonly allowPaths?: readonly string[];
30
+ /**
31
+ * Operator bypass: requests carrying this header name (any non-empty
32
+ * value) pass through. Presence-based — an ops convenience, not auth.
33
+ */
34
+ readonly bypassHeader?: string;
35
+ /** Failure message in the envelope. Default `"Service is under maintenance."` */
36
+ readonly message?: string;
37
+ }
38
+
39
+ /**
40
+ * Short-circuit every HTTP invocation at `onRequest` with a 503 failure
41
+ * envelope (`ServiceUnavailable`) plus an optional `Retry-After`. Excluded
42
+ * prefixes and the bypass header keep ops paths alive. The 503 still flows
43
+ * through `onResponse`, so response-shaping plugins (security headers,
44
+ * compression) apply to it too. Non-HTTP triggers no-op — clock flows and
45
+ * signal subscribers keep running.
46
+ *
47
+ * Accepts static options or a {@link ConfigSource} — the flagship use
48
+ * case: flip `enabled` from the database and drain traffic without a
49
+ * redeploy.
50
+ *
51
+ * @param options - Flag, retry hint, allow-list, bypass — or a config source
52
+ */
53
+ export function maintenanceMode(
54
+ options: MaintenanceModeOptions | ConfigSource<MaintenanceModeOptions> = {},
55
+ ): PluginDef {
56
+ const def = plugin("maintenance-mode", {
57
+ version: "0.0.2",
58
+ config: pluginConfigSnapshot(options),
59
+ }).hook("onRequest", (ctx) => {
60
+ const resolved = resolvePluginOptions(options);
61
+ if ((resolved.enabled ?? true) === false || !ctx.request) return;
62
+ const message = resolved.message ?? "Service is under maintenance.";
63
+
64
+ const { pathname } = new URL(ctx.request.url);
65
+ if (resolved.allowPaths?.some((prefix) => pathname.startsWith(prefix))) return;
66
+
67
+ const bypass = resolved.bypassHeader;
68
+ if (bypass !== undefined) {
69
+ const value = ctx.request.headers.get(bypass);
70
+ if (value !== null && value.length > 0) return;
71
+ }
72
+
73
+ const headers: Record<string, string> = {};
74
+ if (resolved.retryAfter !== undefined) headers["retry-after"] = String(resolved.retryAfter);
75
+ return Response.json(
76
+ {
77
+ data: null,
78
+ error: { code: "ServiceUnavailable", data: { retryAfter: resolved.retryAfter }, message },
79
+ },
80
+ { status: 503, headers },
81
+ );
82
+ });
83
+
84
+ return isConfigSource(options) ? withConfigTable(def, options) : def;
85
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * `security-headers` plugin — defaults on success, presence on failure,
3
+ * and app-wins override semantics through the real pipeline.
4
+ */
5
+
6
+ import { beforeEach, describe, expect, test } from "bun:test";
7
+ import { oke } from "../kernel/app.ts";
8
+ import { flow, resetFlowSeq } from "../kernel/flow.ts";
9
+ import { fail } from "../kernel/index.ts";
10
+ import { on, resetBindings } from "../kernel/on.ts";
11
+ import { http } from "../kernel/triggers.ts";
12
+ import { defaultCspDirectives, securityHeaders } from "./security-headers.ts";
13
+
14
+ beforeEach(() => {
15
+ resetBindings();
16
+ resetFlowSeq();
17
+ });
18
+
19
+ describe("securityHeaders plugin", () => {
20
+ test("sets the default trio on a successful response", async () => {
21
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
22
+ const app = oke({ name: "sec" }).plug(securityHeaders());
23
+
24
+ const res = await app.fetch(new Request("http://localhost/x"));
25
+
26
+ expect(res.status).toBe(200);
27
+ expect(res.headers.get("x-content-type-options")).toBe("nosniff");
28
+ expect(res.headers.get("x-frame-options")).toBe("DENY");
29
+ expect(res.headers.get("referrer-policy")).toBe("no-referrer");
30
+ expect(res.headers.get("content-security-policy")).toBeNull();
31
+ });
32
+
33
+ test("helmet-parity defaults: agent cluster, dns prefetch, download, cross-domain, xss filter", async () => {
34
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
35
+ const app = oke({ name: "sec-parity" }).plug(securityHeaders());
36
+
37
+ const res = await app.fetch(new Request("http://localhost/x"));
38
+
39
+ expect(res.headers.get("origin-agent-cluster")).toBe("?1");
40
+ expect(res.headers.get("x-dns-prefetch-control")).toBe("off");
41
+ expect(res.headers.get("x-download-options")).toBe("noopen");
42
+ expect(res.headers.get("x-permitted-cross-domain-policies")).toBe("none");
43
+ expect(res.headers.get("x-xss-protection")).toBe("0");
44
+ });
45
+
46
+ test("each parity default can be switched off", async () => {
47
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
48
+ const app = oke({ name: "sec-off" }).plug(
49
+ securityHeaders({
50
+ originAgentCluster: false,
51
+ dnsPrefetchControl: false,
52
+ downloadOptions: false,
53
+ xssProtection: false,
54
+ }),
55
+ );
56
+
57
+ const res = await app.fetch(new Request("http://localhost/x"));
58
+
59
+ expect(res.headers.get("origin-agent-cluster")).toBeNull();
60
+ expect(res.headers.get("x-dns-prefetch-control")).toBeNull();
61
+ expect(res.headers.get("x-download-options")).toBeNull();
62
+ expect(res.headers.get("x-xss-protection")).toBeNull();
63
+ expect(res.headers.get("x-permitted-cross-domain-policies")).toBe("none");
64
+ });
65
+
66
+ test("dnsPrefetchControl allow, custom cross-domain policy, COEP stamps when configured", async () => {
67
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
68
+ const app = oke({ name: "sec-tuned" }).plug(
69
+ securityHeaders({
70
+ dnsPrefetchControl: { allow: true },
71
+ permittedCrossDomainPolicies: "by-content-type",
72
+ crossOriginEmbedderPolicy: "require-corp",
73
+ }),
74
+ );
75
+
76
+ const res = await app.fetch(new Request("http://localhost/x"));
77
+
78
+ expect(res.headers.get("x-dns-prefetch-control")).toBe("on");
79
+ expect(res.headers.get("x-permitted-cross-domain-policies")).toBe("by-content-type");
80
+ expect(res.headers.get("cross-origin-embedder-policy")).toBe("require-corp");
81
+ });
82
+
83
+ test("x-powered-by is removed by default; a string sets a decoy; false keeps the app's", async () => {
84
+ const poweredByApp = (options: Parameters<typeof securityHeaders>[0], name: string) => {
85
+ on(
86
+ http.get("/x"),
87
+ flow({
88
+ name: "x.get",
89
+ do: () =>
90
+ new Response("{}", {
91
+ headers: { "content-type": "application/json", "x-powered-by": "Express" },
92
+ }),
93
+ }),
94
+ );
95
+ return oke({ name }).plug(securityHeaders(options));
96
+ };
97
+
98
+ const removed = await poweredByApp({}, "sec-pb-off").fetch(new Request("http://localhost/x"));
99
+ expect(removed.headers.get("x-powered-by")).toBeNull();
100
+
101
+ const decoy = await poweredByApp(
102
+ { poweredBy: "PHP 4.2.0", override: true },
103
+ "sec-pb-decoy",
104
+ ).fetch(new Request("http://localhost/x"));
105
+ expect(decoy.headers.get("x-powered-by")).toBe("PHP 4.2.0");
106
+
107
+ const kept = await poweredByApp({ poweredBy: false }, "sec-pb-keep").fetch(
108
+ new Request("http://localhost/x"),
109
+ );
110
+ expect(kept.headers.get("x-powered-by")).toBe("Express");
111
+ });
112
+
113
+ test("structured CSP merges over helmet's defaults, camelCase keys, report-only mode", async () => {
114
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
115
+ const app = oke({ name: "sec-csp-builder" }).plug(
116
+ securityHeaders({
117
+ contentSecurityPolicy: {
118
+ directives: { scriptSrc: ["'self'", "https://cdn.example.com"] },
119
+ reportOnly: true,
120
+ },
121
+ }),
122
+ );
123
+
124
+ const res = await app.fetch(new Request("http://localhost/x"));
125
+
126
+ expect(res.headers.get("content-security-policy")).toBeNull();
127
+ const reportOnly = res.headers.get("content-security-policy-report-only");
128
+ expect(reportOnly).toContain("script-src 'self' https://cdn.example.com");
129
+ expect(reportOnly).toContain("default-src 'self'");
130
+ expect(reportOnly).toContain("upgrade-insecure-requests");
131
+ });
132
+
133
+ test("structured CSP with useDefaults: false emits exactly the given directives", async () => {
134
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
135
+ const app = oke({ name: "sec-csp-bare" }).plug(
136
+ securityHeaders({
137
+ contentSecurityPolicy: { useDefaults: false, directives: { "default-src": ["'none'"] } },
138
+ }),
139
+ );
140
+
141
+ const res = await app.fetch(new Request("http://localhost/x"));
142
+ expect(res.headers.get("content-security-policy")).toBe("default-src 'none'");
143
+ });
144
+
145
+ test("defaultCspDirectives matches helmet's documented default policy", () => {
146
+ expect(defaultCspDirectives["upgrade-insecure-requests"]).toEqual([]);
147
+ expect(defaultCspDirectives["script-src-attr"]).toEqual(["'none'"]);
148
+ expect(Object.keys(defaultCspDirectives)).toHaveLength(11);
149
+ });
150
+
151
+ test("headers land on failures too (onResponse runs after onError)", async () => {
152
+ on(
153
+ http.get("/deny"),
154
+ flow({ name: "x.deny", do: () => ({ ok: true }) }).hook("beforeHandle", () =>
155
+ fail("Forbidden", {}),
156
+ ),
157
+ );
158
+ const app = oke({ name: "sec-fail" }).plug(securityHeaders());
159
+
160
+ const res = await app.fetch(new Request("http://localhost/deny"));
161
+
162
+ expect(res.status).not.toBe(200);
163
+ expect(res.headers.get("x-content-type-options")).toBe("nosniff");
164
+ });
165
+
166
+ test("adds CSP only when configured", async () => {
167
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
168
+ const app = oke({ name: "sec-csp" }).plug(
169
+ securityHeaders({ contentSecurityPolicy: "default-src 'self'" }),
170
+ );
171
+
172
+ const res = await app.fetch(new Request("http://localhost/x"));
173
+
174
+ expect(res.headers.get("content-security-policy")).toBe("default-src 'self'");
175
+ });
176
+
177
+ test("HSTS is off by default", async () => {
178
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
179
+ const app = oke({ name: "sec-hsts-off" }).plug(securityHeaders());
180
+
181
+ const res = await app.fetch(new Request("http://localhost/x"));
182
+
183
+ expect(res.headers.get("strict-transport-security")).toBeNull();
184
+ });
185
+
186
+ test("hsts: true stamps a one-year max-age", async () => {
187
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
188
+ const app = oke({ name: "sec-hsts-on" }).plug(securityHeaders({ hsts: true }));
189
+
190
+ const res = await app.fetch(new Request("http://localhost/x"));
191
+
192
+ expect(res.headers.get("strict-transport-security")).toBe("max-age=31536000");
193
+ });
194
+
195
+ test("hsts object tunes max-age, subdomains, preload", async () => {
196
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
197
+ const app = oke({ name: "sec-hsts-tuned" }).plug(
198
+ securityHeaders({ hsts: { maxAge: 63072000, includeSubDomains: true, preload: true } }),
199
+ );
200
+
201
+ const res = await app.fetch(new Request("http://localhost/x"));
202
+
203
+ expect(res.headers.get("strict-transport-security")).toBe(
204
+ "max-age=63072000; includeSubDomains; preload",
205
+ );
206
+ });
207
+
208
+ test("permissions-policy and cross-origin policies stamp when configured", async () => {
209
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
210
+ const app = oke({ name: "sec-extra" }).plug(
211
+ securityHeaders({
212
+ permissionsPolicy: "camera=(), microphone=()",
213
+ crossOriginOpenerPolicy: "same-origin",
214
+ crossOriginResourcePolicy: "same-site",
215
+ }),
216
+ );
217
+
218
+ const res = await app.fetch(new Request("http://localhost/x"));
219
+
220
+ expect(res.headers.get("permissions-policy")).toBe("camera=(), microphone=()");
221
+ expect(res.headers.get("cross-origin-opener-policy")).toBe("same-origin");
222
+ expect(res.headers.get("cross-origin-resource-policy")).toBe("same-site");
223
+ });
224
+
225
+ test("an explicit app-set value wins by default", async () => {
226
+ on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
227
+ const app = oke({ name: "sec-keep" });
228
+ app.hook("onResponse", (ctx) => {
229
+ if (!ctx.response) return;
230
+ const headers = new Headers(ctx.response.headers);
231
+ headers.set("x-frame-options", "SAMEORIGIN");
232
+ ctx.response = new Response(ctx.response.body, {
233
+ status: ctx.response.status,
234
+ headers,
235
+ });
236
+ });
237
+ app.plug(securityHeaders());
238
+
239
+ const res = await app.fetch(new Request("http://localhost/x"));
240
+
241
+ expect(res.headers.get("x-frame-options")).toBe("SAMEORIGIN");
242
+ });
243
+ });
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Official `security-headers` plugin — the complete secure-headers set on
3
+ * every HTTP response (helmet parity, API-first defaults). Uses only the
4
+ * public plugin API (unified-theory §14).
5
+ */
6
+
7
+ import { plugin, type PluginDef } from "../kernel/plugin.ts";
8
+ import {
9
+ isConfigSource,
10
+ pluginConfigSnapshot,
11
+ resolvePluginOptions,
12
+ withConfigTable,
13
+ type ConfigSource,
14
+ } from "./config-source.ts";
15
+ import { setUnlessPresent, withHeaders } from "./headers.ts";
16
+
17
+ /** HSTS value options (see {@link SecurityHeadersOptions.hsts}). */
18
+ export interface HstsOptions {
19
+ /** `max-age` in seconds. Default `31536000` (one year). */
20
+ readonly maxAge?: number;
21
+ /** Append `includeSubDomains`. Default `false`. */
22
+ readonly includeSubDomains?: boolean;
23
+ /** Append `preload`. Only meaningful with subdomains + a ≥1-year max-age. */
24
+ readonly preload?: boolean;
25
+ }
26
+
27
+ /** Structured CSP (see {@link SecurityHeadersOptions.contentSecurityPolicy}). */
28
+ export interface CspOptions {
29
+ /**
30
+ * Directive map — kebab-case (`script-src`) or camelCase (`scriptSrc`)
31
+ * keys, values as string arrays. An empty array emits a bare directive
32
+ * (e.g. `upgrade-insecure-requests`).
33
+ */
34
+ readonly directives: Readonly<Record<string, readonly string[]>>;
35
+ /** Merge over {@link defaultCspDirectives}. Default `true`. */
36
+ readonly useDefaults?: boolean;
37
+ /** Emit as `Content-Security-Policy-Report-Only`. Default `false`. */
38
+ readonly reportOnly?: boolean;
39
+ }
40
+
41
+ /**
42
+ * Helmet's default CSP — the baseline `directives` merge over unless
43
+ * `useDefaults: false`.
44
+ */
45
+ export const defaultCspDirectives: Readonly<Record<string, readonly string[]>> = {
46
+ "default-src": ["'self'"],
47
+ "base-uri": ["'self'"],
48
+ "font-src": ["'self'", "https:", "data:"],
49
+ "form-action": ["'self'"],
50
+ "frame-ancestors": ["'self'"],
51
+ "img-src": ["'self'", "data:"],
52
+ "object-src": ["'none'"],
53
+ "script-src": ["'self'"],
54
+ "script-src-attr": ["'none'"],
55
+ "style-src": ["'self'", "https:", "'unsafe-inline'"],
56
+ "upgrade-insecure-requests": [],
57
+ };
58
+
59
+ /** Options for {@link securityHeaders}. */
60
+ export interface SecurityHeadersOptions {
61
+ /**
62
+ * Content-Security-Policy — a raw header string, or a structured
63
+ * {@link CspOptions} (directive builder over helmet's defaults, optional
64
+ * report-only mode). Omitted unless provided.
65
+ */
66
+ readonly contentSecurityPolicy?: string | CspOptions;
67
+ /** X-Frame-Options value. Default `"DENY"`. */
68
+ readonly frameOptions?: "DENY" | "SAMEORIGIN";
69
+ /** Referrer-Policy value. Default `"no-referrer"`. */
70
+ readonly referrerPolicy?: string;
71
+ /**
72
+ * Strict-Transport-Security. `true` → one-year `max-age`; pass an object
73
+ * to tune. Default off — HSTS is sticky, and local dev is plain HTTP, so
74
+ * enable it deliberately for HTTPS deployments.
75
+ */
76
+ readonly hsts?: boolean | HstsOptions;
77
+ /** Permissions-Policy value (e.g. `"camera=(), microphone=()"`). Omitted unless provided. */
78
+ readonly permissionsPolicy?: string;
79
+ /**
80
+ * Cross-Origin-Opener-Policy value. Omitted unless provided — opt-in
81
+ * because OKE serves APIs, where cross-origin clients are legitimate
82
+ * (helmet targets web pages and defaults it on).
83
+ */
84
+ readonly crossOriginOpenerPolicy?: "same-origin" | "same-origin-allow-popups" | "unsafe-none";
85
+ /** Cross-Origin-Resource-Policy value. Omitted unless provided (same API rationale). */
86
+ readonly crossOriginResourcePolicy?: "same-origin" | "same-site" | "cross-origin";
87
+ /** Cross-Origin-Embedder-Policy value. Omitted unless provided (helmet also defaults it off). */
88
+ readonly crossOriginEmbedderPolicy?: "require-corp" | "credentialless";
89
+ /** Origin-Agent-Cluster header. Default `true` → `?1`. */
90
+ readonly originAgentCluster?: boolean;
91
+ /**
92
+ * X-DNS-Prefetch-Control. Default `true` → `off` (privacy-preserving);
93
+ * `{ allow: true }` → `on`; `false` omits the header.
94
+ */
95
+ readonly dnsPrefetchControl?: boolean | { readonly allow: boolean };
96
+ /** X-Download-Options: noopen (legacy IE8 mitigation, helmet parity). Default `true`. */
97
+ readonly downloadOptions?: boolean;
98
+ /** X-Permitted-Cross-Domain-Policies value. Default `"none"`. */
99
+ readonly permittedCrossDomainPolicies?: "none" | "master-only" | "by-content-type" | "all";
100
+ /**
101
+ * X-Powered-By handling. Default `true` → remove the header (it leaks
102
+ * framework fingerprints). A string sets a custom value; `false` keeps it.
103
+ */
104
+ readonly poweredBy?: boolean | string;
105
+ /**
106
+ * X-XSS-Protection: 0 — disables the legacy, buggy browser XSS auditor
107
+ * (helmet parity; modern defense is CSP). Default `true`.
108
+ */
109
+ readonly xssProtection?: boolean;
110
+ /**
111
+ * Replace headers the app already set. Default `false` — an explicit
112
+ * app-level value always wins.
113
+ */
114
+ readonly override?: boolean;
115
+ }
116
+
117
+ /** Render the Strict-Transport-Security header value. */
118
+ function hstsValue(hsts: boolean | HstsOptions): string {
119
+ const opts: HstsOptions = typeof hsts === "object" ? hsts : {};
120
+ let value = `max-age=${opts.maxAge ?? 31536000}`;
121
+ if (opts.includeSubDomains) value += "; includeSubDomains";
122
+ if (opts.preload) value += "; preload";
123
+ return value;
124
+ }
125
+
126
+ /** camelCase directive keys → kebab-case (`scriptSrc` → `script-src`). */
127
+ function directiveName(name: string): string {
128
+ return name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
129
+ }
130
+
131
+ /** Build a CSP header value from a directive map. */
132
+ function buildCsp(directives: Readonly<Record<string, readonly string[]>>): string {
133
+ return Object.entries(directives)
134
+ .map(([name, values]) => {
135
+ const kebab = directiveName(name);
136
+ return values.length === 0 ? kebab : `${kebab} ${values.join(" ")}`;
137
+ })
138
+ .join("; ");
139
+ }
140
+
141
+ /** Resolve the CSP option into header name + value. */
142
+ function cspEntry(option: string | CspOptions): { readonly name: string; readonly value: string } {
143
+ if (typeof option === "string") return { name: "content-security-policy", value: option };
144
+ const useDefaults = option.useDefaults ?? true;
145
+ const merged: Record<string, readonly string[]> = useDefaults
146
+ ? { ...defaultCspDirectives, ...option.directives }
147
+ : { ...option.directives };
148
+ return {
149
+ name:
150
+ (option.reportOnly ?? false)
151
+ ? "content-security-policy-report-only"
152
+ : "content-security-policy",
153
+ value: buildCsp(merged),
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Apply the complete secure-headers set to every HTTP response, including
159
+ * failures (runs at `onResponse`, which fires after `onError` too). Covers
160
+ * every helmet.js middleware; API-first deviations (opt-in HSTS, COOP,
161
+ * CORP) are documented per option.
162
+ *
163
+ * Accepts static options or a {@link ConfigSource} for DB-driven config
164
+ * (e.g. flip `hsts` from the database without a redeploy).
165
+ *
166
+ * @param options - Header values, or a config source
167
+ */
168
+ export function securityHeaders(
169
+ options: SecurityHeadersOptions | ConfigSource<SecurityHeadersOptions> = {},
170
+ ): PluginDef {
171
+ const def = plugin("security-headers", {
172
+ version: "0.1.0",
173
+ config: pluginConfigSnapshot(options),
174
+ }).hook("onResponse", (ctx) => {
175
+ if (!ctx.response) return;
176
+ const resolved = resolvePluginOptions(options);
177
+ const override = resolved.override ?? false;
178
+
179
+ ctx.response = withHeaders(ctx.response, (headers) => {
180
+ setUnlessPresent(headers, "x-content-type-options", "nosniff", override);
181
+ setUnlessPresent(headers, "x-frame-options", resolved.frameOptions ?? "DENY", override);
182
+ setUnlessPresent(
183
+ headers,
184
+ "referrer-policy",
185
+ resolved.referrerPolicy ?? "no-referrer",
186
+ override,
187
+ );
188
+
189
+ if (resolved.originAgentCluster ?? true) {
190
+ setUnlessPresent(headers, "origin-agent-cluster", "?1", override);
191
+ }
192
+ const dns = resolved.dnsPrefetchControl ?? true;
193
+ if (dns !== false) {
194
+ const allow = typeof dns === "object" ? dns.allow : false;
195
+ setUnlessPresent(headers, "x-dns-prefetch-control", allow ? "on" : "off", override);
196
+ }
197
+ if (resolved.downloadOptions ?? true) {
198
+ setUnlessPresent(headers, "x-download-options", "noopen", override);
199
+ }
200
+ setUnlessPresent(
201
+ headers,
202
+ "x-permitted-cross-domain-policies",
203
+ resolved.permittedCrossDomainPolicies ?? "none",
204
+ override,
205
+ );
206
+ if (resolved.xssProtection ?? true) {
207
+ setUnlessPresent(headers, "x-xss-protection", "0", override);
208
+ }
209
+
210
+ const poweredBy = resolved.poweredBy ?? true;
211
+ if (poweredBy === true) {
212
+ headers.delete("x-powered-by");
213
+ } else if (typeof poweredBy === "string") {
214
+ setUnlessPresent(headers, "x-powered-by", poweredBy, override);
215
+ }
216
+
217
+ if (resolved.contentSecurityPolicy !== undefined) {
218
+ const csp = cspEntry(resolved.contentSecurityPolicy);
219
+ setUnlessPresent(headers, csp.name, csp.value, override);
220
+ }
221
+ if (resolved.hsts !== undefined && resolved.hsts !== false) {
222
+ setUnlessPresent(headers, "strict-transport-security", hstsValue(resolved.hsts), override);
223
+ }
224
+ if (resolved.permissionsPolicy !== undefined) {
225
+ setUnlessPresent(headers, "permissions-policy", resolved.permissionsPolicy, override);
226
+ }
227
+ if (resolved.crossOriginOpenerPolicy !== undefined) {
228
+ setUnlessPresent(
229
+ headers,
230
+ "cross-origin-opener-policy",
231
+ resolved.crossOriginOpenerPolicy,
232
+ override,
233
+ );
234
+ }
235
+ if (resolved.crossOriginResourcePolicy !== undefined) {
236
+ setUnlessPresent(
237
+ headers,
238
+ "cross-origin-resource-policy",
239
+ resolved.crossOriginResourcePolicy,
240
+ override,
241
+ );
242
+ }
243
+ if (resolved.crossOriginEmbedderPolicy !== undefined) {
244
+ setUnlessPresent(
245
+ headers,
246
+ "cross-origin-embedder-policy",
247
+ resolved.crossOriginEmbedderPolicy,
248
+ override,
249
+ );
250
+ }
251
+ });
252
+ });
253
+
254
+ return isConfigSource(options) ? withConfigTable(def, options) : def;
255
+ }
@@ -29,7 +29,6 @@ const BUILD_EXTERNALS = ["@duckdb/node-api", "@duckdb/*"] as const;
29
29
  /** Heavy optional / peer packages — not counted in export:* gzip samples. */
30
30
  const EXPORT_BUILD_EXTERNALS = [
31
31
  ...BUILD_EXTERNALS,
32
- "age-encryption",
33
32
  "sently",
34
33
  "sently/*",
35
34
  "ajv",
@@ -515,7 +514,7 @@ export function formatBudgetsMarkdown(snapshot: BudgetsSnapshot): string {
515
514
  "",
516
515
  `_okengine v${snapshot.version} · measured ${snapshot.measuredAt}_`,
517
516
  "",
518
- "Core rows are absolute AGENTS caps. Exports and Drivers fail on regression vs the prior [`budgets.json`](budgets.json) (max +256 B or +2%). Export gzip excludes peers/optionals (`zod`, `age-encryption`, `sently`, `oxc-parser`, `ajv`).",
517
+ "Core rows are absolute AGENTS caps. Exports and Drivers fail on regression vs the prior [`budgets.json`](budgets.json) (max +256 B or +2%). Export gzip excludes peers/optionals (`zod`, `sently`, `oxc-parser`, `ajv`).",
519
518
  ];
520
519
  const order: readonly BudgetGroup[] = ["core", "exports", "drivers"];
521
520
  for (const group of order) {
@@ -32,6 +32,7 @@ import type { OkeApp } from "../kernel/app.ts";
32
32
  import type { BootOptions } from "../kernel/boot.ts";
33
33
  import type { EffectEntry } from "../kernel/effects.ts";
34
34
  import { fail, type FlowFailure } from "../kernel/errors.ts";
35
+ import { isJsonResult } from "../kernel/fx.ts";
35
36
  import { isFlowFailure } from "../kernel/hooks.ts";
36
37
  import type { ResolvedPrincipal } from "../kernel/pipeline.ts";
37
38
  import type { InternalTrigger, Trigger } from "../kernel/triggers.ts";
@@ -151,7 +152,7 @@ export interface TestApp<App extends OkeApp = OkeApp> {
151
152
  export type TestApiCall = (
152
153
  input?: unknown,
153
154
  opts?: TestCallOptions,
154
- ) => Promise<{ data: unknown; error: FlowFailure["error"] | null }>;
155
+ ) => Promise<{ data: unknown; error: FlowFailure["error"] | null; meta?: Record<string, unknown> }>;
155
156
 
156
157
  /** Loose API proxy — units/flows resolve at runtime from the booted app. */
157
158
  export type TestApi = Record<string, Record<string, TestApiCall>>;
@@ -341,7 +342,11 @@ function createTestApi(app: OkeApp, now: () => number): TestApi {
341
342
  flowName: string,
342
343
  input: unknown,
343
344
  opts?: TestCallOptions,
344
- ): Promise<{ data: unknown; error: FlowFailure["error"] | null }> => {
345
+ ): Promise<{
346
+ data: unknown;
347
+ error: FlowFailure["error"] | null;
348
+ meta?: Record<string, unknown>;
349
+ }> => {
345
350
  const flowDef =
346
351
  app.flow(`${unit}.${flowName}`) ?? app.flow(flowName) ?? findFlowByUnit(app, unit, flowName);
347
352
  if (!flowDef) {
@@ -373,6 +378,13 @@ function createTestApi(app: OkeApp, now: () => number): TestApi {
373
378
  if (result.output !== undefined && isFlowFailure(result.output)) {
374
379
  return { data: null, error: result.output.error };
375
380
  }
381
+ if (isJsonResult(result.output)) {
382
+ return {
383
+ data: result.output.value,
384
+ error: null,
385
+ meta: result.output.meta,
386
+ };
387
+ }
376
388
  return { data: result.output ?? null, error: null };
377
389
  };
378
390