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,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