okengine 0.3.6 → 0.5.0

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 (205) hide show
  1. package/AGENTS.md +2 -0
  2. package/package.json +14 -12
  3. package/site/content/docs/ai/index.mdx +24 -0
  4. package/site/content/docs/ai/llms-txt.mdx +3 -0
  5. package/site/content/docs/ai/meta.json +1 -1
  6. package/site/content/docs/ai/skills.mdx +5 -3
  7. package/site/content/docs/console/gates.mdx +46 -8
  8. package/site/content/docs/console/index.mdx +54 -0
  9. package/site/content/docs/console/meta.json +1 -0
  10. package/site/content/docs/elements/ai.mdx +2 -0
  11. package/site/content/docs/elements/channel.mdx +2 -0
  12. package/site/content/docs/elements/clock.mdx +1 -4
  13. package/site/content/docs/elements/flow.mdx +4 -10
  14. package/site/content/docs/elements/gate.mdx +189 -48
  15. package/site/content/docs/elements/index.mdx +45 -0
  16. package/site/content/docs/elements/meta.json +1 -1
  17. package/site/content/docs/elements/signal.mdx +1 -5
  18. package/site/content/docs/elements/store.mdx +27 -6
  19. package/site/content/docs/elements/vault.mdx +10 -11
  20. package/site/content/docs/get-started/basic-usage.mdx +79 -43
  21. package/site/content/docs/get-started/index.mdx +33 -0
  22. package/site/content/docs/get-started/installation.mdx +95 -43
  23. package/site/content/docs/get-started/introduction.mdx +128 -75
  24. package/site/content/docs/get-started/meta.json +1 -1
  25. package/site/content/docs/get-started/why.mdx +141 -0
  26. package/site/content/docs/index.mdx +9 -31
  27. package/site/content/docs/plugins/anonymous.mdx +95 -0
  28. package/site/content/docs/plugins/compression.mdx +2 -2
  29. package/site/content/docs/plugins/cors.mdx +2 -2
  30. package/site/content/docs/plugins/csrf.mdx +2 -2
  31. package/site/content/docs/plugins/email-otp.mdx +111 -0
  32. package/site/content/docs/plugins/{security-headers.mdx → headers.mdx} +2 -2
  33. package/site/content/docs/plugins/index.mdx +69 -0
  34. package/site/content/docs/plugins/ip-allowlist.mdx +1 -2
  35. package/site/content/docs/plugins/magic-link.mdx +112 -0
  36. package/site/content/docs/plugins/maintenance-mode.mdx +8 -8
  37. package/site/content/docs/plugins/meta.json +10 -1
  38. package/site/content/docs/plugins/passkey.mdx +128 -0
  39. package/site/content/docs/plugins/phone-number.mdx +111 -0
  40. package/site/content/docs/plugins/two-factor.mdx +116 -0
  41. package/site/content/docs/plugins/username.mdx +117 -0
  42. package/site/content/docs/reference/client.mdx +331 -0
  43. package/site/content/docs/reference/configuration.mdx +1 -1
  44. package/site/content/docs/reference/environment-variables.mdx +5 -3
  45. package/site/content/docs/reference/fx.mdx +36 -8
  46. package/site/content/docs/reference/index.mdx +45 -0
  47. package/site/content/docs/reference/meta.json +11 -1
  48. package/site/content/docs/reference/plugins.mdx +25 -14
  49. package/src/auth/auth.test.ts +20 -2
  50. package/src/auth/bindings.ts +439 -0
  51. package/src/auth/breach-check.ts +112 -0
  52. package/src/auth/config.ts +288 -0
  53. package/src/auth/cookies.ts +123 -0
  54. package/src/auth/gate-auth.test.ts +379 -0
  55. package/src/auth/identity.ts +190 -0
  56. package/src/auth/index.ts +117 -1
  57. package/src/auth/method-context.ts +33 -0
  58. package/src/auth/operator.ts +27 -1
  59. package/src/auth/password-policy.test.ts +126 -0
  60. package/src/auth/password-policy.ts +77 -0
  61. package/src/auth/plugin.ts +62 -4
  62. package/src/auth/rate.ts +45 -0
  63. package/src/auth/schema.ts +260 -0
  64. package/src/auth/secondary-storage.ts +37 -0
  65. package/src/auth/sessions.ts +58 -1
  66. package/src/auth/tables.ts +4 -0
  67. package/src/auth/verification.ts +78 -0
  68. package/src/cli/competitor-mention-removal.test.ts +117 -0
  69. package/src/cli/dev.test.ts +3 -3
  70. package/src/cli/dev.ts +20 -0
  71. package/src/cli/meilisearch-local.test.ts +69 -0
  72. package/src/cli/meilisearch-local.ts +188 -0
  73. package/src/cli/schema.ts +95 -23
  74. package/src/client/auth.ts +120 -0
  75. package/src/client-react/index.ts +93 -0
  76. package/src/compiler/aot.test.ts +2 -1
  77. package/src/compiler/extract.ts +19 -0
  78. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +5 -1
  79. package/src/compiler/response.ts +16 -2
  80. package/src/console/server/app.ts +10 -6
  81. package/src/console/server/auth-rate.test.ts +3 -3
  82. package/src/console/server/bind.ts +12 -1
  83. package/src/console/server/channels.test.ts +1 -1
  84. package/src/console/server/console-gates.ts +14 -0
  85. package/src/console/server/console.test.ts +6 -6
  86. package/src/console/server/flows-invoke.test.ts +2 -2
  87. package/src/console/server/flows.ts +2 -0
  88. package/src/console/server/gates.ts +8 -1
  89. package/src/console/server/operator-db.test.ts +4 -4
  90. package/src/console/server/operator-db.ts +22 -4
  91. package/src/console/server/security.gate.test.ts +3 -3
  92. package/src/console/server/store.test.ts +1 -1
  93. package/src/console/server/store.ts +11 -1
  94. package/src/console/ui/dist/assets/index-CjxwRGVv.js +10 -0
  95. package/src/console/ui/dist/assets/panel-access-BGv45snf.js +64 -0
  96. package/src/console/ui/dist/assets/{panel-ai-D_m6WQI8.js → panel-ai-B2S7LEii.js} +1 -1
  97. package/src/console/ui/dist/assets/{panel-architecture-CKnXFyUx.js → panel-architecture-D7UJh91v.js} +1 -1
  98. package/src/console/ui/dist/assets/{panel-channels-DCDd4WAC.js → panel-channels-9T3ybqRu.js} +1 -1
  99. package/src/console/ui/dist/assets/panel-clock-Cb1UXGRQ.js +1 -0
  100. package/src/console/ui/dist/assets/{panel-diff-cdonmH8c.js → panel-diff-DmYbKWmN.js} +1 -1
  101. package/src/console/ui/dist/assets/panel-flows-PiHwT55z.js +48 -0
  102. package/src/console/ui/dist/assets/{panel-gates-B5eTE8XH.js → panel-gates-BQGYXvjT.js} +1 -1
  103. package/src/console/ui/dist/assets/panel-overview-BBnRO18l.js +1 -0
  104. package/src/console/ui/dist/assets/{panel-plugins-Cj7DK1er.js → panel-plugins-D0PsmVw2.js} +1 -1
  105. package/src/console/ui/dist/assets/panel-runs-CWuRDe0r.js +1 -0
  106. package/src/console/ui/dist/assets/{panel-signals-whmDXIg3.js → panel-signals-Bbg4ewpP.js} +1 -1
  107. package/src/console/ui/dist/assets/{panel-store-CEMHLvaw.js → panel-store-CPCbsDRa.js} +1 -1
  108. package/src/console/ui/dist/assets/panel-traces-DVAzuA_S.js +1 -0
  109. package/src/console/ui/dist/assets/{panel-vault-C9wjbki8.js → panel-vault-D1_MvOmo.js} +1 -1
  110. package/src/console/ui/dist/assets/{rolldown-runtime-CNC7AqOf.js → rolldown-runtime-B0Z9INg1.js} +1 -1
  111. package/src/console/ui/dist/index.html +2 -2
  112. package/src/console/ui/gates/fixture.ts +4 -0
  113. package/src/console/ui/gates/types.ts +2 -0
  114. package/src/console/ui/shell/client.ts +1 -0
  115. package/src/docker/compose.ts +5 -0
  116. package/src/docker/docker.test.ts +41 -0
  117. package/src/docker/recipes/index.ts +10 -2
  118. package/src/docker/recipes/meilisearch.ts +31 -0
  119. package/src/drivers/conformance.test.ts +16 -1
  120. package/src/drivers/conformance.ts +40 -3
  121. package/src/drivers/index.ts +14 -2
  122. package/src/drivers/libsql.ts +4 -4
  123. package/src/drivers/meilisearch.integration.test.ts +77 -0
  124. package/src/drivers/meilisearch.test.ts +181 -0
  125. package/src/drivers/meilisearch.ts +208 -0
  126. package/src/drivers/memory.ts +4 -4
  127. package/src/drivers/pgvector.ts +6 -6
  128. package/src/drivers/types.ts +93 -12
  129. package/src/drivers/vault-driver-removal.test.ts +6 -0
  130. package/src/drivers/vault-types.ts +4 -4
  131. package/src/elements/ai/runtime.ts +6 -0
  132. package/src/elements/ai.test.ts +22 -0
  133. package/src/elements/gate/boot.ts +136 -0
  134. package/src/elements/gate/config.ts +69 -0
  135. package/src/elements/gate/declare.ts +51 -1
  136. package/src/elements/gate/runtime.ts +3 -1
  137. package/src/elements/gate.test.ts +77 -0
  138. package/src/elements/gate.ts +20 -1
  139. package/src/elements/index.ts +8 -0
  140. package/src/elements/store/index-boot.test.ts +49 -7
  141. package/src/elements/store/runtime.ts +50 -15
  142. package/src/elements/store.ts +2 -0
  143. package/src/elements/vault.test.ts +27 -4
  144. package/src/elements/vault.ts +1 -1
  145. package/src/index.ts +15 -0
  146. package/src/kernel/app.ts +253 -32
  147. package/src/kernel/boot-bind/store.test.ts +9 -0
  148. package/src/kernel/boot-bind/store.ts +30 -2
  149. package/src/kernel/boot.test.ts +40 -3
  150. package/src/kernel/boot.ts +8 -0
  151. package/src/kernel/call.test.ts +46 -2
  152. package/src/kernel/concurrency.test.ts +58 -0
  153. package/src/kernel/concurrency.ts +48 -0
  154. package/src/kernel/edge.test.ts +3 -3
  155. package/src/kernel/flow.test.ts +2 -2
  156. package/src/kernel/fx.test.ts +12 -2
  157. package/src/kernel/fx.ts +97 -5
  158. package/src/kernel/hooks.test.ts +4 -4
  159. package/src/kernel/index.ts +22 -1
  160. package/src/kernel/pipeline.test.ts +12 -8
  161. package/src/kernel/pipeline.ts +23 -4
  162. package/src/kernel/plugin/decorate.test.ts +3 -3
  163. package/src/kernel/plugin/scoping.test.ts +3 -3
  164. package/src/kernel/plugin-elements.test.ts +51 -0
  165. package/src/kernel/plugin-needs.test.ts +83 -0
  166. package/src/kernel/plugin-needs.ts +129 -0
  167. package/src/kernel/plugin.ts +101 -0
  168. package/src/kernel/redacted.ts +74 -0
  169. package/src/kernel/registry-isolation.test.ts +5 -5
  170. package/src/kernel/registry.ts +102 -3
  171. package/src/kernel/router.ts +3 -3
  172. package/src/manifest/types.ts +2 -0
  173. package/src/plugins/anonymous.ts +58 -0
  174. package/src/plugins/auth/shared.ts +121 -0
  175. package/src/plugins/auth-methods.test.ts +176 -0
  176. package/src/plugins/compression.test.ts +5 -5
  177. package/src/plugins/config-source.test.ts +1 -1
  178. package/src/plugins/cors.test.ts +16 -10
  179. package/src/plugins/csrf.test.ts +1 -1
  180. package/src/plugins/email-otp.ts +161 -0
  181. package/src/plugins/index.ts +31 -0
  182. package/src/plugins/ip-allowlist.test.ts +19 -9
  183. package/src/plugins/magic-link.ts +163 -0
  184. package/src/plugins/maintenance-mode.test.ts +9 -5
  185. package/src/plugins/passkey.ts +216 -0
  186. package/src/plugins/phone-number.ts +149 -0
  187. package/src/plugins/security-headers.test.ts +14 -14
  188. package/src/plugins/two-factor.ts +249 -0
  189. package/src/plugins/username.ts +148 -0
  190. package/src/runs/runs.test.ts +6 -2
  191. package/src/runtime/primitives.ts +37 -4
  192. package/src/runtime/serve.test.ts +3 -2
  193. package/src/runtime/types.ts +25 -2
  194. package/src/test/create-test-app.test.ts +1 -1
  195. package/src/test/create-test-app.ts +4 -1
  196. package/src/test/provisions.integration.test.ts +2 -2
  197. package/site/content/docs/get-started/comparison.mdx +0 -65
  198. package/src/console/ui/dist/assets/index-CrKMmO__.js +0 -10
  199. package/src/console/ui/dist/assets/panel-access-C0J2D-a2.js +0 -64
  200. package/src/console/ui/dist/assets/panel-clock-DjGGFPzr.js +0 -1
  201. package/src/console/ui/dist/assets/panel-flows-DlCU5zjA.js +0 -45
  202. package/src/console/ui/dist/assets/panel-overview-BsFvDdts.js +0 -1
  203. package/src/console/ui/dist/assets/panel-runs-C0gmnoYL.js +0 -1
  204. package/src/console/ui/dist/assets/panel-traces-BDiAuVSK.js +0 -1
  205. package/src/drivers/vault-infisical.ts +0 -57
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `oke({ gate })` — nested Gate bag (auth · policies · rate · posture).
3
+ */
4
+
5
+ import { resolveGateAuth, type GateAuthOptions, type ResolvedGateAuth } from "../../auth/config.ts";
6
+ import type { GateDecl } from "./declare.ts";
7
+
8
+ /** Rate-limit defaults under Gate. */
9
+ export interface GateRateLimitOptions {
10
+ /** When true, auth Flows attach stricter rate presets (default true when auth enabled). */
11
+ readonly enabled?: boolean;
12
+ }
13
+
14
+ /**
15
+ * Nested Gate options for {@link oke}.
16
+ *
17
+ * Replaces root `auth` / `gates` / `unguardedHttp`.
18
+ */
19
+ export interface GateOptions {
20
+ /** Builtin hybrid-session auth + core Flows under `basePath`. */
21
+ readonly auth?: GateAuthOptions;
22
+ /** Named policy / rate declarations for the Gate runtime. */
23
+ readonly policies?: readonly GateDecl[];
24
+ /** Global rate-limit posture (auth path presets). */
25
+ readonly rateLimit?: GateRateLimitOptions;
26
+ /**
27
+ * HTTP auth-posture enforcement at boot.
28
+ * Default `"deny"`. `"allow"` is honoured only when `env === "test"`.
29
+ */
30
+ readonly unguardedHttp?: "deny" | "allow";
31
+ }
32
+
33
+ /** Fully resolved Gate config consumed by `oke` / boot. */
34
+ export interface ResolvedGateConfig {
35
+ readonly auth: ResolvedGateAuth | undefined;
36
+ readonly policies: readonly GateDecl[];
37
+ readonly rateLimitEnabled: boolean;
38
+ readonly unguardedHttp: "deny" | "allow";
39
+ }
40
+
41
+ /** Options for {@link resolveGateConfig}. */
42
+ export interface ResolveGateConfigOptions {
43
+ readonly gate?: GateOptions;
44
+ readonly env?: string;
45
+ }
46
+
47
+ /**
48
+ * Resolve the nested `gate` bag for construction / boot.
49
+ *
50
+ * @param options - Gate bag + env (for auth secret rules)
51
+ */
52
+ export function resolveGateConfig(options: ResolveGateConfigOptions = {}): ResolvedGateConfig {
53
+ const bag = options.gate ?? {};
54
+ const auth =
55
+ bag.auth !== undefined ? resolveGateAuth({ auth: bag.auth, env: options.env }) : undefined;
56
+ const rateLimitEnabled =
57
+ bag.rateLimit?.enabled !== undefined
58
+ ? bag.rateLimit.enabled
59
+ : auth !== undefined
60
+ ? true
61
+ : false;
62
+
63
+ return {
64
+ auth,
65
+ policies: bag.policies ?? [],
66
+ rateLimitEnabled,
67
+ unguardedHttp: bag.unguardedHttp ?? "deny",
68
+ };
69
+ }
@@ -63,6 +63,11 @@ export interface PolicyGateDecl {
63
63
  readonly name: string;
64
64
  readonly check: (ctx: GatePolicyContext) => boolean | Promise<boolean>;
65
65
  readonly description?: string;
66
+ /**
67
+ * Scope strings this policy requires (set by {@link GateNamespace.scope}).
68
+ * Same string is the policy id and the scope checked on `auth.scopes`.
69
+ */
70
+ readonly scopes?: readonly string[];
66
71
  }
67
72
 
68
73
  /** Rate gate declaration. */
@@ -94,6 +99,19 @@ export interface GateNamespace {
94
99
  name: string,
95
100
  checkOrOptions: ((ctx: GatePolicyContext) => boolean | Promise<boolean>) | PolicyGateOptions,
96
101
  ): PolicyGateDecl;
102
+ /**
103
+ * Shorthand for a scope check — single source of truth for the scope string.
104
+ *
105
+ * Equivalent to `gate.policy(name, ({ auth }) => auth.scopes.has(name))`.
106
+ *
107
+ * @param name - Scope id (also the policy name / Module:Action when it contains `:`)
108
+ */
109
+ scope(name: string): PolicyGateDecl;
110
+ /**
111
+ * Explicit public sentinel — intentionally unauthenticated HTTP surface.
112
+ * Counts as declared auth posture for boot (see gate boot audit).
113
+ */
114
+ readonly public: PolicyGateDecl;
97
115
  /**
98
116
  * Declare a rate limit (atomic Lua on the kv driver).
99
117
  *
@@ -102,8 +120,11 @@ export interface GateNamespace {
102
120
  rate(options: RateOptions): RateGateDecl;
103
121
  }
104
122
 
123
+ /** Reserved policy name for {@link gate.public}. */
124
+ export const GATE_PUBLIC_NAME = "public";
125
+
105
126
  /**
106
- * Gate element namespace — `gate.policy` · `gate.rate`.
127
+ * Gate element namespace — `gate.policy` · `gate.scope` · `gate.public` · `gate.rate`.
107
128
  */
108
129
  export const gate: GateNamespace = {
109
130
  /**
@@ -116,6 +137,11 @@ export const gate: GateNamespace = {
116
137
  name: string,
117
138
  checkOrOptions: ((ctx: GatePolicyContext) => boolean | Promise<boolean>) | PolicyGateOptions,
118
139
  ): PolicyGateDecl {
140
+ if (name === GATE_PUBLIC_NAME) {
141
+ throw new TypeError(
142
+ 'gate.policy: name "public" is reserved — use gate.public for intentionally unauthenticated surfaces',
143
+ );
144
+ }
119
145
  if (typeof checkOrOptions === "function") {
120
146
  return { kind: "policy", name, check: checkOrOptions };
121
147
  }
@@ -129,6 +155,30 @@ export const gate: GateNamespace = {
129
155
  };
130
156
  },
131
157
 
158
+ /**
159
+ * Shorthand for a scope check — single source of truth for the scope string.
160
+ *
161
+ * @param name - Scope id (also the policy name)
162
+ */
163
+ scope(name: string): PolicyGateDecl {
164
+ if (name === GATE_PUBLIC_NAME) {
165
+ throw new TypeError('gate.scope: name "public" is reserved — use gate.public');
166
+ }
167
+ return {
168
+ kind: "policy",
169
+ name,
170
+ check: ({ auth }) => auth.scopes.has(name),
171
+ scopes: [name],
172
+ };
173
+ },
174
+
175
+ public: {
176
+ kind: "policy",
177
+ name: GATE_PUBLIC_NAME,
178
+ check: () => true,
179
+ description: "Intentionally unauthenticated (public) surface",
180
+ },
181
+
132
182
  /**
133
183
  * Declare a rate limit (atomic Lua on the kv driver).
134
184
  *
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { parseDurationMs } from "../clock/duration.ts";
6
- import type { GateDecl, GatePolicyContext, RateGateDecl } from "./declare.ts";
6
+ import { gate, type GateDecl, type GatePolicyContext, type RateGateDecl } from "./declare.ts";
7
7
  import { takeRate } from "./strategies.ts";
8
8
 
9
9
  /** KV surface required for rate gates. */
@@ -58,6 +58,8 @@ export interface GateRuntime {
58
58
  */
59
59
  export function createGateRuntime(options: CreateGateRuntimeOptions = {}): GateRuntime {
60
60
  const map = new Map<string, GateDecl>();
61
+ // Built-in sentinel — `.gate(gate.public)` works without listing it in `oke({ gates })`.
62
+ map.set(gate.public.name, gate.public);
61
63
  for (const g of options.gates ?? []) {
62
64
  map.set(g.name, g);
63
65
  }
@@ -11,10 +11,12 @@ import { createRedisFakeClient, redisDriver } from "../drivers/redis.ts";
11
11
  import type { Manifest } from "../manifest/types.ts";
12
12
  import {
13
13
  ALL_RATE_STRATEGIES,
14
+ assertHttpGatePosture,
14
15
  createGateRuntime,
15
16
  deriveModuleActions,
16
17
  formatGatesList,
17
18
  gate,
19
+ GateBootError,
18
20
  takeRate,
19
21
  } from "./gate.ts";
20
22
 
@@ -35,6 +37,37 @@ describe("gate declaration", () => {
35
37
  expect(fair.strategy).toBe("sliding-window-counter");
36
38
  });
37
39
 
40
+ test("scope is sugar over policy with auth.scopes.has(name)", async () => {
41
+ const canBook = gate.scope("booking:create");
42
+ expect(canBook.kind).toBe("policy");
43
+ expect(canBook.name).toBe("booking:create");
44
+ expect(canBook.scopes).toEqual(["booking:create"]);
45
+ expect(
46
+ await canBook.check({
47
+ auth: { userId: "u1", scopes: new Set(["booking:create"]), verified: true },
48
+ operator: { id: null },
49
+ }),
50
+ ).toBe(true);
51
+ expect(
52
+ await canBook.check({
53
+ auth: { userId: "u1", scopes: new Set(), verified: true },
54
+ operator: { id: null },
55
+ }),
56
+ ).toBe(false);
57
+ });
58
+
59
+ test("public sentinel always allows and reserves the name", async () => {
60
+ expect(gate.public.name).toBe("public");
61
+ expect(
62
+ await gate.public.check({
63
+ auth: { userId: null, scopes: new Set() },
64
+ operator: { id: null },
65
+ }),
66
+ ).toBe(true);
67
+ expect(() => gate.policy("public", () => true)).toThrow(/reserved/);
68
+ expect(() => gate.scope("public")).toThrow(/reserved/);
69
+ });
70
+
38
71
  test("defaults strategy to sliding-window-counter", () => {
39
72
  const r = gate.rate({ max: 10, per: "1s" });
40
73
  expect(r.strategy).toBe("sliding-window-counter");
@@ -117,7 +150,51 @@ describe("five rate strategies — concurrency", () => {
117
150
  });
118
151
  });
119
152
 
153
+ describe("gate boot posture", () => {
154
+ const unguarded = [
155
+ {
156
+ trigger: { kind: "http" as const, method: "GET", path: "/health", gates: [] as const },
157
+ flow: { name: "health.ping" },
158
+ },
159
+ ];
160
+
161
+ test("GateBootError lists every unguarded HTTP trigger", () => {
162
+ expect(() =>
163
+ assertHttpGatePosture([
164
+ ...unguarded,
165
+ {
166
+ trigger: { kind: "http", method: "POST", path: "/x", gates: [gate.public] },
167
+ flow: { name: "x.public" },
168
+ },
169
+ ]),
170
+ ).toThrow(GateBootError);
171
+ });
172
+
173
+ test("unguardedHttp allow skips audit only when env is test", () => {
174
+ expect(() =>
175
+ assertHttpGatePosture(unguarded, { unguardedHttp: "allow", env: "test" }),
176
+ ).not.toThrow();
177
+ });
178
+
179
+ test("unguardedHttp allow has no effect outside env === test", () => {
180
+ for (const env of ["local", "prod", "docker"] as const) {
181
+ expect(() => assertHttpGatePosture(unguarded, { unguardedHttp: "allow", env })).toThrow(
182
+ GateBootError,
183
+ );
184
+ }
185
+ });
186
+ });
187
+
120
188
  describe("gate runtime", () => {
189
+ test("public sentinel is always registered", async () => {
190
+ const rt = createGateRuntime();
191
+ const ok = await rt.allow(["public"], {
192
+ auth: { userId: null, scopes: new Set() },
193
+ operator: { id: null },
194
+ });
195
+ expect(ok).toBe(true);
196
+ });
197
+
121
198
  test("policy then rate — evaluation order", async () => {
122
199
  const kv = await memoryKvDriver.open({ name: "gate-rt" });
123
200
  const member = gate.policy("member", ({ auth }) => !!auth.verified);
@@ -8,7 +8,7 @@
8
8
  * @module
9
9
  */
10
10
 
11
- export { gate } from "./gate/declare.ts";
11
+ export { gate, GATE_PUBLIC_NAME } from "./gate/declare.ts";
12
12
  export type {
13
13
  GateDecl,
14
14
  GatePolicyContext,
@@ -18,6 +18,25 @@ export type {
18
18
  RateOptions,
19
19
  } from "./gate/declare.ts";
20
20
 
21
+ export {
22
+ resolveGateConfig,
23
+ type GateOptions,
24
+ type GateRateLimitOptions,
25
+ type ResolvedGateConfig,
26
+ type ResolveGateConfigOptions,
27
+ } from "./gate/config.ts";
28
+
29
+ export {
30
+ GateBootError,
31
+ assertHttpGatePosture,
32
+ collectUnguardedHttpGaps,
33
+ hasHttpGatePosture,
34
+ isPublicGateRef,
35
+ unguardedHttpAllowActive,
36
+ type AssertHttpGatePostureOptions,
37
+ type GatePostureGap,
38
+ } from "./gate/boot.ts";
39
+
21
40
  export {
22
41
  createGateRuntime,
23
42
  type CreateGateRuntimeOptions,
@@ -62,6 +62,13 @@ export type { ClockDecl, ClockOptions, ClockRuntime, TimeTravel, DurableResult }
62
62
 
63
63
  export {
64
64
  gate,
65
+ GATE_PUBLIC_NAME,
66
+ GateBootError,
67
+ assertHttpGatePosture,
68
+ collectUnguardedHttpGaps,
69
+ hasHttpGatePosture,
70
+ isPublicGateRef,
71
+ unguardedHttpAllowActive,
65
72
  createGateRuntime,
66
73
  takeRate,
67
74
  deriveModuleActions,
@@ -72,6 +79,7 @@ export {
72
79
  export type {
73
80
  GateDecl,
74
81
  GatePolicyContext,
82
+ GatePostureGap,
75
83
  GateRuntime,
76
84
  RateOptions,
77
85
  RateTakeResult,
@@ -22,8 +22,8 @@ import {
22
22
  createStoreRuntime,
23
23
  index as declareIndex,
24
24
  sql as declareSql,
25
- type IndexStoreFxHandle,
26
25
  type SqlStoreHandle,
26
+ type VectorIndexStoreFxHandle,
27
27
  } from "../store.ts";
28
28
 
29
29
  const WRITE_CTX = (name: string) => ({
@@ -67,7 +67,7 @@ describe("store.index boot wiring — memory", () => {
67
67
  test("boot with no configured index driver resolves memory end to end", async () => {
68
68
  const kb = declareIndex("kb", { dims: 3 });
69
69
  const runtime = bindStore({ stores: [kb] }, "local", () => Date.now());
70
- const handle = (await runtime.open(kb, WRITE_CTX("kb"))) as IndexStoreFxHandle;
70
+ const handle = (await runtime.open(kb, WRITE_CTX("kb"))) as VectorIndexStoreFxHandle;
71
71
  expect(handle.driverId).toBe("memory");
72
72
  await handle.upsert("near", [1, 0, 0]);
73
73
  await handle.upsert("far", [0, 1, 0]);
@@ -89,14 +89,14 @@ describe("store.index boot wiring — libsql", () => {
89
89
  "local",
90
90
  () => Date.now(),
91
91
  );
92
- const handle = (await runtime.open(kb, WRITE_CTX("kb"))) as IndexStoreFxHandle;
92
+ const handle = (await runtime.open(kb, WRITE_CTX("kb"))) as VectorIndexStoreFxHandle;
93
93
  expect(handle.driverId).toBe("libsql");
94
94
 
95
95
  await handle.upsert("near", [1, 0.1, 0], { label: "near" });
96
96
  await handle.upsert("far", [0, 1, 0]);
97
97
  await handle.upsert("farther", [-1, 0, 0]);
98
98
  const hits = await handle.search([1, 0, 0], 3);
99
- expect(hits.map((h) => h.id)).toEqual(["near", "far", "farther"]);
99
+ expect(hits.map((h: { id: string }) => h.id)).toEqual(["near", "far", "farther"]);
100
100
  expect(hits[0]!.score).toBeGreaterThan(hits[1]!.score);
101
101
  expect(hits[0]!.meta).toEqual({ label: "near" });
102
102
  expect(await handle.delete("farther")).toBe(true);
@@ -120,7 +120,7 @@ describe("store.index boot wiring — libsql", () => {
120
120
  index: { kb: { url: ":memory:" } },
121
121
  });
122
122
  // Index first: it must open the connection the sql facet then reuses.
123
- const idxHandle = (await runtime.open(kb, WRITE_CTX("kb"))) as IndexStoreFxHandle;
123
+ const idxHandle = (await runtime.open(kb, WRITE_CTX("kb"))) as VectorIndexStoreFxHandle;
124
124
  await idxHandle.upsert("a", [1, 0, 0]);
125
125
  const sqlHandle = (await runtime.open(docs, {
126
126
  effects: { reads: ["sql:docs"], writes: ["sql:docs"] },
@@ -161,14 +161,14 @@ describe("store.index boot wiring — pglite + pgvector", () => {
161
161
  "local",
162
162
  () => Date.now(),
163
163
  );
164
- const handle = (await runtime.open(kb, WRITE_CTX("kb"))) as IndexStoreFxHandle;
164
+ const handle = (await runtime.open(kb, WRITE_CTX("kb"))) as VectorIndexStoreFxHandle;
165
165
  expect(handle.driverId).toBe("pgvector");
166
166
 
167
167
  await handle.upsert("near", [1, 0.1, 0], { label: "near" });
168
168
  await handle.upsert("far", [0, 1, 0]);
169
169
  await handle.upsert("farther", [-1, 0, 0]);
170
170
  const hits = await handle.search([1, 0, 0], 3);
171
- expect(hits.map((h) => h.id)).toEqual(["near", "far", "farther"]);
171
+ expect(hits.map((h: { id: string }) => h.id)).toEqual(["near", "far", "farther"]);
172
172
  expect(hits[0]!.score).toBeGreaterThan(hits[1]!.score);
173
173
  expect(hits[0]!.meta).toEqual({ label: "near" });
174
174
  expect(await handle.delete("farther")).toBe(true);
@@ -225,6 +225,48 @@ describe("store.index boot wiring — fail loud", () => {
225
225
  await expect(runtime.open(kb, WRITE_CTX("kb"))).rejects.toThrow(/cannot share sql driver/);
226
226
  await runtime.close();
227
227
  });
228
+
229
+ test("meilisearch without OKE_STORE_INDEX_URL throws at bind — never sqlUrl, never memory", () => {
230
+ const prev = process.env.OKE_STORE_INDEX_URL;
231
+ delete process.env.OKE_STORE_INDEX_URL;
232
+ try {
233
+ const kb = declareIndex("kb");
234
+ expect(() =>
235
+ bindStore(
236
+ { config: { drivers: { store: { index: { local: "meilisearch" } } } }, stores: [kb] },
237
+ "local",
238
+ () => Date.now(),
239
+ ),
240
+ ).toThrow(/OKE_STORE_INDEX_URL/);
241
+ } finally {
242
+ if (prev === undefined) delete process.env.OKE_STORE_INDEX_URL;
243
+ else process.env.OKE_STORE_INDEX_URL = prev;
244
+ }
245
+ });
246
+
247
+ test("meilisearch binds its own URL + key (never the sql connection)", async () => {
248
+ const prevUrl = process.env.OKE_STORE_INDEX_URL;
249
+ const prevKey = process.env.OKE_STORE_INDEX_KEY;
250
+ process.env.OKE_STORE_INDEX_URL = "http://127.0.0.1:7700";
251
+ process.env.OKE_STORE_INDEX_KEY = "master-key";
252
+ try {
253
+ const kb = declareIndex("kb");
254
+ const runtime = bindStore(
255
+ { config: { drivers: { store: { index: { local: "meilisearch" } } } }, stores: [kb] },
256
+ "local",
257
+ () => Date.now(),
258
+ );
259
+ // No live server in unit tests: opening must fail loud (unreachable),
260
+ // proving the binding pointed at HTTP, not silently degraded to memory.
261
+ await expect(runtime.open(kb, WRITE_CTX("kb"))).rejects.toThrow(/meilisearch index/);
262
+ await runtime.close();
263
+ } finally {
264
+ if (prevUrl === undefined) delete process.env.OKE_STORE_INDEX_URL;
265
+ else process.env.OKE_STORE_INDEX_URL = prevUrl;
266
+ if (prevKey === undefined) delete process.env.OKE_STORE_INDEX_KEY;
267
+ else process.env.OKE_STORE_INDEX_KEY = prevKey;
268
+ }
269
+ });
228
270
  });
229
271
 
230
272
  const PGVECTOR_URL = process.env.OKE_TEST_PGVECTOR_URL;
@@ -18,7 +18,12 @@ import type {
18
18
  SqlDriver,
19
19
  SqlRole,
20
20
  SqlRow,
21
+ TextIndexSearchOptions,
22
+ TextIndexSearchResult,
23
+ VectorIndexDriver,
24
+ VectorIndexStore,
21
25
  } from "../../drivers/types.ts";
26
+ import { indexDriverNeedsSql } from "../../drivers/types.ts";
22
27
  import { buildClassificationMap, type MaskRowsOptions } from "./classify.ts";
23
28
  import {
24
29
  createStoreCache,
@@ -64,7 +69,15 @@ export interface CreateStoreRuntimeOptions {
64
69
  readonly files?: Readonly<Record<string, { readonly root?: string; readonly client?: unknown }>>;
65
70
  /** Index open options keyed by store name. */
66
71
  readonly index?: Readonly<
67
- Record<string, { readonly dims?: number; readonly url?: string; readonly sql?: SqlConnection }>
72
+ Record<
73
+ string,
74
+ {
75
+ readonly dims?: number;
76
+ readonly url?: string;
77
+ readonly apiKey?: string;
78
+ readonly sql?: SqlConnection;
79
+ }
80
+ >
68
81
  >;
69
82
  /** Clock for cache TTLs. */
70
83
  readonly now?: () => number;
@@ -115,18 +128,30 @@ export interface FilesStoreFxHandle {
115
128
  list(prefix?: string): Promise<string[]>;
116
129
  }
117
130
 
118
- /** Index handle on `fx.store`. */
119
- export interface IndexStoreFxHandle {
131
+ /** Vector index handle on `fx.store`. */
132
+ export interface VectorIndexStoreFxHandle {
120
133
  readonly ref: `index:${string}`;
121
134
  readonly driverId: "memory" | "pgvector" | "libsql";
122
135
  upsert(id: string, vector: readonly number[], meta?: Record<string, unknown>): Promise<void>;
123
136
  search(
124
137
  vector: readonly number[],
125
138
  topK?: number,
126
- ): Promise<Array<{ id: string; score: number; meta?: Record<string, unknown> }>>;
139
+ ): Promise<ReadonlyArray<{ id: string; score: number; meta?: Record<string, unknown> }>>;
127
140
  delete(id: string): Promise<boolean>;
128
141
  }
129
142
 
143
+ /** Full-text index handle on `fx.store`. */
144
+ export interface TextIndexStoreFxHandle {
145
+ readonly ref: `index:${string}`;
146
+ readonly driverId: "meilisearch";
147
+ upsert(id: string, document: Record<string, unknown>): Promise<void>;
148
+ search(q: string, opts?: TextIndexSearchOptions): Promise<TextIndexSearchResult>;
149
+ delete(id: string): Promise<boolean>;
150
+ }
151
+
152
+ /** Index handle on `fx.store` — discriminated by `driverId`. */
153
+ export type IndexStoreFxHandle = VectorIndexStoreFxHandle | TextIndexStoreFxHandle;
154
+
130
155
  /** Store runtime. */
131
156
  export interface StoreRuntime {
132
157
  /** Shared multi-tier cache. */
@@ -259,9 +284,9 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
259
284
  return conn;
260
285
  }
261
286
 
262
- /** Index drivers backed by a SQL engine borrow the sql facet's connection. */
287
+ /** SQL-backed index drivers borrow the sql facet's connection. */
263
288
  async function sqlConnForIndex(
264
- driver: IndexDriver,
289
+ driver: VectorIndexDriver & { id: "pgvector" | "libsql" },
265
290
  url: string | undefined,
266
291
  ): Promise<SqlConnection> {
267
292
  const sqlDriver = options.drivers.sql;
@@ -338,24 +363,34 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
338
363
  let idx = indexes.get(decl.name);
339
364
  if (!idx) {
340
365
  const binding = options.index?.[decl.name] ?? {};
341
- let sql = binding.sql;
342
- if (!sql && driver.id !== "memory") {
343
- sql = await sqlConnForIndex(driver, binding.url);
344
- }
345
366
  idx = await driver.open({
346
367
  name: decl.name,
347
368
  dims: binding.dims ?? decl.dims ?? 3,
348
369
  url: binding.url,
349
- sql,
370
+ apiKey: binding.apiKey,
371
+ sql: indexDriverNeedsSql(driver)
372
+ ? (binding.sql ?? (await sqlConnForIndex(driver, binding.url)))
373
+ : undefined,
350
374
  });
351
375
  indexes.set(decl.name, idx);
352
376
  }
377
+ if (idx.driverId === "meilisearch") {
378
+ const text = idx;
379
+ return {
380
+ ref: `index:${decl.name}`,
381
+ driverId: text.driverId,
382
+ upsert: (id, document) => text.upsert(id, document),
383
+ search: (q, opts) => text.search(q, opts),
384
+ delete: (id) => text.delete(id),
385
+ };
386
+ }
387
+ const vector = idx as VectorIndexStore;
353
388
  return {
354
389
  ref: `index:${decl.name}`,
355
- driverId: idx.driverId,
356
- upsert: (id, vector, meta) => idx!.upsert(id, vector, meta),
357
- search: (vector, topK) => idx!.search(vector, topK),
358
- delete: (id) => idx!.delete(id),
390
+ driverId: vector.driverId,
391
+ upsert: (id, vec, meta) => vector.upsert(id, vec, meta),
392
+ search: (vec, topK) => vector.search(vec, topK),
393
+ delete: (id) => vector.delete(id),
359
394
  };
360
395
  }
361
396
 
@@ -116,6 +116,8 @@ export type {
116
116
  KvStoreFxHandle,
117
117
  FilesStoreFxHandle,
118
118
  IndexStoreFxHandle,
119
+ VectorIndexStoreFxHandle,
120
+ TextIndexStoreFxHandle,
119
121
  } from "./store/runtime.ts";
120
122
 
121
123
  export { fileKeyWarnings, contentAddressedKey, projectFileKeys } from "./store/files-policy.ts";
@@ -9,6 +9,7 @@
9
9
  import { describe, expect, test } from "bun:test";
10
10
  import { envVaultDriver, memoryVaultDriver } from "../drivers/index.ts";
11
11
  import { createFx } from "../kernel/fx.ts";
12
+ import { REDACTED_PLACEHOLDER, Redacted } from "../kernel/redacted.ts";
12
13
  import {
13
14
  createVaultRuntime,
14
15
  fingerprintSecretSync,
@@ -200,17 +201,39 @@ describe("redaction + fingerprints", () => {
200
201
  const lines: Array<{ message: string; data?: Record<string, unknown> }> = [];
201
202
  const { fx } = createFxContextWithVault(runtime, lines);
202
203
 
203
- const value = fx.vault("STRIPE_KEY");
204
- expect(value).toBe(secret);
204
+ const wrapped = fx.vault("STRIPE_KEY");
205
+ expect(wrapped.reveal()).toBe(secret);
205
206
 
206
- fx.log.info(`charging with ${value}`, { key: value, nested: { k: value } });
207
+ fx.log.info(`charging with ${wrapped}`, { key: wrapped, nested: { k: wrapped } });
208
+ // Revealed cleartext still hits the known-value substring scrub.
207
209
  fx.log.error(secret, { stripe: secret });
208
210
 
209
211
  for (const line of lines) {
210
212
  expect(line.message).not.toContain(secret);
211
213
  expect(JSON.stringify(line.data ?? {})).not.toContain(secret);
212
- expect(line.message).toContain(SECRET_MASK);
213
214
  }
215
+ expect(lines[0]!.message).toContain(REDACTED_PLACEHOLDER);
216
+ expect(lines[1]!.message).toContain(SECRET_MASK);
217
+ });
218
+
219
+ test("Redacted from fx.vault is masked in fx.log without a vault runtime", () => {
220
+ const secret = "sk_no_vault_runtime";
221
+ const lines: Array<{ message: string; data?: Record<string, unknown> }> = [];
222
+ const fx = createFx({
223
+ flow: "test.redacted",
224
+ secrets: { STRIPE_KEY: secret },
225
+ onLog: (_level, message, data) => lines.push({ message, data }),
226
+ });
227
+
228
+ const wrapped = fx.vault("STRIPE_KEY");
229
+ expect(wrapped).toBeInstanceOf(Redacted);
230
+ expect(String(wrapped)).toBe(REDACTED_PLACEHOLDER);
231
+ expect(`${wrapped}`).toBe(REDACTED_PLACEHOLDER);
232
+ expect(JSON.stringify({ v: wrapped })).toBe(`{"v":"${REDACTED_PLACEHOLDER}"}`);
233
+
234
+ fx.log.info("paying", { key: wrapped, nested: { k: [wrapped] } });
235
+ expect(JSON.stringify(lines[0]!.data)).not.toContain(secret);
236
+ expect(lines[0]!.data?.key).toBe(REDACTED_PLACEHOLDER);
214
237
  });
215
238
 
216
239
  test("fingerprint is stable and not the value", async () => {
@@ -2,7 +2,7 @@
2
2
  * Vault element — protected knowledge.
3
3
  *
4
4
  * Physics: secrets · config · environment.
5
- * Drivers: `env` · `openbao` · `infisical` · `managed`.
5
+ * Drivers: `env` · `openbao` · `managed`.
6
6
  *
7
7
  * A declaration is a contract, never a value. Boot validates every contract
8
8
  * and lists all gaps at once. Logs/traces receive fingerprints and redaction
package/src/index.ts CHANGED
@@ -34,12 +34,20 @@ export {
34
34
  journey,
35
35
  unit,
36
36
  rateLimit,
37
+ Redacted,
38
+ isRedacted,
39
+ maskRedactedDeep,
40
+ REDACTED_PLACEHOLDER,
41
+ PluginNeedsError,
42
+ assertPluginNeeds,
43
+ freezePrincipal,
37
44
  type OkeApp,
38
45
  type OkeOptions,
39
46
  type FlowDef,
40
47
  type FlowFailure,
41
48
  type FlowErrorValue,
42
49
  type Fx,
50
+ type FxPrincipal,
43
51
  type Binding,
44
52
  type PluginDef,
45
53
  type PluginCapabilities,
@@ -90,13 +98,20 @@ export {
90
98
 
91
99
  export {
92
100
  gate,
101
+ GATE_PUBLIC_NAME,
102
+ GateBootError,
103
+ assertHttpGatePosture,
93
104
  createGateRuntime,
105
+ resolveGateConfig,
94
106
  takeRate,
95
107
  deriveModuleActions,
96
108
  ALL_RATE_STRATEGIES,
97
109
  type GateDecl,
110
+ type GateOptions,
111
+ type GatePostureGap,
98
112
  type GateRuntime,
99
113
  type RateOptions,
114
+ type ResolvedGateConfig,
100
115
  } from "./elements/gate.ts";
101
116
 
102
117
  export {