okengine 0.4.3 → 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 (127) hide show
  1. package/package.json +3 -1
  2. package/site/content/docs/ai/index.mdx +24 -0
  3. package/site/content/docs/ai/llms-txt.mdx +3 -0
  4. package/site/content/docs/ai/meta.json +1 -1
  5. package/site/content/docs/console/gates.mdx +46 -8
  6. package/site/content/docs/console/index.mdx +54 -0
  7. package/site/content/docs/console/meta.json +1 -0
  8. package/site/content/docs/elements/gate.mdx +187 -48
  9. package/site/content/docs/elements/index.mdx +45 -0
  10. package/site/content/docs/elements/meta.json +1 -1
  11. package/site/content/docs/get-started/basic-usage.mdx +4 -3
  12. package/site/content/docs/get-started/index.mdx +33 -0
  13. package/site/content/docs/get-started/meta.json +1 -1
  14. package/site/content/docs/index.mdx +9 -31
  15. package/site/content/docs/plugins/anonymous.mdx +95 -0
  16. package/site/content/docs/plugins/compression.mdx +2 -2
  17. package/site/content/docs/plugins/cors.mdx +2 -2
  18. package/site/content/docs/plugins/csrf.mdx +2 -2
  19. package/site/content/docs/plugins/email-otp.mdx +111 -0
  20. package/site/content/docs/plugins/{security-headers.mdx → headers.mdx} +1 -1
  21. package/site/content/docs/plugins/index.mdx +69 -0
  22. package/site/content/docs/plugins/magic-link.mdx +112 -0
  23. package/site/content/docs/plugins/maintenance-mode.mdx +8 -8
  24. package/site/content/docs/plugins/meta.json +10 -1
  25. package/site/content/docs/plugins/passkey.mdx +128 -0
  26. package/site/content/docs/plugins/phone-number.mdx +111 -0
  27. package/site/content/docs/plugins/two-factor.mdx +116 -0
  28. package/site/content/docs/plugins/username.mdx +117 -0
  29. package/site/content/docs/reference/client.mdx +331 -0
  30. package/site/content/docs/reference/fx.mdx +20 -5
  31. package/site/content/docs/reference/index.mdx +45 -0
  32. package/site/content/docs/reference/meta.json +11 -1
  33. package/site/content/docs/reference/plugins.mdx +25 -14
  34. package/src/auth/auth.test.ts +20 -2
  35. package/src/auth/bindings.ts +439 -0
  36. package/src/auth/breach-check.ts +112 -0
  37. package/src/auth/config.ts +288 -0
  38. package/src/auth/cookies.ts +123 -0
  39. package/src/auth/gate-auth.test.ts +379 -0
  40. package/src/auth/identity.ts +190 -0
  41. package/src/auth/index.ts +117 -1
  42. package/src/auth/method-context.ts +33 -0
  43. package/src/auth/operator.ts +27 -1
  44. package/src/auth/password-policy.test.ts +126 -0
  45. package/src/auth/password-policy.ts +77 -0
  46. package/src/auth/plugin.ts +62 -4
  47. package/src/auth/rate.ts +45 -0
  48. package/src/auth/schema.ts +260 -0
  49. package/src/auth/secondary-storage.ts +37 -0
  50. package/src/auth/sessions.ts +58 -1
  51. package/src/auth/tables.ts +4 -0
  52. package/src/auth/verification.ts +78 -0
  53. package/src/cli/dev.test.ts +3 -3
  54. package/src/cli/schema.ts +95 -23
  55. package/src/client/auth.ts +120 -0
  56. package/src/client-react/index.ts +93 -0
  57. package/src/compiler/aot.test.ts +2 -1
  58. package/src/compiler/extract.ts +19 -0
  59. package/src/compiler/response.ts +16 -2
  60. package/src/console/server/app.ts +10 -6
  61. package/src/console/server/auth-rate.test.ts +3 -3
  62. package/src/console/server/bind.ts +12 -1
  63. package/src/console/server/channels.test.ts +1 -1
  64. package/src/console/server/console-gates.ts +14 -0
  65. package/src/console/server/console.test.ts +6 -6
  66. package/src/console/server/flows-invoke.test.ts +2 -2
  67. package/src/console/server/flows.ts +2 -0
  68. package/src/console/server/gates.ts +8 -1
  69. package/src/console/server/operator-db.test.ts +4 -4
  70. package/src/console/server/operator-db.ts +22 -4
  71. package/src/console/server/security.gate.test.ts +3 -3
  72. package/src/console/ui/gates/fixture.ts +4 -0
  73. package/src/console/ui/gates/types.ts +2 -0
  74. package/src/console/ui/shell/client.ts +1 -0
  75. package/src/elements/gate/boot.ts +136 -0
  76. package/src/elements/gate/config.ts +69 -0
  77. package/src/elements/gate/declare.ts +51 -1
  78. package/src/elements/gate/runtime.ts +3 -1
  79. package/src/elements/gate.test.ts +77 -0
  80. package/src/elements/gate.ts +20 -1
  81. package/src/elements/index.ts +8 -0
  82. package/src/index.ts +11 -0
  83. package/src/kernel/app.ts +253 -32
  84. package/src/kernel/boot.test.ts +40 -3
  85. package/src/kernel/boot.ts +8 -0
  86. package/src/kernel/call.test.ts +46 -2
  87. package/src/kernel/edge.test.ts +3 -3
  88. package/src/kernel/flow.test.ts +2 -2
  89. package/src/kernel/fx.test.ts +1 -0
  90. package/src/kernel/fx.ts +60 -0
  91. package/src/kernel/hooks.test.ts +4 -4
  92. package/src/kernel/index.ts +12 -0
  93. package/src/kernel/pipeline.test.ts +12 -8
  94. package/src/kernel/pipeline.ts +23 -4
  95. package/src/kernel/plugin/decorate.test.ts +3 -3
  96. package/src/kernel/plugin/scoping.test.ts +3 -3
  97. package/src/kernel/plugin-elements.test.ts +51 -0
  98. package/src/kernel/plugin-needs.test.ts +83 -0
  99. package/src/kernel/plugin-needs.ts +129 -0
  100. package/src/kernel/plugin.ts +101 -0
  101. package/src/kernel/registry-isolation.test.ts +5 -5
  102. package/src/kernel/registry.ts +102 -3
  103. package/src/manifest/types.ts +2 -0
  104. package/src/plugins/anonymous.ts +58 -0
  105. package/src/plugins/auth/shared.ts +121 -0
  106. package/src/plugins/auth-methods.test.ts +176 -0
  107. package/src/plugins/compression.test.ts +5 -5
  108. package/src/plugins/config-source.test.ts +1 -1
  109. package/src/plugins/cors.test.ts +16 -10
  110. package/src/plugins/csrf.test.ts +1 -1
  111. package/src/plugins/email-otp.ts +161 -0
  112. package/src/plugins/index.ts +31 -0
  113. package/src/plugins/ip-allowlist.test.ts +19 -9
  114. package/src/plugins/magic-link.ts +163 -0
  115. package/src/plugins/maintenance-mode.test.ts +9 -5
  116. package/src/plugins/passkey.ts +216 -0
  117. package/src/plugins/phone-number.ts +149 -0
  118. package/src/plugins/security-headers.test.ts +14 -14
  119. package/src/plugins/two-factor.ts +249 -0
  120. package/src/plugins/username.ts +148 -0
  121. package/src/runs/runs.test.ts +6 -2
  122. package/src/runtime/primitives.ts +37 -4
  123. package/src/runtime/serve.test.ts +3 -2
  124. package/src/runtime/types.ts +25 -2
  125. package/src/test/create-test-app.test.ts +1 -1
  126. package/src/test/create-test-app.ts +4 -1
  127. package/src/test/provisions.integration.test.ts +1 -1
@@ -14,8 +14,14 @@
14
14
  * @see docs/spec/console.md §9.15
15
15
  */
16
16
 
17
+ import type { ChannelTemplateDecl } from "../elements/channel/declare.ts";
18
+ import type { ClockDecl } from "../elements/clock/declare.ts";
19
+ import type { GateDecl } from "../elements/gate/declare.ts";
20
+ import type { SignalDecl } from "../elements/signal/declare.ts";
21
+ import type { VaultSecretDecl } from "../elements/vault/declare.ts";
17
22
  import type { SchemaInput } from "../validation/standard-schema.ts";
18
23
  import type { AnyFlowDef, FlowErrorMap } from "./flow.ts";
24
+ import type { Binding } from "./on.ts";
19
25
  import type { HookFn, HookStage } from "./hooks.ts";
20
26
 
21
27
  /** Brand carrying accumulated decoration types through the builder. */
@@ -226,6 +232,13 @@ export interface PluginApi {
226
232
  columns?: Readonly<Record<string, unknown>>,
227
233
  options?: PluginTableOptions,
228
234
  ): PluginApi;
235
+ /**
236
+ * Contribute an HTTP (or other) Binding into the app router + adopted set.
237
+ * Required for auth method plugins — registry `flows[]` alone does not route.
238
+ *
239
+ * @param binding - Trigger + flow pair
240
+ */
241
+ binding(binding: Binding): PluginApi;
229
242
  /**
230
243
  * Contribute a flow.
231
244
  *
@@ -245,6 +258,36 @@ export interface PluginApi {
245
258
  * @param schema - Standard Schema
246
259
  */
247
260
  config(schema: SchemaInput): PluginApi;
261
+ /**
262
+ * Contribute a vault secret/config contract (merged into boot secrets).
263
+ *
264
+ * @param secret - Vault contract from `vault.secret(...)` / `vault.config(...)`
265
+ */
266
+ vault(secret: VaultSecretDecl): PluginApi;
267
+ /**
268
+ * Contribute a named clock schedule (merged into boot clocks).
269
+ *
270
+ * @param decl - Clock from `clock(...)`
271
+ */
272
+ clock(decl: ClockDecl): PluginApi;
273
+ /**
274
+ * Contribute a signal declaration (merged into boot signals).
275
+ *
276
+ * @param decl - Signal from `signal(...)`
277
+ */
278
+ signal(decl: SignalDecl): PluginApi;
279
+ /**
280
+ * Contribute a gate declaration (merged into boot gates).
281
+ *
282
+ * @param decl - Gate from `gate.policy` / `gate.scope` / `gate.rate` / `gate.public`
283
+ */
284
+ gate(decl: GateDecl): PluginApi;
285
+ /**
286
+ * Contribute a channel template (merged into boot channel templates).
287
+ *
288
+ * @param decl - Template from `channel.email.template(...)` etc.
289
+ */
290
+ channelTemplate(decl: ChannelTemplateDecl): PluginApi;
248
291
  }
249
292
 
250
293
  /** Captured capability lists for the Manifest. */
@@ -276,9 +319,15 @@ export interface PluginRegistration {
276
319
  readonly cli: readonly CliContribution[];
277
320
  readonly images: readonly ImageRecipeContribution[];
278
321
  readonly flows: readonly AnyFlowDef[];
322
+ readonly bindings: readonly Binding[];
279
323
  readonly errors: FlowErrorMap;
280
324
  readonly client: readonly ClientExtensionContribution[];
281
325
  readonly configSchema: SchemaInput | undefined;
326
+ readonly vaultSecrets: readonly VaultSecretDecl[];
327
+ readonly clocks: readonly ClockDecl[];
328
+ readonly signals: readonly SignalDecl[];
329
+ readonly gates: readonly GateDecl[];
330
+ readonly channelTemplates: readonly ChannelTemplateDecl[];
282
331
  }
283
332
 
284
333
  /**
@@ -383,6 +432,12 @@ export interface PluginDef<D extends Record<string, unknown> = {}> {
383
432
  columns?: Readonly<Record<string, unknown>>,
384
433
  options?: PluginTableOptions,
385
434
  ): PluginDef<D>;
435
+ /**
436
+ * Queue a Binding contribution (joins router + adopted on app.plug).
437
+ *
438
+ * @param binding - Trigger + flow
439
+ */
440
+ binding(binding: Binding): PluginDef<D>;
386
441
  /**
387
442
  * Queue a flow contribution.
388
443
  *
@@ -402,6 +457,16 @@ export interface PluginDef<D extends Record<string, unknown> = {}> {
402
457
  * @param schema - Standard Schema
403
458
  */
404
459
  config(schema: SchemaInput): PluginDef<D>;
460
+ /** Queue a vault secret/config contract. */
461
+ vault(secret: VaultSecretDecl): PluginDef<D>;
462
+ /** Queue a clock schedule. */
463
+ clock(decl: ClockDecl): PluginDef<D>;
464
+ /** Queue a signal declaration. */
465
+ signal(decl: SignalDecl): PluginDef<D>;
466
+ /** Queue a gate declaration. */
467
+ gate(decl: GateDecl): PluginDef<D>;
468
+ /** Queue a channel template. */
469
+ channelTemplate(decl: ChannelTemplateDecl): PluginDef<D>;
405
470
  }
406
471
 
407
472
  /** Extract accumulated decoration types from a {@link PluginDef}. */
@@ -505,6 +570,12 @@ export function plugin(name: string, options: PluginOptions): PluginDef {
505
570
  });
506
571
  return def;
507
572
  },
573
+ binding(binding) {
574
+ steps.push((api) => {
575
+ api.binding(binding);
576
+ });
577
+ return def;
578
+ },
508
579
  flow(flowDef) {
509
580
  steps.push((api) => {
510
581
  api.flow(flowDef);
@@ -523,6 +594,36 @@ export function plugin(name: string, options: PluginOptions): PluginDef {
523
594
  });
524
595
  return def;
525
596
  },
597
+ vault(secret) {
598
+ steps.push((api) => {
599
+ api.vault(secret);
600
+ });
601
+ return def;
602
+ },
603
+ clock(decl) {
604
+ steps.push((api) => {
605
+ api.clock(decl);
606
+ });
607
+ return def;
608
+ },
609
+ signal(decl) {
610
+ steps.push((api) => {
611
+ api.signal(decl);
612
+ });
613
+ return def;
614
+ },
615
+ gate(decl) {
616
+ steps.push((api) => {
617
+ api.gate(decl);
618
+ });
619
+ return def;
620
+ },
621
+ channelTemplate(decl) {
622
+ steps.push((api) => {
623
+ api.channelTemplate(decl);
624
+ });
625
+ return def;
626
+ },
526
627
  };
527
628
 
528
629
  return def;
@@ -22,11 +22,11 @@ afterEach(() => {
22
22
  describe("registry isolation", () => {
23
23
  test("consume (default): App B does not serve App A routes", async () => {
24
24
  on(http.get("/a"), flow({ name: "app-a.ping", do: () => "a" }));
25
- const appA = oke({ name: "app-a" });
25
+ const appA = oke({ autoBoot: false, name: "app-a" });
26
26
 
27
27
  // No resetBindings() between the apps — the pre-fix leak scenario.
28
28
  on(http.get("/b"), flow({ name: "app-b.ping", do: () => "b" }));
29
- const appB = oke({ name: "app-b" });
29
+ const appB = oke({ autoBoot: false, name: "app-b" });
30
30
 
31
31
  // Pre-fix this was 200: App B adopted App A's leftover binding.
32
32
  expect((await appB.fetch(new Request("http://localhost/a"))).status).toBe(404);
@@ -46,10 +46,10 @@ describe("registry isolation", () => {
46
46
 
47
47
  test("keep: today's behavior — registry survives construction", async () => {
48
48
  on(http.get("/a"), flow({ name: "kept.ping", do: () => "a" }));
49
- oke({ name: "app-a", registry: "keep" });
49
+ oke({ autoBoot: false, name: "app-a", registry: "keep" });
50
50
  expect(listBindings()).toHaveLength(1);
51
51
 
52
- const appB = oke({ name: "app-b", registry: "keep" });
52
+ const appB = oke({ autoBoot: false, name: "app-b", registry: "keep" });
53
53
  expect((await appB.fetch(new Request("http://localhost/a"))).status).toBe(200);
54
54
  });
55
55
 
@@ -60,7 +60,7 @@ describe("registry isolation", () => {
60
60
  trigger: http.get("/b"),
61
61
  flow: flow({ name: "only-b.ping", do: () => "b" }),
62
62
  };
63
- const app = oke({ name: "app-b", registry: "ignore", bindings: [onlyB] });
63
+ const app = oke({ autoBoot: false, name: "app-b", registry: "ignore", bindings: [onlyB] });
64
64
 
65
65
  expect((await app.fetch(new Request("http://localhost/stray"))).status).toBe(404);
66
66
 
@@ -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.
@@ -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 };