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
@@ -12,9 +12,16 @@
12
12
  * @see docs/spec/console.md §9.15
13
13
  */
14
14
 
15
- import type { FlowErrorMap } from "./flow.ts";
15
+ import type { ChannelTemplateDecl } from "../elements/channel/declare.ts";
16
+ import type { ClockDecl } from "../elements/clock/declare.ts";
17
+ import type { GateDecl } from "../elements/gate/declare.ts";
18
+ import type { SignalDecl } from "../elements/signal/declare.ts";
19
+ import type { VaultSecretDecl } from "../elements/vault/declare.ts";
20
+ import type { SchemaInput } from "../validation/standard-schema.ts";
21
+ import type { AnyFlowDef, FlowErrorMap } from "./flow.ts";
16
22
  import type { HookFn, HookMap, HookStage } from "./hooks.ts";
17
23
  import { tagHookWithPlugin } from "./hook-timing.ts";
24
+ import type { Binding } from "./on.ts";
18
25
  import type {
19
26
  CliContribution,
20
27
  ClientExtensionContribution,
@@ -30,8 +37,6 @@ import type {
30
37
  PluginTableOptions,
31
38
  TableContribution,
32
39
  } from "./plugin.ts";
33
- import type { SchemaInput } from "../validation/standard-schema.ts";
34
- import type { AnyFlowDef } from "./flow.ts";
35
40
 
36
41
  /** Attachment-point scope — the position is the scope. No escape hatch. */
37
42
  export type PluginScope =
@@ -72,8 +77,14 @@ export function createRecordingApi(identity: { readonly name: string; readonly v
72
77
  const cli: CliContribution[] = [];
73
78
  const images: ImageRecipeContribution[] = [];
74
79
  const flows: AnyFlowDef[] = [];
80
+ const bindings: Binding[] = [];
75
81
  const errors: FlowErrorMap = {};
76
82
  const client: ClientExtensionContribution[] = [];
83
+ const vaultSecrets: VaultSecretDecl[] = [];
84
+ const clocks: ClockDecl[] = [];
85
+ const signals: SignalDecl[] = [];
86
+ const gates: GateDecl[] = [];
87
+ const channelTemplates: ChannelTemplateDecl[] = [];
77
88
  const needs: string[] = [];
78
89
  const declares: string[] = [];
79
90
  const intercepts: string[] = [];
@@ -146,6 +157,13 @@ export function createRecordingApi(identity: { readonly name: string; readonly v
146
157
  pushDeclare(`table:${name}`);
147
158
  return api;
148
159
  },
160
+ binding(binding) {
161
+ bindings.push(binding);
162
+ flows.push(binding.flow);
163
+ pushDeclare(`binding:${binding.flow.name}`);
164
+ pushDeclare(`flow:${binding.flow.name}`);
165
+ return api;
166
+ },
149
167
  flow(flowDef) {
150
168
  flows.push(flowDef);
151
169
  pushDeclare(`flow:${flowDef.name}`);
@@ -161,6 +179,31 @@ export function createRecordingApi(identity: { readonly name: string; readonly v
161
179
  pushDeclare("config");
162
180
  return api;
163
181
  },
182
+ vault(secret) {
183
+ vaultSecrets.push(secret);
184
+ pushDeclare(`vault:${secret.name}`);
185
+ return api;
186
+ },
187
+ clock(decl) {
188
+ clocks.push(decl);
189
+ pushDeclare(`clock:${decl.name}`);
190
+ return api;
191
+ },
192
+ signal(decl) {
193
+ signals.push(decl);
194
+ pushDeclare(`signal:${decl.name}`);
195
+ return api;
196
+ },
197
+ gate(decl) {
198
+ gates.push(decl);
199
+ pushDeclare(`gate:${decl.name}`);
200
+ return api;
201
+ },
202
+ channelTemplate(decl) {
203
+ channelTemplates.push(decl);
204
+ pushDeclare(`channel:${decl.name}`);
205
+ return api;
206
+ },
164
207
  };
165
208
 
166
209
  return {
@@ -186,9 +229,15 @@ export function createRecordingApi(identity: { readonly name: string; readonly v
186
229
  cli: cli.slice(),
187
230
  images: images.slice(),
188
231
  flows: flows.slice(),
232
+ bindings: bindings.slice(),
189
233
  errors: { ...errors },
190
234
  client: client.slice(),
191
235
  configSchema,
236
+ vaultSecrets: vaultSecrets.slice(),
237
+ clocks: clocks.slice(),
238
+ signals: signals.slice(),
239
+ gates: gates.slice(),
240
+ channelTemplates: channelTemplates.slice(),
192
241
  };
193
242
  },
194
243
  };
@@ -259,6 +308,18 @@ export interface PluginRegistry {
259
308
  * plugin name). Used by `oke db` emit to merge plugin `field.*` tables.
260
309
  */
261
310
  tableContributions(): readonly TableContribution[];
311
+ /** Vault contracts from installed plugins (first install per plugin name). */
312
+ vaultContributions(): readonly VaultSecretDecl[];
313
+ /** Clock schedules from installed plugins. */
314
+ clockContributions(): readonly ClockDecl[];
315
+ /** Signal declarations from installed plugins. */
316
+ signalContributions(): readonly SignalDecl[];
317
+ /** Gate declarations from installed plugins. */
318
+ gateContributions(): readonly GateDecl[];
319
+ /** Channel templates from installed plugins. */
320
+ channelTemplateContributions(): readonly ChannelTemplateDecl[];
321
+ /** HTTP Bindings from installed plugins (auth method plugins). */
322
+ bindingContributions(): readonly Binding[];
262
323
  }
263
324
 
264
325
  /**
@@ -413,11 +474,49 @@ export function createPluginRegistry(): PluginRegistry {
413
474
  }
414
475
  return out;
415
476
  },
477
+ vaultContributions() {
478
+ return firstPerPlugin(installed, (r) => r.vaultSecrets);
479
+ },
480
+ clockContributions() {
481
+ return firstPerPlugin(installed, (r) => r.clocks);
482
+ },
483
+ signalContributions() {
484
+ return firstPerPlugin(installed, (r) => r.signals);
485
+ },
486
+ gateContributions() {
487
+ return firstPerPlugin(installed, (r) => r.gates);
488
+ },
489
+ channelTemplateContributions() {
490
+ return firstPerPlugin(installed, (r) => r.channelTemplates);
491
+ },
492
+ bindingContributions() {
493
+ return firstPerPlugin(installed, (r) => r.bindings);
494
+ },
416
495
  };
417
496
 
418
497
  return registry;
419
498
  }
420
499
 
500
+ /**
501
+ * Collect contributions from the first install of each plugin name.
502
+ *
503
+ * @param installed - Registry installs
504
+ * @param pick - Selector
505
+ */
506
+ function firstPerPlugin<T>(
507
+ installed: readonly InstalledPlugin[],
508
+ pick: (r: PluginRegistration) => readonly T[],
509
+ ): T[] {
510
+ const seenPlugins = new Set<string>();
511
+ const out: T[] = [];
512
+ for (const entry of installed) {
513
+ if (seenPlugins.has(entry.plugin.name)) continue;
514
+ seenPlugins.add(entry.plugin.name);
515
+ out.push(...pick(entry.registration));
516
+ }
517
+ return out;
518
+ }
519
+
421
520
  /**
422
521
  * Normalize plugin `.table()` args — supports columns + options, and legacy
423
522
  * `{ plane }` as the second argument.
@@ -1,6 +1,6 @@
1
1
  /**
2
- * HTTP router — compiled RegExp matching with a Trie fallback
3
- * (Hono's SmartRouter idea), plus a linear preset for cold-start edge builds.
2
+ * HTTP router — compiled RegExp matching with a Trie fallback, plus a linear
3
+ * preset for cold-start edge builds.
4
4
  *
5
5
  * Chosen at build/startup: try RegExp first; on unsupported patterns fall
6
6
  * back to Trie. The `edge` preset uses LinearRouter for fast registration.
@@ -141,7 +141,7 @@ function matchCompiled<T>(dyn: CompiledDynamic<T>, path: string): RouteMatch<T>
141
141
 
142
142
  /**
143
143
  * Compiled RegExp router — O(1) static map, plus per-bucket compiled
144
- * regular expressions for parametric routes (Hono RegExpRouter idea).
144
+ * regular expressions for parametric routes.
145
145
  */
146
146
  export class RegExpRouter<T> implements Router<T> {
147
147
  readonly name = "RegExpRouter";
@@ -327,6 +327,8 @@ export interface Plugin {
327
327
  version?: string;
328
328
  declares?: string[];
329
329
  intercepts?: string[];
330
+ /** Declared `.needs()` dependencies (plugin names or element/driver ids). */
331
+ needs?: string[];
330
332
  /** Optional metadata for `table:*` contributions. */
331
333
  tables?: Record<string, PluginTable>;
332
334
  }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Anonymous sign-in Gate auth method plugin.
3
+ */
4
+
5
+ import { issueSessionWithScopes } from "../auth/sessions.ts";
6
+ import { plugin, type PluginDef } from "../kernel/plugin.ts";
7
+ import {
8
+ AuthFailed,
9
+ AuthRateLimited,
10
+ SessionTokensOut,
11
+ bindPublicAuth,
12
+ createMethodRuntime,
13
+ flow,
14
+ type AuthMethodOptions,
15
+ } from "./auth/shared.ts";
16
+
17
+ /** Options for {@link anonymous}. */
18
+ export interface AnonymousPluginOptions extends AuthMethodOptions {
19
+ /**
20
+ * Reserved for future disposable emails (`user@domain`). Unused in v1.
21
+ */
22
+ readonly emailDomain?: string;
23
+ }
24
+
25
+ /**
26
+ * Issue a user-plane session with a random principal id (no password).
27
+ *
28
+ * @param opts - Secret / session overrides
29
+ */
30
+ export function anonymous(opts: AnonymousPluginOptions = {}): PluginDef {
31
+ const runtime = createMethodRuntime(opts);
32
+
33
+ const signIn = flow({
34
+ name: "auth.signInAnonymous",
35
+ unit: "auth",
36
+ plane: "user",
37
+ out: SessionTokensOut,
38
+ errors: { AuthFailed, AuthRateLimited },
39
+ do: async () => {
40
+ const userId = crypto.randomUUID();
41
+ const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
42
+ id: userId,
43
+ plane: "user",
44
+ scopes: [],
45
+ });
46
+ return {
47
+ accessToken: issued.accessToken,
48
+ refreshToken: issued.refreshToken,
49
+ accessExpiresAt: issued.accessExpiresAt,
50
+ userId,
51
+ };
52
+ },
53
+ });
54
+
55
+ return plugin("anonymous", { version: "0.0.1", config: { method: "anonymous" } })
56
+ .needs("auth")
57
+ .binding(bindPublicAuth("/sign-in/anonymous", signIn, "signIn"));
58
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Shared helpers for Gate auth method plugins.
3
+ */
4
+
5
+ import { z } from "zod";
6
+ import { AUTH_RATE_PRESETS, AUTH_SESSION_GATE, bindAuthHttp } from "../../auth/bindings.ts";
7
+ import { getActiveGateAuthContext } from "../../auth/method-context.ts";
8
+ import { createSessionStore, type SessionCrypto, type SessionStore } from "../../auth/sessions.ts";
9
+ import { gate } from "../../elements/gate.ts";
10
+ import type { GateDecl } from "../../elements/gate/declare.ts";
11
+ import { fail } from "../../kernel/errors.ts";
12
+ import { flow, type AnyFlowDef } from "../../kernel/flow.ts";
13
+ import type { Binding } from "../../kernel/on.ts";
14
+ import { http } from "../../kernel/triggers.ts";
15
+ import { createEnv } from "../../runtime/primitives.ts";
16
+
17
+ export const AuthFailed = z.object({ reason: z.string().optional() });
18
+ export const AuthRateLimited = z.object({ reason: z.string() });
19
+
20
+ export const SessionTokensOut = z.object({
21
+ accessToken: z.string(),
22
+ refreshToken: z.string(),
23
+ accessExpiresAt: z.number(),
24
+ userId: z.string().optional(),
25
+ });
26
+
27
+ /** Common options for auth method plugins (session issue). */
28
+ export interface AuthMethodOptions {
29
+ /** HMAC secret; falls back to `OKE_AUTH_SECRET`, then a dev default. */
30
+ readonly secret?: string;
31
+ /** Shared session store (prefer the same store as `gate.auth`). */
32
+ readonly sessions?: SessionStore;
33
+ /** Injectable clock. */
34
+ readonly now?: () => number;
35
+ }
36
+
37
+ /**
38
+ * Resolve HMAC secret for method plugins.
39
+ *
40
+ * @param opts - Optional explicit secret
41
+ */
42
+ export function resolveMethodSecret(opts: { readonly secret?: string } = {}): string {
43
+ if (opts.secret && opts.secret.length > 0) return opts.secret;
44
+ const active = getActiveGateAuthContext();
45
+ if (active?.secret) return active.secret;
46
+ const fromEnv = createEnv().get("OKE_AUTH_SECRET");
47
+ if (fromEnv && fromEnv.length > 0) return fromEnv;
48
+ return "oke-dev-auth-secret-change-me";
49
+ }
50
+
51
+ /**
52
+ * Session store + crypto closed over by a method plugin factory.
53
+ * Prefers explicit opts, then the active `gate.auth` context from `oke()`.
54
+ *
55
+ * @param opts - Common method options
56
+ */
57
+ export function createMethodRuntime(opts: AuthMethodOptions = {}): {
58
+ readonly sessions: SessionStore;
59
+ readonly crypto: SessionCrypto;
60
+ now(): number;
61
+ } {
62
+ const active = getActiveGateAuthContext();
63
+ const sessions = opts.sessions ?? active?.sessions ?? createSessionStore();
64
+ const now = opts.now ?? active?.now ?? (() => Date.now());
65
+ return {
66
+ sessions,
67
+ now,
68
+ crypto: {
69
+ secret: resolveMethodSecret(opts),
70
+ now,
71
+ },
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Public + rate gate chain for credential-ish auth paths.
77
+ *
78
+ * @param kind - Preset key
79
+ */
80
+ export function authPublicGates(kind: keyof typeof AUTH_RATE_PRESETS = "otp"): GateDecl[] {
81
+ const preset = AUTH_RATE_PRESETS[kind];
82
+ return [
83
+ gate.public,
84
+ gate.rate({
85
+ max: preset.max,
86
+ per: preset.per,
87
+ keyBy: preset.keyBy,
88
+ description: `Auth ${kind} rate limit`,
89
+ }),
90
+ ];
91
+ }
92
+
93
+ /**
94
+ * Bind a public auth Flow under `/auth/...`.
95
+ *
96
+ * @param path - Path under `/auth` (e.g. `/sign-in/username`)
97
+ * @param flowDef - Flow
98
+ * @param kind - Rate preset
99
+ */
100
+ export function bindPublicAuth(
101
+ path: string,
102
+ flowDef: AnyFlowDef,
103
+ kind: keyof typeof AUTH_RATE_PRESETS = "otp",
104
+ ): Binding {
105
+ const gates = authPublicGates(kind);
106
+ let trigger = http.post(`/auth${path}`);
107
+ for (const g of gates) trigger = trigger.gate(g);
108
+ return bindAuthHttp(trigger, flowDef);
109
+ }
110
+
111
+ /**
112
+ * Bind a session-gated auth Flow under `/auth/...`.
113
+ *
114
+ * @param path - Path under `/auth`
115
+ * @param flowDef - Flow
116
+ */
117
+ export function bindSessionAuth(path: string, flowDef: AnyFlowDef): Binding {
118
+ return bindAuthHttp(http.post(`/auth${path}`).gate(AUTH_SESSION_GATE), flowDef);
119
+ }
120
+
121
+ export { AUTH_SESSION_GATE, fail, flow, http, z };
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Phase 2 Gate auth method plugins — username, anonymous, magic-link, email OTP.
3
+ */
4
+
5
+ import { afterEach, describe, expect, test } from "bun:test";
6
+ import { oke } from "../kernel/app.ts";
7
+ import { resetFlowSeq } from "../kernel/flow.ts";
8
+ import { resetBindings } from "../kernel/on.ts";
9
+ import { anonymous } from "./anonymous.ts";
10
+ import { emailOtp } from "./email-otp.ts";
11
+ import { magicLink } from "./magic-link.ts";
12
+ import { username } from "./username.ts";
13
+
14
+ afterEach(() => {
15
+ resetBindings();
16
+ resetFlowSeq();
17
+ });
18
+
19
+ function authApp() {
20
+ return oke({
21
+ name: "auth-plugins",
22
+ env: "test",
23
+ registry: "ignore",
24
+ gate: {
25
+ auth: {
26
+ secret: "test-secret-at-least-16",
27
+ emailAndPassword: { enabled: true },
28
+ },
29
+ },
30
+ })
31
+ .plug(username())
32
+ .plug(anonymous())
33
+ .plug(magicLink({ exposeDevToken: true }))
34
+ .plug(emailOtp({ exposeDevOtp: true }));
35
+ }
36
+
37
+ describe("auth method plugins", () => {
38
+ test("bindings appear on router after .plug()", async () => {
39
+ const app = authApp();
40
+ expect(app.router.match("POST", "/auth/sign-up/username")).toBeTruthy();
41
+ expect(app.router.match("POST", "/auth/sign-in/username")).toBeTruthy();
42
+ expect(app.router.match("POST", "/auth/sign-in/anonymous")).toBeTruthy();
43
+ expect(app.router.match("POST", "/auth/magic-link/request")).toBeTruthy();
44
+ expect(app.router.match("POST", "/auth/magic-link/verify")).toBeTruthy();
45
+ expect(app.router.match("POST", "/auth/email-otp/request")).toBeTruthy();
46
+ expect(app.router.match("POST", "/auth/email-otp/verify")).toBeTruthy();
47
+ await app.boot({ env: "test" });
48
+ await app.stop();
49
+ });
50
+
51
+ test("username sign-up + sign-in", async () => {
52
+ const app = authApp();
53
+ await app.boot({ env: "test" });
54
+
55
+ const signUp = await app.fetch(
56
+ new Request("http://localhost/auth/sign-up/username", {
57
+ method: "POST",
58
+ headers: { "content-type": "application/json" },
59
+ body: JSON.stringify({ username: "alice", password: "CorrectHorse1" }),
60
+ }),
61
+ );
62
+ expect(signUp.status).toBe(200);
63
+ const created = (await signUp.json()) as {
64
+ data: { accessToken: string; userId: string };
65
+ };
66
+ expect(created.data.accessToken).toBeTruthy();
67
+ expect(created.data.userId).toBeTruthy();
68
+
69
+ const bad = await app.fetch(
70
+ new Request("http://localhost/auth/sign-in/username", {
71
+ method: "POST",
72
+ headers: { "content-type": "application/json" },
73
+ body: JSON.stringify({ username: "alice", password: "wrong-password" }),
74
+ }),
75
+ );
76
+ const badBody = (await bad.json()) as { error: { data?: { reason?: string } } };
77
+ expect(badBody.error.data?.reason).toBe("invalid_credentials");
78
+
79
+ const signIn = await app.fetch(
80
+ new Request("http://localhost/auth/sign-in/username", {
81
+ method: "POST",
82
+ headers: { "content-type": "application/json" },
83
+ body: JSON.stringify({ username: "alice", password: "CorrectHorse1" }),
84
+ }),
85
+ );
86
+ expect(signIn.status).toBe(200);
87
+ const session = (await signIn.json()) as { data: { accessToken: string } };
88
+ expect(session.data.accessToken).toBeTruthy();
89
+
90
+ await app.stop();
91
+ });
92
+
93
+ test("anonymous sign-in issues session", async () => {
94
+ const app = authApp();
95
+ await app.boot({ env: "test" });
96
+
97
+ const res = await app.fetch(
98
+ new Request("http://localhost/auth/sign-in/anonymous", {
99
+ method: "POST",
100
+ headers: { "content-type": "application/json" },
101
+ body: "{}",
102
+ }),
103
+ );
104
+ expect(res.status).toBe(200);
105
+ const body = (await res.json()) as {
106
+ data: { accessToken: string; userId: string };
107
+ };
108
+ expect(body.data.accessToken).toBeTruthy();
109
+ expect(body.data.userId).toBeTruthy();
110
+
111
+ await app.stop();
112
+ });
113
+
114
+ test("magic-link request + verify with devToken", async () => {
115
+ const app = authApp();
116
+ await app.boot({ env: "test" });
117
+
118
+ const req = await app.fetch(
119
+ new Request("http://localhost/auth/magic-link/request", {
120
+ method: "POST",
121
+ headers: { "content-type": "application/json" },
122
+ body: JSON.stringify({ email: "ml@example.com" }),
123
+ }),
124
+ );
125
+ expect(req.status).toBe(200);
126
+ const requested = (await req.json()) as { data: { ok: true; devToken?: string } };
127
+ expect(requested.data.ok).toBe(true);
128
+ expect(requested.data.devToken).toBeTruthy();
129
+
130
+ const verify = await app.fetch(
131
+ new Request("http://localhost/auth/magic-link/verify", {
132
+ method: "POST",
133
+ headers: { "content-type": "application/json" },
134
+ body: JSON.stringify({ token: requested.data.devToken }),
135
+ }),
136
+ );
137
+ expect(verify.status).toBe(200);
138
+ const session = (await verify.json()) as { data: { accessToken: string; userId: string } };
139
+ expect(session.data.accessToken).toBeTruthy();
140
+ expect(session.data.userId).toBeTruthy();
141
+
142
+ await app.stop();
143
+ });
144
+
145
+ test("email OTP request + verify with devOtp", async () => {
146
+ const app = authApp();
147
+ await app.boot({ env: "test" });
148
+
149
+ const req = await app.fetch(
150
+ new Request("http://localhost/auth/email-otp/request", {
151
+ method: "POST",
152
+ headers: { "content-type": "application/json" },
153
+ body: JSON.stringify({ email: "otp@example.com" }),
154
+ }),
155
+ );
156
+ expect(req.status).toBe(200);
157
+ const requested = (await req.json()) as { data: { ok: true; devOtp?: string } };
158
+ expect(requested.data.devOtp).toMatch(/^\d{6}$/);
159
+
160
+ const verify = await app.fetch(
161
+ new Request("http://localhost/auth/email-otp/verify", {
162
+ method: "POST",
163
+ headers: { "content-type": "application/json" },
164
+ body: JSON.stringify({
165
+ email: "otp@example.com",
166
+ otp: requested.data.devOtp,
167
+ }),
168
+ }),
169
+ );
170
+ expect(verify.status).toBe(200);
171
+ const session = (await verify.json()) as { data: { accessToken: string } };
172
+ expect(session.data.accessToken).toBeTruthy();
173
+
174
+ await app.stop();
175
+ });
176
+ });
@@ -38,7 +38,7 @@ describe("acceptsGzip", () => {
38
38
  describe("compression plugin", () => {
39
39
  test("gzips a large JSON body when the client accepts gzip", async () => {
40
40
  on(http.get("/big"), flow({ name: "big.get", do: bigPayload }));
41
- const app = oke({ name: "zip" }).plug(compression());
41
+ const app = oke({ autoBoot: false, name: "zip" }).plug(compression());
42
42
 
43
43
  const res = await app.fetch(
44
44
  new Request("http://localhost/big", { headers: { "accept-encoding": "gzip" } }),
@@ -59,7 +59,7 @@ describe("compression plugin", () => {
59
59
 
60
60
  test("passes through untouched without Accept-Encoding: gzip", async () => {
61
61
  on(http.get("/big"), flow({ name: "big.get", do: bigPayload }));
62
- const app = oke({ name: "zip-plain" }).plug(compression());
62
+ const app = oke({ autoBoot: false, name: "zip-plain" }).plug(compression());
63
63
 
64
64
  const res = await app.fetch(new Request("http://localhost/big"));
65
65
 
@@ -70,7 +70,7 @@ describe("compression plugin", () => {
70
70
 
71
71
  test("skips bodies under minSize", async () => {
72
72
  on(http.get("/small"), flow({ name: "small.get", do: () => ({ ok: true }) }));
73
- const app = oke({ name: "zip-small" }).plug(compression());
73
+ const app = oke({ autoBoot: false, name: "zip-small" }).plug(compression());
74
74
 
75
75
  const res = await app.fetch(
76
76
  new Request("http://localhost/small", { headers: { "accept-encoding": "gzip" } }),
@@ -81,7 +81,7 @@ describe("compression plugin", () => {
81
81
 
82
82
  test("minSize: 0 compresses even tiny bodies", async () => {
83
83
  on(http.get("/small"), flow({ name: "small.get", do: () => ({ ok: true }) }));
84
- const app = oke({ name: "zip-zero" }).plug(compression({ minSize: 0 }));
84
+ const app = oke({ autoBoot: false, name: "zip-zero" }).plug(compression({ minSize: 0 }));
85
85
 
86
86
  const res = await app.fetch(
87
87
  new Request("http://localhost/small", { headers: { "accept-encoding": "gzip" } }),
@@ -112,7 +112,7 @@ describe("compression plugin", () => {
112
112
  ),
113
113
  }),
114
114
  );
115
- const app = oke({ name: "zip-skip" }).plug(compression());
115
+ const app = oke({ autoBoot: false, name: "zip-skip" }).plug(compression());
116
116
 
117
117
  const img = await app.fetch(
118
118
  new Request("http://localhost/img", { headers: { "accept-encoding": "gzip" } }),
@@ -156,7 +156,7 @@ describe("configSource — plugin wiring", () => {
156
156
  });
157
157
 
158
158
  on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
159
- const app = oke({ name: "cfg-live" }).plug(maintenanceMode(source));
159
+ const app = oke({ autoBoot: false, name: "cfg-live" }).plug(maintenanceMode(source));
160
160
 
161
161
  expect((await app.fetch(new Request("http://localhost/x"))).status).toBe(200);
162
162