feat: add ESLint, lint-staged, and update pre-commit hook + CI

- Add ESLint 9 flat config (eslint.config.mjs) with typescript-eslint
  recommended rules and eslint-config-prettier
- Add lint-staged to run eslint+prettier only on staged files
- Update pre-commit hook to use lint-staged instead of full prettier check
- Add `lint` and `format:check` scripts to package.json
- Add Lint step to CI workflow
- Fix resulting lint errors: unused vars (_ctx, _options, catch binding),
  any→unknown in type declarations, stale eslint-disable comments

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-21 09:49:20 +02:00
parent e93bbb8d3e
commit 3aea41f862
11 changed files with 1799 additions and 32 deletions
+27 -9
View File
@@ -71,9 +71,11 @@ function timingSafeEqual(a: string, b: string): boolean {
const aBytes = enc.encode(a);
const bBytes = enc.encode(b);
// Try native timing-safe implementation first (Cloudflare Workers runtime)
if (typeof (crypto.subtle as any).timingSafeEqual === "function") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const subtle = crypto.subtle as any;
if (typeof subtle.timingSafeEqual === "function") {
if (aBytes.length !== bBytes.length) return false;
return (crypto.subtle as any).timingSafeEqual(aBytes, bBytes);
return subtle.timingSafeEqual(aBytes, bBytes);
}
// Constant-time fallback for Node (test environment): encode length
// mismatch into `diff` so the loop always runs over the full length.
@@ -97,7 +99,9 @@ async function authMiddleware(c: Context, next: () => Promise<void>) {
// Proxy auth: only active when both env vars are present
if (env.PROXY_AUTH_SECRET && env.PROXY_TRUSTED_IPS) {
const trustedIps = env.PROXY_TRUSTED_IPS.split(",").map((s) => s.trim()).filter(Boolean);
const trustedIps = env.PROXY_TRUSTED_IPS.split(",")
.map((s) => s.trim())
.filter(Boolean);
const clientIp = c.req.header("CF-Connecting-IP") ?? "";
const providedSecret = c.req.header("X-Auth-Proxy-Secret") ?? "";
const remoteUser =
@@ -113,7 +117,11 @@ async function authMiddleware(c: Context, next: () => Promise<void>) {
}
// Fallback: signed cookie
const authCookie = await getSignedCookie(c, env.ADMIN_PASSWORD, ADMIN_COOKIE_NAME);
const authCookie = await getSignedCookie(
c,
env.ADMIN_PASSWORD,
ADMIN_COOKIE_NAME,
);
if (authCookie !== "1") {
return c.redirect("/admin/login");
}
@@ -146,6 +154,7 @@ const authSchema = z.object({
});
// Base HTML layout with design system
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const layout = (title: string, content: any) => {
return html`<!DOCTYPE html>
<html>
@@ -1610,11 +1619,14 @@ app.post("/feeds/create", async (c) => {
if (isJson) {
const body = await c.req.json<Record<string, unknown>>();
title = String(body.title ?? "");
description = body.description != null ? String(body.description) : undefined;
description =
body.description != null ? String(body.description) : undefined;
language = String(body.language ?? "en");
view = "list";
allowedSenders = Array.isArray(body.allowedSenders)
? normalizeAllowedSenders((body.allowedSenders as unknown[]).map(String))
? normalizeAllowedSenders(
(body.allowedSenders as unknown[]).map(String),
)
: [];
} else {
const formData = await c.req.formData();
@@ -1930,8 +1942,9 @@ async function purgeFeedKeysStep(
});
if (emailKeys.length > 0) {
const emailDataResults = await Promise.allSettled(
emailKeys.map((k) =>
emailStorage.get(k, { type: "json" }) as Promise<EmailData | null>,
emailKeys.map(
(k) =>
emailStorage.get(k, { type: "json" }) as Promise<EmailData | null>,
),
);
const attachmentIds = emailDataResults
@@ -1976,7 +1989,12 @@ app.post("/feeds/:feedId/delete", async (c) => {
// Best-effort cleanup in the background so the request stays fast.
// Use the UI purge endpoint for full, user-visible progress.
waitUntilSafe(c, purgeFeedKeysStep(emailStorage, feedId, { bucket: env.ATTACHMENT_BUCKET }));
waitUntilSafe(
c,
purgeFeedKeysStep(emailStorage, feedId, {
bucket: env.ATTACHMENT_BUCKET,
}),
);
if (wantsJson) {
return c.json({ ok: true, feedId });
}