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.
- package/AGENTS.md +2 -0
- package/package.json +14 -12
- package/site/content/docs/ai/index.mdx +24 -0
- package/site/content/docs/ai/llms-txt.mdx +3 -0
- package/site/content/docs/ai/meta.json +1 -1
- package/site/content/docs/ai/skills.mdx +5 -3
- package/site/content/docs/console/gates.mdx +46 -8
- package/site/content/docs/console/index.mdx +54 -0
- package/site/content/docs/console/meta.json +1 -0
- package/site/content/docs/elements/ai.mdx +2 -0
- package/site/content/docs/elements/channel.mdx +2 -0
- package/site/content/docs/elements/clock.mdx +1 -4
- package/site/content/docs/elements/flow.mdx +4 -10
- package/site/content/docs/elements/gate.mdx +189 -48
- package/site/content/docs/elements/index.mdx +45 -0
- package/site/content/docs/elements/meta.json +1 -1
- package/site/content/docs/elements/signal.mdx +1 -5
- package/site/content/docs/elements/store.mdx +27 -6
- package/site/content/docs/elements/vault.mdx +10 -11
- package/site/content/docs/get-started/basic-usage.mdx +79 -43
- package/site/content/docs/get-started/index.mdx +33 -0
- package/site/content/docs/get-started/installation.mdx +95 -43
- package/site/content/docs/get-started/introduction.mdx +128 -75
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/why.mdx +141 -0
- package/site/content/docs/index.mdx +9 -31
- package/site/content/docs/plugins/anonymous.mdx +95 -0
- package/site/content/docs/plugins/compression.mdx +2 -2
- package/site/content/docs/plugins/cors.mdx +2 -2
- package/site/content/docs/plugins/csrf.mdx +2 -2
- package/site/content/docs/plugins/email-otp.mdx +111 -0
- package/site/content/docs/plugins/{security-headers.mdx → headers.mdx} +2 -2
- package/site/content/docs/plugins/index.mdx +69 -0
- package/site/content/docs/plugins/ip-allowlist.mdx +1 -2
- package/site/content/docs/plugins/magic-link.mdx +112 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +8 -8
- package/site/content/docs/plugins/meta.json +10 -1
- package/site/content/docs/plugins/passkey.mdx +128 -0
- package/site/content/docs/plugins/phone-number.mdx +111 -0
- package/site/content/docs/plugins/two-factor.mdx +116 -0
- package/site/content/docs/plugins/username.mdx +117 -0
- package/site/content/docs/reference/client.mdx +331 -0
- package/site/content/docs/reference/configuration.mdx +1 -1
- package/site/content/docs/reference/environment-variables.mdx +5 -3
- package/site/content/docs/reference/fx.mdx +36 -8
- package/site/content/docs/reference/index.mdx +45 -0
- package/site/content/docs/reference/meta.json +11 -1
- package/site/content/docs/reference/plugins.mdx +25 -14
- package/src/auth/auth.test.ts +20 -2
- package/src/auth/bindings.ts +439 -0
- package/src/auth/breach-check.ts +112 -0
- package/src/auth/config.ts +288 -0
- package/src/auth/cookies.ts +123 -0
- package/src/auth/gate-auth.test.ts +379 -0
- package/src/auth/identity.ts +190 -0
- package/src/auth/index.ts +117 -1
- package/src/auth/method-context.ts +33 -0
- package/src/auth/operator.ts +27 -1
- package/src/auth/password-policy.test.ts +126 -0
- package/src/auth/password-policy.ts +77 -0
- package/src/auth/plugin.ts +62 -4
- package/src/auth/rate.ts +45 -0
- package/src/auth/schema.ts +260 -0
- package/src/auth/secondary-storage.ts +37 -0
- package/src/auth/sessions.ts +58 -1
- package/src/auth/tables.ts +4 -0
- package/src/auth/verification.ts +78 -0
- package/src/cli/competitor-mention-removal.test.ts +117 -0
- package/src/cli/dev.test.ts +3 -3
- package/src/cli/dev.ts +20 -0
- package/src/cli/meilisearch-local.test.ts +69 -0
- package/src/cli/meilisearch-local.ts +188 -0
- package/src/cli/schema.ts +95 -23
- package/src/client/auth.ts +120 -0
- package/src/client-react/index.ts +93 -0
- package/src/compiler/aot.test.ts +2 -1
- package/src/compiler/extract.ts +19 -0
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +5 -1
- package/src/compiler/response.ts +16 -2
- package/src/console/server/app.ts +10 -6
- package/src/console/server/auth-rate.test.ts +3 -3
- package/src/console/server/bind.ts +12 -1
- package/src/console/server/channels.test.ts +1 -1
- package/src/console/server/console-gates.ts +14 -0
- package/src/console/server/console.test.ts +6 -6
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/server/flows.ts +2 -0
- package/src/console/server/gates.ts +8 -1
- package/src/console/server/operator-db.test.ts +4 -4
- package/src/console/server/operator-db.ts +22 -4
- package/src/console/server/security.gate.test.ts +3 -3
- package/src/console/server/store.test.ts +1 -1
- package/src/console/server/store.ts +11 -1
- package/src/console/ui/dist/assets/index-CjxwRGVv.js +10 -0
- package/src/console/ui/dist/assets/panel-access-BGv45snf.js +64 -0
- package/src/console/ui/dist/assets/{panel-ai-D_m6WQI8.js → panel-ai-B2S7LEii.js} +1 -1
- package/src/console/ui/dist/assets/{panel-architecture-CKnXFyUx.js → panel-architecture-D7UJh91v.js} +1 -1
- package/src/console/ui/dist/assets/{panel-channels-DCDd4WAC.js → panel-channels-9T3ybqRu.js} +1 -1
- package/src/console/ui/dist/assets/panel-clock-Cb1UXGRQ.js +1 -0
- package/src/console/ui/dist/assets/{panel-diff-cdonmH8c.js → panel-diff-DmYbKWmN.js} +1 -1
- package/src/console/ui/dist/assets/panel-flows-PiHwT55z.js +48 -0
- package/src/console/ui/dist/assets/{panel-gates-B5eTE8XH.js → panel-gates-BQGYXvjT.js} +1 -1
- package/src/console/ui/dist/assets/panel-overview-BBnRO18l.js +1 -0
- package/src/console/ui/dist/assets/{panel-plugins-Cj7DK1er.js → panel-plugins-D0PsmVw2.js} +1 -1
- package/src/console/ui/dist/assets/panel-runs-CWuRDe0r.js +1 -0
- package/src/console/ui/dist/assets/{panel-signals-whmDXIg3.js → panel-signals-Bbg4ewpP.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-CEMHLvaw.js → panel-store-CPCbsDRa.js} +1 -1
- package/src/console/ui/dist/assets/panel-traces-DVAzuA_S.js +1 -0
- package/src/console/ui/dist/assets/{panel-vault-C9wjbki8.js → panel-vault-D1_MvOmo.js} +1 -1
- package/src/console/ui/dist/assets/{rolldown-runtime-CNC7AqOf.js → rolldown-runtime-B0Z9INg1.js} +1 -1
- package/src/console/ui/dist/index.html +2 -2
- package/src/console/ui/gates/fixture.ts +4 -0
- package/src/console/ui/gates/types.ts +2 -0
- package/src/console/ui/shell/client.ts +1 -0
- package/src/docker/compose.ts +5 -0
- package/src/docker/docker.test.ts +41 -0
- package/src/docker/recipes/index.ts +10 -2
- package/src/docker/recipes/meilisearch.ts +31 -0
- package/src/drivers/conformance.test.ts +16 -1
- package/src/drivers/conformance.ts +40 -3
- package/src/drivers/index.ts +14 -2
- package/src/drivers/libsql.ts +4 -4
- package/src/drivers/meilisearch.integration.test.ts +77 -0
- package/src/drivers/meilisearch.test.ts +181 -0
- package/src/drivers/meilisearch.ts +208 -0
- package/src/drivers/memory.ts +4 -4
- package/src/drivers/pgvector.ts +6 -6
- package/src/drivers/types.ts +93 -12
- package/src/drivers/vault-driver-removal.test.ts +6 -0
- package/src/drivers/vault-types.ts +4 -4
- package/src/elements/ai/runtime.ts +6 -0
- package/src/elements/ai.test.ts +22 -0
- package/src/elements/gate/boot.ts +136 -0
- package/src/elements/gate/config.ts +69 -0
- package/src/elements/gate/declare.ts +51 -1
- package/src/elements/gate/runtime.ts +3 -1
- package/src/elements/gate.test.ts +77 -0
- package/src/elements/gate.ts +20 -1
- package/src/elements/index.ts +8 -0
- package/src/elements/store/index-boot.test.ts +49 -7
- package/src/elements/store/runtime.ts +50 -15
- package/src/elements/store.ts +2 -0
- package/src/elements/vault.test.ts +27 -4
- package/src/elements/vault.ts +1 -1
- package/src/index.ts +15 -0
- package/src/kernel/app.ts +253 -32
- package/src/kernel/boot-bind/store.test.ts +9 -0
- package/src/kernel/boot-bind/store.ts +30 -2
- package/src/kernel/boot.test.ts +40 -3
- package/src/kernel/boot.ts +8 -0
- package/src/kernel/call.test.ts +46 -2
- package/src/kernel/concurrency.test.ts +58 -0
- package/src/kernel/concurrency.ts +48 -0
- package/src/kernel/edge.test.ts +3 -3
- package/src/kernel/flow.test.ts +2 -2
- package/src/kernel/fx.test.ts +12 -2
- package/src/kernel/fx.ts +97 -5
- package/src/kernel/hooks.test.ts +4 -4
- package/src/kernel/index.ts +22 -1
- package/src/kernel/pipeline.test.ts +12 -8
- package/src/kernel/pipeline.ts +23 -4
- package/src/kernel/plugin/decorate.test.ts +3 -3
- package/src/kernel/plugin/scoping.test.ts +3 -3
- package/src/kernel/plugin-elements.test.ts +51 -0
- package/src/kernel/plugin-needs.test.ts +83 -0
- package/src/kernel/plugin-needs.ts +129 -0
- package/src/kernel/plugin.ts +101 -0
- package/src/kernel/redacted.ts +74 -0
- package/src/kernel/registry-isolation.test.ts +5 -5
- package/src/kernel/registry.ts +102 -3
- package/src/kernel/router.ts +3 -3
- package/src/manifest/types.ts +2 -0
- package/src/plugins/anonymous.ts +58 -0
- package/src/plugins/auth/shared.ts +121 -0
- package/src/plugins/auth-methods.test.ts +176 -0
- package/src/plugins/compression.test.ts +5 -5
- package/src/plugins/config-source.test.ts +1 -1
- package/src/plugins/cors.test.ts +16 -10
- package/src/plugins/csrf.test.ts +1 -1
- package/src/plugins/email-otp.ts +161 -0
- package/src/plugins/index.ts +31 -0
- package/src/plugins/ip-allowlist.test.ts +19 -9
- package/src/plugins/magic-link.ts +163 -0
- package/src/plugins/maintenance-mode.test.ts +9 -5
- package/src/plugins/passkey.ts +216 -0
- package/src/plugins/phone-number.ts +149 -0
- package/src/plugins/security-headers.test.ts +14 -14
- package/src/plugins/two-factor.ts +249 -0
- package/src/plugins/username.ts +148 -0
- package/src/runs/runs.test.ts +6 -2
- package/src/runtime/primitives.ts +37 -4
- package/src/runtime/serve.test.ts +3 -2
- package/src/runtime/types.ts +25 -2
- package/src/test/create-test-app.test.ts +1 -1
- package/src/test/create-test-app.ts +4 -1
- package/src/test/provisions.integration.test.ts +2 -2
- package/site/content/docs/get-started/comparison.mdx +0 -65
- package/src/console/ui/dist/assets/index-CrKMmO__.js +0 -10
- package/src/console/ui/dist/assets/panel-access-C0J2D-a2.js +0 -64
- package/src/console/ui/dist/assets/panel-clock-DjGGFPzr.js +0 -1
- package/src/console/ui/dist/assets/panel-flows-DlCU5zjA.js +0 -45
- package/src/console/ui/dist/assets/panel-overview-BsFvDdts.js +0 -1
- package/src/console/ui/dist/assets/panel-runs-C0gmnoYL.js +0 -1
- package/src/console/ui/dist/assets/panel-traces-BDiAuVSK.js +0 -1
- package/src/drivers/vault-infisical.ts +0 -57
package/src/kernel/registry.ts
CHANGED
|
@@ -12,9 +12,16 @@
|
|
|
12
12
|
* @see docs/spec/console.md §9.15
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type {
|
|
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.
|
package/src/kernel/router.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HTTP router — compiled RegExp matching with a Trie fallback
|
|
3
|
-
*
|
|
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
|
|
144
|
+
* regular expressions for parametric routes.
|
|
145
145
|
*/
|
|
146
146
|
export class RegExpRouter<T> implements Router<T> {
|
|
147
147
|
readonly name = "RegExpRouter";
|
package/src/manifest/types.ts
CHANGED
|
@@ -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
|
|