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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional secondary storage for hot auth data (Phase 1a) — `store.kv`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ResolvedGateAuth } from "./config.ts";
|
|
6
|
+
|
|
7
|
+
/** Minimal kv surface used by auth secondary storage. */
|
|
8
|
+
export interface AuthKv {
|
|
9
|
+
get(key: string): Promise<unknown>;
|
|
10
|
+
set(key: string, value: unknown, opts?: { readonly ttlMs?: number }): Promise<void>;
|
|
11
|
+
delete(key: string): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a namespaced kv helper for session / challenge hot paths.
|
|
16
|
+
*
|
|
17
|
+
* @param kv - Kv handle
|
|
18
|
+
* @param config - Resolved auth
|
|
19
|
+
*/
|
|
20
|
+
export function createAuthSecondaryStorage(
|
|
21
|
+
kv: AuthKv,
|
|
22
|
+
config: ResolvedGateAuth,
|
|
23
|
+
): AuthKv | undefined {
|
|
24
|
+
if (!config.secondaryStorage.enabled) return undefined;
|
|
25
|
+
const prefix = config.secondaryStorage.prefix;
|
|
26
|
+
return {
|
|
27
|
+
async get(key) {
|
|
28
|
+
return kv.get(`${prefix}${key}`);
|
|
29
|
+
},
|
|
30
|
+
async set(key, value, opts) {
|
|
31
|
+
await kv.set(`${prefix}${key}`, value, opts);
|
|
32
|
+
},
|
|
33
|
+
async delete(key) {
|
|
34
|
+
await kv.delete(`${prefix}${key}`);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
package/src/auth/sessions.ts
CHANGED
|
@@ -52,6 +52,21 @@ export interface SessionCrypto {
|
|
|
52
52
|
readonly now?: () => number;
|
|
53
53
|
readonly accessTtlMs?: number;
|
|
54
54
|
readonly refreshTtlMs?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Idle timeout — reject refresh when `now - lastActiveAt > idleTtlMs`.
|
|
57
|
+
* Opt-in (undefined = disabled).
|
|
58
|
+
*/
|
|
59
|
+
readonly idleTtlMs?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Absolute lifetime from `session.createdAt` (hard stop even if refresh would extend).
|
|
62
|
+
* Opt-in; when set should be ≤ refreshTtlMs.
|
|
63
|
+
*/
|
|
64
|
+
readonly absoluteTtlMs?: number;
|
|
65
|
+
/**
|
|
66
|
+
* When true, issuing a session revokes other active families for the same principal.
|
|
67
|
+
* Opt-in (default false).
|
|
68
|
+
*/
|
|
69
|
+
readonly singleSessionPerUser?: boolean;
|
|
55
70
|
/**
|
|
56
71
|
* Audience stamped on issued access tokens.
|
|
57
72
|
* MCP tokens must use `"oke-mcp"` (console §10.3 — never accept another aud).
|
|
@@ -97,9 +112,15 @@ export async function issueSession(
|
|
|
97
112
|
const t = now();
|
|
98
113
|
const accessTtl = crypto.accessTtlMs ?? ACCESS_TTL_MS;
|
|
99
114
|
const refreshTtl = crypto.refreshTtlMs ?? REFRESH_TTL_MS;
|
|
115
|
+
const absoluteCap =
|
|
116
|
+
crypto.absoluteTtlMs !== undefined ? Math.min(refreshTtl, crypto.absoluteTtlMs) : refreshTtl;
|
|
100
117
|
const sessionId = cryptoRandomId();
|
|
101
118
|
const familyId = cryptoRandomId();
|
|
102
119
|
|
|
120
|
+
if (crypto.singleSessionPerUser) {
|
|
121
|
+
revokePrincipalSessions(store, principal.plane, principal.id, t);
|
|
122
|
+
}
|
|
123
|
+
|
|
103
124
|
const session: SessionRow = {
|
|
104
125
|
id: sessionId,
|
|
105
126
|
plane: principal.plane,
|
|
@@ -107,7 +128,8 @@ export async function issueSession(
|
|
|
107
128
|
familyId,
|
|
108
129
|
revokedAt: null,
|
|
109
130
|
createdAt: t,
|
|
110
|
-
expiresAt: t +
|
|
131
|
+
expiresAt: t + absoluteCap,
|
|
132
|
+
lastActiveAt: t,
|
|
111
133
|
};
|
|
112
134
|
store.sessions.set(sessionId, session);
|
|
113
135
|
if (crypto.audience !== undefined) {
|
|
@@ -174,9 +196,17 @@ export async function rotateRefresh(
|
|
|
174
196
|
if (existing.expiresAt <= t || session.expiresAt <= t) {
|
|
175
197
|
throw new SessionError("refresh token expired");
|
|
176
198
|
}
|
|
199
|
+
if (crypto.absoluteTtlMs !== undefined && session.createdAt + crypto.absoluteTtlMs <= t) {
|
|
200
|
+
throw new SessionError("session absolute lifetime exceeded");
|
|
201
|
+
}
|
|
202
|
+
const lastActive = session.lastActiveAt ?? session.createdAt;
|
|
203
|
+
if (crypto.idleTtlMs !== undefined && lastActive + crypto.idleTtlMs <= t) {
|
|
204
|
+
throw new SessionError("session idle timeout exceeded");
|
|
205
|
+
}
|
|
177
206
|
|
|
178
207
|
existing.usedAt = t;
|
|
179
208
|
existing.revokedAt = t;
|
|
209
|
+
session.lastActiveAt = t;
|
|
180
210
|
|
|
181
211
|
const accessTtl = crypto.accessTtlMs ?? ACCESS_TTL_MS;
|
|
182
212
|
const refreshTtl = crypto.refreshTtlMs ?? REFRESH_TTL_MS;
|
|
@@ -305,6 +335,8 @@ export async function verifyAccess(
|
|
|
305
335
|
if (session.principalId !== claims.sub) {
|
|
306
336
|
throw new SessionError("session does not belong to requester");
|
|
307
337
|
}
|
|
338
|
+
// Touch idle clock on successful access verify.
|
|
339
|
+
session.lastActiveAt = now();
|
|
308
340
|
return claims;
|
|
309
341
|
}
|
|
310
342
|
|
|
@@ -324,6 +356,31 @@ export function revokeFamily(store: SessionStore, familyId: string, at: number =
|
|
|
324
356
|
}
|
|
325
357
|
}
|
|
326
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Revoke all active sessions for a principal (single-session-per-user).
|
|
361
|
+
*
|
|
362
|
+
* @param store - Session store
|
|
363
|
+
* @param plane - Auth plane
|
|
364
|
+
* @param principalId - Subject id
|
|
365
|
+
* @param at - Timestamp
|
|
366
|
+
*/
|
|
367
|
+
export function revokePrincipalSessions(
|
|
368
|
+
store: SessionStore,
|
|
369
|
+
plane: AuthPlane,
|
|
370
|
+
principalId: string,
|
|
371
|
+
at: number = Date.now(),
|
|
372
|
+
): void {
|
|
373
|
+
for (const session of store.sessions.values()) {
|
|
374
|
+
if (
|
|
375
|
+
session.plane === plane &&
|
|
376
|
+
session.principalId === principalId &&
|
|
377
|
+
session.revokedAt === null
|
|
378
|
+
) {
|
|
379
|
+
revokeFamily(store, session.familyId, at);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
327
384
|
/** Session / token error. */
|
|
328
385
|
export class SessionError extends Error {
|
|
329
386
|
/** @param message - Diagnostic */
|
package/src/auth/tables.ts
CHANGED
|
@@ -23,6 +23,8 @@ export const AUTH_TABLES = {
|
|
|
23
23
|
// Session tables (auth element owns these)
|
|
24
24
|
sessions: "oke_sessions",
|
|
25
25
|
refreshTokens: "oke_refresh_tokens",
|
|
26
|
+
// Challenges / OTP / magic-link (Gate auth + method plugins)
|
|
27
|
+
verifications: "oke_verifications",
|
|
26
28
|
} as const;
|
|
27
29
|
|
|
28
30
|
/** Plane column values stored on shared tables. */
|
|
@@ -117,6 +119,8 @@ export interface SessionRow {
|
|
|
117
119
|
revokedAt: number | null;
|
|
118
120
|
createdAt: number;
|
|
119
121
|
expiresAt: number;
|
|
122
|
+
/** Last activity epoch-ms (idle timeout). Updated on refresh / access touch. */
|
|
123
|
+
lastActiveAt: number;
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
/** Refresh token (hashed at rest); rotation with reuse detection. */
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared verification / challenge store for magic-link, OTP, phone, etc.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** One pending challenge. */
|
|
6
|
+
export interface VerificationRow {
|
|
7
|
+
id: string;
|
|
8
|
+
identifier: string;
|
|
9
|
+
value: string;
|
|
10
|
+
expiresAt: number;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
consumedAt: number | null;
|
|
13
|
+
attempts: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** In-memory verification store. */
|
|
17
|
+
export interface VerificationStore {
|
|
18
|
+
rows: Map<string, VerificationRow>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create an empty verification store.
|
|
23
|
+
*/
|
|
24
|
+
export function createVerificationStore(): VerificationStore {
|
|
25
|
+
return { rows: new Map() };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Store a challenge (hashed value recommended by callers).
|
|
30
|
+
*
|
|
31
|
+
* @param store - Store
|
|
32
|
+
* @param row - Challenge row
|
|
33
|
+
*/
|
|
34
|
+
export function putVerification(store: VerificationStore, row: VerificationRow): void {
|
|
35
|
+
store.rows.set(row.id, row);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Find a non-consumed, non-expired challenge by identifier.
|
|
40
|
+
*
|
|
41
|
+
* @param store - Store
|
|
42
|
+
* @param identifier - Email / phone / username key
|
|
43
|
+
* @param now - Clock
|
|
44
|
+
*/
|
|
45
|
+
export function findActiveVerification(
|
|
46
|
+
store: VerificationStore,
|
|
47
|
+
identifier: string,
|
|
48
|
+
now: number,
|
|
49
|
+
): VerificationRow | undefined {
|
|
50
|
+
for (const row of store.rows.values()) {
|
|
51
|
+
if (row.identifier !== identifier) continue;
|
|
52
|
+
if (row.consumedAt !== null) continue;
|
|
53
|
+
if (row.expiresAt <= now) continue;
|
|
54
|
+
return row;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* SHA-256 hex hash for challenge secrets / OTPs.
|
|
61
|
+
*
|
|
62
|
+
* @param raw - Raw secret
|
|
63
|
+
*/
|
|
64
|
+
export async function hashChallenge(raw: string): Promise<string> {
|
|
65
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(raw));
|
|
66
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generate a numeric OTP of `digits` length.
|
|
71
|
+
*
|
|
72
|
+
* @param digits - Length (default 6)
|
|
73
|
+
*/
|
|
74
|
+
export function generateOtp(digits = 6): string {
|
|
75
|
+
const max = 10 ** digits;
|
|
76
|
+
const n = crypto.getRandomValues(new Uint32Array(1))[0]! % max;
|
|
77
|
+
return n.toString().padStart(digits, "0");
|
|
78
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate: named competitor comparisons stay gone from the tracked tree.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors {@link ./doc-staleness.test.ts} — `git grep` must find zero hits
|
|
5
|
+
* (the security-headers decoy header fixture may still use a common
|
|
6
|
+
* `X-Powered-By` value; that path is allow-listed).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
const ROOT = join(import.meta.dir, "../..");
|
|
13
|
+
|
|
14
|
+
/** Forbidden peer name — split so this file does not match itself. */
|
|
15
|
+
const FORBIDDEN_HONO = ["Ho", "no"].join("");
|
|
16
|
+
/** Forbidden peer name. */
|
|
17
|
+
const FORBIDDEN_ELYSIA = ["Ely", "sia"].join("");
|
|
18
|
+
/** Forbidden peer name (covers Enc + ore.ts). */
|
|
19
|
+
const FORBIDDEN_ENCORE = ["Enc", "ore"].join("");
|
|
20
|
+
/** Forbidden peer name. */
|
|
21
|
+
const FORBIDDEN_NESTJS = ["Nest", "JS"].join("");
|
|
22
|
+
/** Forbidden peer name. */
|
|
23
|
+
const FORBIDDEN_FASTIFY = ["Fast", "ify"].join("");
|
|
24
|
+
/** Forbidden peer site — avoid bare `iii` (matches lockfile hashes). */
|
|
25
|
+
const FORBIDDEN_III_DEV = ["iii", ".", "dev"].join("");
|
|
26
|
+
/** Deleted Comparison page path residue. */
|
|
27
|
+
const FORBIDDEN_COMPARISON_PATH = ["get-started/", "comparison"].join("");
|
|
28
|
+
/** Word-boundary classic Node framework — `-F` false-positives on `CallExpression`. */
|
|
29
|
+
const FORBIDDEN_EXPRESS = ["\\b", "Ex", "press", "\\b"].join("");
|
|
30
|
+
|
|
31
|
+
/** Decoy `X-Powered-By` fixture value in the security-headers plugin tests. */
|
|
32
|
+
const EXPRESS_ALLOW = ["src/plugins/security-headers.test.ts:"];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run `git grep -F` and assert zero matches after ignoring allow-listed paths.
|
|
36
|
+
*
|
|
37
|
+
* @param pattern - Fixed-string pattern
|
|
38
|
+
* @param ignorePrefixes - Path prefixes to ignore (`path:line:` hits)
|
|
39
|
+
*/
|
|
40
|
+
function assertZeroGitGrepFixed(pattern: string, ignorePrefixes: readonly string[] = []): void {
|
|
41
|
+
const proc = Bun.spawnSync(["git", "grep", "-F", "-n", "-e", pattern, "--"], {
|
|
42
|
+
cwd: ROOT,
|
|
43
|
+
stdout: "pipe",
|
|
44
|
+
stderr: "pipe",
|
|
45
|
+
});
|
|
46
|
+
// git grep exits 1 when there are no matches.
|
|
47
|
+
if (proc.exitCode === 1) {
|
|
48
|
+
expect(proc.stdout.toString()).toBe("");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const hits = proc.stdout
|
|
52
|
+
.toString()
|
|
53
|
+
.trim()
|
|
54
|
+
.split("\n")
|
|
55
|
+
.filter((line) => line.length > 0)
|
|
56
|
+
.filter((line) => !ignorePrefixes.some((prefix) => line.startsWith(prefix)));
|
|
57
|
+
expect(hits.join("\n")).toBe("");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Run `git grep` with a basic regex and assert zero matches after allow-list.
|
|
62
|
+
*
|
|
63
|
+
* @param pattern - Basic regex (no `-F`)
|
|
64
|
+
* @param ignorePrefixes - Path prefixes to ignore (`path:line:` hits)
|
|
65
|
+
*/
|
|
66
|
+
function assertZeroGitGrepRegex(pattern: string, ignorePrefixes: readonly string[] = []): void {
|
|
67
|
+
const proc = Bun.spawnSync(["git", "grep", "-n", "-e", pattern, "--"], {
|
|
68
|
+
cwd: ROOT,
|
|
69
|
+
stdout: "pipe",
|
|
70
|
+
stderr: "pipe",
|
|
71
|
+
});
|
|
72
|
+
if (proc.exitCode === 1) {
|
|
73
|
+
expect(proc.stdout.toString()).toBe("");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const hits = proc.stdout
|
|
77
|
+
.toString()
|
|
78
|
+
.trim()
|
|
79
|
+
.split("\n")
|
|
80
|
+
.filter((line) => line.length > 0)
|
|
81
|
+
.filter((line) => !ignorePrefixes.some((prefix) => line.startsWith(prefix)));
|
|
82
|
+
expect(hits.join("\n")).toBe("");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe("named competitor mention removal gate", () => {
|
|
86
|
+
test("tracked tree has zero mentions of the thin multi-runtime router peer", () => {
|
|
87
|
+
assertZeroGitGrepFixed(FORBIDDEN_HONO);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("tracked tree has zero mentions of the Bun-first peer framework", () => {
|
|
91
|
+
assertZeroGitGrepFixed(FORBIDDEN_ELYSIA);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("tracked tree has zero mentions of the Rust-core peer platform", () => {
|
|
95
|
+
assertZeroGitGrepFixed(FORBIDDEN_ENCORE);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("tracked tree has zero mentions of the decorator DI peer", () => {
|
|
99
|
+
assertZeroGitGrepFixed(FORBIDDEN_NESTJS);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("tracked tree has zero mentions of the Node HTTP peer", () => {
|
|
103
|
+
assertZeroGitGrepFixed(FORBIDDEN_FASTIFY);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("tracked tree has zero mentions of the polyglot peer site host", () => {
|
|
107
|
+
assertZeroGitGrepFixed(FORBIDDEN_III_DEV);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("tracked tree has zero deleted Comparison path residue", () => {
|
|
111
|
+
assertZeroGitGrepFixed(FORBIDDEN_COMPARISON_PATH);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("tracked tree has zero classic Node framework peer mentions outside decoy fixture", () => {
|
|
115
|
+
assertZeroGitGrepRegex(FORBIDDEN_EXPRESS, EXPRESS_ALLOW);
|
|
116
|
+
});
|
|
117
|
+
});
|
package/src/cli/dev.test.ts
CHANGED
|
@@ -396,14 +396,14 @@ describe("oke dev docs MCP", () => {
|
|
|
396
396
|
async function writePingApp(dir: string, version: string): Promise<void> {
|
|
397
397
|
await Bun.write(
|
|
398
398
|
join(dir, "src/flows/ping.ts"),
|
|
399
|
-
`import { on, flow, http } from ${JSON.stringify(OKE_INDEX)};
|
|
399
|
+
`import { on, flow, http, gate } from ${JSON.stringify(OKE_INDEX)};
|
|
400
400
|
|
|
401
|
-
export const ping = on(http.get("/ping"), flow({
|
|
401
|
+
export const ping = on(http.get("/ping").gate(gate.public), flow({
|
|
402
402
|
name: "ping",
|
|
403
403
|
do: () => ({ version: ${JSON.stringify(version)} as const }),
|
|
404
404
|
}));
|
|
405
405
|
|
|
406
|
-
export const slow = on(http.get("/slow"), flow({
|
|
406
|
+
export const slow = on(http.get("/slow").gate(gate.public), flow({
|
|
407
407
|
name: "slow",
|
|
408
408
|
do: async () => {
|
|
409
409
|
const started = ${JSON.stringify(version)};
|
package/src/cli/dev.ts
CHANGED
|
@@ -339,6 +339,24 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
339
339
|
loadedConfig = null;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
// Local mode + a configured `meilisearch` store.index: bring the server up
|
|
343
|
+
// as a fourth local process (binary on PATH — a documented prerequisite,
|
|
344
|
+
// like Docker for --docker). Docker mode instead relies on the recipe.
|
|
345
|
+
let meili: import("./meilisearch-local.ts").LocalMeilisearchHandle | null = null;
|
|
346
|
+
if (mode === "local") {
|
|
347
|
+
const indexId = resolveDriverId(loadedConfig?.drivers?.store?.index, "local") ?? "memory";
|
|
348
|
+
if (indexId === "meilisearch") {
|
|
349
|
+
try {
|
|
350
|
+
const { startLocalMeilisearch } = await import("./meilisearch-local.ts");
|
|
351
|
+
meili = await startLocalMeilisearch({ cwd });
|
|
352
|
+
write(formatStatusLine(`meilisearch local server on ${meili.url}`));
|
|
353
|
+
} catch (err) {
|
|
354
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
355
|
+
return { code: 1 };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
342
360
|
if (mode === "docker") {
|
|
343
361
|
let images = options.images;
|
|
344
362
|
if (!images) {
|
|
@@ -567,6 +585,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
567
585
|
OKE_KV_DRIVER: stackKvDriver,
|
|
568
586
|
}
|
|
569
587
|
: {}),
|
|
588
|
+
...(meili?.overlay ?? {}),
|
|
570
589
|
};
|
|
571
590
|
process.env.OKE_DEV_REQUEST_LOG = "1";
|
|
572
591
|
|
|
@@ -755,6 +774,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
755
774
|
stopped = true;
|
|
756
775
|
autoPushRunner.cancel();
|
|
757
776
|
watcher.close();
|
|
777
|
+
meili?.stop();
|
|
758
778
|
app.stop();
|
|
759
779
|
consoleServer.stop();
|
|
760
780
|
mcpServer?.stop();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Meilisearch lifecycle — persisted master key material and fail-loud
|
|
3
|
+
* binary resolution. Never spawns a real server here.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
7
|
+
import { mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
ensureMasterKey,
|
|
12
|
+
MEILISEARCH_STATE_DIR_REL,
|
|
13
|
+
MeilisearchLocalError,
|
|
14
|
+
startLocalMeilisearch,
|
|
15
|
+
} from "./meilisearch-local.ts";
|
|
16
|
+
|
|
17
|
+
const dirs: string[] = [];
|
|
18
|
+
|
|
19
|
+
function tempStateDir(): string {
|
|
20
|
+
const dir = mkdtempSync(join(tmpdir(), "oke-meili-"));
|
|
21
|
+
dirs.push(dir);
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
while (dirs.length > 0) {
|
|
27
|
+
const dir = dirs.pop();
|
|
28
|
+
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("ensureMasterKey", () => {
|
|
33
|
+
test("generates once, persists 0600, reuses on next call", () => {
|
|
34
|
+
const stateDir = tempStateDir();
|
|
35
|
+
const first = ensureMasterKey(stateDir);
|
|
36
|
+
const second = ensureMasterKey(stateDir);
|
|
37
|
+
expect(first).toBe(second);
|
|
38
|
+
expect(first.length).toBeGreaterThanOrEqual(32);
|
|
39
|
+
const keyPath = join(stateDir, "master.key");
|
|
40
|
+
expect(statSync(keyPath).mode & 0o777).toBe(0o600);
|
|
41
|
+
expect(statSync(stateDir).mode & 0o777).toBe(0o700);
|
|
42
|
+
expect(readFileSync(keyPath, "utf8").trim()).toBe(first);
|
|
43
|
+
// No leftover temp files.
|
|
44
|
+
expect(readdirSync(stateDir).filter((f) => f.includes(".tmp-"))).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("a too-short persisted key fails loud", () => {
|
|
48
|
+
const stateDir = tempStateDir();
|
|
49
|
+
const keyPath = join(stateDir, "master.key");
|
|
50
|
+
// Write a weak key directly (bypasses the writer) to simulate corruption.
|
|
51
|
+
writeFileSync(keyPath, "short\n");
|
|
52
|
+
expect(() => ensureMasterKey(stateDir)).toThrow(MeilisearchLocalError);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("startLocalMeilisearch", () => {
|
|
57
|
+
test("missing binary fails loud with an install hint (never silent memory)", async () => {
|
|
58
|
+
await expect(
|
|
59
|
+
startLocalMeilisearch({
|
|
60
|
+
binary: "meilisearch-definitely-not-on-path-xyz",
|
|
61
|
+
stateDir: tempStateDir(),
|
|
62
|
+
}),
|
|
63
|
+
).rejects.toThrow(/not found on PATH|never auto-downloads/i);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("state dir rel lives under .oke", () => {
|
|
67
|
+
expect(MEILISEARCH_STATE_DIR_REL).toBe(join(".oke", "meilisearch"));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Meilisearch lifecycle for `oke dev` (local mode).
|
|
3
|
+
*
|
|
4
|
+
* A Meilisearch server is a fourth moving part only when the app actually
|
|
5
|
+
* opts into the `meilisearch` store.index driver. This module:
|
|
6
|
+
*
|
|
7
|
+
* - generates the master key once and persists it under `.oke/meilisearch/`
|
|
8
|
+
* (`0700` dir / `0600` file, atomic write — same material discipline as
|
|
9
|
+
* OpenBao, minus the init/unseal ceremony: a single static key),
|
|
10
|
+
* - resolves the `meilisearch` binary from `PATH` (a documented prerequisite,
|
|
11
|
+
* like Docker for `--docker`; we never auto-download binaries),
|
|
12
|
+
* - spawns it via `Bun.spawn` with its own data dir + port, polls `/health`,
|
|
13
|
+
* and hands back an env overlay (`OKE_STORE_INDEX_URL` / `…_KEY`) plus a
|
|
14
|
+
* `stop()` that kills the child alongside the app.
|
|
15
|
+
*
|
|
16
|
+
* Fail-loud: missing binary / unhealthy server / never-ready all throw
|
|
17
|
+
* {@link MeilisearchLocalError} — never a silent memory fallback.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { randomBytes } from "node:crypto";
|
|
21
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { dirname, join, resolve } from "node:path";
|
|
23
|
+
|
|
24
|
+
/** Material directory relative to the project root. */
|
|
25
|
+
export const MEILISEARCH_STATE_DIR_REL = join(".oke", "meilisearch");
|
|
26
|
+
/** Default loopback port for the local server. */
|
|
27
|
+
export const MEILISEARCH_LOCAL_PORT = 7700;
|
|
28
|
+
|
|
29
|
+
/** Fail-loud local Meilisearch error (missing binary / never healthy). */
|
|
30
|
+
export class MeilisearchLocalError extends Error {
|
|
31
|
+
constructor(message: string) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "MeilisearchLocalError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Options for {@link startLocalMeilisearch}. */
|
|
38
|
+
export interface LocalMeilisearchOptions {
|
|
39
|
+
readonly cwd?: string;
|
|
40
|
+
/** Loopback port (default {@link MEILISEARCH_LOCAL_PORT}). */
|
|
41
|
+
readonly port?: number;
|
|
42
|
+
/** Override state dir (tests). */
|
|
43
|
+
readonly stateDir?: string;
|
|
44
|
+
/** Binary name/path resolved via `Bun.which` (default `meilisearch`). */
|
|
45
|
+
readonly binary?: string;
|
|
46
|
+
/** Max ms to wait for `/health` to report available (default 30_000). */
|
|
47
|
+
readonly readyTimeoutMs?: number;
|
|
48
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A running local Meilisearch child. */
|
|
52
|
+
export interface LocalMeilisearchHandle {
|
|
53
|
+
/** Base URL the app should talk to. */
|
|
54
|
+
readonly url: string;
|
|
55
|
+
/** Master key (also persisted on disk). */
|
|
56
|
+
readonly masterKey: string;
|
|
57
|
+
/** Env overlay for the app child. */
|
|
58
|
+
readonly overlay: Record<string, string>;
|
|
59
|
+
/** Kill the child (idempotent). */
|
|
60
|
+
readonly stop: () => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Env overlay for the app child. */
|
|
64
|
+
export function meilisearchStackEnv(handle: LocalMeilisearchHandle): Record<string, string> {
|
|
65
|
+
return handle.overlay;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function writeMaterial(path: string, value: string): void {
|
|
69
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
70
|
+
chmodSync(dirname(path), 0o700);
|
|
71
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
72
|
+
writeFileSync(tmp, `${value}\n`, { mode: 0o600 });
|
|
73
|
+
renameSync(tmp, path);
|
|
74
|
+
chmodSync(path, 0o600);
|
|
75
|
+
const back = readFileSync(path, "utf8").trim();
|
|
76
|
+
if (back !== value) {
|
|
77
|
+
throw new MeilisearchLocalError(`meilisearch local: verify failed reading ${path}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Read the persisted master key, or generate + persist a fresh one. */
|
|
82
|
+
export function ensureMasterKey(stateDir: string): string {
|
|
83
|
+
const keyPath = join(stateDir, "master.key");
|
|
84
|
+
if (existsSync(keyPath)) {
|
|
85
|
+
const key = readFileSync(keyPath, "utf8").trim();
|
|
86
|
+
if (key.length >= 16) return key;
|
|
87
|
+
throw new MeilisearchLocalError(
|
|
88
|
+
`meilisearch local: ${keyPath} exists but is too short — delete it to regenerate`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const key = randomBytes(32).toString("hex");
|
|
92
|
+
writeMaterial(keyPath, key);
|
|
93
|
+
return key;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Start a local Meilisearch server and wait for `/health`.
|
|
98
|
+
*
|
|
99
|
+
* @param options - cwd / port / binary / injected fetch
|
|
100
|
+
*/
|
|
101
|
+
export async function startLocalMeilisearch(
|
|
102
|
+
options: LocalMeilisearchOptions = {},
|
|
103
|
+
): Promise<LocalMeilisearchHandle> {
|
|
104
|
+
const cwd = options.cwd ?? process.cwd();
|
|
105
|
+
const port = options.port ?? MEILISEARCH_LOCAL_PORT;
|
|
106
|
+
const stateDir = options.stateDir ?? resolve(cwd, MEILISEARCH_STATE_DIR_REL);
|
|
107
|
+
const dataDir = join(stateDir, "data");
|
|
108
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
109
|
+
const readyTimeoutMs = options.readyTimeoutMs ?? 30_000;
|
|
110
|
+
|
|
111
|
+
const binary = options.binary ?? "meilisearch";
|
|
112
|
+
const resolved = Bun.which(binary);
|
|
113
|
+
if (!resolved) {
|
|
114
|
+
throw new MeilisearchLocalError(
|
|
115
|
+
"meilisearch local: `meilisearch` not found on PATH — install it to use the store.index FTS driver " +
|
|
116
|
+
"(brew install meilisearch · curl -L https://install.meilisearch.com | sh), " +
|
|
117
|
+
"or run `oke dev --docker` instead. OKE never auto-downloads binaries.",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const masterKey = ensureMasterKey(stateDir);
|
|
122
|
+
mkdirSync(dataDir, { recursive: true });
|
|
123
|
+
|
|
124
|
+
const url = `http://127.0.0.1:${port}`;
|
|
125
|
+
const proc = Bun.spawn(
|
|
126
|
+
[
|
|
127
|
+
resolved,
|
|
128
|
+
"--http-addr",
|
|
129
|
+
`127.0.0.1:${port}`,
|
|
130
|
+
"--master-key",
|
|
131
|
+
masterKey,
|
|
132
|
+
"--db-path",
|
|
133
|
+
dataDir,
|
|
134
|
+
"--env",
|
|
135
|
+
"development",
|
|
136
|
+
"--no-analytics",
|
|
137
|
+
],
|
|
138
|
+
{
|
|
139
|
+
cwd,
|
|
140
|
+
stdout: "ignore",
|
|
141
|
+
stderr: "inherit",
|
|
142
|
+
env: { ...(process.env as Record<string, string>) },
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
let stopped = false;
|
|
147
|
+
const stop = () => {
|
|
148
|
+
if (stopped) return;
|
|
149
|
+
stopped = true;
|
|
150
|
+
try {
|
|
151
|
+
proc.kill("SIGKILL");
|
|
152
|
+
} catch {
|
|
153
|
+
// Already gone.
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const deadline = Date.now() + readyTimeoutMs;
|
|
158
|
+
for (;;) {
|
|
159
|
+
if (proc.exitCode !== null) {
|
|
160
|
+
throw new MeilisearchLocalError(
|
|
161
|
+
`meilisearch local: server exited (${proc.exitCode}) before becoming healthy`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const res = await fetchFn(`${url}/health`);
|
|
166
|
+
if (res.ok) break;
|
|
167
|
+
} catch {
|
|
168
|
+
// Not up yet.
|
|
169
|
+
}
|
|
170
|
+
if (Date.now() > deadline) {
|
|
171
|
+
stop();
|
|
172
|
+
throw new MeilisearchLocalError(
|
|
173
|
+
`meilisearch local: no /health from ${url} within ${readyTimeoutMs}ms`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
url,
|
|
181
|
+
masterKey,
|
|
182
|
+
overlay: {
|
|
183
|
+
OKE_STORE_INDEX_URL: url,
|
|
184
|
+
OKE_STORE_INDEX_KEY: masterKey,
|
|
185
|
+
},
|
|
186
|
+
stop,
|
|
187
|
+
};
|
|
188
|
+
}
|