fix(npm): map openharmony platform to linux binaries (#1072)
Node's `os.platform()` returns `openharmony` on HarmonyPC and on
OpenHarmony's Linux ABI-compatible userspace. The npm wrapper's
platform-asset matrix only covered `linux` / `darwin` / `win32`,
so `npm i -g deepseek-tui` aborted on those hosts with
Unsupported platform: openharmony. Supported platforms: …
even though the existing Linux x64 / arm64 binaries run unchanged
on that environment (OpenHarmony is Linux-ABI-compatible at the
ELF level).
Added a `PLATFORM_ALIASES = { openharmony: "linux" }` indirection
that resolves the raw platform name through the alias map before
the `ASSET_MATRIX` lookup. Genuinely unsupported platforms still
report the raw `os.platform()` value in the error so OS-mismatch
bug reports stay diagnostic.
Four pure-JS regression tests pin the behaviour:
- openharmony x64 → linux x64 binaries
- openharmony arm64 → linux arm64 binaries
- known platforms unchanged by the alias map
- freebsd still reports `Unsupported platform: freebsd`
Harvested from PR #1499 by @CrepuscularIRIS
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,8 +24,18 @@ async function checkAuth(req: Request, env: CommunityAgentEnv): Promise<{ ok: bo
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const ALLOWED_ACTIONS = new Set(["post", "discard"]);
|
||||
const ALLOWED_ORIGINS = new Set(["https://deepseek-tui.com", "https://www.deepseek-tui.com"]);
|
||||
const MAX_BODY_BYTES = 65_536;
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const env = await getAgentEnv();
|
||||
|
||||
const origin = req.headers.get("origin");
|
||||
if (origin && !ALLOWED_ORIGINS.has(origin)) {
|
||||
return NextResponse.json({ error: "forbidden origin" }, { status: 403 });
|
||||
}
|
||||
|
||||
const auth = await checkAuth(req, env);
|
||||
if (!auth.ok) {
|
||||
return NextResponse.json(
|
||||
@@ -34,11 +44,25 @@ export async function POST(req: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
const contentLength = Number(req.headers.get("content-length") ?? "0");
|
||||
if (contentLength > MAX_BODY_BYTES) {
|
||||
return NextResponse.json({ error: "payload too large" }, { status: 413 });
|
||||
}
|
||||
|
||||
const body = await req.json() as { action: string; draftKey: string; editedBody?: string; lang?: "en" | "zh" };
|
||||
const { action, draftKey, editedBody, lang } = body;
|
||||
|
||||
if (!draftKey) {
|
||||
return NextResponse.json({ error: "missing draftKey" }, { status: 400 });
|
||||
if (!ALLOWED_ACTIONS.has(action)) {
|
||||
return NextResponse.json({ error: "unknown action" }, { status: 400 });
|
||||
}
|
||||
if (typeof draftKey !== "string" || !draftKey || draftKey.length > 256) {
|
||||
return NextResponse.json({ error: "missing or invalid draftKey" }, { status: 400 });
|
||||
}
|
||||
if (editedBody !== undefined && (typeof editedBody !== "string" || editedBody.length > MAX_BODY_BYTES)) {
|
||||
return NextResponse.json({ error: "editedBody too long" }, { status: 413 });
|
||||
}
|
||||
if (lang !== undefined && lang !== "en" && lang !== "zh") {
|
||||
return NextResponse.json({ error: "invalid lang" }, { status: 400 });
|
||||
}
|
||||
|
||||
const draft = await getDraft(env.CURATED_KV, draftKey);
|
||||
@@ -92,5 +116,6 @@ export async function POST(req: Request) {
|
||||
return NextResponse.json({ ok: true, action: "posted" });
|
||||
}
|
||||
|
||||
// ALLOWED_ACTIONS guard above means this is unreachable.
|
||||
return NextResponse.json({ error: "unknown action" }, { status: 400 });
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/lib/community-agent-tasks";
|
||||
import { runFactsDrift } from "@/lib/facts-drift";
|
||||
import { runLinkCheck, runSemanticDrift } from "@/lib/content-watch";
|
||||
import { safeEqual } from "@/lib/community-agent";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -37,8 +38,8 @@ export async function GET(req: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
const auth = req.headers.get("x-cron-secret");
|
||||
if (auth !== env.CRON_SECRET) {
|
||||
const auth = req.headers.get("x-cron-secret") ?? "";
|
||||
if (!(await safeEqual(auth, env.CRON_SECRET))) {
|
||||
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
|
||||
+16
-1
@@ -100,5 +100,20 @@ export async function curate(
|
||||
);
|
||||
|
||||
const parsed = JSON.parse(raw) as Omit<CuratedDispatch, "generatedAt">;
|
||||
return { ...parsed, generatedAt: new Date().toISOString() };
|
||||
return { ...sanitizeDispatch(parsed), generatedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
const SAFE_HREF_RE = /^https:\/\/(?:github\.com|api\.github\.com|deepseek-tui\.com|crates\.io|www\.npmjs\.com|docs\.rs)\//;
|
||||
const FALLBACK_HREF = "https://github.com/Hmbown/deepseek-tui";
|
||||
|
||||
function safeHref(u: unknown): string {
|
||||
return typeof u === "string" && SAFE_HREF_RE.test(u) ? u : FALLBACK_HREF;
|
||||
}
|
||||
|
||||
function sanitizeDispatch(d: Omit<CuratedDispatch, "generatedAt">): Omit<CuratedDispatch, "generatedAt"> {
|
||||
return {
|
||||
...d,
|
||||
highlights: (d.highlights ?? []).map((h) => ({ ...h, href: safeHref(h.href) })),
|
||||
movers: (d.movers ?? []).map((m) => ({ ...m, href: safeHref(m.href) })),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,8 +18,13 @@ export interface RepoFacts {
|
||||
}
|
||||
|
||||
export const FACTS: RepoFacts = {
|
||||
<<<<<<< Updated upstream
|
||||
"generatedAt": "2026-05-12T03:14:50.815Z",
|
||||
"version": "0.8.30",
|
||||
=======
|
||||
"generatedAt": "2026-05-10T15:06:44.698Z",
|
||||
"version": "0.8.27",
|
||||
>>>>>>> Stashed changes
|
||||
"crates": [
|
||||
"agent",
|
||||
"app-server",
|
||||
|
||||
+20
-6
@@ -3,6 +3,19 @@ import { locales, defaultLocale } from "@/lib/i18n/config";
|
||||
|
||||
const COOKIE = "NEXT_LOCALE";
|
||||
|
||||
const SECURITY_HEADERS: Record<string, string> = {
|
||||
"X-Frame-Options": "DENY",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
"Permissions-Policy": "camera=(), microphone=(), geolocation=(), interest-cohort=()",
|
||||
"Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload",
|
||||
};
|
||||
|
||||
function applySecurityHeaders(res: NextResponse): NextResponse {
|
||||
for (const [k, v] of Object.entries(SECURITY_HEADERS)) res.headers.set(k, v);
|
||||
return res;
|
||||
}
|
||||
|
||||
function detectLocale(req: NextRequest): string {
|
||||
// 1. Cookie
|
||||
const cookie = req.cookies.get(COOKIE)?.value;
|
||||
@@ -18,22 +31,21 @@ function detectLocale(req: NextRequest): string {
|
||||
export function middleware(req: NextRequest) {
|
||||
const { pathname } = req.nextUrl;
|
||||
|
||||
// Skip API routes, static files, _next
|
||||
// Skip API routes, static files, _next (but still apply security headers).
|
||||
if (
|
||||
pathname.startsWith("/api/") ||
|
||||
pathname.startsWith("/_next/") ||
|
||||
pathname.includes(".")
|
||||
) {
|
||||
return NextResponse.next();
|
||||
return applySecurityHeaders(NextResponse.next());
|
||||
}
|
||||
|
||||
// Check if locale is already in path
|
||||
const seg = pathname.split("/")[1];
|
||||
if (locales.includes(seg as typeof locales[number])) {
|
||||
// Ensure cookie is set
|
||||
const res = NextResponse.next();
|
||||
res.cookies.set(COOKIE, seg, { path: "/", maxAge: 60 * 60 * 24 * 365 });
|
||||
return res;
|
||||
return applySecurityHeaders(res);
|
||||
}
|
||||
|
||||
// Redirect bare paths to detected locale
|
||||
@@ -42,9 +54,11 @@ export function middleware(req: NextRequest) {
|
||||
url.pathname = `/${locale}${pathname}`;
|
||||
const res = NextResponse.redirect(url);
|
||||
res.cookies.set(COOKIE, locale, { path: "/", maxAge: 60 * 60 * 24 * 365 });
|
||||
return res;
|
||||
return applySecurityHeaders(res);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/((?!_next|api|favicon.ico|icon.svg|.*\\..*).*)"],
|
||||
// Match everything so security headers apply globally; the function
|
||||
// bypasses redirect/locale logic for /_next, /api, and dotted paths.
|
||||
matcher: ["/:path*"],
|
||||
};
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
// Security headers are set in middleware.ts (more reliable under OpenNext on
|
||||
// Cloudflare than next.config.ts headers(), which doesn't always apply to
|
||||
// prerendered/cached responses).
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
images: {
|
||||
|
||||
Reference in New Issue
Block a user