diff --git a/eslint.config.js b/eslint.config.js index ff446cd..2465ec0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -4,7 +4,7 @@ import tsEslint from "typescript-eslint"; const vitestEslintRecommended = vitestEslint.configs.recommended; -/** @type {import("eslint").Linter.Config} */ +/** @type {import("eslint").Linter.Config[]} */ export default [ { ignores: ["lib/"], @@ -17,12 +17,38 @@ export default [ cyfConfig, ...tsEslint.configs.strict, ...tsEslint.configs.stylistic, + ...typeScriptOnly( + tsEslint.configs.strictTypeCheckedOnly, + tsEslint.configs.stylisticTypeCheckedOnly, + ), { - files: ["src/**/*.test.ts"], + files: ["setupTests.ts", "src/**/*.test.ts"], ...vitestEslintRecommended, rules: { "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-unsafe-assignment": "off", ...vitestEslintRecommended.rules, }, }, ]; + +/** + * Apply the supplied configurations only to TypeScript files. + * + * @param {import("eslint").Linter.Config[][]} configs + * @returns {import("eslint").Linter.Config[]} + */ +function typeScriptOnly(...configs) { + return configs.flat().map((config) => ({ + ...config, + files: [...(config.files ?? []), "**/*.ts"], + languageOptions: { + ...config.languageOptions, + parserOptions: { + ...config.languageOptions?.parserOptions, + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + })); +} diff --git a/src/github.ts b/src/github.ts index b25606f..d942198 100644 --- a/src/github.ts +++ b/src/github.ts @@ -18,7 +18,7 @@ export const validatePayload = async (body: string, signature: string): Promise< if (!(await verify(secret, body, signature))) { throw new Error("payload validation failed"); } - const payload: PingEvent | RepositoryEvent = JSON.parse(body); + const payload = JSON.parse(body) as PingEvent | RepositoryEvent; if (!("action" in payload) || payload.action !== "created") { console.log(`Ignoring event: ${"action" in payload ? payload.action : "ping"}`); return; diff --git a/src/netlify/slack_interaction.test.ts b/src/netlify/slack_interaction.test.ts index f850dfc..352f534 100644 --- a/src/netlify/slack_interaction.test.ts +++ b/src/netlify/slack_interaction.test.ts @@ -169,12 +169,12 @@ describe("slack interaction handler", () => { const timestamp = Math.floor(Date.now() / 1_000); const body = new URLSearchParams(payload).toString(); const hmac = createHmac("sha256", secret); - hmac.update(`v0:${timestamp}:${body}`); + hmac.update(`v0:${timestamp.toFixed(0)}:${body}`); const signature = hmac.digest("hex"); return { body, signature, timestamp }; }; const makeRequest = (body: string, signature: string, timestamp: number): Promise => { - return handler({ body, headers: { "x-slack-request-timestamp": `${timestamp}`, "x-slack-signature": `v0=${signature}` } }); + return handler({ body, headers: { "x-slack-request-timestamp": timestamp.toFixed(0), "x-slack-signature": `v0=${signature}` } }); }; }); diff --git a/src/slack.test.ts b/src/slack.test.ts index 468be12..f867107 100644 --- a/src/slack.test.ts +++ b/src/slack.test.ts @@ -4,20 +4,20 @@ import { validatePayload } from "./slack.js"; import type { Maybe, MessageRef } from "./types.js"; describe("validatePayload", () => { - it("rejects invalid version", async () => { + it("rejects invalid version", () => { expect(() => attemptValidation({ signature: "v1=abc123", })).toThrow("invalid signature version"); }); - it("rejects old timestamp", async () => { + it("rejects old timestamp", () => { expect(() => attemptValidation({ signature: "v0=abc123", timestamp: Math.floor((Date.now() / 1_000) - (6 * 60)), })).toThrow("timestamp too old"); }); - it("rejects invalid hash", async () => { + it("rejects invalid hash", () => { const body = "goodbye=world"; process.env.SLACK_SIGNING_SECRET = "keepitquiet"; const { signature, timestamp } = sign(body, "someothersecret"); @@ -28,7 +28,7 @@ describe("validatePayload", () => { })).toThrow("payload validation failed"); }); - it("turns the body into an object", async () => { + it("turns the body into an object", () => { const body = `hello=world&payload=${JSON.stringify({})}`; const secret = "secretsquirrel"; process.env.SLACK_SIGNING_SECRET = secret; @@ -40,7 +40,7 @@ describe("validatePayload", () => { })).toBeUndefined(); }); - it("extracts the relevant delete action", async () => { + it("extracts the relevant delete action", () => { const payload = { actions: [{ action_id: "delete-repository", @@ -71,7 +71,7 @@ describe("validatePayload", () => { }); }); - it("extracts the relevant dismiss action", async () => { + it("extracts the relevant dismiss action", () => { const payload = { actions: [{ action_id: "dismiss-deletion", @@ -114,6 +114,6 @@ describe("validatePayload", () => { const sign = (payload: string, secret: string): { signature: string, timestamp: number } => { const timestamp = Math.floor(Date.now() / 1_000); const hmac = createHmac("sha256", secret); - hmac.update(`v0:${timestamp}:${payload}`); + hmac.update(`v0:${timestamp.toFixed(0)}:${payload}`); return { signature: `v0=${hmac.digest("hex")}`, timestamp }; }; diff --git a/src/slack.ts b/src/slack.ts index 73b2f3d..6b4ecdd 100644 --- a/src/slack.ts +++ b/src/slack.ts @@ -80,7 +80,8 @@ export const validatePayload = (body: string, signature: string, timestamp: numb if (!isValid(body, signature, timestamp)) { throw new Error("payload validation failed"); } - return getPayload(JSON.parse(Object.fromEntries(new URLSearchParams(body).entries()).payload)); + const payload = JSON.parse(Object.fromEntries(new URLSearchParams(body).entries()).payload) as SlackInteraction; + return getPayload(payload); }; const actionsSection = (repo: Repository): ActionsBlock => ({ @@ -120,8 +121,8 @@ const fiveMinutesAgo = () => Math.floor((Date.now() / 1_000) - (5 * 60)); const getPayload = ({ actions = [], message, user }: SlackInteraction): Maybe => { const action = actions.find(({ action_id }) => [DELETE_ACTION_ID, DISMISS_ACTION_ID].includes(action_id ?? "")); - if (action && action.value) { - const repo: Repository = JSON.parse(action.value); + if (action?.value) { + const repo = JSON.parse(action.value) as Repository; return { action: action.action_id === DELETE_ACTION_ID ? "delete" : "dismiss", messageTs: message.ts, @@ -153,7 +154,7 @@ const repoSection = ({ repoName, repoUrl, userLogin, userName, userUrl }: Reposi const lines = [ `A new repository <${repoUrl}|\`${repoName}\`> was just created by <${userUrl}|${userName ? userName : `\`${userLogin}\``}>.`, ]; - const match = repoUrl.match(/-\d+$/); + const match = /-\d+$/.exec(repoUrl); if (match !== null) { lines.push(`:redflag: *The \`${match[0]}\` makes this likely a mistake.*`); }