mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
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:
@@ -5,7 +5,7 @@ import { processEmail, RawAttachment } from "./email-processor";
|
||||
export async function handleCloudflareEmail(
|
||||
message: ForwardableEmailMessage,
|
||||
env: Env,
|
||||
ctx: ExecutionContext,
|
||||
_ctx: ExecutionContext,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const email = await PostalMime.parse(message.raw);
|
||||
|
||||
+27
-9
@@ -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 });
|
||||
}
|
||||
|
||||
+13
-10
@@ -71,7 +71,7 @@ class MockCache implements Cache {
|
||||
|
||||
async match(
|
||||
request: RequestInfo,
|
||||
options?: CacheQueryOptions,
|
||||
_options?: CacheQueryOptions,
|
||||
): Promise<Response | undefined> {
|
||||
const key = request instanceof Request ? request.url : request;
|
||||
const response = this.store.get(key);
|
||||
@@ -80,7 +80,7 @@ class MockCache implements Cache {
|
||||
|
||||
async delete(
|
||||
request: RequestInfo,
|
||||
options?: CacheQueryOptions,
|
||||
_options?: CacheQueryOptions,
|
||||
): Promise<boolean> {
|
||||
const key = request instanceof Request ? request.url : request;
|
||||
return this.store.delete(key);
|
||||
@@ -107,7 +107,6 @@ beforeAll(() => {
|
||||
server.listen({ onUnhandledRequest: "error" });
|
||||
|
||||
// Type-safe access to Node's global object for setting Workers-like globals in tests
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const g = globalThis as any;
|
||||
|
||||
// Mock Cloudflare Workers runtime globals
|
||||
@@ -118,31 +117,26 @@ beforeAll(() => {
|
||||
|
||||
// Mock crypto for generating random values
|
||||
if (!g.crypto) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
g.crypto = require("crypto").webcrypto;
|
||||
}
|
||||
|
||||
// Ensure other required globals are available
|
||||
if (!g.FormData) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { FormData } = require("undici");
|
||||
g.FormData = FormData;
|
||||
}
|
||||
|
||||
if (!g.Headers) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { Headers } = require("undici");
|
||||
g.Headers = Headers;
|
||||
}
|
||||
|
||||
if (!g.Request) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { Request } = require("undici");
|
||||
g.Request = Request;
|
||||
}
|
||||
|
||||
if (!g.Response) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { Response } = require("undici");
|
||||
g.Response = Response;
|
||||
}
|
||||
@@ -173,7 +167,13 @@ export class MockR2 {
|
||||
|
||||
async put(
|
||||
key: string,
|
||||
value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob,
|
||||
value:
|
||||
| ReadableStream
|
||||
| ArrayBuffer
|
||||
| ArrayBufferView
|
||||
| string
|
||||
| null
|
||||
| Blob,
|
||||
options?: {
|
||||
httpMetadata?: { contentType?: string; contentDisposition?: string };
|
||||
},
|
||||
@@ -216,7 +216,10 @@ export class MockR2 {
|
||||
headers.set("Content-Type", entry.httpMetadata.contentType);
|
||||
}
|
||||
if (entry.httpMetadata?.contentDisposition) {
|
||||
headers.set("Content-Disposition", entry.httpMetadata.contentDisposition);
|
||||
headers.set(
|
||||
"Content-Disposition",
|
||||
entry.httpMetadata.contentDisposition,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ declare global {
|
||||
// This is not an ideal solution but works for our example
|
||||
interface KVNamespace {
|
||||
get(key: string, options?: { type: "text" }): Promise<string | null>;
|
||||
get(key: string, options: { type: "json" }): Promise<any | null>;
|
||||
get(key: string, options: { type: "json" }): Promise<unknown>;
|
||||
get(
|
||||
key: string,
|
||||
options: { type: "arrayBuffer" },
|
||||
|
||||
Vendored
+2
-2
@@ -15,7 +15,7 @@ declare module "rss" {
|
||||
generator?: string;
|
||||
categories?: string[];
|
||||
custom_namespaces?: Record<string, string>;
|
||||
custom_elements?: any[];
|
||||
custom_elements?: unknown[];
|
||||
}
|
||||
|
||||
interface RSSItemOptions {
|
||||
@@ -34,7 +34,7 @@ declare module "rss" {
|
||||
size?: number;
|
||||
type?: string;
|
||||
};
|
||||
custom_elements?: any[];
|
||||
custom_elements?: unknown[];
|
||||
}
|
||||
|
||||
interface RSSXMLOptions {
|
||||
|
||||
@@ -7,6 +7,7 @@ export class EmailParser {
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static parseForwardEmailPayload(payload: any): EmailData {
|
||||
if (!payload) {
|
||||
throw new Error("Missing or invalid webhook payload");
|
||||
@@ -30,6 +31,7 @@ export class EmailParser {
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private static extractHeaders(payload: any): Record<string, string> {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
@@ -62,7 +64,7 @@ export class EmailParser {
|
||||
if (encoding.toUpperCase() === "B") {
|
||||
try {
|
||||
return atob(text);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
} else if (encoding.toUpperCase() === "Q") {
|
||||
|
||||
Reference in New Issue
Block a user