diff --git a/.changeset/api-client-rate-limit-retry.md b/.changeset/api-client-rate-limit-retry.md
new file mode 100644
index 00000000..13ae9bab
--- /dev/null
+++ b/.changeset/api-client-rate-limit-retry.md
@@ -0,0 +1,5 @@
+---
+"everything-dev": patch
+---
+
+The API client retries rate-limited (429) responses up to three times, honoring the server's `Retry-After` (capped at 2s) before failing — transient edge saturation self-heals in queries and the session check instead of surfacing errors.
diff --git a/.changeset/better-near-auth-error-formatting.md b/.changeset/better-near-auth-error-formatting.md
new file mode 100644
index 00000000..9415aee3
--- /dev/null
+++ b/.changeset/better-near-auth-error-formatting.md
@@ -0,0 +1,5 @@
+---
+"better-near-auth": patch
+---
+
+Stringify unknown throwables safely in plugin error messages.
diff --git a/.changeset/every-plugin-error-formatting.md b/.changeset/every-plugin-error-formatting.md
new file mode 100644
index 00000000..d6d92eae
--- /dev/null
+++ b/.changeset/every-plugin-error-formatting.md
@@ -0,0 +1,5 @@
+---
+"every-plugin": patch
+---
+
+Harden error rendering: unknown throwables (symbols, objects, unserializable values) stringify safely in runtime error messages and plugin-load logs instead of throwing or printing `[object Object]`.
diff --git a/.changeset/host-rate-limit-health.md b/.changeset/host-rate-limit-health.md
new file mode 100644
index 00000000..105942b2
--- /dev/null
+++ b/.changeset/host-rate-limit-health.md
@@ -0,0 +1,11 @@
+---
+"host": minor
+---
+
+Retune the edge rate limiter for practical use:
+
+- General budget raised from 300 to **2000 requests / 15 min** per client — normal browsing (page navigations, API calls, SSR renders) no longer brushes the cap, especially behind shared NAT/VPN egress.
+- New **mutation tier** (60 / 15 min, `RATE_LIMIT_MUTATION_MAX`) for non-GET requests outside the oRPC (`/api/rpc/`), Better-Auth (`/api/auth/`), and MCP (`/api/mcp`) transports — brute-force protection on direct REST writes without touching sessions, procedures, or agents (those layers own their throttling).
+- `x-forwarded-for` keying now trusts only the **last** hop (the address our proxy appended) — a client can no longer mint unlimited keys by rotating a spoofed first hop.
+- A rate-limited **document navigation** now returns a styled HTML retry page instead of raw JSON, so a saturated browser session sees a "please wait" screen, not a broken site. API requests keep the JSON body and `Retry-After`.
+- `GET/HEAD /health` is exempt from both tiers — infrastructure probes never consume a client's budget.
diff --git a/.changeset/locale-negotiation-ssr.md b/.changeset/locale-negotiation-ssr.md
new file mode 100644
index 00000000..eb7bbfd0
--- /dev/null
+++ b/.changeset/locale-negotiation-ssr.md
@@ -0,0 +1,5 @@
+---
+"everything-dev": minor
+---
+
+Deepen i18n: locale negotiation from `citynode_locale` cookie + `Accept-Language` (with q-values) during SSR, `Vary: Cookie, Accept-Language` on localized responses, request-scoped locale in router context, a shared locale selection across the main UI and auth bundle runtimes, and localized router-error/API-connection/tenant-validation copy.
diff --git a/.changeset/publish-pin-verification-gate.md b/.changeset/publish-pin-verification-gate.md
new file mode 100644
index 00000000..cf7d8521
--- /dev/null
+++ b/.changeset/publish-pin-verification-gate.md
@@ -0,0 +1,5 @@
+---
+"everything-dev": minor
+---
+
+Publish gate: before writing to the registry, every pinned slot's bytes are fetched from its production URL and checked against the pin's SRI. A publish whose pinned bytes are missing or mismatched is blocked instead of silently republishing broken pointers.
diff --git a/.changeset/session-query-recovery.md b/.changeset/session-query-recovery.md
new file mode 100644
index 00000000..81a98a07
--- /dev/null
+++ b/.changeset/session-query-recovery.md
@@ -0,0 +1,5 @@
+---
+"everything-dev": patch
+---
+
+A failing `getSession` request no longer caches `null` as "signed out" — the session query errors instead, so a transient auth-service outage can't sign the UI out client-side.
diff --git a/.changeset/staging-input-schema.md b/.changeset/staging-input-schema.md
new file mode 100644
index 00000000..f06c95d2
--- /dev/null
+++ b/.changeset/staging-input-schema.md
@@ -0,0 +1,5 @@
+---
+"everything-dev": patch
+---
+
+Parse the authored `staging` block through `BosConfigInputSchema` — previously the interface accepted it but the schema dropped it during validation.
diff --git a/host/README.md b/host/README.md
index bdedfd18..bda7e48a 100644
--- a/host/README.md
+++ b/host/README.md
@@ -38,7 +38,7 @@ On top of that fixed server core, the host now supports request-scoped tenant UI
- tenant config must extend the base BOS runtime
- tenant requests may override UI-facing remotes and sidebar metadata without changing the server core
-For full host/plugin/auth/api hot-swap, see `docs/plans/` for design docs. That is still a larger future design than the fixed-core tenant mode implemented now.
+For full host/plugin/auth/api hot-swap, see `plans/` for design docs. That is still a larger future design than the fixed-core tenant mode implemented now.
## Development
diff --git a/host/server.ts b/host/server.ts
index 9e05e982..8ff47422 100644
--- a/host/server.ts
+++ b/host/server.ts
@@ -34,4 +34,4 @@ try {
// server boots so SSR container loads and any config fetches route through it.
installBundleFetchFromEnv({});
-runServerBlocking({ config });
+await runServerBlocking({ config });
diff --git a/host/src/middleware/security.ts b/host/src/middleware/security.ts
index f2f16f2d..fd07be35 100644
--- a/host/src/middleware/security.ts
+++ b/host/src/middleware/security.ts
@@ -1,6 +1,6 @@
import { getConnInfo } from "@hono/node-server/conninfo";
import { Config, Context, Effect, Layer } from "effect";
-import type { MiddlewareHandler } from "hono";
+import type { Context as HonoContext, MiddlewareHandler } from "hono";
import { cors } from "hono/cors";
import { NONCE, secureHeaders } from "hono/secure-headers";
import { rateLimiter } from "hono-rate-limiter";
@@ -9,7 +9,8 @@ import type { RuntimePlugin } from "../types";
import { logger } from "../utils/logger";
export const RATE_LIMIT_WINDOW_MS = Number(process.env.RATE_LIMIT_WINDOW_MS) || 900_000;
-export const RATE_LIMIT_MAX = Number(process.env.RATE_LIMIT_MAX) || 300;
+export const RATE_LIMIT_MAX = Number(process.env.RATE_LIMIT_MAX) || 2000;
+export const RATE_LIMIT_MUTATION_MAX = Number(process.env.RATE_LIMIT_MUTATION_MAX) || 60;
export const BODY_LIMIT_MAX = Number(process.env.BODY_LIMIT_MAX) || 10 * 1024 * 1024;
export const API_TIMEOUT_MS = Number(process.env.API_TIMEOUT_MS) || 30_000;
@@ -31,6 +32,89 @@ export function storageUploadTimeoutMs(): number {
export const STATIC_ASSET_PATTERN =
/\.(js|css|png|jpg|jpeg|gif|svg|ico|json|md|webmanifest|woff2?|ttf|eot|webp|avif|map|txt|xml)$/i;
+/**
+ * Edge-limited transports that own their throttling: oRPC (all procedure
+ * calls are POSTs — a method-based mutation tier would collapse the whole
+ * API into the strict bucket), Better-Auth (its core limiter is enabled in
+ * production), and MCP (agents are legitimately chatty).
+ */
+const SENSITIVE_MUTATION_EXEMPT_PREFIXES = ["/api/rpc/", "/api/auth/", "/api/mcp"];
+
+export function clientKeyOf(c: HonoContext): string {
+ // The trusted proxy appends the real client address as the LAST hop;
+ // earlier entries are client-controlled and must not be trusted as an
+ // identity (rotating a spoofed first hop would mint unlimited keys).
+ const forwarded = c.req.header("x-forwarded-for");
+ if (forwarded) {
+ const hops = forwarded.split(",");
+ return hops[hops.length - 1]!.trim();
+ }
+ try {
+ const info = getConnInfo(c);
+ return info.remote.address ?? "unknown";
+ } catch {
+ return "unknown";
+ }
+}
+
+export function isProbeOrStatic(c: HonoContext): boolean {
+ if (c.req.path === "/health" && (c.req.method === "GET" || c.req.method === "HEAD")) {
+ return true;
+ }
+ const { pathname } = new URL(c.req.url);
+ const lastSegment = pathname.split("/").pop() ?? "";
+ return STATIC_ASSET_PATTERN.test(lastSegment);
+}
+
+export function isSensitiveMutation(c: HonoContext): boolean {
+ if (c.req.method === "GET" || c.req.method === "HEAD" || c.req.method === "OPTIONS") {
+ return false;
+ }
+ if (!c.req.path.startsWith("/api/")) {
+ return false;
+ }
+ return !SENSITIVE_MUTATION_EXEMPT_PREFIXES.some(
+ (prefix) =>
+ c.req.path === prefix || c.req.path.startsWith(`${prefix}/`) || c.req.path.startsWith(prefix),
+ );
+}
+
+export function rateLimitExceededResponse(c: HonoContext): Response {
+ const acceptsHtml = (c.req.header("accept") ?? "").includes("text/html");
+ if (acceptsHtml) {
+ return c.html(rateLimitPage(c.req.path), 429);
+ }
+ return c.json({ error: "Too many requests, please try again later." }, 429);
+}
+
+function escapeHtml(value: string): string {
+ return value
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
+function rateLimitPage(path: string): string {
+ const safePath = escapeHtml(path);
+ return `
+
+
+
+
+Too many requests
+
+
+
+Too many requests
+You’re going faster than we can handle. Please wait a moment, then try again.
+Try again
+
+
+`;
+}
+
export function getCspStrict(isDev: boolean): boolean {
return process.env.CSP_STRICT === "false" ? false : !isDev;
}
@@ -41,6 +125,7 @@ export class SecurityMiddleware extends Context.Service<
cors: MiddlewareHandler;
csrf: MiddlewareHandler;
rateLimit: MiddlewareHandler;
+ rateLimitMutation: MiddlewareHandler;
csp: MiddlewareHandler;
}
>()("host/SecurityMiddleware") {
@@ -109,23 +194,18 @@ export class SecurityMiddleware extends Context.Service<
const rateLimit: MiddlewareHandler = rateLimiter({
windowMs: RATE_LIMIT_WINDOW_MS,
limit: RATE_LIMIT_MAX,
- keyGenerator: (c) => {
- const forwarded = c.req.header("x-forwarded-for");
- if (forwarded) {
- return forwarded.split(",")[0]!.trim();
- }
- try {
- const info = getConnInfo(c);
- return info.remote.address ?? "unknown";
- } catch {
- return "unknown";
- }
- },
- skip: (c) => {
- const { pathname } = new URL(c.req.url);
- const lastSegment = pathname.split("/").pop() ?? "";
- return STATIC_ASSET_PATTERN.test(lastSegment);
- },
+ keyGenerator: (c) => clientKeyOf(c),
+ skip: (c) => isProbeOrStatic(c) || isSensitiveMutation(c),
+ handler: (c) => rateLimitExceededResponse(c),
+ message: { error: "Too many requests, please try again later." },
+ });
+
+ const rateLimitMutation: MiddlewareHandler = rateLimiter({
+ windowMs: RATE_LIMIT_WINDOW_MS,
+ limit: RATE_LIMIT_MUTATION_MAX,
+ keyGenerator: (c) => clientKeyOf(c),
+ skip: (c) => isProbeOrStatic(c) || !isSensitiveMutation(c),
+ handler: (c) => rateLimitExceededResponse(c),
message: { error: "Too many requests, please try again later." },
});
@@ -208,7 +288,7 @@ export class SecurityMiddleware extends Context.Service<
})(c, next);
};
- return { cors: corsHandler, csrf, rateLimit, csp };
+ return { cors: corsHandler, csrf, rateLimit, rateLimitMutation, csp };
}),
);
}
diff --git a/host/src/program.ts b/host/src/program.ts
index dd0fb754..a9639f31 100644
--- a/host/src/program.ts
+++ b/host/src/program.ts
@@ -94,6 +94,7 @@ export const createStartServer = (onReady?: () => void) =>
app.use("/*", security.cors);
app.use("/*", security.csrf);
app.use("/*", security.rateLimit);
+ app.use("/*", security.rateLimitMutation);
app.use("*", security.csp);
if (ssrEnabled) {
@@ -204,7 +205,7 @@ export const createStartServer = (onReady?: () => void) =>
const startHttpServer = () => {
const hostname = process.env.HOST || "0.0.0.0";
- const proxiedFetch = (req: Request): Response | Promise => {
+ const proxiedFetch: typeof app.fetch = (req, env, executionCtx) => {
const url = new URL(req.url);
const forwardedProto = req.headers.get("x-forwarded-proto");
const forwardedHost = req.headers.get("x-forwarded-host");
@@ -220,7 +221,7 @@ export const createStartServer = (onReady?: () => void) =>
req = new Request(url, req);
}
- return app.fetch(req);
+ return app.fetch(req, env, executionCtx);
};
const server = serve({ fetch: proxiedFetch, port, hostname }, () => {
diff --git a/host/src/services/errors.ts b/host/src/services/errors.ts
index 514ef0b6..d96fd36f 100644
--- a/host/src/services/errors.ts
+++ b/host/src/services/errors.ts
@@ -20,7 +20,14 @@ export class FederationError extends Data.TaggedError("FederationError")<{
override get message() {
if (this.detail) return this.detail;
const raw = this.cause instanceof FederationError ? this.cause.cause : this.cause;
- const detail = raw instanceof Error ? raw.message : String(raw ?? "");
+ const detail =
+ raw instanceof Error
+ ? raw.message
+ : typeof raw === "string"
+ ? raw
+ : raw == null
+ ? ""
+ : (JSON.stringify(raw) ?? "[Unserializable value]");
return `Failed to load ${this.remoteName}${this.remoteUrl ? ` from ${this.remoteUrl}` : ""}: ${detail}`;
}
}
@@ -32,7 +39,14 @@ export class PluginError extends Data.TaggedError("PluginError")<{
}> {
override get message() {
const raw = this.cause instanceof PluginError ? this.cause.cause : this.cause;
- const detail = raw instanceof Error ? raw.message : String(raw ?? "");
+ const detail =
+ raw instanceof Error
+ ? raw.message
+ : typeof raw === "string"
+ ? raw
+ : raw == null
+ ? ""
+ : (JSON.stringify(raw) ?? "[Unserializable value]");
return `Plugin ${this.pluginName ?? "unknown"}${this.pluginUrl ? ` at ${this.pluginUrl}` : ""} failed: ${detail}`;
}
}
diff --git a/host/src/services/mcp.ts b/host/src/services/mcp.ts
index ce5adb0c..89ea838b 100644
--- a/host/src/services/mcp.ts
+++ b/host/src/services/mcp.ts
@@ -144,7 +144,9 @@ export async function mountMcpRoute(
resolvedPath = resolvedPath.replace(`{${key}}`, encodeURIComponent(String(value)));
} else if (queryParamNames.includes(key)) {
if (value !== undefined && value !== null) {
- queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
+ const encodedValue =
+ typeof value === "string" ? value : (JSON.stringify(value) ?? "");
+ queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(encodedValue)}`);
}
} else {
bodyFields[key] = value;
diff --git a/host/src/services/plugins.ts b/host/src/services/plugins.ts
index 04510d2a..e8cc3248 100644
--- a/host/src/services/plugins.ts
+++ b/host/src/services/plugins.ts
@@ -184,14 +184,14 @@ function formatError(error: unknown): string {
if (error.cause instanceof Error) {
msg += ` (caused by: ${error.cause.message || error.cause.name})`;
} else if (error.cause) {
- msg += ` (caused by: ${String(error.cause)})`;
+ msg += ` (caused by: ${error.cause instanceof Error ? error.cause.message : (JSON.stringify(error.cause) ?? "[Unserializable cause]")})`;
}
return msg;
}
if (typeof error === "object" && error !== null) {
const err = error as Record;
- if (err.message) return String(err.message);
- if (err._tag) return `[${err._tag}] ${JSON.stringify(error)}`;
+ if (typeof err.message === "string") return err.message;
+ if (typeof err._tag === "string") return `[${err._tag}] ${JSON.stringify(error)}`;
return JSON.stringify(error);
}
return String(error);
@@ -584,7 +584,7 @@ export const initializePlugins = Effect.gen(function* () {
secrets: {},
});
- const mfInstance = (runtime as any).__mfInstance as any | undefined;
+ const mfInstance = (runtime as any).__mfInstance as any;
if (mfInstance) {
installIntegrityFetchHook(mfInstance, integrityRegistry);
}
diff --git a/host/src/utils/errors.ts b/host/src/utils/errors.ts
index 8c024d43..505c7ae9 100644
--- a/host/src/utils/errors.ts
+++ b/host/src/utils/errors.ts
@@ -27,7 +27,7 @@ export function extractErrorDetails(error: unknown): {
details.cause = `[Effect Cause] ${JSON.stringify(error.cause)}`;
}
} else {
- details.cause = String(error.cause);
+ details.cause = JSON.stringify(error.cause) ?? "[Unserializable cause]";
}
}
@@ -51,5 +51,10 @@ export function extractErrorDetails(error: unknown): {
return { message: JSON.stringify(error) };
}
- return { message: String(error) };
+ if (typeof error === "string") return { message: error };
+ if (typeof error === "symbol") return { message: error.description ?? "" };
+ if (typeof error === "number" || typeof error === "bigint" || typeof error === "boolean") {
+ return { message: `${error}` };
+ }
+ return { message: "Unknown error" };
}
diff --git a/host/tests/e2e/runtime-remote.spec.ts b/host/tests/e2e/runtime-remote.spec.ts
index 3f57ef18..9ec52f2b 100644
--- a/host/tests/e2e/runtime-remote.spec.ts
+++ b/host/tests/e2e/runtime-remote.spec.ts
@@ -21,9 +21,11 @@ function expectNoHydrationFailure(pageErrors: string[]) {
}
for (const scenario of scenarios) {
- const suite = scenario.available ? test.describe : test.describe.skip;
+ const describeScenario = scenario.available
+ ? test.describe
+ : (title: string, fn: () => void) => test.describe.skip(title, fn);
- suite(`Remote runtime browser smoke: ${scenario.title}`, () => {
+ describeScenario(`Remote runtime browser smoke: ${scenario.title}`, () => {
test.describe.configure({ mode: "serial" });
let runtime: RuntimeRemoteHost;
diff --git a/host/tests/integration/rate-limit.test.ts b/host/tests/integration/rate-limit.test.ts
new file mode 100644
index 00000000..e8061d84
--- /dev/null
+++ b/host/tests/integration/rate-limit.test.ts
@@ -0,0 +1,223 @@
+import { createServer, request } from "node:http";
+import type { AddressInfo } from "node:net";
+import { createServer as createProbeServer } from "node:net";
+import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
+
+// The security module reads its env at import time — pin a small burst
+// budget here so the loops below stay fast regardless of production defaults.
+vi.hoisted(() => {
+ process.env.RATE_LIMIT_MAX = "100";
+});
+
+import { RATE_LIMIT_MAX, RATE_LIMIT_MUTATION_MAX } from "../../src/middleware/security";
+import { runServer, type ServerHandle } from "../../src/program";
+import type { RuntimeConfig } from "../../src/services/config";
+import { getAvailablePort } from "../helpers/ports";
+
+describe("host rate limiting", () => {
+ let upstream: ReturnType;
+ let host: ServerHandle;
+ let origin: string;
+ let proxyArgIndex: number;
+ let port: number;
+
+ beforeAll(async () => {
+ port = await getAvailablePort();
+ origin = `http://127.0.0.1:${port}`;
+ });
+
+ beforeEach(async () => {
+ vi.stubEnv("NODE_ENV", "production");
+ vi.stubEnv("HOST", "127.0.0.1");
+ proxyArgIndex = process.argv.length;
+ process.argv.push("--proxy");
+ upstream = createServer((_req, res) => {
+ res.setHeader("content-type", "application/json");
+ res.end('{"ok":true}');
+ });
+ await new Promise((resolve) => upstream.listen(0, "127.0.0.1", resolve));
+ const upstreamUrl = `http://127.0.0.1:${(upstream.address() as AddressInfo).port}`;
+ vi.stubEnv("PORT", String(port));
+ const config: RuntimeConfig = {
+ env: "production",
+ account: "rate-limit.testnet",
+ domain: "rate-limit.test",
+ networkId: "testnet",
+ host: { name: "host", url: origin, entry: `${origin}/mf-manifest.json`, source: "local" },
+ ui: {
+ name: "ui",
+ url: upstreamUrl,
+ entry: `${upstreamUrl}/mf-manifest.json`,
+ source: "remote",
+ },
+ api: {
+ name: "api",
+ url: upstreamUrl,
+ entry: `${upstreamUrl}/mf-manifest.json`,
+ source: "remote",
+ proxy: upstreamUrl,
+ },
+ };
+ host = runServer({ config });
+ await host.ready;
+ });
+
+ afterEach(async () => {
+ await host?.shutdown();
+ await new Promise((resolve, reject) => {
+ upstream?.close((error) => (error ? reject(error) : resolve()));
+ });
+ vi.unstubAllEnvs();
+ process.argv.splice(proxyArgIndex, 1);
+ });
+
+ async function get(path: string, localAddress = "127.0.0.1", headers = {}) {
+ return new Promise<{
+ status: number;
+ headers: import("node:http").IncomingHttpHeaders;
+ body: string;
+ }>((resolve, reject) => {
+ const req = request(`${origin}${path}`, { localAddress, headers, agent: false }, (res) => {
+ let body = "";
+ res.setEncoding("utf8");
+ res.on("data", (chunk) => {
+ body += chunk;
+ });
+ res.on("end", () => resolve({ status: res.statusCode!, headers: res.headers, body }));
+ });
+ req.on("error", reject);
+ req.end();
+ });
+ }
+
+ it("keeps health probes available without consuming the client's request budget", async () => {
+ for (let i = 0; i <= RATE_LIMIT_MAX; i++) {
+ expect((await get("/health")).status).toBe(200);
+ }
+ expect((await get("/api/ping")).status).toBe(200);
+ for (let i = 1; i < RATE_LIMIT_MAX; i++) {
+ expect((await get("/api/ping")).status).toBe(200);
+ }
+ const limited = await get("/api/ping");
+ expect(limited.status).toBe(429);
+ expect(JSON.parse(limited.body)).toEqual({
+ error: "Too many requests, please try again later.",
+ });
+ expect(Number(limited.headers["retry-after"])).toBeGreaterThan(0);
+ expect((await get("/health")).status).toBe(200);
+ });
+
+ it("isolates socket clients even when the public request URL is rewritten", async () => {
+ const headers = { "x-forwarded-host": "rate-limit.test", "x-forwarded-proto": "https" };
+ for (let i = 0; i < RATE_LIMIT_MAX; i++) {
+ expect((await get("/api/ping", "127.0.0.1", headers)).status).toBe(200);
+ }
+ expect((await get("/api/ping", "127.0.0.1", headers)).status).toBe(429);
+ // Distinct 127/8 addresses bind natively on Linux; macOS needs an explicit
+ // `ifconfig lo0 alias` — skip the second-client isolation when the local
+ // alias does not exist rather than failing on an environment artifact.
+ if (await localAddressAvailable("127.0.0.2")) {
+ expect((await get("/api/ping", "127.0.0.2", headers)).status).toBe(200);
+ }
+ });
+
+ it("keys forwarded clients by the LAST proxy hop and still limits page requests", async () => {
+ // Only the last x-forwarded-for hop is trusted (our proxy appends the
+ // real client); two clients sharing a first hop but differing in the
+ // last hop must land in separate buckets.
+ const headers = { "x-forwarded-for": "192.0.2.1, 10.0.0.1" };
+ for (let i = 0; i < RATE_LIMIT_MAX; i++) {
+ expect((await get("/api/ping", "127.0.0.1", headers)).status).toBe(200);
+ }
+ expect((await get("/", "127.0.0.1", headers)).status).toBe(429);
+ expect(
+ (await get("/api/ping", "127.0.0.1", { "x-forwarded-for": "192.0.2.1, 10.0.0.2" })).status,
+ ).toBe(200);
+ });
+
+ it("answers document navigations with an HTML retry page instead of JSON", async () => {
+ const headers = { "x-forwarded-for": "192.0.2.9" };
+ for (let i = 0; i < RATE_LIMIT_MAX; i++) {
+ await get("/api/ping", "127.0.0.1", headers);
+ }
+ const page = await get("/", "127.0.0.1", { ...headers, accept: "text/html" });
+ expect(page.status).toBe(429);
+ expect(page.headers["content-type"]).toContain("text/html");
+ expect(page.body).toContain("Too many requests");
+ expect(page.body).toContain("");
+ const api = await get("/api/ping", "127.0.0.1", headers);
+ expect(api.status).toBe(429);
+ expect(api.headers["content-type"]).toContain("application/json");
+ expect(JSON.parse(api.body)).toEqual({
+ error: "Too many requests, please try again later.",
+ });
+ });
+
+ it("applies the stricter mutation tier to non-RPC API writes without touching reads", async () => {
+ const write = { "x-forwarded-for": "192.0.2.20", "content-type": "application/json" };
+ for (let i = 0; i < RATE_LIMIT_MUTATION_MAX; i++) {
+ expect(
+ (
+ await rawRequest(`${origin}/api/ping`, {
+ method: "POST",
+ localAddress: "127.0.0.1",
+ headers: write,
+ agent: false,
+ })
+ ).status,
+ ).toBe(200);
+ }
+ expect(
+ (
+ await rawRequest(`${origin}/api/ping`, {
+ method: "POST",
+ localAddress: "127.0.0.1",
+ headers: write,
+ agent: false,
+ })
+ ).status,
+ ).toBe(429);
+ // oRPC transport is exempt from the mutation tier...
+ expect(
+ (
+ await rawRequest(`${origin}/api/rpc/example`, {
+ method: "POST",
+ localAddress: "127.0.0.1",
+ headers: write,
+ agent: false,
+ })
+ ).status,
+ ).toBe(200);
+ // ...and reads keep their own bucket after the write bucket saturated.
+ expect((await get("/api/ping", "127.0.0.1", { "x-forwarded-for": "192.0.2.20" })).status).toBe(
+ 200,
+ );
+ });
+});
+
+async function rawRequest(
+ url: string,
+ options: import("node:http").RequestOptions,
+): Promise<{ status: number; headers: import("node:http").IncomingHttpHeaders; body: string }> {
+ return new Promise((resolve, reject) => {
+ const req = request(url, options, (res) => {
+ let body = "";
+ res.setEncoding("utf8");
+ res.on("data", (chunk) => {
+ body += chunk;
+ });
+ res.on("end", () => resolve({ status: res.statusCode!, headers: res.headers, body }));
+ });
+ req.on("error", reject);
+ req.end();
+ });
+}
+
+async function localAddressAvailable(address: string): Promise {
+ return new Promise((resolve) => {
+ const probe = createProbeServer();
+ probe.once("error", () => resolve(false));
+ probe.once("listening", () => probe.close(() => resolve(true)));
+ probe.listen(0, address);
+ });
+}
diff --git a/host/tests/integration/tenant-runtime.test.ts b/host/tests/integration/tenant-runtime.test.ts
index acb93cd8..64e65b02 100644
--- a/host/tests/integration/tenant-runtime.test.ts
+++ b/host/tests/integration/tenant-runtime.test.ts
@@ -29,7 +29,7 @@ let remoteConfigs: Record = {};
function setRemoteConfigs(...entries: Array>) {
remoteConfigs = {};
for (const entry of entries) {
- remoteConfigs[`bos://${entry.account}/${entry.domain}`] = entry;
+ remoteConfigs[`bos://${String(entry.account)}/${String(entry.domain)}`] = entry;
}
}
diff --git a/host/tests/unit/serving-swap-under-traffic.test.ts b/host/tests/unit/serving-swap-under-traffic.test.ts
index 9d8e8442..7bd5d9ba 100644
--- a/host/tests/unit/serving-swap-under-traffic.test.ts
+++ b/host/tests/unit/serving-swap-under-traffic.test.ts
@@ -72,7 +72,7 @@ const ssrEntryBytes = (gen: number) => `console.log('ssr entry gen ${gen}');`;
const sriOf = (bytes: string) => `sha384-${createHash("sha384").update(bytes).digest("base64")}`;
const fakeCdnFetch = async (input: string | URL | Request): Promise => {
- const url = String(input);
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
const genMatch = url.match(/gen-(\d+)/);
if (url.endsWith("/manifest.gen.json")) {
return new Response(JSON.stringify(CORE_MANIFEST), { status: 200 });
diff --git a/host/tests/unit/snapshot-coordinator.test.ts b/host/tests/unit/snapshot-coordinator.test.ts
index dbfcd93f..120059a1 100644
--- a/host/tests/unit/snapshot-coordinator.test.ts
+++ b/host/tests/unit/snapshot-coordinator.test.ts
@@ -81,7 +81,7 @@ vi.mock("../../src/services/ui-compose", async (importOriginal) => {
beforeAll(() => {
const fakeCdnFetch = async (input: string | URL | Request): Promise => {
- const url = String(input);
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
if (url.endsWith("/remoteEntry.cf71.js")) return new Response(entryBytes, { status: 200 });
return new Response("not found", { status: 404 });
};
diff --git a/packages/better-near-auth/src/client.ts b/packages/better-near-auth/src/client.ts
index e6b72c2c..24efa236 100644
--- a/packages/better-near-auth/src/client.ts
+++ b/packages/better-near-auth/src/client.ts
@@ -65,7 +65,6 @@ export {
type NearClientAtoms,
type NearNetwork,
type NearState,
- readSessionNearAccountId,
} from "./store.js";
export interface AuthCallbacks {
diff --git a/packages/better-near-auth/src/gas-key.test.ts b/packages/better-near-auth/src/gas-key.test.ts
index 2df4dfd1..b8ab8968 100644
--- a/packages/better-near-auth/src/gas-key.test.ts
+++ b/packages/better-near-auth/src/gas-key.test.ts
@@ -126,7 +126,7 @@ function setupClient(initial: unknown, fetchRoutes: Record = {}
}
async function setActiveNetworkTestnet(actions: ReturnType["actions"]) {
- await actions.near.setNetwork("testnet");
+ actions.near.setNetwork("testnet");
}
function setSignedIn(plugin: ReturnType["plugin"], accountId = "test.testnet") {
diff --git a/packages/better-near-auth/src/index.ts b/packages/better-near-auth/src/index.ts
index 4ff5729b..65fae2fa 100644
--- a/packages/better-near-auth/src/index.ts
+++ b/packages/better-near-auth/src/index.ts
@@ -1783,7 +1783,7 @@ export const siwn = (options: SIWNPluginOptions) => {
return ctx.json(RelayStatusResponse.parse({ status: "pending" }));
} catch (error: unknown) {
console.warn(
- `[siwn] relay status lookup failed, reporting pending: ${error instanceof Error ? error.message : error}`,
+ `[siwn] relay status lookup failed, reporting pending: ${String(error instanceof Error ? error.message : error)}`,
);
return ctx.json(RelayStatusResponse.parse({ status: "pending" }));
}
diff --git a/packages/every-plugin/src/dev/serve.ts b/packages/every-plugin/src/dev/serve.ts
index 401be1d1..665700c3 100644
--- a/packages/every-plugin/src/dev/serve.ts
+++ b/packages/every-plugin/src/dev/serve.ts
@@ -206,7 +206,7 @@ export async function startPluginDevServer(
}
const handlers: { rpc: any; api: any } = { rpc: null, api: null };
- const effectContextHolder: { context: unknown | null } = { context: null };
+ const effectContextHolder: { context: unknown } = { context: null };
let runtime: { shutdown: () => Promise } | null = null;
const pathnameOf = (req: http.IncomingMessage): string =>
diff --git a/packages/every-plugin/src/runtime/errors.ts b/packages/every-plugin/src/runtime/errors.ts
index 76363a4a..06919988 100644
--- a/packages/every-plugin/src/runtime/errors.ts
+++ b/packages/every-plugin/src/runtime/errors.ts
@@ -25,6 +25,22 @@ export class ValidationError extends Data.TaggedError("ValidationError")<{
readonly zodError: z.ZodError;
}> {}
+const stringifyUnknown = (value: unknown): string => {
+ if (value === null) return "null";
+ if (value === undefined) return "undefined";
+ if (value instanceof Error) return value.message;
+ if (typeof value === "string") return value;
+ if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
+ return `${value}`;
+ }
+ if (typeof value === "symbol") return value.description ?? "";
+ try {
+ return JSON.stringify(value) ?? "[Unserializable value]";
+ } catch {
+ return "[Unserializable value]";
+ }
+};
+
const extractErrorMessage = (error: unknown): string => {
if (!error) return "Unknown error";
@@ -43,7 +59,7 @@ const extractErrorMessage = (error: unknown): string => {
return String((error as any).message);
}
- return String(error);
+ return stringifyUnknown(error);
};
/**
@@ -95,7 +111,7 @@ const formatDataPreview = (data: unknown, maxLength = 100): string => {
return `${str.slice(0, maxLength)}...`;
} catch {
- return String(data).slice(0, maxLength);
+ return stringifyUnknown(data).slice(0, maxLength);
}
};
@@ -257,7 +273,7 @@ export interface PluginLoadFailureInfo {
pluginUrl?: string;
/** failing runtime stage when the cause is stage-attributed */
operation?: string;
- kind: PluginFailureKind | string;
+ kind: string;
retryable: boolean;
message: string;
suggestion?: string;
diff --git a/packages/every-plugin/src/runtime/services/module-federation.service.ts b/packages/every-plugin/src/runtime/services/module-federation.service.ts
index 6f787b51..8fb1d7d4 100644
--- a/packages/every-plugin/src/runtime/services/module-federation.service.ts
+++ b/packages/every-plugin/src/runtime/services/module-federation.service.ts
@@ -89,7 +89,7 @@ const createModuleFederationInstance = Effect.cached(
return instance;
} catch (error) {
- throw new Error(`Failed to initialize Module Federation: ${error}`);
+ throw new Error(`Failed to initialize Module Federation: ${String(error)}`);
}
}),
);
diff --git a/packages/every-plugin/src/runtime/services/plugin-loader.service.ts b/packages/every-plugin/src/runtime/services/plugin-loader.service.ts
index 2e7b2f32..48dfe998 100644
--- a/packages/every-plugin/src/runtime/services/plugin-loader.service.ts
+++ b/packages/every-plugin/src/runtime/services/plugin-loader.service.ts
@@ -142,7 +142,7 @@ export const PluginLoaderServiceDefault = Layer.effect(
yield* moduleFederationService.registerRemote(pluginId, resolvedUrl).pipe(
Effect.tapError((error) =>
- Effect.logError(`Plugin ${pluginId} failed during register-remote: ${error}`),
+ Effect.logError(`Plugin ${pluginId} failed during register-remote: ${String(error)}`),
),
Effect.mapError((error) =>
toPluginRuntimeError(error, pluginId, undefined, "register-remote"),
@@ -155,7 +155,7 @@ export const PluginLoaderServiceDefault = Layer.effect(
.loadRemoteConstructor(pluginId, resolvedUrl)
.pipe(
Effect.tapError((error) =>
- Effect.logError(`Plugin ${pluginId} failed during load-remote: ${error}`),
+ Effect.logError(`Plugin ${pluginId} failed during load-remote: ${String(error)}`),
),
Effect.mapError((error) =>
toPluginRuntimeError(error, pluginId, undefined, "load-remote"),
@@ -298,7 +298,9 @@ export const PluginLoaderServiceDefault = Layer.effect(
),
),
Effect.tapError((error) =>
- Effect.logError(`Plugin ${plugin.id} failed during initialize-plugin: ${error}`),
+ Effect.logError(
+ `Plugin ${plugin.id} failed during initialize-plugin: ${String(error)}`,
+ ),
),
Effect.mapError((error) =>
toPluginRuntimeError(error, plugin.id, undefined, "initialize-plugin"),
diff --git a/packages/every-plugin/src/ui/manifest/descriptor.ts b/packages/every-plugin/src/ui/manifest/descriptor.ts
index 1481b3d0..a95756c7 100644
--- a/packages/every-plugin/src/ui/manifest/descriptor.ts
+++ b/packages/every-plugin/src/ui/manifest/descriptor.ts
@@ -45,7 +45,7 @@ export function App(input: AppInput): AppInput {
return input;
}
-export function Plugin(
+export function Plugin>(
name: K,
): {
local: (path: string) => PluginRefInput;
diff --git a/packages/every-plugin/src/ui/manifest/mount-registry.ts b/packages/every-plugin/src/ui/manifest/mount-registry.ts
index e911f23f..b25d17a7 100644
--- a/packages/every-plugin/src/ui/manifest/mount-registry.ts
+++ b/packages/every-plugin/src/ui/manifest/mount-registry.ts
@@ -29,7 +29,7 @@ export const MOUNT_REGISTRY = {
team: { label: "team", gate: "team", parameterized: true, implemented: false },
} satisfies Record;
-export type MountId = keyof typeof MOUNT_REGISTRY & string;
+export type MountId = Extract;
export const MOUNTS = Object.keys(MOUNT_REGISTRY) as MountId[];
diff --git a/packages/every-plugin/tests/unit/watch-kill.test.ts b/packages/every-plugin/tests/unit/watch-kill.test.ts
index bd695950..89a26cb9 100644
--- a/packages/every-plugin/tests/unit/watch-kill.test.ts
+++ b/packages/every-plugin/tests/unit/watch-kill.test.ts
@@ -19,7 +19,7 @@ const makeFakeChild = (opts: FakeChildOptions = {}) => {
child.killed = true;
if (exitsOnSignal === signal) {
child.exitCode = 0;
- for (const listener of [...exitListeners]) listener();
+ for (const listener of exitListeners) listener();
}
},
addListener: (_event: "exit", listener: () => void) => {
diff --git a/packages/everything-dev/src/api-contract.ts b/packages/everything-dev/src/api-contract.ts
index 547d2330..172a5e86 100644
--- a/packages/everything-dev/src/api-contract.ts
+++ b/packages/everything-dev/src/api-contract.ts
@@ -100,7 +100,7 @@ export class ApiContractResolver extends Context.Service<
>()("everything-dev/api-contract/ApiContractResolver") {
static readonly layer: Layer.Layer = Layer.effect(
ApiContractResolver,
- Effect.gen(function* () {
+ Effect.sync(() => {
const manifest = Effect.fn("ApiContractResolver.manifest")(function* (
apiBaseUrl: string,
): Effect.fn.Return {
diff --git a/packages/everything-dev/src/cdn-deploy.ts b/packages/everything-dev/src/cdn-deploy.ts
index 42871b76..3051a8d4 100644
--- a/packages/everything-dev/src/cdn-deploy.ts
+++ b/packages/everything-dev/src/cdn-deploy.ts
@@ -162,7 +162,7 @@ export async function probeStorageOrigin(
try {
response = await fetchImpl(probeUrl, { headers: { accept: "application/json" } });
} catch (error) {
- return `unreachable (${error instanceof Error ? error.message : error})`;
+ return `unreachable (${String(error instanceof Error ? error.message : error)})`;
}
if (!response.ok) {
return `GET /.well-known/mcp.json responded ${response.status}`;
diff --git a/packages/everything-dev/src/dev-session.ts b/packages/everything-dev/src/dev-session.ts
index f411650c..a217892c 100644
--- a/packages/everything-dev/src/dev-session.ts
+++ b/packages/everything-dev/src/dev-session.ts
@@ -295,7 +295,7 @@ export const runDevSession = (
),
Effect.tapError((err) =>
Effect.sync(() => {
- callbacks.onLog(pkg, `Failed to start: ${err}`, true);
+ callbacks.onLog(pkg, `Failed to start: ${String(err)}`, true);
callbacks.onStatus(pkg, "error");
}),
),
@@ -462,11 +462,18 @@ const runApp = (
process.on("SIGINT", handleSignal);
process.on("SIGTERM", handleSignal);
- Effect.runPromiseExit(program).then((exit) => {
- clearInterval(orphanWatch);
- if (forceExitTimer) clearTimeout(forceExitTimer);
- process.exit(Exit.isSuccess(exit) ? 0 : 1);
- });
+ void Effect.runPromiseExit(program)
+ .then((exit) => {
+ clearInterval(orphanWatch);
+ if (forceExitTimer) clearTimeout(forceExitTimer);
+ process.exit(Exit.isSuccess(exit) ? 0 : 1);
+ })
+ .catch((error: unknown) => {
+ clearInterval(orphanWatch);
+ if (forceExitTimer) clearTimeout(forceExitTimer);
+ console.error("[Dev] Failed to observe application exit:", error);
+ process.exit(1);
+ });
};
export const devApp = runApp;
diff --git a/packages/everything-dev/src/mf.ts b/packages/everything-dev/src/mf.ts
index 59104f6e..0add2e33 100644
--- a/packages/everything-dev/src/mf.ts
+++ b/packages/everything-dev/src/mf.ts
@@ -198,7 +198,7 @@ export interface FederationCompatReport {
}
function parseSemver(v: unknown): { major: number; minor: number; patch: number } | null {
- const m = /^(\d+)\.(\d+)\.(\d+)/.exec(String(v ?? ""));
+ const m = typeof v === "string" ? /^(\d+)\.(\d+)\.(\d+)/.exec(v) : null;
return m ? { major: +m[1]!, minor: +m[2]!, patch: +m[3]! } : null;
}
diff --git a/packages/everything-dev/src/orchestrator.ts b/packages/everything-dev/src/orchestrator.ts
index 5b02da63..08ac82e0 100644
--- a/packages/everything-dev/src/orchestrator.ts
+++ b/packages/everything-dev/src/orchestrator.ts
@@ -153,11 +153,22 @@ const patchConsole = (name: string, callbacks: ProcessCallbacks): (() => void) =
const parts = [`${arg.name}: ${arg.message}`];
if (arg.cause instanceof Error)
parts.push(`(cause: ${arg.cause.name}: ${arg.cause.message})`);
- else if (arg.cause) parts.push(`(cause: ${String(arg.cause)})`);
+ else if (arg.cause) {
+ const cause = arg.cause;
+ parts.push(
+ `(cause: ${cause instanceof Error ? cause.message : typeof cause === "string" ? cause : (JSON.stringify(cause) ?? "[Unserializable cause]")})`,
+ );
+ }
if (isError && arg.stack) parts.push(arg.stack);
return parts.join("\n");
}
- return typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg);
+ if (arg === null) return "null";
+ if (typeof arg === "string") return arg;
+ if (typeof arg === "number" || typeof arg === "bigint" || typeof arg === "boolean")
+ return `${arg}`;
+ if (typeof arg === "symbol") return arg.description ?? "";
+ if (typeof arg === "undefined") return "undefined";
+ return JSON.stringify(arg, null, 2) ?? "[Unserializable value]";
})
.join(" ");
};
@@ -272,7 +283,7 @@ const spawnRemoteHost = (descriptor: ServiceDescriptor, callbacks: ProcessCallba
}
} catch (e) {
warnOutsideEffect(
- `[Orchestrator] Failed to fetch or parse manifest from ${manifestUrl}, falling back to remoteEntryUrl: ${e}`,
+ `[Orchestrator] Failed to fetch or parse manifest from ${manifestUrl}, falling back to remoteEntryUrl: ${String(e)}`,
);
}
return remoteEntryUrl;
diff --git a/packages/everything-dev/src/publish.ts b/packages/everything-dev/src/publish.ts
index ba6ea26a..156705af 100644
--- a/packages/everything-dev/src/publish.ts
+++ b/packages/everything-dev/src/publish.ts
@@ -28,6 +28,7 @@ import {
import { getNetworkIdForAccount } from "./network";
import { platformUrlDeployEntries, pluginUiUrlDeployEntries } from "./platform-deploy";
import { openResolution } from "./resolution/session";
+import { verifyRollbackSnapshot } from "./rollback";
import { collectDistFiles, uploadBundle, uploadWorkspaceDist } from "./storage-upload";
import type { BosConfig, BosConfigInput, PublishConfig, RuntimeConfig } from "./types";
import { padRight } from "./utils/string";
@@ -699,6 +700,25 @@ export async function publishToFastKv(input: PublishToFastKvInput): Promise !check.ok)
+ .map((check) => ` ${check.slot}: ${check.reason}`)
+ .join("\n");
+ console.error(
+ ` ${colors.error(icons.err)} Publish blocked — a pinned slot's bytes do not serve at its production URL:\n${failures}`,
+ );
+ return {
+ status: "error",
+ registryUrl,
+ built,
+ skipped,
+ deployResults,
+ error: `Publish blocked — pinned slot bytes missing or SRI mismatched:\n${failures}`,
+ };
+ }
+
const registryKey = `apps/${account}/${gateway}/bos.config.json`;
const publishedAt = new Date().toISOString();
// version identity printed + returned for every deploy (atomic-deploys 12)
diff --git a/packages/everything-dev/src/types.ts b/packages/everything-dev/src/types.ts
index 476af428..9dd26381 100644
--- a/packages/everything-dev/src/types.ts
+++ b/packages/everything-dev/src/types.ts
@@ -251,6 +251,7 @@ export const BosConfigInputSchema: z.ZodType = z.lazy(() =>
domain: z.string().optional(),
status: z.enum(["active", "suspended", "pending_deletion"]).optional(),
testnet: z.string().optional(),
+ staging: BosStagingSchema.optional(),
template: z.string().optional(),
gateway: z
.object({
diff --git a/packages/everything-dev/src/ui/api.ts b/packages/everything-dev/src/ui/api.ts
index aa6c21db..ae9ea90d 100644
--- a/packages/everything-dev/src/ui/api.ts
+++ b/packages/everything-dev/src/ui/api.ts
@@ -12,6 +12,7 @@ export type ApiClient = ContractRoute
export interface ClientServiceConfig {
hostUrl: string;
rpcBase: `/${string}`;
+ connectionError?: () => { title: string; description: string };
}
type ClientRouterContext = {
@@ -40,9 +41,13 @@ function createRpcLink(config: ClientServiceConfig, url: `/${string}`, headers?:
) {
void import("sonner")
.then(({ toast }) => {
- toast.error("Unable to connect to API", {
- id: "api-connection-error",
+ const message = config.connectionError?.() ?? {
+ title: "Unable to connect to API",
description: "The API is currently unavailable. Please try again later.",
+ };
+ toast.error(message.title, {
+ id: "api-connection-error",
+ description: message.description,
});
})
.catch(() => {});
@@ -51,7 +56,7 @@ function createRpcLink(config: ClientServiceConfig, url: `/${string}`, headers?:
}),
],
fetch(fetchUrl: RequestInfo | URL, options?: RequestInit) {
- return fetch(fetchUrl, {
+ return fetchWithRateLimitRetry(fetchUrl, {
...options,
credentials: "include",
headers: headers
@@ -62,6 +67,31 @@ function createRpcLink(config: ClientServiceConfig, url: `/${string}`, headers?:
});
}
+const RATE_LIMIT_MAX_RETRIES = 3;
+const RATE_LIMIT_RETRY_CAP_MS = 2_000;
+
+/**
+ * A 429 from the host's edge limiter is transient saturation, not an
+ * application failure — retry within the server's `Retry-After` (capped) so
+ * queries and the session check self-heal instead of surfacing errors.
+ */
+async function fetchWithRateLimitRetry(
+ fetchUrl: RequestInfo | URL,
+ options?: RequestInit,
+): Promise {
+ let response = await fetch(fetchUrl, options);
+ for (let attempt = 1; attempt <= RATE_LIMIT_MAX_RETRIES && response.status === 429; attempt++) {
+ const retryAfterSeconds = Number(response.headers.get("retry-after"));
+ const delay =
+ Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0
+ ? Math.min(retryAfterSeconds * 1000, RATE_LIMIT_RETRY_CAP_MS)
+ : 100 * attempt;
+ await new Promise((resolve) => setTimeout(resolve, delay));
+ response = await fetch(fetchUrl, options);
+ }
+ return response;
+}
+
function buildClient(
config: ClientServiceConfig,
url: `/${string}`,
diff --git a/packages/everything-dev/src/ui/auth.ts b/packages/everything-dev/src/ui/auth.ts
index 6380de39..bbe61908 100644
--- a/packages/everything-dev/src/ui/auth.ts
+++ b/packages/everything-dev/src/ui/auth.ts
@@ -156,9 +156,10 @@ export function sessionQueryOptions(authClient: AuthClient) {
return {
queryKey: sessionQueryKey,
queryFn: async () => {
- const { data: session } = await authClient.getSession({
+ const { data: session, error } = await authClient.getSession({
query: { disableCookieCache: true },
});
+ if (error) throw new Error(error.message || "Unable to load your session");
return session ?? null;
},
staleTime: 60 * 1000,
diff --git a/packages/everything-dev/src/ui/hydrate.tsx b/packages/everything-dev/src/ui/hydrate.tsx
index df704b87..1b55d002 100644
--- a/packages/everything-dev/src/ui/hydrate.tsx
+++ b/packages/everything-dev/src/ui/hydrate.tsx
@@ -11,7 +11,7 @@
import type { QueryClient } from "@tanstack/react-query";
import type { ClientRuntimeConfig } from "../types";
-import { createApiClient } from "./api";
+import { type ClientServiceConfig, createApiClient } from "./api";
import { createAuthClient } from "./auth";
import {
CORE_UI_PLUGIN_KEY,
@@ -78,6 +78,8 @@ export interface CoreHydrateOptions {
createQueryClient?: () => QueryClient;
/** Overrides the runtime config source (tests, embeds). */
config?: ClientRuntimeConfig;
+ /** Localized copy for the API connection-failure toast (see `createApiClient`). */
+ apiConnectionError?: ClientServiceConfig["connectionError"];
}
interface ComposedTree {
@@ -321,9 +323,11 @@ export async function hydrate(options: CoreHydrateOptions) {
queryClient: client,
runtimeConfig,
cspNonce,
+ locale: isServerRendered() ? document.documentElement.lang : undefined,
apiClient: createApiClient({
hostUrl: runtimeConfig.hostUrl,
rpcBase: runtimeConfig.rpcBase,
+ connectionError: options.apiConnectionError,
}),
authClient: createAuthClient({ runtimeConfig, cspNonce }),
},
@@ -360,10 +364,35 @@ export async function hydrate(options: CoreHydrateOptions) {
);
}
+ if (composed?.degraded) {
+ const { toast } = await import("sonner");
+ toast.error("Some application features couldn't load", {
+ id: "application-compose-error",
+ description: "Plugin pages are unavailable. Reload to try again.",
+ duration: Number.POSITIVE_INFINITY,
+ action: { label: "Reload", onClick: () => window.location.reload() },
+ });
+ }
+
console.log("[Hydrate] Complete!");
})().catch((error) => {
console.error("[Hydrate] Failed:", error);
window.__EVERYTHING_DEV_HYDRATE_PROMISE__ = undefined;
+ const message = document.createElement("main");
+ message.setAttribute("role", "alert");
+ message.dataset.testid = "application-startup-error";
+ message.className =
+ "min-h-screen flex flex-col items-center justify-center gap-4 bg-background text-foreground p-6";
+ const heading = document.createElement("h1");
+ heading.textContent = "The application couldn't load";
+ const detail = document.createElement("p");
+ detail.textContent = "Check your connection and reload to try again.";
+ const retry = document.createElement("button");
+ retry.textContent = "Reload";
+ retry.className = "rounded-md border border-border px-4 py-2";
+ retry.addEventListener("click", () => window.location.reload());
+ message.append(heading, detail, retry);
+ document.body.replaceChildren(message);
throw error;
});
diff --git a/packages/everything-dev/src/ui/i18n.tsx b/packages/everything-dev/src/ui/i18n.tsx
index ea8d7ff3..6c990281 100644
--- a/packages/everything-dev/src/ui/i18n.tsx
+++ b/packages/everything-dev/src/ui/i18n.tsx
@@ -9,6 +9,25 @@ import {
useMemo,
useSyncExternalStore,
} from "react";
+import { matchLocale, resolveLocale, serializeLocaleCookie } from "./locale";
+
+export { Trans } from "@lingui/react";
+export {
+ matchLocale,
+ readAcceptLanguage,
+ readLocaleCookie,
+ resolveLocale,
+ serializeLocaleCookie,
+} from "./locale";
+export { createMessageCatalogs } from "./message-catalogs";
+
+type SharedLocaleState = {
+ cookieName: string;
+ locale: string;
+ selectLocale: (locale: string) => Promise;
+};
+
+const SharedLocaleContext = createContext(null);
type LocaleStore = {
get: () => string;
@@ -35,61 +54,6 @@ function createLocaleStore(initialLocale: string): LocaleStore {
};
}
-export function readLocaleCookie(cookie: string, cookieName: string): string | undefined {
- for (const part of cookie.split(";")) {
- const [name, ...valueParts] = part.trim().split("=");
- if (name !== cookieName) continue;
- try {
- return decodeURIComponent(valueParts.join("="));
- } catch {
- return undefined;
- }
- }
- return undefined;
-}
-
-export function matchLocale(
- candidate: string | null | undefined,
- locales: readonly Locale[],
-): Locale | undefined {
- if (!candidate) return undefined;
- const normalized = candidate.toLowerCase();
- const exact = locales.find((locale) => locale.toLowerCase() === normalized);
- if (exact) return exact;
- const base = normalized.split("-")[0];
- return locales.find((locale) => locale.toLowerCase() === base);
-}
-
-export function resolveLocale({
- preferredLocale,
- cookie,
- browserLocales,
- locales,
- defaultLocale,
- cookieName,
-}: {
- preferredLocale?: string | null;
- cookie: string;
- browserLocales: readonly string[];
- locales: readonly Locale[];
- defaultLocale: Locale;
- cookieName: string;
-}): Locale {
- const preferred = matchLocale(preferredLocale, locales);
- if (preferred) return preferred;
- const saved = matchLocale(readLocaleCookie(cookie, cookieName), locales);
- if (saved) return saved;
- for (const browserLocale of browserLocales) {
- const matched = matchLocale(browserLocale, locales);
- if (matched) return matched;
- }
- return defaultLocale;
-}
-
-export function serializeLocaleCookie(cookieName: string, locale: string, secure: boolean): string {
- return `${cookieName}=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax${secure ? "; Secure" : ""}`;
-}
-
export function createLocaleRuntime({
locales,
defaultLocale,
@@ -122,11 +86,12 @@ export function createLocaleRuntime({
cookieName,
});
- const getStore = (preferredLocale?: string | null) => {
- if (typeof window === "undefined") return createLocaleStore(detectLocale(preferredLocale));
+ const getStore = (initialLocale?: string | null) => {
+ const initial = matchLocale(initialLocale, locales) ?? detectLocale();
+ if (typeof window === "undefined") return createLocaleStore(initial);
const existing = browserStores.get(cookieName);
if (existing) return existing;
- const store = createLocaleStore(detectLocale(preferredLocale));
+ const store = createLocaleStore(initial);
browserStores.set(cookieName, store);
return store;
};
@@ -134,37 +99,60 @@ export function createLocaleRuntime({
function LocaleProvider({
children,
messages,
+ initialLocale,
preferredLocale,
onLocaleChange,
}: {
children: ReactNode;
messages: (locale: Locale) => Messages;
+ initialLocale?: string | null;
preferredLocale?: string | null;
onLocaleChange?: (locale: Locale) => void | Promise;
}) {
- const store = useMemo(() => getStore(preferredLocale), []);
- const locale = useSyncExternalStore(store.subscribe, store.get, () => defaultLocale) as Locale;
+ const parent = useContext(SharedLocaleContext);
+ const sharedParent = parent?.cookieName === cookieName ? parent : null;
+ const resolvedInitial =
+ matchLocale(preferredLocale, locales) ??
+ matchLocale(initialLocale, locales) ??
+ matchLocale(sharedParent?.locale, locales);
+ const serverLocale = resolvedInitial ?? defaultLocale;
+ const store = useMemo(() => getStore(resolvedInitial), []);
+ const locale = useSyncExternalStore(store.subscribe, store.get, () => serverLocale) as Locale;
const i18n = useMemo(
() => setupI18n({ locale, messages: { [locale]: messages(locale) } }),
[locale, messages],
);
useEffect(() => {
+ if (sharedParent) return;
const resolved = matchLocale(preferredLocale, locales);
- if (resolved) store.set(resolved);
- }, [preferredLocale, store]);
+ if (resolved) {
+ Reflect.set(
+ document,
+ "cookie",
+ serializeLocaleCookie(cookieName, resolved, window.location.protocol === "https:"),
+ );
+ store.set(resolved);
+ }
+ }, [preferredLocale, sharedParent, store]);
useEffect(() => {
+ if (sharedParent) return;
const previousLocale = document.documentElement.lang;
document.documentElement.lang = locale;
return () => {
document.documentElement.lang = previousLocale;
};
- }, [locale]);
+ }, [locale, sharedParent]);
const selectLocale = useCallback(
async (nextLocale: Locale) => {
if (!locales.includes(nextLocale)) return;
+ if (sharedParent && !onLocaleChange) {
+ await sharedParent.selectLocale(nextLocale);
+ return;
+ }
+ if (onLocaleChange) await onLocaleChange(nextLocale);
if (typeof document !== "undefined") {
Reflect.set(
document,
@@ -177,16 +165,28 @@ export function createLocaleRuntime({
);
}
store.set(nextLocale);
- await onLocaleChange?.(nextLocale);
},
- [onLocaleChange, store],
+ [onLocaleChange, sharedParent, store],
);
const value = useMemo(() => ({ locale, selectLocale }), [locale, selectLocale]);
+ const sharedValue = useMemo(
+ () => ({
+ cookieName,
+ locale,
+ selectLocale: async (next: string) => {
+ const matched = matchLocale(next, locales);
+ if (matched) await selectLocale(matched);
+ },
+ }),
+ [locale, selectLocale],
+ );
return (
-
- {children}
-
+
+
+ {children}
+
+
);
}
@@ -204,5 +204,8 @@ export function createLocaleRuntime({
);
}
- return { LocaleProvider, useLocale, useTranslation, detectLocale };
+ const getLocale = (): Locale =>
+ matchLocale(browserStores.get(cookieName)?.get(), locales) ?? detectLocale();
+
+ return { LocaleProvider, useLocale, useTranslation, detectLocale, getLocale };
}
diff --git a/packages/everything-dev/src/ui/locale.ts b/packages/everything-dev/src/ui/locale.ts
new file mode 100644
index 00000000..31da3e19
--- /dev/null
+++ b/packages/everything-dev/src/ui/locale.ts
@@ -0,0 +1,70 @@
+export type LocaleOptions = {
+ locales: readonly Locale[];
+ defaultLocale: Locale;
+ cookieName: string;
+};
+
+export function readLocaleCookie(cookie: string, cookieName: string): string | undefined {
+ for (const part of cookie.split(";")) {
+ const [name, ...valueParts] = part.trim().split("=");
+ if (name !== cookieName) continue;
+ try {
+ return decodeURIComponent(valueParts.join("="));
+ } catch {
+ return undefined;
+ }
+ }
+ return undefined;
+}
+
+export function matchLocale(
+ candidate: string | null | undefined,
+ locales: readonly Locale[],
+): Locale | undefined {
+ if (!candidate) return undefined;
+ const normalized = candidate.trim().toLowerCase();
+ const exact = locales.find((locale) => locale.toLowerCase() === normalized);
+ if (exact) return exact;
+ const base = normalized.split("-")[0];
+ return locales.find((locale) => locale.toLowerCase() === base);
+}
+
+export function readAcceptLanguage(header: string | null): string[] {
+ return (header ?? "")
+ .split(",")
+ .map((part, index) => {
+ const [language = "", ...parameters] = part.trim().split(";");
+ const quality = parameters.find((parameter) => parameter.trim().startsWith("q="));
+ return { language, quality: quality ? Number(quality.trim().slice(2)) : 1, index };
+ })
+ .filter(({ language, quality }) => language && quality > 0 && quality <= 1)
+ .sort((a, b) => b.quality - a.quality || a.index - b.index)
+ .map(({ language }) => language);
+}
+
+export function resolveLocale({
+ preferredLocale,
+ cookie,
+ browserLocales,
+ locales,
+ defaultLocale,
+ cookieName,
+}: LocaleOptions & {
+ preferredLocale?: string | null;
+ cookie: string;
+ browserLocales: readonly string[];
+}): Locale {
+ const preferred = matchLocale(preferredLocale, locales);
+ if (preferred) return preferred;
+ const saved = matchLocale(readLocaleCookie(cookie, cookieName), locales);
+ if (saved) return saved;
+ for (const browserLocale of browserLocales) {
+ const matched = matchLocale(browserLocale, locales);
+ if (matched) return matched;
+ }
+ return defaultLocale;
+}
+
+export function serializeLocaleCookie(cookieName: string, locale: string, secure: boolean): string {
+ return `${cookieName}=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax${secure ? "; Secure" : ""}`;
+}
diff --git a/packages/everything-dev/src/ui/message-catalogs.ts b/packages/everything-dev/src/ui/message-catalogs.ts
new file mode 100644
index 00000000..135853aa
--- /dev/null
+++ b/packages/everything-dev/src/ui/message-catalogs.ts
@@ -0,0 +1,11 @@
+export function createMessageCatalogs<
+ const Locales extends readonly string[],
+ const Entries extends Record,
+>(locales: Locales, entries: Entries) {
+ return Object.fromEntries(
+ locales.map((locale, index) => [
+ locale,
+ Object.fromEntries(Object.entries(entries).map(([id, messages]) => [id, messages[index]])),
+ ]),
+ ) as { [Locale in Locales[number]]: { [Id in keyof Entries]: string } };
+}
diff --git a/packages/everything-dev/src/ui/router-client.tsx b/packages/everything-dev/src/ui/router-client.tsx
index b52f08af..01932e0a 100644
--- a/packages/everything-dev/src/ui/router-client.tsx
+++ b/packages/everything-dev/src/ui/router-client.tsx
@@ -58,6 +58,7 @@ export function createRouter<
cspNonce: opts.context.cspNonce,
}),
session: opts.context.session,
+ locale: opts.context.locale,
},
...(cspNonce ? { ssr: { nonce: cspNonce } } : {}),
defaultPreload: "intent",
diff --git a/packages/everything-dev/src/ui/router-error.tsx b/packages/everything-dev/src/ui/router-error.tsx
index 81fdf84c..6e1d4371 100644
--- a/packages/everything-dev/src/ui/router-error.tsx
+++ b/packages/everything-dev/src/ui/router-error.tsx
@@ -4,7 +4,15 @@
* the app's UI kit, so primitives are plain markup + semantic tokens.
*/
-export function RouterError({ error, reset }: { error: Error; reset?: () => void }) {
+export function RouterError({
+ error,
+ reset,
+ messages,
+}: {
+ error: Error;
+ reset?: () => void;
+ messages?: { title: string; body: string; home: string; retry: string };
+}) {
return (
void
-
This page didn’t load
+
+ {messages?.title ?? "This page didn’t load"}
+
- Something went wrong on our side. Try again, or head back home.
+ {messages?.body ?? "Something went wrong on our side. Try again, or head back home."}
@@ -29,7 +39,7 @@ export function RouterError({ error, reset }: { error: Error; reset?: () => void
data-testid="router-error-home"
className="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground"
>
- Back home
+ {messages?.home ?? "Back home"}
void
data-testid="router-error-retry"
className="inline-flex h-9 items-center justify-center rounded-md border border-border bg-background px-4 text-sm font-medium text-foreground"
>
- Try again
+ {messages?.retry ?? "Try again"}
diff --git a/packages/everything-dev/src/ui/router-server.tsx b/packages/everything-dev/src/ui/router-server.tsx
index f3891b37..96291f26 100644
--- a/packages/everything-dev/src/ui/router-server.tsx
+++ b/packages/everything-dev/src/ui/router-server.tsx
@@ -19,6 +19,7 @@ import {
} from "@tanstack/react-router/ssr/server";
import { createApiClient } from "./api";
import { createAuthClient } from "./auth";
+import { type LocaleOptions, readAcceptLanguage, resolveLocale } from "./locale";
import { collectHeadData } from "./router";
import {
defaultNotFoundComponent,
@@ -46,6 +47,8 @@ export interface ServerRouterModuleOptions QueryClient;
+ /** Locale negotiation for SSR — resolves the request-scoped locale into router context. */
+ locale?: LocaleOptions;
}
type ServerRouterOptions = CreateRouterOptions & {
@@ -86,6 +89,7 @@ export function createServerRouterModule
}),
session: context?.session,
cspNonce,
+ locale: context?.locale,
pluginNav: context?.pluginNav,
},
...(cspNonce ? { ssr: { nonce: cspNonce } } : {}),
@@ -146,6 +150,7 @@ export function createServerRouterModule
authClient:
context?.authClient ?? createAuthClient({ runtimeConfig, cspNonce: context?.cspNonce }),
session: context?.session,
+ locale: context?.locale,
},
});
@@ -181,6 +186,13 @@ export function createServerRouterModule
session: renderOptions.session,
cspNonce: renderOptions.cspNonce,
pluginNav: renderOptions.pluginNav,
+ locale: options.locale
+ ? resolveLocale({
+ ...options.locale,
+ cookie: request.headers.get("cookie") ?? "",
+ browserLocales: readAcceptLanguage(request.headers.get("accept-language")),
+ })
+ : undefined,
},
});
queryClientRef = built.queryClient ?? localQueryClient;
@@ -201,6 +213,8 @@ export function createServerRouterModule
}),
);
+ if (options.locale) response.headers.append("Vary", "Cookie, Accept-Language");
+
return {
stream: response.body!,
statusCode: response.status,
diff --git a/packages/everything-dev/src/ui/runtime.ts b/packages/everything-dev/src/ui/runtime.ts
index 49d8cd1e..095ef24f 100644
--- a/packages/everything-dev/src/ui/runtime.ts
+++ b/packages/everything-dev/src/ui/runtime.ts
@@ -29,6 +29,12 @@ export function getCspNonce() {
return window.__CSP_NONCE__;
}
+export function getAppName(config?: Partial): string {
+ const runtimeConfig =
+ config ?? (typeof window === "undefined" ? undefined : window.__RUNTIME_CONFIG__);
+ return runtimeConfig?.runtime?.title ?? runtimeConfig?.account ?? "";
+}
+
export function buildRuntimeHref(pathname: string, runtimeConfig?: Partial) {
const basePath = runtimeConfig?.runtime?.runtimeBasePath ?? "/";
if (basePath === "/") {
diff --git a/packages/everything-dev/src/ui/tenant.ts b/packages/everything-dev/src/ui/tenant.ts
index 836f4287..d2be4ba9 100644
--- a/packages/everything-dev/src/ui/tenant.ts
+++ b/packages/everything-dev/src/ui/tenant.ts
@@ -112,53 +112,69 @@ export interface TenantUiOverride {
export const INTEGRITY_PATTERN = /^sha384-[A-Za-z0-9+/=]+$/;
-const optionalUrl = z
- .string()
- .trim()
- .url("must be a valid URL")
- .or(z.literal(""))
- .transform((value) => value.trim());
-
-const optionalIntegrity = z
- .string()
- .trim()
- .regex(INTEGRITY_PATTERN, "must look like sha384-… (base64)")
- .or(z.literal(""));
-
-const optionalManifestName = z
- .string()
- .trim()
- .regex(/^versions\/[0-9a-f]{16}\.json$/, "must look like versions/.json")
- .or(z.literal(""));
-
-export const tenantConfigDraftSchema = z
- .object({
- title: z.string().trim().min(1, "title is required"),
- description: z.string().trim().min(1, "description is required"),
- repository: optionalUrl,
- uiProduction: optionalUrl,
- uiIntegrity: optionalIntegrity,
- uiManifest: optionalManifestName,
- uiPinIntegrity: optionalIntegrity,
- ssrUrl: optionalUrl,
- ssrIntegrity: optionalIntegrity,
- })
- .refine((draft) => !!draft.uiProduction === !!(draft.uiIntegrity || draft.uiManifest), {
- message: "a UI bundle needs both its URL and its integrity (direct hash or manifest pin)",
- path: ["uiIntegrity"],
- })
- .refine((draft) => !draft.uiIntegrity || !draft.uiManifest, {
- message: "choose one integrity mode — a direct entry hash or a version-manifest pin, not both",
- path: ["uiManifest"],
- })
- .refine((draft) => !!draft.uiManifest === !!draft.uiPinIntegrity, {
- message: "a version-manifest pin needs both the manifest filename and its integrity",
- path: ["uiPinIntegrity"],
- })
- .refine((draft) => !!draft.ssrUrl === !!draft.ssrIntegrity, {
- message: "an SSR bundle needs both its URL and its integrity hash",
- path: ["ssrIntegrity"],
- });
+export const defaultTenantConfigDraftMessages = {
+ url: "must be a valid URL",
+ integrity: "must look like sha384-… (base64)",
+ manifest: "must look like versions/.json",
+ title: "title is required",
+ description: "description is required",
+ uiPair: "a UI bundle needs both its URL and its integrity (direct hash or manifest pin)",
+ integrityMode:
+ "choose one integrity mode — a direct entry hash or a version-manifest pin, not both",
+ pinPair: "a version-manifest pin needs both the manifest filename and its integrity",
+ ssrPair: "an SSR bundle needs both its URL and its integrity hash",
+};
+
+export function createTenantConfigDraftSchema(
+ messages: Partial = {},
+) {
+ const copy = { ...defaultTenantConfigDraftMessages, ...messages };
+ const optionalUrl = z
+ .string()
+ .trim()
+ .url(copy.url)
+ .or(z.literal(""))
+ .transform((value) => value.trim());
+ const optionalIntegrity = z
+ .string()
+ .trim()
+ .regex(INTEGRITY_PATTERN, copy.integrity)
+ .or(z.literal(""));
+ const optionalManifestName = z
+ .string()
+ .trim()
+ .regex(/^versions\/[0-9a-f]{16}\.json$/, copy.manifest)
+ .or(z.literal(""));
+ return z
+ .object({
+ title: z.string().trim().min(1, copy.title),
+ description: z.string().trim().min(1, copy.description),
+ repository: optionalUrl,
+ uiProduction: optionalUrl,
+ uiIntegrity: optionalIntegrity,
+ uiManifest: optionalManifestName,
+ uiPinIntegrity: optionalIntegrity,
+ ssrUrl: optionalUrl,
+ ssrIntegrity: optionalIntegrity,
+ })
+ .refine((draft) => !!draft.uiProduction === !!(draft.uiIntegrity || draft.uiManifest), {
+ message: copy.uiPair,
+ path: ["uiIntegrity"],
+ })
+ .refine((draft) => !draft.uiIntegrity || !draft.uiManifest, {
+ message: copy.integrityMode,
+ path: ["uiManifest"],
+ })
+ .refine((draft) => !!draft.uiManifest === !!draft.uiPinIntegrity, {
+ message: copy.pinPair,
+ path: ["uiPinIntegrity"],
+ })
+ .refine((draft) => !!draft.ssrUrl === !!draft.ssrIntegrity, {
+ message: copy.ssrPair,
+ path: ["ssrIntegrity"],
+ });
+}
+export const tenantConfigDraftSchema = createTenantConfigDraftSchema();
export type TenantConfigDraft = z.infer;
diff --git a/packages/everything-dev/src/ui/types.ts b/packages/everything-dev/src/ui/types.ts
index 3623aca7..edebdff6 100644
--- a/packages/everything-dev/src/ui/types.ts
+++ b/packages/everything-dev/src/ui/types.ts
@@ -8,6 +8,7 @@ export interface RouterContext {
runtimeConfig?: Partial;
session?: TSession;
cspNonce?: string;
+ locale?: string;
/** nav manifest derived from composed routes' staticData.nav */
pluginNav?: NavManifest;
}
diff --git a/packages/everything-dev/src/utils/theme.ts b/packages/everything-dev/src/utils/theme.ts
index 7ccd6d4b..17370f34 100644
--- a/packages/everything-dev/src/utils/theme.ts
+++ b/packages/everything-dev/src/utils/theme.ts
@@ -11,7 +11,7 @@ const truecolorRamp =
const rgb = (code: string) => [1, 3, 5].map((i) => parseInt(code.slice(i, i + 2), 16));
const [r1, g1, b1] = rgb(from);
const [r2, g2, b2] = rgb(to);
- const chars = [...text];
+ const chars = Array.from(text);
const total = chars.filter((ch) => ch !== "\n").length;
let seen = 0;
return chars
diff --git a/packages/everything-dev/tests/integration/aborted-train.test.ts b/packages/everything-dev/tests/integration/aborted-train.test.ts
index 9ec96dd5..6dee3119 100644
--- a/packages/everything-dev/tests/integration/aborted-train.test.ts
+++ b/packages/everything-dev/tests/integration/aborted-train.test.ts
@@ -204,6 +204,16 @@ describe("an aborted deploy train is a no-op (ticket 05)", () => {
beforeEach(async () => {
storage = new MockStorage();
await storage.ready;
+ const originalFetch = globalThis.fetch.bind(globalThis);
+ vi.stubGlobal("fetch", (input: RequestInfo | URL, init?: RequestInit) => {
+ const inputUrl =
+ typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
+ const url = new URL(inputUrl);
+ if (url.origin === "https://cdn.example.test") {
+ return originalFetch(new URL(`${url.pathname}${url.search}`, storage.origin), init);
+ }
+ return originalFetch(input, init);
+ });
configDir = mkdtempSync(join(tmpdir(), "bos-aborted-train-"));
savedEnv = {
BOS_BUNDLE_CDN_ORIGIN: process.env.BOS_BUNDLE_CDN_ORIGIN,
@@ -241,6 +251,7 @@ describe("an aborted deploy train is a no-op (ticket 05)", () => {
});
afterEach(() => {
+ vi.unstubAllGlobals();
storage.close();
rmSync(configDir, { recursive: true, force: true });
for (const [key, value] of Object.entries(savedEnv)) {
diff --git a/packages/everything-dev/tests/ui/api.test.ts b/packages/everything-dev/tests/ui/api.test.ts
index b55d11dc..9e418118 100644
--- a/packages/everything-dev/tests/ui/api.test.ts
+++ b/packages/everything-dev/tests/ui/api.test.ts
@@ -10,8 +10,11 @@ import {
const contextHolder = vi.hoisted(() => ({
context: {} as ClientRouterContext,
+ notifyError: vi.fn(),
}));
+vi.mock("sonner", () => ({ toast: { error: contextHolder.notifyError } }));
+
vi.mock("@tanstack/react-router", () => ({
useRouter: () => ({ options: { context: contextHolder.context } }),
}));
@@ -43,6 +46,7 @@ function pingClient(client: unknown) {
beforeEach(() => {
stubFetch();
+ contextHolder.notifyError.mockClear();
});
afterEach(() => {
@@ -82,6 +86,47 @@ describe("browser singleton", () => {
(globalThis as { window?: unknown }).window = originalWindow;
});
+ it("resolves connection notices when the request fails so language changes are respected", async () => {
+ vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new TypeError("Failed to fetch")));
+ let language = "es";
+ const connectionError = vi.fn(() => ({
+ title: language === "es" ? "Conexión no disponible" : "Connexion indisponible",
+ description: language === "es" ? "Inténtalo de nuevo." : "Réessayez.",
+ }));
+ const client = createApiClient({
+ hostUrl: "https://translated.test",
+ rpcBase: "/api/rpc",
+ connectionError,
+ });
+ expect(connectionError).not.toHaveBeenCalled();
+ await pingClient(client);
+ await vi.waitFor(() =>
+ expect(contextHolder.notifyError).toHaveBeenLastCalledWith("Conexión no disponible", {
+ id: "api-connection-error",
+ description: "Inténtalo de nuevo.",
+ }),
+ );
+ language = "fr";
+ await pingClient(client);
+ await vi.waitFor(() =>
+ expect(contextHolder.notifyError).toHaveBeenLastCalledWith("Connexion indisponible", {
+ id: "api-connection-error",
+ description: "Réessayez.",
+ }),
+ );
+ });
+
+ it("keeps the default connection notice for apps without a message resolver", async () => {
+ vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new TypeError("Failed to fetch")));
+ await pingClient(createApiClient({ hostUrl: "https://offline.test", rpcBase: "/api/rpc" }));
+ await vi.waitFor(() =>
+ expect(contextHolder.notifyError).toHaveBeenCalledWith("Unable to connect to API", {
+ id: "api-connection-error",
+ description: "The API is currently unavailable. Please try again later.",
+ }),
+ );
+ });
+
it("memoizes per endpoint", () => {
const first = createApiClient(cfg);
const second = createApiClient(cfg);
@@ -126,3 +171,35 @@ describe("context hooks", () => {
expect(usePluginClients()).toEqual({});
});
});
+
+describe("rate-limit retry", () => {
+ it("retries a 429 with backoff until the request succeeds", async () => {
+ let calls = 0;
+ globalThis.fetch = vi.fn(async () => {
+ calls += 1;
+ if (calls < 3) {
+ return new Response("{}", { status: 429, headers: { "retry-after": "0" } });
+ }
+ return new Response(JSON.stringify({ json: "pong" }), {
+ status: 200,
+ headers: { "content-type": "application/json" },
+ });
+ }) as unknown as typeof fetch;
+
+ const clients = createServiceClients(cfg, ["api"]);
+ await pingClient(clients.api);
+ expect(calls).toBe(3);
+ });
+
+ it("gives up after the retry budget and surfaces the 429", async () => {
+ let calls = 0;
+ globalThis.fetch = vi.fn(async () => {
+ calls += 1;
+ return new Response("{}", { status: 429, headers: { "retry-after": "0" } });
+ }) as unknown as typeof fetch;
+
+ const clients = createServiceClients(cfg, ["api"]);
+ await expect((clients.api as { ping(): Promise }).ping()).rejects.toThrow();
+ expect(calls).toBe(4);
+ });
+});
diff --git a/packages/everything-dev/tests/ui/auth-guards.test.ts b/packages/everything-dev/tests/ui/auth-guards.test.ts
index 581b08a1..5b2fd0db 100644
--- a/packages/everything-dev/tests/ui/auth-guards.test.ts
+++ b/packages/everything-dev/tests/ui/auth-guards.test.ts
@@ -25,7 +25,7 @@ const bannedSession = {
session: { id: "session-1", activeOrganizationId: null },
} as unknown as SessionData;
-function createAuthClientMock(responses: Array): AuthClient {
+function createAuthClientMock(responses: Array): AuthClient {
let call = 0;
return {
getSession: vi.fn(async () => {
@@ -41,7 +41,7 @@ function createContext(queryClient: QueryClient, authClient: AuthClient) {
queryClient,
authClient,
apiClient: {} as ApiClient,
- session: undefined as SessionData | null | undefined,
+ session: undefined as SessionData,
};
}
diff --git a/packages/everything-dev/tests/ui/hydrate.test.tsx b/packages/everything-dev/tests/ui/hydrate.test.tsx
index c1889b68..64698de1 100644
--- a/packages/everything-dev/tests/ui/hydrate.test.tsx
+++ b/packages/everything-dev/tests/ui/hydrate.test.tsx
@@ -8,6 +8,7 @@ const bootstrap = vi.hoisted(() => ({
createRouter: vi.fn(() => ({ router: {} })),
render: vi.fn(),
hydrateRoot: vi.fn(),
+ toastError: vi.fn(),
coreRouteConfig: {
routeConfigLoaders: {},
rootMeta: { head: () => ({ meta: [{ name: "core", content: "1" }] }) },
@@ -34,6 +35,8 @@ vi.mock("react-dom/client", () => ({
hydrateRoot: bootstrap.hydrateRoot,
}));
+vi.mock("sonner", () => ({ toast: { error: bootstrap.toastError } }));
+
const composeMocks = vi.hoisted(() => ({
loadRemote: vi.fn(),
registerRemotes: vi.fn(),
@@ -121,11 +124,30 @@ const runHydrate = async (config: Record) => {
};
describe("client bootstrap", () => {
+ it("forwards the SSR document language into the composed router context", async () => {
+ const previousLocale = document.documentElement.lang;
+ document.documentElement.lang = "fr";
+ document.documentElement.setAttribute("data-everything-ssr", "");
+ try {
+ await runHydrate(bootstrap.config);
+ expect(bootstrap.createRouter).toHaveBeenCalledWith(
+ expect.objectContaining({
+ context: expect.objectContaining({ locale: "fr" }),
+ }),
+ );
+ } finally {
+ document.documentElement.lang = previousLocale;
+ }
+ });
+
it("rejects missing config before loading the router and can retry", async () => {
bootstrap.config.hostUrl = "";
await expect(runHydrate(bootstrap.config)).rejects.toThrow("Missing hostUrl or rpcBase");
expect(bootstrap.routerLoads).toBe(0);
expect(bootstrap.createRouter).not.toHaveBeenCalled();
+ expect(
+ document.querySelector('[data-testid="application-startup-error"]')?.textContent,
+ ).toContain("Reload");
expect(window.__EVERYTHING_DEV_HYDRATE_PROMISE__).toBeUndefined();
bootstrap.config.hostUrl = "https://example.test";
@@ -333,6 +355,13 @@ describe("client bootstrap", () => {
// the page client-renders the degraded tree.
expect(bootstrap.hydrateRoot).not.toHaveBeenCalled();
expect(bootstrap.render).toHaveBeenCalledOnce();
+ expect(bootstrap.toastError).toHaveBeenCalledWith(
+ "Some application features couldn't load",
+ expect.objectContaining({
+ duration: Number.POSITIVE_INFINITY,
+ action: expect.objectContaining({ label: "Reload" }),
+ }),
+ );
});
it("drops only the failed remote and composes the healthy subset", async () => {
@@ -416,5 +445,6 @@ describe("client bootstrap", () => {
);
expect(bootstrap.hydrateRoot).not.toHaveBeenCalled();
expect(bootstrap.render).toHaveBeenCalledOnce();
+ expect(bootstrap.toastError).toHaveBeenCalledOnce();
});
});
diff --git a/packages/everything-dev/tests/ui/i18n-browser.test.tsx b/packages/everything-dev/tests/ui/i18n-browser.test.tsx
new file mode 100644
index 00000000..53115bef
--- /dev/null
+++ b/packages/everything-dev/tests/ui/i18n-browser.test.tsx
@@ -0,0 +1,112 @@
+// @vitest-environment jsdom
+import { act } from "react";
+import { createRoot, type Root } from "react-dom/client";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { createLocaleRuntime, readLocaleCookie } from "../../src/ui/i18n";
+
+const locales = ["en", "es", "fr", "zh"] as const;
+let container: HTMLDivElement;
+let root: Root;
+let sequence = 0;
+beforeEach(() => {
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
+ container = document.createElement("div");
+ document.body.append(container);
+ root = createRoot(container);
+});
+afterEach(async () => {
+ await act(async () => root.unmount());
+ container.remove();
+ vi.unstubAllGlobals();
+});
+
+describe("language persistence across browser bundles", () => {
+ it("shares account selection with the auth bundle, cookie, and document language", async () => {
+ const cookieName = `bundle_language_${sequence++}`;
+ const main = createLocaleRuntime({ locales, defaultLocale: "en", cookieName });
+ const auth = createLocaleRuntime({ locales, defaultLocale: "en", cookieName });
+ const save = vi.fn(async () => undefined);
+ let select: ReturnType["selectLocale"];
+ function AuthSettings() {
+ const language = auth.useLocale();
+ select = language.selectLocale;
+ return {language.locale} ;
+ }
+ await act(async () =>
+ root.render(
+ ({})}
+ initialLocale="es"
+ preferredLocale="fr"
+ onLocaleChange={save}
+ >
+ ({})}>
+
+
+ ,
+ ),
+ );
+ expect(container.textContent).toBe("fr");
+ expect(readLocaleCookie(document.cookie, cookieName)).toBe("fr");
+ await act(async () => select("zh"));
+ expect(save).toHaveBeenCalledExactlyOnceWith("zh");
+ expect(container.textContent).toBe("zh");
+ expect(main.getLocale()).toBe("zh");
+ expect(document.documentElement.lang).toBe("zh");
+ expect(readLocaleCookie(document.cookie, cookieName)).toBe("zh");
+ });
+
+ it("keeps the previous language and cookie when saving the account preference fails", async () => {
+ const cookieName = `failed_language_${sequence++}`;
+ const runtime = createLocaleRuntime({ locales, defaultLocale: "en", cookieName });
+ document.cookie = `${cookieName}=es; Path=/`;
+ let select: ReturnType["selectLocale"];
+ function Settings() {
+ const language = runtime.useLocale();
+ select = language.selectLocale;
+ return {language.locale} ;
+ }
+ await act(async () =>
+ root.render(
+ ({})}
+ onLocaleChange={async () => {
+ throw new Error("save failed");
+ }}
+ >
+
+ ,
+ ),
+ );
+ await act(async () => expect(select("fr")).rejects.toThrow("save failed"));
+ expect(container.textContent).toBe("es");
+ expect(readLocaleCookie(document.cookie, cookieName)).toBe("es");
+ });
+
+ it("keeps the last account language after signing out and remounting", async () => {
+ const cookieName = `signed_out_language_${sequence++}`;
+ const runtime = createLocaleRuntime({ locales, defaultLocale: "en", cookieName });
+ document.cookie = `${cookieName}=es; Path=/`;
+ function Settings() {
+ return {runtime.useLocale().locale} ;
+ }
+ await act(async () =>
+ root.render(
+ ({})} preferredLocale="fr">
+
+ ,
+ ),
+ );
+ await act(async () =>
+ root.render(
+ ({})}>
+
+ ,
+ ),
+ );
+ expect(container.textContent).toBe("fr");
+ expect(readLocaleCookie(document.cookie, cookieName)).toBe("fr");
+ const reloaded = createLocaleRuntime({ locales, defaultLocale: "en", cookieName });
+ expect(reloaded.detectLocale()).toBe("fr");
+ });
+});
diff --git a/packages/everything-dev/tests/ui/i18n.test.ts b/packages/everything-dev/tests/ui/i18n.test.ts
index 0b4cfeaf..11e6621d 100644
--- a/packages/everything-dev/tests/ui/i18n.test.ts
+++ b/packages/everything-dev/tests/ui/i18n.test.ts
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
import {
createLocaleRuntime,
matchLocale,
+ readAcceptLanguage,
readLocaleCookie,
resolveLocale,
serializeLocaleCookie,
@@ -18,24 +19,33 @@ function resolve(overrides: Partial[0]> = {}) {
browserLocales: [],
locales,
defaultLocale: "en",
- cookieName: "app_locale",
+ cookieName: "citynode_locale",
...overrides,
});
}
describe("shared locale resolution", () => {
+ it("honors quality preferences and excludes rejected header languages", () => {
+ expect(readAcceptLanguage("de;q=0, fr-CA;q=0.7, es;q=0.9, en;q=invalid")).toEqual([
+ "es",
+ "fr-CA",
+ ]);
+ expect(resolve({ browserLocales: readAcceptLanguage("fr-CA, es;q=0.8") })).toBe("fr");
+ });
it("uses account preference before cookie and browser language", () => {
expect(
resolve({
preferredLocale: "zh-CN",
- cookie: "app_locale=fr",
+ cookie: "citynode_locale=fr",
browserLocales: ["es-MX"],
}),
).toBe("zh");
});
it("uses a persisted locale before browser language", () => {
- expect(resolve({ cookie: "theme=dark; app_locale=fr", browserLocales: ["es-MX"] })).toBe("fr");
+ expect(resolve({ cookie: "theme=dark; citynode_locale=fr", browserLocales: ["es-MX"] })).toBe(
+ "fr",
+ );
});
it("matches the first supported browser language", () => {
@@ -43,8 +53,8 @@ describe("shared locale resolution", () => {
});
it("falls back to English for unsupported or malformed preferences", () => {
- expect(resolve({ cookie: "app_locale=invalid", browserLocales: ["de-DE"] })).toBe("en");
- expect(resolve({ cookie: "app_locale=%E0%A4%A", browserLocales: [] })).toBe("en");
+ expect(resolve({ cookie: "citynode_locale=invalid", browserLocales: ["de-DE"] })).toBe("en");
+ expect(resolve({ cookie: "citynode_locale=%E0%A4%A", browserLocales: [] })).toBe("en");
});
});
@@ -55,15 +65,61 @@ describe("shared locale persistence", () => {
});
it("reads and serializes the locale cookie", () => {
- expect(readLocaleCookie("theme=dark; app_locale=zh", "app_locale")).toBe("zh");
- expect(serializeLocaleCookie("app_locale", "es", false)).toBe(
- "app_locale=es; Path=/; Max-Age=31536000; SameSite=Lax",
+ expect(readLocaleCookie("theme=dark; citynode_locale=zh", "citynode_locale")).toBe("zh");
+ expect(serializeLocaleCookie("citynode_locale", "es", false)).toBe(
+ "citynode_locale=es; Path=/; Max-Age=31536000; SameSite=Lax",
);
- expect(serializeLocaleCookie("app_locale", "es", true)).toContain("; Secure");
+ expect(serializeLocaleCookie("citynode_locale", "es", true)).toContain("; Secure");
});
});
describe("shared locale provider", () => {
+ it("renders the request locale and account preference without leaking between requests", () => {
+ const runtime = createLocaleRuntime({
+ locales,
+ defaultLocale: "en",
+ cookieName: "server_locale",
+ });
+ function Greeting() {
+ const t = runtime.useTranslation<"greeting">();
+ return createElement("span", null, t("greeting"));
+ }
+ const messages = (locale: string) => ({
+ greeting: locale === "fr" ? "Bonjour" : locale === "es" ? "Hola" : "Hello",
+ });
+ const render = (initialLocale: string, preferredLocale?: string) =>
+ renderToString(
+ createElement(
+ runtime.LocaleProvider,
+ { messages, initialLocale, preferredLocale },
+ createElement(Greeting),
+ ),
+ );
+ expect(render("fr")).toContain("Bonjour");
+ expect(render("fr", "es")).toContain("Hola");
+ expect(render("en")).toContain("Hello");
+ });
+
+ it("shares the server locale across separately configured bundle providers", () => {
+ const main = createLocaleRuntime({ locales, defaultLocale: "en", cookieName: "bundle_locale" });
+ const auth = createLocaleRuntime({ locales, defaultLocale: "en", cookieName: "bundle_locale" });
+ function AuthGreeting() {
+ const t = auth.useTranslation<"greeting">();
+ return createElement("span", null, t("greeting"));
+ }
+ const html = renderToString(
+ createElement(
+ main.LocaleProvider,
+ { initialLocale: "fr", messages: () => ({}) },
+ createElement(
+ auth.LocaleProvider,
+ { messages: (locale) => ({ greeting: locale === "fr" ? "Bonjour" : "Hello" }) },
+ createElement(AuthGreeting),
+ ),
+ ),
+ );
+ expect(html).toContain("Bonjour");
+ });
it("renders when the server runtime exposes navigator without language preferences", () => {
const navigatorDescriptor = Object.getOwnPropertyDescriptor(globalThis, "navigator");
Object.defineProperty(globalThis, "navigator", { configurable: true, value: {} });
@@ -72,7 +128,7 @@ describe("shared locale provider", () => {
const runtime = createLocaleRuntime({
locales,
defaultLocale: "en",
- cookieName: "app_locale",
+ cookieName: "citynode_locale",
});
const html = renderToString(
createElement(
diff --git a/packages/everything-dev/tests/ui/router-locale.test.tsx b/packages/everything-dev/tests/ui/router-locale.test.tsx
new file mode 100644
index 00000000..ff9cf3ef
--- /dev/null
+++ b/packages/everything-dev/tests/ui/router-locale.test.tsx
@@ -0,0 +1,64 @@
+import { createRootRouteWithContext, createRoute, Outlet } from "@tanstack/react-router";
+import { describe, expect, it } from "vitest";
+import { createServerRouterModule } from "../../src/ui/router-server";
+import type { RenderOptionsWithApi, RouterContextWithApi } from "../../src/ui/types";
+
+const root = createRootRouteWithContext()({
+ component: function Document() {
+ const { locale } = root.useRouteContext();
+ return (
+
+
+
+
+
+
+ );
+ },
+});
+const home = createRoute({ getParentRoute: () => root, path: "/", component: () => Home });
+const about = createRoute({
+ getParentRoute: () => root,
+ path: "/about",
+ component: () => About ,
+});
+const module = createServerRouterModule({
+ defaultRouteTree: root.addChildren([home, about]),
+ locale: { locales: ["en", "es", "fr", "zh"], defaultLocale: "en", cookieName: "citynode_locale" },
+});
+const options = {
+ runtimeConfig: {
+ hostUrl: "http://localhost",
+ rpcBase: "/api",
+ authBase: "/api/auth",
+ networkId: "mainnet",
+ auth: { variables: { siwn: { recipient: "app.near" } } },
+ },
+ session: null,
+} as RenderOptionsWithApi;
+
+async function render(path: string, headers: HeadersInit) {
+ const result = await module.renderToStream(
+ new Request(`http://localhost${path}`, { headers }),
+ options,
+ );
+ expect(result.headers.get("Vary")).toContain("Cookie");
+ expect(result.headers.get("Vary")).toContain("Accept-Language");
+ return new Response(result.stream).text();
+}
+
+describe("request-scoped SSR locale", () => {
+ it("renders the requested route and cookie language independently for concurrent requests", async () => {
+ const [french, spanish, chinese] = await Promise.all([
+ render("/about", { cookie: "citynode_locale=fr", "accept-language": "es" }),
+ render("/", { "accept-language": "es-MX, en;q=0.5" }),
+ render("/about", { cookie: "citynode_locale=zh" }),
+ ]);
+ expect(french).toContain(' {
+ it("does not cache a transient failure as a signed-out session and recovers on retry", async () => {
+ const session = { user: { id: "member" }, session: { activeOrganizationId: "org" } };
+ const getSession = vi
+ .fn()
+ .mockResolvedValueOnce({ data: null, error: { message: "Auth temporarily unavailable" } })
+ .mockResolvedValueOnce({ data: session, error: null });
+ const auth = { getSession } as unknown as AuthClient;
+ const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ await expect(client.fetchQuery(sessionQueryOptions(auth))).rejects.toThrow(
+ "Auth temporarily unavailable",
+ );
+ expect(client.getQueryData(sessionQueryKey)).toBeUndefined();
+ await client.fetchQuery(sessionQueryOptions(auth));
+ expect(client.getQueryData(sessionQueryKey)).toEqual(session);
+ client.clear();
+ });
+
+ it("preserves the current organization when a background session read fails", async () => {
+ const session = { user: { id: "member" }, session: { activeOrganizationId: "org" } };
+ const auth = {
+ getSession: async () => ({ data: null, error: { message: "Network failure" } }),
+ } as unknown as AuthClient;
+ const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ client.setQueryData(sessionQueryKey, session);
+ await expect(client.fetchQuery({ ...sessionQueryOptions(auth), staleTime: 0 })).rejects.toThrow(
+ "Network failure",
+ );
+ expect(client.getQueryData(sessionQueryKey)).toEqual(session);
+ client.clear();
+ });
+});
diff --git a/packages/everything-dev/tests/ui/tenant.test.ts b/packages/everything-dev/tests/ui/tenant.test.ts
index 66584bcd..a8767425 100644
--- a/packages/everything-dev/tests/ui/tenant.test.ts
+++ b/packages/everything-dev/tests/ui/tenant.test.ts
@@ -6,6 +6,7 @@ import {
computeSsrEntryIntegrity,
computeSubresourceIntegrity,
computeUiEntryIntegrity,
+ createTenantConfigDraftSchema,
diffDraft,
draftUiOverride,
emptyTenantConfigDraft,
@@ -160,6 +161,40 @@ describe("gatewayForAccount", () => {
});
describe("tenantConfigDraftSchema", () => {
+ it("uses localized pin validation while preserving bundle constraints", () => {
+ const schema = createTenantConfigDraftSchema({
+ manifest: "Formato de manifiesto no válido",
+ pinPair: "Falta la integridad del manifiesto",
+ integrityMode: "Elige un modo de integridad",
+ });
+ const draft = {
+ ...emptyTenantConfigDraft,
+ title: "Chicago",
+ description: "Chicago",
+ uiProduction: "https://cdn.example.com/ui",
+ uiManifest: "versions/8f3ac1d2feedbeef.json",
+ };
+ const pin = schema.safeParse(draft);
+ expect(pin.error?.issues).toContainEqual(
+ expect.objectContaining({
+ path: ["uiPinIntegrity"],
+ message: "Falta la integridad del manifiesto",
+ }),
+ );
+ const malformed = schema.safeParse({ ...draft, uiManifest: "versions/invalid.json" });
+ expect(malformed.error?.issues).toContainEqual(
+ expect.objectContaining({ path: ["uiManifest"], message: "Formato de manifiesto no válido" }),
+ );
+ const mixed = schema.safeParse({
+ ...draft,
+ uiPinIntegrity: "sha384-pin",
+ uiIntegrity: "sha384-direct",
+ });
+ expect(mixed.error?.issues).toContainEqual(
+ expect.objectContaining({ path: ["uiManifest"], message: "Elige un modo de integridad" }),
+ );
+ });
+
it("accepts a metadata-only draft", () => {
const result = tenantConfigDraftSchema.safeParse({
...emptyTenantConfigDraft,
@@ -431,7 +466,7 @@ describe("integrity preflight", () => {
const stubBundle = (content: string) => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
- const url = String(input);
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
if (url.includes("remoteEntry.server.js")) return new Response(`${content}-server`);
return new Response(content);
});
@@ -473,7 +508,7 @@ describe("integrity preflight", () => {
it("verifies a pinned slot by SRI-comparing the manifest document", async () => {
const manifestBody = JSON.stringify({ entry: "remoteEntry.aaa.js" });
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
- const url = String(input);
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
if (url.endsWith("versions/8f3ac1d2feedbeef.json")) {
return new Response(manifestBody);
}
@@ -502,7 +537,7 @@ describe("integrity preflight", () => {
it("rejects the html landing page a bare base url serves", async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
- const url = String(input);
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
if (url === "https://cdn.example.com/ui/remoteEntry.js")
return new Response("console.log('hi')");
return new Response("landing page");
diff --git a/packages/everything-dev/tests/unit/auth-login.test.ts b/packages/everything-dev/tests/unit/auth-login.test.ts
index 2b6dc338..b14b6fc0 100644
--- a/packages/everything-dev/tests/unit/auth-login.test.ts
+++ b/packages/everything-dev/tests/unit/auth-login.test.ts
@@ -108,17 +108,21 @@ describe("device login client", () => {
expect(approval.accountId).toBe("alice.near");
const claim = calls.find((call) => call.url.includes("/device-link/claim"));
- expect(JSON.parse(String(claim?.init.body ?? "{}"))).toMatchObject({
- token: "session-token-1",
- client_id: "bos-cli",
- });
+ expect(JSON.parse((await new Response(claim?.init.body ?? null).text()) || "{}")).toMatchObject(
+ {
+ token: "session-token-1",
+ client_id: "bos-cli",
+ },
+ );
const keyCreate = calls.find((call) => call.url.includes("/api-key/create"));
const keyHeaders = new Headers(keyCreate?.init.headers);
expect(keyHeaders.get("cookie")).toBe(CLAIMED_COOKIE);
expect(keyHeaders.get("origin")).toBe(SITE);
- const keyBody = JSON.parse(String(keyCreate?.init.body ?? "{}")) as {
+ const keyBody = JSON.parse(
+ (await new Response(keyCreate?.init.body ?? null).text()) || "{}",
+ ) as {
configId: string;
name: string;
expiresIn: number;
diff --git a/packages/everything-dev/tests/unit/config-resolved.test.ts b/packages/everything-dev/tests/unit/config-resolved.test.ts
index eae92e79..434ba496 100644
--- a/packages/everything-dev/tests/unit/config-resolved.test.ts
+++ b/packages/everything-dev/tests/unit/config-resolved.test.ts
@@ -13,6 +13,7 @@ import {
writeResolvedConfig,
} from "../../src/config";
import { openResolution } from "../../src/resolution/session";
+import { BosConfigInputSchema } from "../../src/types";
vi.mock("../../src/version-manifest-resolve", () => ({
resolveSlotVersion: vi.fn(async () => ({
@@ -55,6 +56,16 @@ vi.mock("../../src/http-client", async (importOriginal) => {
};
});
+describe("authored staging configuration", () => {
+ it("preserves the staging account used by the key publication prompt", () => {
+ const parsed = BosConfigInputSchema.parse({
+ account: "production.near",
+ staging: { domain: "staging.example.test", account: "staging.near" },
+ });
+ expect(parsed.staging).toEqual({ domain: "staging.example.test", account: "staging.near" });
+ });
+});
+
describe("findConfigPath cache", () => {
afterEach(() => {
resetConfigPathCache();
diff --git a/packages/everything-dev/tests/unit/db-run-migrations.test.ts b/packages/everything-dev/tests/unit/db-run-migrations.test.ts
index 8cbf9800..b9e96cfb 100644
--- a/packages/everything-dev/tests/unit/db-run-migrations.test.ts
+++ b/packages/everything-dev/tests/unit/db-run-migrations.test.ts
@@ -1,7 +1,7 @@
import { PGlite } from "@electric-sql/pglite";
import { sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/pglite";
-import { Effect } from "effect";
+import { Cause, Effect, Exit } from "effect";
import { describe, expect, it } from "vitest";
import { migrate as apiMigrate } from "../../../../api/src/db/migrate";
import { migrate as proposalsMigrate } from "../../../../plugins/proposals/src/db/migrate";
@@ -23,6 +23,8 @@ type Runner = (
schemaName?: string,
) => Effect.Effect;
+const apiMigrationRunner: Runner = apiMigrate;
+
function migration(idx: number, tag: string, statements: string[]): Migration {
return {
idx,
@@ -40,6 +42,17 @@ function makeDb() {
const JOURNAL = { schema: "drizzle", table: "__drizzle_migrations", slug: "test" } as const;
+async function runMigration(effect: Effect.Effect): Promise {
+ const exit = await Effect.runPromiseExit(effect);
+ if (Exit.isSuccess(exit)) return exit.value;
+ throw Cause.squash(exit.cause);
+}
+
+function runApiMigration(db: MigrationDatabase, migrations: Migration[]): Promise {
+ const effect = apiMigrationRunner(db, migrations, JOURNAL);
+ return runMigration(effect);
+}
+
describe("db migration runners (008 characterization)", () => {
it("fresh schema: sequential migrations apply, journal tracks hashes, rerun is a no-op", async () => {
const db = makeDb();
@@ -51,10 +64,10 @@ describe("db migration runners (008 characterization)", () => {
]),
];
- const applied = await Effect.runPromise(apiMigrate(db as never, migrations, JOURNAL) as never);
+ const applied = await runApiMigration(db as unknown as MigrationDatabase, migrations);
expect(applied).toBe(2);
- const rerun = await Effect.runPromise(apiMigrate(db as never, migrations, JOURNAL) as never);
+ const rerun = await runApiMigration(db as unknown as MigrationDatabase, migrations);
expect(rerun).toBe(0);
const rows = (await db.execute(
@@ -76,7 +89,7 @@ describe("db migration runners (008 characterization)", () => {
]),
];
- const applied = await Effect.runPromise(runner(db, migrations, JOURNAL));
+ const applied = await runMigration(runner(db, migrations, JOURNAL));
expect(applied).toBe(1);
const exists = (await db.execute(
@@ -95,7 +108,7 @@ describe("db migration runners (008 characterization)", () => {
migration(1, "fresh", ['CREATE TABLE "t_fresh" (id int)']),
];
- const applied = await Effect.runPromise(apiMigrate(db as never, migrations, JOURNAL) as never);
+ const applied = await runApiMigration(db as unknown as MigrationDatabase, migrations);
expect(applied).toBe(2);
const journal = (await db.execute(
diff --git a/packages/everything-dev/tests/unit/docker-autostart.test.ts b/packages/everything-dev/tests/unit/docker-autostart.test.ts
index 9a82ca7e..eec6e09b 100644
--- a/packages/everything-dev/tests/unit/docker-autostart.test.ts
+++ b/packages/everything-dev/tests/unit/docker-autostart.test.ts
@@ -1,7 +1,29 @@
-import { describe, expect, it } from "vitest";
-import { isDockerTestMode, shouldAutoStartDocker, takeTail } from "../../src/infra/docker";
+import { describe, expect, it, vi } from "vitest";
+import {
+ isDockerTestMode,
+ runDockerComposeUp,
+ shouldAutoStartDocker,
+ takeTail,
+} from "../../src/infra/docker";
import type { PreflightFailure } from "../../src/infra/preflight";
+const command = vi.hoisted(() => vi.fn());
+vi.mock("execa", () => ({ execa: command }));
+
+describe("runDockerComposeUp", () => {
+ it("preserves line boundaries and failure diagnostics from both output streams", async () => {
+ command.mockResolvedValue({
+ exitCode: 1,
+ stdout: "Starting database\n",
+ stderr: "\u001b[31mDatabase failed\u001b[0m\nCheck credentials\n",
+ });
+ expect(await runDockerComposeUp("/test/project")).toEqual({
+ ok: false,
+ tail: "Starting database\nDatabase failed\nCheck credentials",
+ });
+ });
+});
+
const unreachable = (secret: string): PreflightFailure => ({
secret,
host: "localhost",
diff --git a/packages/everything-dev/tests/unit/process-kill.test.ts b/packages/everything-dev/tests/unit/process-kill.test.ts
index bd832a5b..e9d65404 100644
--- a/packages/everything-dev/tests/unit/process-kill.test.ts
+++ b/packages/everything-dev/tests/unit/process-kill.test.ts
@@ -13,7 +13,12 @@ import { killProcessGroupEscalating, reapGroup } from "../../src/process-kill";
import { isPidAlive } from "../../src/process-registry";
let tempDir = mkdtempSync(join(tmpdir(), "process-kill-"));
-const cleanup: string[] = [];
+const cleanup: number[] = [];
+
+function processGroupId(pid: number | undefined): number {
+ if (pid === undefined) throw new Error("Child process did not expose a pid");
+ return -pid;
+}
const fakeLsof = (script: string): string => {
const path = join(tempDir, `lsof-${Math.random().toString(36).slice(2)}`);
@@ -23,7 +28,7 @@ const fakeLsof = (script: string): string => {
const spawnSleeper = (): { pid: number; wait: Promise } => {
const child = spawn("sleep", ["300"], { detached: true, stdio: "ignore" });
- cleanup.push(-child.pid);
+ cleanup.push(processGroupId(child.pid));
return {
pid: child.pid,
wait: new Promise((resolve) => child.once("exit", resolve)),
@@ -119,7 +124,7 @@ describe("process-kill escalation", () => {
const path = join(tempDir, `parent-${Date.now()}.sh`);
writeFileSync(path, script, { mode: 0o755 });
const parent = spawn(path, { detached: true, stdio: "ignore" });
- cleanup.push(-parent.pid);
+ cleanup.push(processGroupId(parent.pid));
await new Promise((resolve) => setTimeout(resolve, 300));
const members = await groupMembersOf(parent.pid);
expect(members.length).toBeGreaterThanOrEqual(2);
@@ -155,7 +160,7 @@ function spawnSleeperWithSigtermIgnored(): { pid: number; wait: Promise }
const path = join(tempDir, `sleeper-${Date.now()}.sh`);
writeFileSync(path, script, { mode: 0o755 });
const child = spawn(path, { detached: true, stdio: "ignore" });
- cleanup.push(-child.pid);
+ cleanup.push(processGroupId(child.pid));
return {
pid: child.pid,
wait: new Promise((resolve) => child.once("exit", resolve)),
diff --git a/packages/everything-dev/tests/unit/publish-pin-verification.test.ts b/packages/everything-dev/tests/unit/publish-pin-verification.test.ts
new file mode 100644
index 00000000..ac501a4f
--- /dev/null
+++ b/packages/everything-dev/tests/unit/publish-pin-verification.test.ts
@@ -0,0 +1,159 @@
+import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import type { BosConfig } from "../../src/types";
+
+const {
+ verifyRollbackSnapshotMock,
+ fetchBosConfigFromFastKvMock,
+ resolveSigningStrategyMock,
+ submitRegistryWriteMock,
+ openResolutionMock,
+ buildWorkspaceTargetsMock,
+} = vi.hoisted(() => ({
+ verifyRollbackSnapshotMock: vi.fn(),
+ fetchBosConfigFromFastKvMock: vi.fn(),
+ resolveSigningStrategyMock: vi.fn(),
+ submitRegistryWriteMock: vi.fn(),
+ openResolutionMock: vi.fn(),
+ buildWorkspaceTargetsMock: vi.fn(),
+}));
+
+vi.mock("../../src/rollback", () => ({
+ verifyRollbackSnapshot: verifyRollbackSnapshotMock,
+}));
+
+vi.mock("../../src/fastkv", async (importOriginal) => {
+ const actual = await importOriginal();
+ return { ...actual, fetchBosConfigFromFastKv: fetchBosConfigFromFastKvMock };
+});
+
+vi.mock("../../src/near-signer", async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ resolveSigningStrategy: resolveSigningStrategyMock,
+ submitRegistryWrite: submitRegistryWriteMock,
+ };
+});
+
+vi.mock("../../src/resolution/session", () => ({
+ openResolution: openResolutionMock,
+}));
+
+vi.mock("../../src/build", async (importOriginal) => {
+ const actual = await importOriginal();
+ return { ...actual, buildWorkspaceTargets: buildWorkspaceTargetsMock };
+});
+
+import { publishToFastKv } from "../../src/publish";
+
+const bosConfig = {
+ account: "dev.everything.near",
+ domain: "dev.everything.dev",
+ cdn: { origin: "https://cdn.example.test" },
+ app: {
+ host: {
+ development: "local:host",
+ production: "https://host.example",
+ pin: { manifest: "versions/aaaaaaaaaaaaaaaa.json", integrity: "sha384-host" },
+ },
+ ui: { development: "local:ui", production: "https://ui.example" },
+ api: { development: "local:api", production: "https://api.example" },
+ },
+ plugins: {
+ votes: {
+ development: "local:plugins/votes",
+ production: "https://votes.example",
+ },
+ },
+} as BosConfig;
+
+const baseInput = {
+ bosConfig,
+ runtimeConfig: null,
+ env: "production" as const,
+ build: false,
+ dryRun: false,
+ verbose: false,
+ packages: "",
+ privateKey: "ed25519:0000000000000000000000000000000000000000000000000000000000000000",
+};
+
+let configDir: string;
+
+describe("publishToFastKv pin verification gate", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ configDir = mkdtempSync(join(tmpdir(), "bos-pin-verify-"));
+ writeFileSync(join(configDir, "bos.config.json"), JSON.stringify(bosConfig, null, 2));
+ buildWorkspaceTargetsMock.mockResolvedValue({ built: [], skipped: [], deployResults: [] });
+ openResolutionMock.mockResolvedValue({ config: bosConfig, rawConfig: bosConfig });
+ fetchBosConfigFromFastKvMock.mockResolvedValue(JSON.parse(JSON.stringify(bosConfig)));
+ resolveSigningStrategyMock.mockResolvedValue({
+ strategy: "near-kit",
+ privateKey: "ed25519:test",
+ source: "provided",
+ });
+ });
+
+ afterEach(() => {
+ rmSync(configDir, { recursive: true, force: true });
+ vi.restoreAllMocks();
+ });
+
+ it("blocks the publish when a pinned slot's bytes fail verification", async () => {
+ verifyRollbackSnapshotMock.mockResolvedValue({
+ verifiable: true,
+ ok: false,
+ slots: [
+ {
+ slot: "plugins.registry",
+ manifest: "versions/309a65a9735d13e7.json",
+ ok: false,
+ reason: "manifest fetch failed: 404",
+ },
+ ],
+ });
+
+ const result = await publishToFastKv({ ...baseInput, configDir });
+
+ expect(result.status).toBe("error");
+ expect(result.error).toContain("Publish blocked");
+ expect(result.error).toContain("plugins.registry");
+ expect(result.error).toContain("manifest fetch failed: 404");
+ expect(verifyRollbackSnapshotMock).toHaveBeenCalledWith(
+ expect.objectContaining({ account: "dev.everything.near" }),
+ );
+ expect(submitRegistryWriteMock).not.toHaveBeenCalled();
+ });
+
+ it("publishes when every pinned slot verifies", async () => {
+ verifyRollbackSnapshotMock.mockResolvedValue({
+ verifiable: true,
+ ok: true,
+ slots: [
+ {
+ slot: "app.host",
+ manifest: "versions/aaaaaaaaaaaaaaaa.json",
+ ok: true,
+ },
+ ],
+ });
+
+ const result = await publishToFastKv({ ...baseInput, configDir });
+
+ expect(result.status).toBe("published");
+ expect(submitRegistryWriteMock).not.toHaveBeenCalled();
+ });
+
+ it("a config with no pins skips the gate", async () => {
+ verifyRollbackSnapshotMock.mockResolvedValue({ verifiable: false, ok: false, slots: [] });
+
+ const result = await publishToFastKv({ ...baseInput, configDir });
+
+ expect(result.status).toBe("published");
+ expect(submitRegistryWriteMock).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/everything-dev/tests/unit/resolution-session.test.ts b/packages/everything-dev/tests/unit/resolution-session.test.ts
index 91173df2..5b8551e6 100644
--- a/packages/everything-dev/tests/unit/resolution-session.test.ts
+++ b/packages/everything-dev/tests/unit/resolution-session.test.ts
@@ -200,7 +200,7 @@ describe("openResolution", () => {
production: "https://stage.test/host",
pin: {
manifest: `versions/${hostEntry}.json`,
- integrity: `sha384-${await createHash("sha384").update(JSON.stringify(hostManifest)).digest("base64")}`,
+ integrity: `sha384-${createHash("sha384").update(JSON.stringify(hostManifest)).digest("base64")}`,
},
},
},
diff --git a/packages/everything-dev/tests/unit/storage-upload.test.ts b/packages/everything-dev/tests/unit/storage-upload.test.ts
index e1959188..b5f7655c 100644
--- a/packages/everything-dev/tests/unit/storage-upload.test.ts
+++ b/packages/everything-dev/tests/unit/storage-upload.test.ts
@@ -64,7 +64,7 @@ describe("uploadBundle", () => {
const headers = new Headers(captured!.init.headers);
expect(headers.get("x-api-key")).toBe("edk_test");
expect(headers.get("content-type")).toBe("application/json");
- const body = JSON.parse(String(captured!.init.body));
+ const body = JSON.parse(await new Response(captured!.init.body).text());
expect(body.account).toBe("v1.citynode.near");
expect(body.files[0].contentBase64).toBe(Buffer.from("entry").toString("base64"));
expect(result.integrity["remoteEntry.js"]).toBe("sha384-abc");
@@ -219,7 +219,7 @@ describe("uploadWorkspaceDist", () => {
it("batches files across requests under the per-request ceiling and merges integrity", async () => {
const calls: number[][] = [];
globalThis.fetch = (async (_url: string | URL, init?: RequestInit) => {
- const body = JSON.parse(String(init!.body));
+ const body = JSON.parse(await new Response(init!.body).text());
calls.push(body.files.map((f: { path: string }) => f.path.length));
return new Response(
JSON.stringify({
diff --git a/tests/regression/http/security_headers_test.go b/tests/regression/http/security_headers_test.go
index 651c385c..d05fba5f 100644
--- a/tests/regression/http/security_headers_test.go
+++ b/tests/regression/http/security_headers_test.go
@@ -118,46 +118,58 @@ func TestRateLimiting(t *testing.T) {
time.Sleep(1200 * time.Millisecond)
// Fire 150 requests concurrently so they all land inside a single 1000ms
- // sliding window. The regression env sets RATE_LIMIT_MAX=100, so the burst
- // must trip it. 150 > 100 guarantees at least one 429.
+ // window. The regression env pins RATE_LIMIT_WINDOW_MS=1000 and
+ // RATE_LIMIT_MAX=100.
const workers = 10
const perWorker = 15
- var (
- mu sync.Mutex
- got200, got429 bool
- lastRateLimitBody string
- wg sync.WaitGroup
- )
-
- // Use a plain client (no cookie jar) per worker; http.Client is safe for
- // concurrent use but jars are not.
- for w := 0; w < workers; w++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- plain := &http.Client{}
- for i := 0; i < perWorker; i++ {
- resp, err := plain.Get(baseURL + "/health")
- if err != nil {
- continue
+ // burst fires `path` concurrently with plain (no cookie jar) clients and
+ // records which statuses were observed.
+ burst := func(path string) (got200, got429 bool, lastRateLimitBody string) {
+ var (
+ mu sync.Mutex
+ wg sync.WaitGroup
+ )
+ for w := 0; w < workers; w++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ plain := &http.Client{}
+ for i := 0; i < perWorker; i++ {
+ resp, err := plain.Get(baseURL + path)
+ if err != nil {
+ continue
+ }
+ b, _ := io.ReadAll(resp.Body)
+ resp.Body.Close()
+ mu.Lock()
+ if resp.StatusCode == 200 {
+ got200 = true
+ }
+ if resp.StatusCode == 429 {
+ got429 = true
+ lastRateLimitBody = string(b)
+ }
+ mu.Unlock()
}
- b, _ := io.ReadAll(resp.Body)
- resp.Body.Close()
- mu.Lock()
- if resp.StatusCode == 200 {
- got200 = true
- }
- if resp.StatusCode == 429 {
- got429 = true
- lastRateLimitBody = string(b)
- }
- mu.Unlock()
- }
- }()
+ }()
+ }
+ wg.Wait()
+ return got200, got429, lastRateLimitBody
}
- wg.Wait()
+ // Health probes are exempt from the limiter — the burst must never trip
+ // it, no matter how large (Kubernetes probes share the client budget).
+ probe200, probe429, _ := burst("/health")
+ if !probe200 {
+ t.Fatal("expected health probes to answer 200")
+ }
+ if probe429 {
+ t.Fatal("health probes must be exempt from the rate limit")
+ }
+
+ // A regular API endpoint is limited: 150 > 100 guarantees at least one 429.
+ got200, got429, lastRateLimitBody := burst("/api/ping")
if !got200 {
t.Fatal("expected at least one 200 in the burst before the rate limit applies")
}
@@ -170,6 +182,6 @@ func TestRateLimiting(t *testing.T) {
// After the sliding window passes the server must recover.
time.Sleep(1200 * time.Millisecond)
- status, _, body := regtest.GetRaw(t, client, baseURL+"/health")
+ status, _, body := regtest.GetRaw(t, client, baseURL+"/api/ping")
regtest.MustStatus(t, status, 200, body)
-}
\ No newline at end of file
+}