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/plugins/cors.test.ts
CHANGED
|
@@ -29,7 +29,9 @@ function preflight(path = "/x", headers: Record<string, string> = {}): Request {
|
|
|
29
29
|
describe("cors plugin — preflight", () => {
|
|
30
30
|
test("answers preflight for a path bound to another method (edge)", async () => {
|
|
31
31
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
32
|
-
const app = oke({ name: "cors-pre" }).plug(
|
|
32
|
+
const app = oke({ autoBoot: false, name: "cors-pre" }).plug(
|
|
33
|
+
cors({ origin: "https://app.example.com" }),
|
|
34
|
+
);
|
|
33
35
|
|
|
34
36
|
const res = await app.fetch(preflight());
|
|
35
37
|
|
|
@@ -41,7 +43,9 @@ describe("cors plugin — preflight", () => {
|
|
|
41
43
|
|
|
42
44
|
test("denied origins get a 204 without CORS headers — the browser blocks quietly", async () => {
|
|
43
45
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
44
|
-
const app = oke({ name: "cors-deny" }).plug(
|
|
46
|
+
const app = oke({ autoBoot: false, name: "cors-deny" }).plug(
|
|
47
|
+
cors({ origin: "https://other.example.com" }),
|
|
48
|
+
);
|
|
45
49
|
|
|
46
50
|
const res = await app.fetch(preflight());
|
|
47
51
|
|
|
@@ -51,7 +55,7 @@ describe("cors plugin — preflight", () => {
|
|
|
51
55
|
|
|
52
56
|
test("no origin configured answers nothing — cross-origin closed by default", async () => {
|
|
53
57
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
54
|
-
const app = oke({ name: "cors-closed" }).plug(cors());
|
|
58
|
+
const app = oke({ autoBoot: false, name: "cors-closed" }).plug(cors());
|
|
55
59
|
|
|
56
60
|
const res = await app.fetch(preflight());
|
|
57
61
|
expect(res.headers.get("access-control-allow-origin")).toBeNull();
|
|
@@ -59,7 +63,7 @@ describe("cors plugin — preflight", () => {
|
|
|
59
63
|
|
|
60
64
|
test("a plain unmatched OPTIONS without preflight headers still 404s", async () => {
|
|
61
65
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
62
|
-
const app = oke({ name: "cors-404" }).plug(cors({ origin: "*" }));
|
|
66
|
+
const app = oke({ autoBoot: false, name: "cors-404" }).plug(cors({ origin: "*" }));
|
|
63
67
|
|
|
64
68
|
const res = await app.fetch(new Request("http://localhost/x", { method: "OPTIONS" }));
|
|
65
69
|
expect(res.status).toBe(404);
|
|
@@ -67,7 +71,7 @@ describe("cors plugin — preflight", () => {
|
|
|
67
71
|
|
|
68
72
|
test("reflects request headers by default, honors configured lists and maxAge", async () => {
|
|
69
73
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
70
|
-
const app = oke({ name: "cors-hdr" }).plug(
|
|
74
|
+
const app = oke({ autoBoot: false, name: "cors-hdr" }).plug(
|
|
71
75
|
cors({ origin: "*", maxAge: 600, allowedHeaders: ["x-custom"] }),
|
|
72
76
|
);
|
|
73
77
|
|
|
@@ -88,7 +92,7 @@ describe("cors plugin — preflight", () => {
|
|
|
88
92
|
|
|
89
93
|
test("allowlist + credentials reflects only listed origins", async () => {
|
|
90
94
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
91
|
-
const app = oke({ name: "cors-cred" }).plug(
|
|
95
|
+
const app = oke({ autoBoot: false, name: "cors-cred" }).plug(
|
|
92
96
|
cors({ origin: ["https://app.example.com", "https://admin.example.com"], credentials: true }),
|
|
93
97
|
);
|
|
94
98
|
|
|
@@ -106,7 +110,7 @@ describe("cors plugin — preflight", () => {
|
|
|
106
110
|
describe("cors plugin — actual requests", () => {
|
|
107
111
|
test("allowed origins get allow-origin (+ exposed headers) on matched responses", async () => {
|
|
108
112
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
109
|
-
const app = oke({ name: "cors-actual" }).plug(
|
|
113
|
+
const app = oke({ autoBoot: false, name: "cors-actual" }).plug(
|
|
110
114
|
cors({ origin: ["https://app.example.com"], exposedHeaders: ["x-total"] }),
|
|
111
115
|
);
|
|
112
116
|
|
|
@@ -121,7 +125,7 @@ describe("cors plugin — actual requests", () => {
|
|
|
121
125
|
|
|
122
126
|
test("allowlist + credentials on matched responses reflects only listed origins", async () => {
|
|
123
127
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
124
|
-
const app = oke({ name: "cors-actual-cred" }).plug(
|
|
128
|
+
const app = oke({ autoBoot: false, name: "cors-actual-cred" }).plug(
|
|
125
129
|
cors({ origin: ["https://app.example.com"], credentials: true }),
|
|
126
130
|
);
|
|
127
131
|
|
|
@@ -142,7 +146,9 @@ describe("cors plugin — actual requests", () => {
|
|
|
142
146
|
|
|
143
147
|
test("denied origins get no CORS headers on matched responses", async () => {
|
|
144
148
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
145
|
-
const app = oke({ name: "cors-actual-deny" }).plug(
|
|
149
|
+
const app = oke({ autoBoot: false, name: "cors-actual-deny" }).plug(
|
|
150
|
+
cors({ origin: "https://ok.example.com" }),
|
|
151
|
+
);
|
|
146
152
|
|
|
147
153
|
const res = await app.fetch(
|
|
148
154
|
new Request("http://localhost/x", { headers: { origin: "https://evil.example.com" } }),
|
|
@@ -154,7 +160,7 @@ describe("cors plugin — actual requests", () => {
|
|
|
154
160
|
|
|
155
161
|
test("requests without an Origin header are untouched", async () => {
|
|
156
162
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
157
|
-
const app = oke({ name: "cors-no-origin" }).plug(cors({ origin: "*" }));
|
|
163
|
+
const app = oke({ autoBoot: false, name: "cors-no-origin" }).plug(cors({ origin: "*" }));
|
|
158
164
|
|
|
159
165
|
const res = await app.fetch(new Request("http://localhost/x"));
|
|
160
166
|
expect(res.headers.get("access-control-allow-origin")).toBeNull();
|
package/src/plugins/csrf.test.ts
CHANGED
|
@@ -22,7 +22,7 @@ function mutating(headers: Record<string, string> = {}): Request {
|
|
|
22
22
|
function appWith(options: Parameters<typeof csrf>[0]) {
|
|
23
23
|
on(http.post("/transfer"), flow({ name: "transfer.create", do: () => ({ moved: true }) }));
|
|
24
24
|
on(http.get("/balance"), flow({ name: "balance.get", do: () => ({ balance: 5 }) }));
|
|
25
|
-
return oke({ name: `csrf-${crypto.randomUUID()}` }).plug(csrf(options));
|
|
25
|
+
return oke({ autoBoot: false, name: `csrf-${crypto.randomUUID()}` }).plug(csrf(options));
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
describe("csrf plugin", () => {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email OTP Gate auth method plugin.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createIdentityStore,
|
|
7
|
+
normalizeEmail,
|
|
8
|
+
type IdentityStore,
|
|
9
|
+
type UserIdentityRow,
|
|
10
|
+
} from "../auth/identity.ts";
|
|
11
|
+
import { issueSessionWithScopes } from "../auth/sessions.ts";
|
|
12
|
+
import {
|
|
13
|
+
createVerificationStore,
|
|
14
|
+
findActiveVerification,
|
|
15
|
+
generateOtp,
|
|
16
|
+
hashChallenge,
|
|
17
|
+
putVerification,
|
|
18
|
+
type VerificationStore,
|
|
19
|
+
} from "../auth/verification.ts";
|
|
20
|
+
import { plugin, type PluginDef } from "../kernel/plugin.ts";
|
|
21
|
+
import {
|
|
22
|
+
AuthFailed,
|
|
23
|
+
AuthRateLimited,
|
|
24
|
+
SessionTokensOut,
|
|
25
|
+
bindPublicAuth,
|
|
26
|
+
createMethodRuntime,
|
|
27
|
+
fail,
|
|
28
|
+
flow,
|
|
29
|
+
z,
|
|
30
|
+
type AuthMethodOptions,
|
|
31
|
+
} from "./auth/shared.ts";
|
|
32
|
+
|
|
33
|
+
const DEFAULT_TTL_MS = 10 * 60 * 1000;
|
|
34
|
+
const MAX_ATTEMPTS = 5;
|
|
35
|
+
|
|
36
|
+
/** Options for {@link emailOtp}. */
|
|
37
|
+
export interface EmailOtpOptions extends AuthMethodOptions {
|
|
38
|
+
/** Challenge TTL (default 10m). */
|
|
39
|
+
readonly ttlMs?: number;
|
|
40
|
+
readonly identities?: IdentityStore;
|
|
41
|
+
readonly verifications?: VerificationStore;
|
|
42
|
+
/** Return raw OTP in the request response (test / local). */
|
|
43
|
+
readonly exposeDevOtp?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Email OTP request + verify (6-digit, hashed, 5 attempts).
|
|
48
|
+
*
|
|
49
|
+
* @param opts - TTL / stores / dev OTP
|
|
50
|
+
*/
|
|
51
|
+
export function emailOtp(opts: EmailOtpOptions = {}): PluginDef {
|
|
52
|
+
const runtime = createMethodRuntime(opts);
|
|
53
|
+
const identities = opts.identities ?? createIdentityStore();
|
|
54
|
+
const verifications = opts.verifications ?? createVerificationStore();
|
|
55
|
+
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
56
|
+
|
|
57
|
+
const request = flow({
|
|
58
|
+
name: "auth.requestEmailOtp",
|
|
59
|
+
unit: "auth",
|
|
60
|
+
plane: "user",
|
|
61
|
+
in: z.object({ email: z.string().min(3) }),
|
|
62
|
+
out: z.object({
|
|
63
|
+
ok: z.literal(true),
|
|
64
|
+
devOtp: z.string().optional(),
|
|
65
|
+
}),
|
|
66
|
+
errors: { AuthFailed, AuthRateLimited },
|
|
67
|
+
do: async (input) => {
|
|
68
|
+
const email = normalizeEmail(input.email);
|
|
69
|
+
if (!email.includes("@")) return fail("AuthFailed", { reason: "invalid_email" });
|
|
70
|
+
const otp = generateOtp(6);
|
|
71
|
+
const now = runtime.now();
|
|
72
|
+
// Invalidate prior active challenges for this email.
|
|
73
|
+
for (const row of verifications.rows.values()) {
|
|
74
|
+
if (row.identifier === `email-otp:${email}` && row.consumedAt === null) {
|
|
75
|
+
row.consumedAt = now;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
putVerification(verifications, {
|
|
79
|
+
id: crypto.randomUUID(),
|
|
80
|
+
identifier: `email-otp:${email}`,
|
|
81
|
+
value: await hashChallenge(otp),
|
|
82
|
+
expiresAt: now + ttlMs,
|
|
83
|
+
createdAt: now,
|
|
84
|
+
consumedAt: null,
|
|
85
|
+
attempts: 0,
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
ok: true as const,
|
|
89
|
+
...(opts.exposeDevOtp ? { devOtp: otp } : {}),
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const verify = flow({
|
|
95
|
+
name: "auth.verifyEmailOtp",
|
|
96
|
+
unit: "auth",
|
|
97
|
+
plane: "user",
|
|
98
|
+
in: z.object({
|
|
99
|
+
email: z.string().min(3),
|
|
100
|
+
otp: z.string().min(4).max(8),
|
|
101
|
+
}),
|
|
102
|
+
out: SessionTokensOut,
|
|
103
|
+
errors: { AuthFailed, AuthRateLimited },
|
|
104
|
+
do: async (input) => {
|
|
105
|
+
const email = normalizeEmail(input.email);
|
|
106
|
+
const now = runtime.now();
|
|
107
|
+
const row = findActiveVerification(verifications, `email-otp:${email}`, now);
|
|
108
|
+
if (!row) return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
109
|
+
if (row.attempts >= MAX_ATTEMPTS) {
|
|
110
|
+
row.consumedAt = now;
|
|
111
|
+
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
112
|
+
}
|
|
113
|
+
const hash = await hashChallenge(input.otp.trim());
|
|
114
|
+
if (hash !== row.value) {
|
|
115
|
+
row.attempts += 1;
|
|
116
|
+
if (row.attempts >= MAX_ATTEMPTS) row.consumedAt = now;
|
|
117
|
+
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
118
|
+
}
|
|
119
|
+
row.consumedAt = now;
|
|
120
|
+
const user = ensureUserByEmail(identities, email, now);
|
|
121
|
+
const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
|
|
122
|
+
id: user.id,
|
|
123
|
+
plane: "user",
|
|
124
|
+
scopes: [],
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
accessToken: issued.accessToken,
|
|
128
|
+
refreshToken: issued.refreshToken,
|
|
129
|
+
accessExpiresAt: issued.accessExpiresAt,
|
|
130
|
+
userId: user.id,
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return plugin("emailOtp", { version: "0.0.1", config: { method: "email-otp" } })
|
|
136
|
+
.needs("auth")
|
|
137
|
+
.binding(bindPublicAuth("/email-otp/request", request, "otp"))
|
|
138
|
+
.binding(bindPublicAuth("/email-otp/verify", verify, "otp"));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ensureUserByEmail(store: IdentityStore, email: string, now: number): UserIdentityRow {
|
|
142
|
+
const existingId = store.byEmail.get(email);
|
|
143
|
+
if (existingId) {
|
|
144
|
+
const existing = store.users.get(existingId);
|
|
145
|
+
if (existing) return existing;
|
|
146
|
+
}
|
|
147
|
+
const id = crypto.randomUUID();
|
|
148
|
+
const user: UserIdentityRow = {
|
|
149
|
+
id,
|
|
150
|
+
email,
|
|
151
|
+
name: email.split("@")[0] || "user",
|
|
152
|
+
emailVerified: true,
|
|
153
|
+
status: "active",
|
|
154
|
+
createdAt: now,
|
|
155
|
+
updatedAt: now,
|
|
156
|
+
extra: {},
|
|
157
|
+
};
|
|
158
|
+
store.users.set(id, user);
|
|
159
|
+
store.byEmail.set(email, id);
|
|
160
|
+
return user;
|
|
161
|
+
}
|
package/src/plugins/index.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* `.plug()`.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
export { anonymous, type AnonymousPluginOptions } from "./anonymous.ts";
|
|
7
8
|
export { compression, type CompressionOptions } from "./compression.ts";
|
|
8
9
|
export {
|
|
9
10
|
configSource,
|
|
@@ -15,8 +16,23 @@ export {
|
|
|
15
16
|
} from "./config-source.ts";
|
|
16
17
|
export { cors, type CorsOptions } from "./cors.ts";
|
|
17
18
|
export { csrf, type CsrfOptions } from "./csrf.ts";
|
|
19
|
+
export { emailOtp, type EmailOtpOptions } from "./email-otp.ts";
|
|
18
20
|
export { ipAllowlist, type IpAllowlistOptions } from "./ip-allowlist.ts";
|
|
21
|
+
export { magicLink, type MagicLinkOptions } from "./magic-link.ts";
|
|
19
22
|
export { maintenanceMode, type MaintenanceModeOptions } from "./maintenance-mode.ts";
|
|
23
|
+
export {
|
|
24
|
+
createPasskeyStore,
|
|
25
|
+
passkey,
|
|
26
|
+
type PasskeyCredential,
|
|
27
|
+
type PasskeyOptions,
|
|
28
|
+
type PasskeyStore,
|
|
29
|
+
} from "./passkey.ts";
|
|
30
|
+
export {
|
|
31
|
+
createPhoneStore,
|
|
32
|
+
phoneNumber,
|
|
33
|
+
type PhoneNumberOptions,
|
|
34
|
+
type PhoneStore,
|
|
35
|
+
} from "./phone-number.ts";
|
|
20
36
|
export {
|
|
21
37
|
defaultCspDirectives,
|
|
22
38
|
securityHeaders,
|
|
@@ -24,3 +40,18 @@ export {
|
|
|
24
40
|
type HstsOptions,
|
|
25
41
|
type SecurityHeadersOptions,
|
|
26
42
|
} from "./security-headers.ts";
|
|
43
|
+
export {
|
|
44
|
+
createTwoFactorStore,
|
|
45
|
+
twoFactor,
|
|
46
|
+
verifyTotp,
|
|
47
|
+
type TwoFactorOptions,
|
|
48
|
+
type TwoFactorRow,
|
|
49
|
+
type TwoFactorStore,
|
|
50
|
+
} from "./two-factor.ts";
|
|
51
|
+
export {
|
|
52
|
+
createUsernameStore,
|
|
53
|
+
username,
|
|
54
|
+
type UsernamePluginOptions,
|
|
55
|
+
type UsernameRow,
|
|
56
|
+
type UsernameStore,
|
|
57
|
+
} from "./username.ts";
|
|
@@ -26,7 +26,7 @@ function get(ip?: string): Request {
|
|
|
26
26
|
describe("ipAllowlist plugin", () => {
|
|
27
27
|
test("allow list: listed IPs pass, others get 403 Forbidden", async () => {
|
|
28
28
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
29
|
-
const app = oke({ name: "ips" }).plug(ipAllowlist({ allow: ["203.0.113.7"] }));
|
|
29
|
+
const app = oke({ autoBoot: false, name: "ips" }).plug(ipAllowlist({ allow: ["203.0.113.7"] }));
|
|
30
30
|
|
|
31
31
|
const ok = await app.fetch(get("203.0.113.7"));
|
|
32
32
|
expect(ok.status).toBe(200);
|
|
@@ -40,7 +40,9 @@ describe("ipAllowlist plugin", () => {
|
|
|
40
40
|
|
|
41
41
|
test("deny list: blocked IPs get 403 ip_denied, others pass", async () => {
|
|
42
42
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
43
|
-
const app = oke({ name: "ips-deny" }).plug(
|
|
43
|
+
const app = oke({ autoBoot: false, name: "ips-deny" }).plug(
|
|
44
|
+
ipAllowlist({ deny: ["198.51.100.9"] }),
|
|
45
|
+
);
|
|
44
46
|
|
|
45
47
|
const blocked = await app.fetch(get("198.51.100.9"));
|
|
46
48
|
expect(blocked.status).toBe(403);
|
|
@@ -53,7 +55,7 @@ describe("ipAllowlist plugin", () => {
|
|
|
53
55
|
|
|
54
56
|
test("deny wins over allow on overlap", async () => {
|
|
55
57
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
56
|
-
const app = oke({ name: "ips-both" }).plug(
|
|
58
|
+
const app = oke({ autoBoot: false, name: "ips-both" }).plug(
|
|
57
59
|
ipAllowlist({ allow: ["203.0.113.7"], deny: ["203.0.113.7"] }),
|
|
58
60
|
);
|
|
59
61
|
|
|
@@ -63,7 +65,9 @@ describe("ipAllowlist plugin", () => {
|
|
|
63
65
|
|
|
64
66
|
test("XFF last hop is the client; a spoofed first hop cannot bypass allow", async () => {
|
|
65
67
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
66
|
-
const app = oke({ name: "ips-xff" }).plug(
|
|
68
|
+
const app = oke({ autoBoot: false, name: "ips-xff" }).plug(
|
|
69
|
+
ipAllowlist({ allow: ["203.0.113.7"] }),
|
|
70
|
+
);
|
|
67
71
|
|
|
68
72
|
// Attacker-controlled first hop + trusted proxy appended the real client (last).
|
|
69
73
|
const spoofedFirst = await app.fetch(get("203.0.113.7, 198.51.100.9"));
|
|
@@ -75,7 +79,9 @@ describe("ipAllowlist plugin", () => {
|
|
|
75
79
|
|
|
76
80
|
test("spoofed first hop cannot bypass a deny rule", async () => {
|
|
77
81
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
78
|
-
const app = oke({ name: "ips-xff-deny" }).plug(
|
|
82
|
+
const app = oke({ autoBoot: false, name: "ips-xff-deny" }).plug(
|
|
83
|
+
ipAllowlist({ deny: ["198.51.100.9"] }),
|
|
84
|
+
);
|
|
79
85
|
|
|
80
86
|
// Spoof a non-denied IP first; proxy appended the real (denied) client last.
|
|
81
87
|
const spoofed = await app.fetch(get("203.0.113.7, 198.51.100.9"));
|
|
@@ -88,7 +94,7 @@ describe("ipAllowlist plugin", () => {
|
|
|
88
94
|
test("trustedProxyDepth selects the hop behind N trusted proxies", async () => {
|
|
89
95
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
90
96
|
// Chain: spoofed, real-client, cdn-egress — depth 2 skips the nearest proxy hop.
|
|
91
|
-
const app = oke({ name: "ips-depth" }).plug(
|
|
97
|
+
const app = oke({ autoBoot: false, name: "ips-depth" }).plug(
|
|
92
98
|
ipAllowlist({ allow: ["203.0.113.7"], trustedProxyDepth: 2 }),
|
|
93
99
|
);
|
|
94
100
|
|
|
@@ -108,7 +114,9 @@ describe("ipAllowlist plugin", () => {
|
|
|
108
114
|
|
|
109
115
|
test("missing header: denied when allow is set, permitted for deny-only", async () => {
|
|
110
116
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
111
|
-
const strict = oke({ name: "ips-missing-allow" }).plug(
|
|
117
|
+
const strict = oke({ autoBoot: false, name: "ips-missing-allow" }).plug(
|
|
118
|
+
ipAllowlist({ allow: ["203.0.113.7"] }),
|
|
119
|
+
);
|
|
112
120
|
|
|
113
121
|
const denied = await strict.fetch(get());
|
|
114
122
|
expect(denied.status).toBe(403);
|
|
@@ -117,14 +125,16 @@ describe("ipAllowlist plugin", () => {
|
|
|
117
125
|
resetFlowSeq();
|
|
118
126
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
119
127
|
|
|
120
|
-
const lax = oke({ name: "ips-missing-deny" }).plug(
|
|
128
|
+
const lax = oke({ autoBoot: false, name: "ips-missing-deny" }).plug(
|
|
129
|
+
ipAllowlist({ deny: ["198.51.100.9"] }),
|
|
130
|
+
);
|
|
121
131
|
const ok = await lax.fetch(get());
|
|
122
132
|
expect(ok.status).toBe(200);
|
|
123
133
|
});
|
|
124
134
|
|
|
125
135
|
test("custom header name is honored", async () => {
|
|
126
136
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
127
|
-
const app = oke({ name: "ips-custom" }).plug(
|
|
137
|
+
const app = oke({ autoBoot: false, name: "ips-custom" }).plug(
|
|
128
138
|
ipAllowlist({ allow: ["203.0.113.7"], header: "x-real-ip" }),
|
|
129
139
|
);
|
|
130
140
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Magic-link Gate auth method plugin.
|
|
3
|
+
*
|
|
4
|
+
* Delivery via Channel is optional in v1 — when {@link MagicLinkOptions.exposeDevToken}
|
|
5
|
+
* is set, the request response includes `devToken` for tests / local DX.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createIdentityStore,
|
|
10
|
+
normalizeEmail,
|
|
11
|
+
type IdentityStore,
|
|
12
|
+
type UserIdentityRow,
|
|
13
|
+
} from "../auth/identity.ts";
|
|
14
|
+
import { issueSessionWithScopes } from "../auth/sessions.ts";
|
|
15
|
+
import {
|
|
16
|
+
createVerificationStore,
|
|
17
|
+
hashChallenge,
|
|
18
|
+
putVerification,
|
|
19
|
+
type VerificationStore,
|
|
20
|
+
} from "../auth/verification.ts";
|
|
21
|
+
import { plugin, type PluginDef } from "../kernel/plugin.ts";
|
|
22
|
+
import {
|
|
23
|
+
AuthFailed,
|
|
24
|
+
AuthRateLimited,
|
|
25
|
+
SessionTokensOut,
|
|
26
|
+
bindPublicAuth,
|
|
27
|
+
createMethodRuntime,
|
|
28
|
+
fail,
|
|
29
|
+
flow,
|
|
30
|
+
z,
|
|
31
|
+
type AuthMethodOptions,
|
|
32
|
+
} from "./auth/shared.ts";
|
|
33
|
+
|
|
34
|
+
const DEFAULT_TTL_MS = 10 * 60 * 1000;
|
|
35
|
+
|
|
36
|
+
/** Options for {@link magicLink}. */
|
|
37
|
+
export interface MagicLinkOptions extends AuthMethodOptions {
|
|
38
|
+
/** Challenge TTL (default 10m). */
|
|
39
|
+
readonly ttlMs?: number;
|
|
40
|
+
/** Identity store for email → user. */
|
|
41
|
+
readonly identities?: IdentityStore;
|
|
42
|
+
/** Verification / challenge store. */
|
|
43
|
+
readonly verifications?: VerificationStore;
|
|
44
|
+
/** Return raw token in the request response (test / local). */
|
|
45
|
+
readonly exposeDevToken?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Magic-link request + verify (`oke_verifications` challenge rows).
|
|
50
|
+
*
|
|
51
|
+
* @param opts - TTL / stores / dev token
|
|
52
|
+
*/
|
|
53
|
+
export function magicLink(opts: MagicLinkOptions = {}): PluginDef {
|
|
54
|
+
const runtime = createMethodRuntime(opts);
|
|
55
|
+
const identities = opts.identities ?? createIdentityStore();
|
|
56
|
+
const verifications = opts.verifications ?? createVerificationStore();
|
|
57
|
+
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
58
|
+
|
|
59
|
+
const request = flow({
|
|
60
|
+
name: "auth.requestMagicLink",
|
|
61
|
+
unit: "auth",
|
|
62
|
+
plane: "user",
|
|
63
|
+
in: z.object({ email: z.string().min(3) }),
|
|
64
|
+
out: z.object({
|
|
65
|
+
ok: z.literal(true),
|
|
66
|
+
devToken: z.string().optional(),
|
|
67
|
+
}),
|
|
68
|
+
errors: { AuthFailed, AuthRateLimited },
|
|
69
|
+
do: async (input) => {
|
|
70
|
+
const email = normalizeEmail(input.email);
|
|
71
|
+
if (!email.includes("@")) return fail("AuthFailed", { reason: "invalid_email" });
|
|
72
|
+
const token = `ml_${crypto.randomUUID().replace(/-/g, "")}`;
|
|
73
|
+
const now = runtime.now();
|
|
74
|
+
putVerification(verifications, {
|
|
75
|
+
id: crypto.randomUUID(),
|
|
76
|
+
identifier: `magic:${email}`,
|
|
77
|
+
value: await hashChallenge(token),
|
|
78
|
+
expiresAt: now + ttlMs,
|
|
79
|
+
createdAt: now,
|
|
80
|
+
consumedAt: null,
|
|
81
|
+
attempts: 0,
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
ok: true as const,
|
|
85
|
+
...(opts.exposeDevToken ? { devToken: token } : {}),
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const verify = flow({
|
|
91
|
+
name: "auth.verifyMagicLink",
|
|
92
|
+
unit: "auth",
|
|
93
|
+
plane: "user",
|
|
94
|
+
in: z.object({ token: z.string().min(1) }),
|
|
95
|
+
out: SessionTokensOut,
|
|
96
|
+
errors: { AuthFailed, AuthRateLimited },
|
|
97
|
+
do: async (input) => {
|
|
98
|
+
const hash = await hashChallenge(input.token);
|
|
99
|
+
const now = runtime.now();
|
|
100
|
+
let row:
|
|
101
|
+
| {
|
|
102
|
+
id: string;
|
|
103
|
+
identifier: string;
|
|
104
|
+
value: string;
|
|
105
|
+
expiresAt: number;
|
|
106
|
+
consumedAt: number | null;
|
|
107
|
+
attempts: number;
|
|
108
|
+
}
|
|
109
|
+
| undefined;
|
|
110
|
+
for (const candidate of verifications.rows.values()) {
|
|
111
|
+
if (!candidate.identifier.startsWith("magic:")) continue;
|
|
112
|
+
if (candidate.consumedAt !== null) continue;
|
|
113
|
+
if (candidate.expiresAt <= now) continue;
|
|
114
|
+
if (candidate.value === hash) {
|
|
115
|
+
row = candidate;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!row) return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
120
|
+
row.consumedAt = now;
|
|
121
|
+
const email = row.identifier.slice("magic:".length);
|
|
122
|
+
const user = ensureUserByEmail(identities, email, now);
|
|
123
|
+
const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
|
|
124
|
+
id: user.id,
|
|
125
|
+
plane: "user",
|
|
126
|
+
scopes: [],
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
accessToken: issued.accessToken,
|
|
130
|
+
refreshToken: issued.refreshToken,
|
|
131
|
+
accessExpiresAt: issued.accessExpiresAt,
|
|
132
|
+
userId: user.id,
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return plugin("magicLink", { version: "0.0.1", config: { method: "magic-link" } })
|
|
138
|
+
.needs("auth")
|
|
139
|
+
.binding(bindPublicAuth("/magic-link/request", request, "otp"))
|
|
140
|
+
.binding(bindPublicAuth("/magic-link/verify", verify, "otp"));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function ensureUserByEmail(store: IdentityStore, email: string, now: number): UserIdentityRow {
|
|
144
|
+
const existingId = store.byEmail.get(email);
|
|
145
|
+
if (existingId) {
|
|
146
|
+
const existing = store.users.get(existingId);
|
|
147
|
+
if (existing) return existing;
|
|
148
|
+
}
|
|
149
|
+
const id = crypto.randomUUID();
|
|
150
|
+
const user: UserIdentityRow = {
|
|
151
|
+
id,
|
|
152
|
+
email,
|
|
153
|
+
name: email.split("@")[0] || "user",
|
|
154
|
+
emailVerified: true,
|
|
155
|
+
status: "active",
|
|
156
|
+
createdAt: now,
|
|
157
|
+
updatedAt: now,
|
|
158
|
+
extra: {},
|
|
159
|
+
};
|
|
160
|
+
store.users.set(id, user);
|
|
161
|
+
store.byEmail.set(email, id);
|
|
162
|
+
return user;
|
|
163
|
+
}
|
|
@@ -18,7 +18,7 @@ beforeEach(() => {
|
|
|
18
18
|
describe("maintenanceMode plugin", () => {
|
|
19
19
|
test("returns 503 with ServiceUnavailable envelope and Retry-After", async () => {
|
|
20
20
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
21
|
-
const app = oke({ name: "maint" }).plug(maintenanceMode({ retryAfter: 120 }));
|
|
21
|
+
const app = oke({ autoBoot: false, name: "maint" }).plug(maintenanceMode({ retryAfter: 120 }));
|
|
22
22
|
|
|
23
23
|
const res = await app.fetch(new Request("http://localhost/x"));
|
|
24
24
|
|
|
@@ -36,7 +36,9 @@ describe("maintenanceMode plugin", () => {
|
|
|
36
36
|
|
|
37
37
|
test("enabled: false passes traffic through", async () => {
|
|
38
38
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
39
|
-
const app = oke({ name: "maint-off" }).plug(
|
|
39
|
+
const app = oke({ autoBoot: false, name: "maint-off" }).plug(
|
|
40
|
+
maintenanceMode({ enabled: false }),
|
|
41
|
+
);
|
|
40
42
|
|
|
41
43
|
const res = await app.fetch(new Request("http://localhost/x"));
|
|
42
44
|
|
|
@@ -46,7 +48,9 @@ describe("maintenanceMode plugin", () => {
|
|
|
46
48
|
test("allowPaths prefixes keep serving", async () => {
|
|
47
49
|
on(http.get("/health"), flow({ name: "health.get", do: () => ({ up: true }) }));
|
|
48
50
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
49
|
-
const app = oke({ name: "maint-allow" }).plug(
|
|
51
|
+
const app = oke({ autoBoot: false, name: "maint-allow" }).plug(
|
|
52
|
+
maintenanceMode({ allowPaths: ["/health"] }),
|
|
53
|
+
);
|
|
50
54
|
|
|
51
55
|
const health = await app.fetch(new Request("http://localhost/health"));
|
|
52
56
|
expect(health.status).toBe(200);
|
|
@@ -57,7 +61,7 @@ describe("maintenanceMode plugin", () => {
|
|
|
57
61
|
|
|
58
62
|
test("bypass header lets operators through (any non-empty value)", async () => {
|
|
59
63
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
60
|
-
const app = oke({ name: "maint-bypass" }).plug(
|
|
64
|
+
const app = oke({ autoBoot: false, name: "maint-bypass" }).plug(
|
|
61
65
|
maintenanceMode({ bypassHeader: "x-ops-token" }),
|
|
62
66
|
);
|
|
63
67
|
|
|
@@ -72,7 +76,7 @@ describe("maintenanceMode plugin", () => {
|
|
|
72
76
|
|
|
73
77
|
test("the 503 still flows through onResponse (other plugins stamp it)", async () => {
|
|
74
78
|
on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
|
|
75
|
-
const app = oke({ name: "maint-stamp" }).plug(maintenanceMode());
|
|
79
|
+
const app = oke({ autoBoot: false, name: "maint-stamp" }).plug(maintenanceMode());
|
|
76
80
|
app.hook("onResponse", (ctx) => {
|
|
77
81
|
if (!ctx.response) return;
|
|
78
82
|
const headers = new Headers(ctx.response.headers);
|