From f818ebe60fb95b9a7f507efbea44f1c953f0f0be Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:42:05 -0400 Subject: [PATCH 01/18] Rename all .js files to .ts for TypeScript migration Pure renames with no content changes to preserve git history. --- functions/event-reminders-daily/{index.js => index.ts} | 0 functions/event-reminders-hourly/{index.js => index.ts} | 0 functions/event-reminders-weekly/{index.js => index.ts} | 0 functions/slack-events/{index.js => index.ts} | 0 functions/slack-events/{messages.js => messages.ts} | 0 functions/slack-interactivity/{index.js => index.ts} | 0 functions/slack-send-message-background/{index.js => index.ts} | 0 .../zoom-meeting-webhook-handler/{airtable.js => airtable.ts} | 0 functions/zoom-meeting-webhook-handler/{index.js => index.ts} | 0 functions/zoom-meeting-webhook-handler/{slack.js => slack.ts} | 0 scripts/{build.js => build.ts} | 0 util/{slack.js => slack.ts} | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename functions/event-reminders-daily/{index.js => index.ts} (100%) rename functions/event-reminders-hourly/{index.js => index.ts} (100%) rename functions/event-reminders-weekly/{index.js => index.ts} (100%) rename functions/slack-events/{index.js => index.ts} (100%) rename functions/slack-events/{messages.js => messages.ts} (100%) rename functions/slack-interactivity/{index.js => index.ts} (100%) rename functions/slack-send-message-background/{index.js => index.ts} (100%) rename functions/zoom-meeting-webhook-handler/{airtable.js => airtable.ts} (100%) rename functions/zoom-meeting-webhook-handler/{index.js => index.ts} (100%) rename functions/zoom-meeting-webhook-handler/{slack.js => slack.ts} (100%) rename scripts/{build.js => build.ts} (100%) rename util/{slack.js => slack.ts} (100%) diff --git a/functions/event-reminders-daily/index.js b/functions/event-reminders-daily/index.ts similarity index 100% rename from functions/event-reminders-daily/index.js rename to functions/event-reminders-daily/index.ts diff --git a/functions/event-reminders-hourly/index.js b/functions/event-reminders-hourly/index.ts similarity index 100% rename from functions/event-reminders-hourly/index.js rename to functions/event-reminders-hourly/index.ts diff --git a/functions/event-reminders-weekly/index.js b/functions/event-reminders-weekly/index.ts similarity index 100% rename from functions/event-reminders-weekly/index.js rename to functions/event-reminders-weekly/index.ts diff --git a/functions/slack-events/index.js b/functions/slack-events/index.ts similarity index 100% rename from functions/slack-events/index.js rename to functions/slack-events/index.ts diff --git a/functions/slack-events/messages.js b/functions/slack-events/messages.ts similarity index 100% rename from functions/slack-events/messages.js rename to functions/slack-events/messages.ts diff --git a/functions/slack-interactivity/index.js b/functions/slack-interactivity/index.ts similarity index 100% rename from functions/slack-interactivity/index.js rename to functions/slack-interactivity/index.ts diff --git a/functions/slack-send-message-background/index.js b/functions/slack-send-message-background/index.ts similarity index 100% rename from functions/slack-send-message-background/index.js rename to functions/slack-send-message-background/index.ts diff --git a/functions/zoom-meeting-webhook-handler/airtable.js b/functions/zoom-meeting-webhook-handler/airtable.ts similarity index 100% rename from functions/zoom-meeting-webhook-handler/airtable.js rename to functions/zoom-meeting-webhook-handler/airtable.ts diff --git a/functions/zoom-meeting-webhook-handler/index.js b/functions/zoom-meeting-webhook-handler/index.ts similarity index 100% rename from functions/zoom-meeting-webhook-handler/index.js rename to functions/zoom-meeting-webhook-handler/index.ts diff --git a/functions/zoom-meeting-webhook-handler/slack.js b/functions/zoom-meeting-webhook-handler/slack.ts similarity index 100% rename from functions/zoom-meeting-webhook-handler/slack.js rename to functions/zoom-meeting-webhook-handler/slack.ts diff --git a/scripts/build.js b/scripts/build.ts similarity index 100% rename from scripts/build.js rename to scripts/build.ts diff --git a/util/slack.js b/util/slack.ts similarity index 100% rename from util/slack.js rename to util/slack.ts From d580b177a60f1ce7a26fcdf61cdff9291bd3bdc2 Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:43:06 -0400 Subject: [PATCH 02/18] Switch to pnpm and update project infrastructure - Migrate from yarn to pnpm - Add TypeScript 6.0.2 with tsconfig.json - Update to Node 24 - Update dependencies: @netlify/functions v5, @slack/web-api v7, airtable v0.12, graphql-request v7, luxon v3.7 - Remove node-fetch (using native fetch) and graphql peer dep - Move dotenv to devDependencies - Add type packages and tsx for build script - Update netlify.toml for pnpm and v2 functions config --- .nvmrc | 2 +- netlify.toml | 6 +- package.json | 30 +- pnpm-lock.yaml | 811 +++++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 19 ++ yarn.lock | 319 ------------------- 6 files changed, 853 insertions(+), 334 deletions(-) create mode 100644 pnpm-lock.yaml create mode 100644 tsconfig.json delete mode 100644 yarn.lock diff --git a/.nvmrc b/.nvmrc index 209e3ef..a45fd52 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +24 diff --git a/netlify.toml b/netlify.toml index 287f05a..0d175dc 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,6 +1,8 @@ [build] - functions = "functions" - command = "yarn run build" + command = "pnpm run build" + +[functions] + directory = "functions" [[redirects]] from = "/zoom-meeting-webhook-handler" diff --git a/package.json b/package.json index ec0ea47..6103b37 100644 --- a/package.json +++ b/package.json @@ -2,25 +2,31 @@ "name": "webhooks", "version": "1.0.0", "description": "webhooks for VC", - "main": "index.js", + "type": "module", "repository": "git@github.com:Virtual-Coffee/webhooks.git", "author": "Dan Ott ", "license": "MIT", "engines": { - "node": "^20.12" + "node": ">=24" }, "dependencies": { - "@netlify/functions": "^1.2.0", - "@slack/web-api": "^6.7.2", - "airtable": "^0.11.4", - "dotenv": "^16.0.2", - "graphql": "^16.6.0", - "graphql-request": "^5.0.0", - "luxon": "^3.0.3", - "node-fetch": "^2.6.1", + "@netlify/functions": "^5.1.5", + "@slack/web-api": "^7.15.0", + "airtable": "^0.12.2", + "graphql-request": "^7.4.0", + "luxon": "^3.7.2", "slackify-html": "^1.0.1" }, + "devDependencies": { + "@types/luxon": "^3.7.1", + "@types/node": "^24.12.0", + "@types/slackify-html": "^1.0.8", + "dotenv": "^16.0.2", + "tsx": "^4.21.0", + "typescript": "^6.0.2" + }, "scripts": { - "build": "node scripts/build.js" + "build": "tsx scripts/build.ts", + "typecheck": "tsc --noEmit" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..f072b96 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,811 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@netlify/functions': + specifier: ^5.1.5 + version: 5.1.5 + '@slack/web-api': + specifier: ^7.15.0 + version: 7.15.0 + airtable: + specifier: ^0.12.2 + version: 0.12.2 + graphql-request: + specifier: ^7.4.0 + version: 7.4.0(graphql@16.13.2) + luxon: + specifier: ^3.7.2 + version: 3.7.2 + slackify-html: + specifier: ^1.0.1 + version: 1.0.1 + devDependencies: + '@types/luxon': + specifier: ^3.7.1 + version: 3.7.1 + '@types/node': + specifier: ^25.5.0 + version: 25.5.0 + '@types/slackify-html': + specifier: ^1.0.8 + version: 1.0.8 + dotenv: + specifier: ^16.0.2 + version: 16.6.1 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^6.0.2 + version: 6.0.2 + +packages: + + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@graphql-typed-document-node/core@3.2.0': + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@netlify/functions@5.1.5': + resolution: {integrity: sha512-mhTl6x3TWoRwNgz8HZ9zvSR9OHB/hDEA6VinBmWY5ubgycKNCerf6XyFaFnujH2Ygx3c32yg6QOOr1v9y8euug==} + engines: {node: '>=18.0.0'} + + '@netlify/types@2.6.0': + resolution: {integrity: sha512-yD20EizHJDQxajJ66Vo8RTwLwR2jMNVxufPG8MHd2AScX8jW4z0VPnnJHArq2GYPFTFZRHmiAhDrXr5m8zof6w==} + engines: {node: ^18.14.0 || >=20} + + '@slack/logger@4.0.1': + resolution: {integrity: sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + + '@slack/types@2.20.1': + resolution: {integrity: sha512-eWX2mdt1ktpn8+40iiMc404uGrih+2fxiky3zBcPjtXKj6HLRdYlmhrPkJi7JTJm8dpXR6BWVWEDBXtaWMKD6A==} + engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} + + '@slack/web-api@7.15.0': + resolution: {integrity: sha512-va7zYIt3QHG1x9M/jqXXRPFMoOVlVSSRHC5YH+DzKYsrz5xUKOA3lR4THsu/Zxha9N1jOndbKFKLtr0WOPW1Vw==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + + '@types/luxon@3.7.1': + resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==} + + '@types/node@14.18.63': + resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} + + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + '@types/slackify-html@1.0.8': + resolution: {integrity: sha512-wA1YZkD/MyxXfLphMiUPOqPQOVle31z6sQFa0lQFTZUT3QbgmWKOenh4oetheasCt+Kmi/oh5VrdAEM/EgaXSQ==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + abortcontroller-polyfill@1.7.8: + resolution: {integrity: sha512-9f1iZ2uWh92VcrU9Y8x+LdM4DLj75VE0MJB8zuF1iUnroEptStw+DQ8EQPMUdfe5k+PkB1uUfDQfWbhstH8LrQ==} + + airtable@0.12.2: + resolution: {integrity: sha512-HS3VytUBTKj8A0vPl7DDr5p/w3IOGv6RXL0fv7eczOWAtj9Xe8ri4TAiZRXoOyo+Z/COADCj+oARFenbxhmkIg==} + engines: {node: '>=8.0.0'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.14.0: + resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graphql-request@7.4.0: + resolution: {integrity: sha512-xfr+zFb/QYbs4l4ty0dltqiXIp07U6sl+tOKAb0t50/EnQek6CVVBLjETXi+FghElytvgaAWtIOt3EV7zLzIAQ==} + peerDependencies: + graphql: 14 - 16 + + graphql@16.13.2: + resolution: {integrity: sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + html-entities@1.4.0: + resolution: {integrity: sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==} + + htmlparser@1.7.7: + resolution: {integrity: sha512-zpK66ifkT0fauyFh2Mulrq4AqGTucxGtOhZ8OjkbSfcCpkqQEI8qRkY0tSQSJNAQ4HUZkgWaU4fK4EH6SVH9PQ==} + engines: {node: '>=0.1.33'} + + is-electron@2.2.2: + resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + slackify-html@1.0.1: + resolution: {integrity: sha512-9e5Wo8Z2QSORedN6vqImnjIUwaHI8mpjeQQfXBcIcvIewoJ9SGB56MN2FVIPt6ACn+g4gLsQZHeGXwe5VQMnzA==} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + +snapshots: + + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + + '@graphql-typed-document-node/core@3.2.0(graphql@16.13.2)': + dependencies: + graphql: 16.13.2 + + '@netlify/functions@5.1.5': + dependencies: + '@netlify/types': 2.6.0 + + '@netlify/types@2.6.0': {} + + '@slack/logger@4.0.1': + dependencies: + '@types/node': 25.5.0 + + '@slack/types@2.20.1': {} + + '@slack/web-api@7.15.0': + dependencies: + '@slack/logger': 4.0.1 + '@slack/types': 2.20.1 + '@types/node': 25.5.0 + '@types/retry': 0.12.0 + axios: 1.14.0 + eventemitter3: 5.0.4 + form-data: 4.0.5 + is-electron: 2.2.2 + is-stream: 2.0.1 + p-queue: 6.6.2 + p-retry: 4.6.2 + retry: 0.13.1 + transitivePeerDependencies: + - debug + + '@types/luxon@3.7.1': {} + + '@types/node@14.18.63': {} + + '@types/node@25.5.0': + dependencies: + undici-types: 7.18.2 + + '@types/retry@0.12.0': {} + + '@types/slackify-html@1.0.8': {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + abortcontroller-polyfill@1.7.8: {} + + airtable@0.12.2: + dependencies: + '@types/node': 14.18.63 + abort-controller: 3.0.0 + abortcontroller-polyfill: 1.7.8 + lodash: 4.18.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + asynckit@0.4.0: {} + + axios@1.14.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + delayed-stream@1.0.0: {} + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + + event-target-shim@5.0.1: {} + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.4: {} + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + + gopd@1.2.0: {} + + graphql-request@7.4.0(graphql@16.13.2): + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.13.2) + graphql: 16.13.2 + + graphql@16.13.2: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + html-entities@1.4.0: {} + + htmlparser@1.7.7: {} + + is-electron@2.2.2: {} + + is-stream@2.0.1: {} + + lodash@4.18.1: {} + + luxon@3.7.2: {} + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + p-finally@1.0.0: {} + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + proxy-from-env@2.1.0: {} + + resolve-pkg-maps@1.0.0: {} + + retry@0.13.1: {} + + slackify-html@1.0.1: + dependencies: + html-entities: 1.4.0 + htmlparser: 1.7.7 + + tr46@0.0.3: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.4 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 + + typescript@6.0.2: {} + + undici-types@7.18.2: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7239331 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "nodenext", + "moduleResolution": "nodenext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noEmit": true, + "types": ["node"] + }, + "include": [ + "functions/**/*.ts", + "util/**/*.ts", + "scripts/**/*.ts" + ], + "exclude": ["node_modules"] +} diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 52827bf..0000000 --- a/yarn.lock +++ /dev/null @@ -1,319 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@graphql-typed-document-node/core@^3.1.1": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" - integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== - -"@netlify/functions@^1.2.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@netlify/functions/-/functions-1.6.0.tgz#c373423e6fef0e6f7422ac0345e8bbf2cb692366" - integrity sha512-6G92AlcpFrQG72XU8YH8pg94eDnq7+Q0YJhb8x4qNpdGsvuzvrfHWBmqFGp/Yshmv4wex9lpsTRZOocdrA2erQ== - dependencies: - is-promise "^4.0.0" - -"@slack/logger@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@slack/logger/-/logger-3.0.0.tgz#b736d4e1c112c22a10ffab0c2d364620aedcb714" - integrity sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA== - dependencies: - "@types/node" ">=12.0.0" - -"@slack/types@^2.11.0": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@slack/types/-/types-2.11.0.tgz#948c556081c3db977dfa8433490cc2ff41f47203" - integrity sha512-UlIrDWvuLaDly3QZhCPnwUSI/KYmV1N9LyhuH6EDKCRS1HWZhyTG3Ja46T3D0rYfqdltKYFXbJSSRPwZpwO0cQ== - -"@slack/web-api@^6.7.2": - version "6.12.0" - resolved "https://registry.yarnpkg.com/@slack/web-api/-/web-api-6.12.0.tgz#d0487d90e3db2f7bfabe3430fa5da0cc03d2d9cb" - integrity sha512-RPw6F8rWfGveGkZEJ4+4jUin5iazxRK2q3FpQDz/FvdgzC3nZmPyLx8WRzc6nh0w3MBjEbphNnp2VZksfhpBIQ== - dependencies: - "@slack/logger" "^3.0.0" - "@slack/types" "^2.11.0" - "@types/is-stream" "^1.1.0" - "@types/node" ">=12.0.0" - axios "^1.6.5" - eventemitter3 "^3.1.0" - form-data "^2.5.0" - is-electron "2.2.2" - is-stream "^1.1.0" - p-queue "^6.6.1" - p-retry "^4.0.0" - -"@types/is-stream@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@types/is-stream/-/is-stream-1.1.0.tgz#b84d7bb207a210f2af9bed431dc0fbe9c4143be1" - integrity sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg== - dependencies: - "@types/node" "*" - -"@types/node@*", "@types/node@>=12.0.0": - version "20.12.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384" - integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg== - dependencies: - undici-types "~5.26.4" - -"@types/node@>=8.0.0 <15": - version "14.18.63" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" - integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== - -"@types/retry@0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" - integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== - -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - -abortcontroller-polyfill@^1.4.0: - version "1.7.5" - resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz#6738495f4e901fbb57b6c0611d0c75f76c485bed" - integrity sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ== - -airtable@^0.11.4: - version "0.11.6" - resolved "https://registry.yarnpkg.com/airtable/-/airtable-0.11.6.tgz#3b90f9c671ee93c4ad647eb131d630dea9f1f84a" - integrity sha512-Na67L2TO1DflIJ1yOGhQG5ilMfL2beHpsR+NW/jhaYOa4QcoxZOtDFs08cpSd1tBMsLpz5/rrz/VMX/pGL/now== - dependencies: - "@types/node" ">=8.0.0 <15" - abort-controller "^3.0.0" - abortcontroller-polyfill "^1.4.0" - lodash "^4.17.21" - node-fetch "^2.6.7" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -axios@^1.6.5: - version "1.6.8" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66" - integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ== - dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - -combined-stream@^1.0.6, combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -cross-fetch@^3.1.5: - version "3.1.8" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" - integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== - dependencies: - node-fetch "^2.6.12" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -dotenv@^16.0.2: - version "16.4.5" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" - integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== - -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -eventemitter3@^3.1.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" - integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== - -eventemitter3@^4.0.4: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - -extract-files@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a" - integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ== - -follow-redirects@^1.15.6: - version "1.15.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== - -form-data@^2.5.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" - integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -form-data@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" - integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -graphql-request@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-5.2.0.tgz#a05fb54a517d91bb2d7aefa17ade4523dc5ebdca" - integrity sha512-pLhKIvnMyBERL0dtFI3medKqWOz/RhHdcgbZ+hMMIb32mEPa5MJSzS4AuXxfI4sRAu6JVVk5tvXuGfCWl9JYWQ== - dependencies: - "@graphql-typed-document-node/core" "^3.1.1" - cross-fetch "^3.1.5" - extract-files "^9.0.0" - form-data "^3.0.0" - -graphql@^16.6.0: - version "16.8.1" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" - integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== - -html-entities@^1.1.3: - version "1.4.0" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc" - integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA== - -htmlparser@^1.7.7: - version "1.7.7" - resolved "https://registry.yarnpkg.com/htmlparser/-/htmlparser-1.7.7.tgz#19e7b3997ff6fbac99ae5a7d2766489efe7e2d0e" - integrity sha512-zpK66ifkT0fauyFh2Mulrq4AqGTucxGtOhZ8OjkbSfcCpkqQEI8qRkY0tSQSJNAQ4HUZkgWaU4fK4EH6SVH9PQ== - -is-electron@2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/is-electron/-/is-electron-2.2.2.tgz#3778902a2044d76de98036f5dc58089ac4d80bb9" - integrity sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg== - -is-promise@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" - integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== - -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -luxon@^3.0.3: - version "3.4.4" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.4.tgz#cf20dc27dc532ba41a169c43fdcc0063601577af" - integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA== - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7: - version "2.7.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== - -p-queue@^6.6.1: - version "6.6.2" - resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" - integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== - dependencies: - eventemitter3 "^4.0.4" - p-timeout "^3.2.0" - -p-retry@^4.0.0: - version "4.6.2" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" - integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== - dependencies: - "@types/retry" "0.12.0" - retry "^0.13.1" - -p-timeout@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" - integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== - dependencies: - p-finally "^1.0.0" - -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -retry@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" - integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== - -slackify-html@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/slackify-html/-/slackify-html-1.0.1.tgz#83a936bfb49aa745c3e1e5e6d6d7c8beed4dbf0b" - integrity sha512-9e5Wo8Z2QSORedN6vqImnjIUwaHI8mpjeQQfXBcIcvIewoJ9SGB56MN2FVIPt6ACn+g4gLsQZHeGXwe5VQMnzA== - dependencies: - html-entities "^1.1.3" - htmlparser "^1.7.7" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" From 3f9a21663a66251b6122bb34d9f35fbd9e71d306 Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:43:55 -0400 Subject: [PATCH 03/18] Migrate shared utilities to TypeScript + ESM - util/slack.ts: ESM imports, type annotations, drop node-fetch/dotenv - functions/slack-events/messages.ts: ESM exports, add SlackUser types --- functions/slack-events/messages.ts | 26 +++++++++++++++++--------- util/slack.ts | 24 +++++++++++++++--------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/functions/slack-events/messages.ts b/functions/slack-events/messages.ts index 42e9518..9388d54 100644 --- a/functions/slack-events/messages.ts +++ b/functions/slack-events/messages.ts @@ -1,4 +1,17 @@ -function getWelcomeBlocks(user) { +interface SlackUser { + id: string; + name: string; +} + +interface TeamJoinEvent { + user: SlackUser; +} + +interface AppHomeOpenedEvent { + user: string; +} + +function getWelcomeBlocks(user?: SlackUser) { return [ { type: 'section', @@ -126,17 +139,17 @@ function getWelcomeBlocks(user) { ]; } -function appHome({ event }) { +export function appHome({ event }: { event: AppHomeOpenedEvent }) { return { user_id: event.user, view: { - type: 'home', + type: 'home' as const, blocks: getWelcomeBlocks(), }, }; } -function welcome({ event }) { +export function welcome({ event }: { event: TeamJoinEvent }) { return { link_names: true, unfurl_links: false, @@ -146,8 +159,3 @@ function welcome({ event }) { blocks: getWelcomeBlocks(event.user), }; } - -module.exports = { - welcome, - appHome, -}; diff --git a/util/slack.ts b/util/slack.ts index 333b504..2859e0a 100644 --- a/util/slack.ts +++ b/util/slack.ts @@ -1,6 +1,5 @@ -require('dotenv').config(); -const fetch = require('node-fetch'); -const { WebClient } = require('@slack/web-api'); +import { WebClient } from '@slack/web-api'; +import type { ChatPostMessageArguments, ChatUpdateArguments, ViewsPublishArguments } from '@slack/web-api'; const SLACK_BOT_TOKEN = process.env.TEST_SLACK_BOT_TOKEN || process.env.SLACK_BOT_TOKEN; @@ -8,7 +7,7 @@ const APP_HOST = process.env.TEST_APP_HOST || process.env.APP_HOST; const web = new WebClient(SLACK_BOT_TOKEN); -async function postBackgroundAction(json = {}) { +async function postBackgroundAction(json: Record = {}) { return await fetch(`${APP_HOST}/slack-send-message`, { method: 'POST', body: JSON.stringify({ @@ -18,7 +17,10 @@ async function postBackgroundAction(json = {}) { }); } -async function postMessage(message, { background = false } = {}) { +export async function postMessage( + message: ChatPostMessageArguments, + { background = false } = {}, +) { return background ? await postBackgroundAction({ message, @@ -27,7 +29,10 @@ async function postMessage(message, { background = false } = {}) { : await web.chat.postMessage(message); } -async function updateMessage(message, { background = false } = {}) { +export async function updateMessage( + message: ChatUpdateArguments, + { background = false } = {}, +) { return background ? await postBackgroundAction({ message, @@ -36,7 +41,10 @@ async function updateMessage(message, { background = false } = {}) { : await web.chat.update(message); } -async function publishView(message, { background = false } = {}) { +export async function publishView( + message: ViewsPublishArguments, + { background = false } = {}, +) { return background ? await postBackgroundAction({ message, @@ -44,5 +52,3 @@ async function publishView(message, { background = false } = {}) { }) : await web.views.publish(message); } - -module.exports = { postMessage, updateMessage, publishView }; From aecbbef4f62c6e1c5640237a57b13098eb497bd0 Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:44:12 -0400 Subject: [PATCH 04/18] update lock --- pnpm-lock.yaml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f072b96..7d4ec14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,8 +31,8 @@ importers: specifier: ^3.7.1 version: 3.7.1 '@types/node': - specifier: ^25.5.0 - version: 25.5.0 + specifier: ^24.12.0 + version: 24.12.0 '@types/slackify-html': specifier: ^1.0.8 version: 1.0.8 @@ -235,8 +235,8 @@ packages: '@types/node@14.18.63': resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} - '@types/node@25.5.0': - resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + '@types/node@24.12.0': + resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} @@ -454,8 +454,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -555,7 +555,7 @@ snapshots: '@slack/logger@4.0.1': dependencies: - '@types/node': 25.5.0 + '@types/node': 24.12.0 '@slack/types@2.20.1': {} @@ -563,7 +563,7 @@ snapshots: dependencies: '@slack/logger': 4.0.1 '@slack/types': 2.20.1 - '@types/node': 25.5.0 + '@types/node': 24.12.0 '@types/retry': 0.12.0 axios: 1.14.0 eventemitter3: 5.0.4 @@ -580,9 +580,9 @@ snapshots: '@types/node@14.18.63': {} - '@types/node@25.5.0': + '@types/node@24.12.0': dependencies: - undici-types: 7.18.2 + undici-types: 7.16.0 '@types/retry@0.12.0': {} @@ -801,7 +801,7 @@ snapshots: typescript@6.0.2: {} - undici-types@7.18.2: {} + undici-types@7.16.0: {} webidl-conversions@3.0.1: {} From a9a57aebd1ea10cabbe5f35141e06d6d81a0f161 Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:46:06 -0400 Subject: [PATCH 05/18] Migrate event reminder functions to Netlify Functions v2 - Convert daily, hourly, and weekly reminders to v2 Request/Response - Replace schedule() wrapper with export const config - Convert CommonJS to ESM imports - Add shared CMS GraphQL types in types/cms.ts - Remove dotenv dependency (Netlify injects env vars) --- functions/event-reminders-daily/index.ts | 47 +++-- functions/event-reminders-hourly/index.ts | 220 +++++++++++----------- functions/event-reminders-weekly/index.ts | 57 +++--- types/cms.ts | 22 +++ 4 files changed, 183 insertions(+), 163 deletions(-) create mode 100644 types/cms.ts diff --git a/functions/event-reminders-daily/index.ts b/functions/event-reminders-daily/index.ts index 7ad9e36..63a75ee 100644 --- a/functions/event-reminders-daily/index.ts +++ b/functions/event-reminders-daily/index.ts @@ -1,9 +1,9 @@ -require('dotenv').config(); -const { GraphQLClient, gql } = require('graphql-request'); -const { DateTime } = require('luxon'); -const { postMessage } = require('../../util/slack'); -const { schedule } = require('@netlify/functions'); -var slackify = require('slackify-html'); +import { GraphQLClient, gql } from 'graphql-request'; +import { DateTime } from 'luxon'; +import { postMessage } from '../../util/slack.js'; +import slackify from 'slackify-html'; +import type { Config } from '@netlify/functions'; +import type { CalendarsResponse, EventsResponse } from '../../types/cms.js'; const SLACK_ANNOUNCEMENTS_CHANNEL = process.env.TEST_SLACK_ANNOUNCEMENTS_CHANNEL || @@ -21,7 +21,7 @@ const calendarsQuery = gql` } `; -function createEventsQuery(calendars) { +function createEventsQuery(calendars: CalendarsResponse) { return gql` query getEvents($rangeStart: String!, $rangeEnd: String!) { solspace_calendar { @@ -47,7 +47,7 @@ function createEventsQuery(calendars) { `; } -const handler = async function (event, context) { +export default async (req: Request) => { const graphQLClient = new GraphQLClient(`${process.env.CMS_URL}/api`, { headers: { Authorization: `bearer ${process.env.CMS_TOKEN}`, @@ -63,14 +63,15 @@ const handler = async function (event, context) { console.log('Fetching events', rangeStart, rangeEnd); try { - const calendarsResponse = await graphQLClient.request(calendarsQuery); + const calendarsResponse = + await graphQLClient.request(calendarsQuery); - const eventsResponse = await graphQLClient.request( + const eventsResponse = await graphQLClient.request( createEventsQuery(calendarsResponse), { rangeStart, rangeEnd, - } + }, ); const eventsList = eventsResponse.solspace_calendar.events; @@ -82,11 +83,11 @@ const handler = async function (event, context) { } const dailyMessage = { - channel: SLACK_ANNOUNCEMENTS_CHANNEL, + channel: SLACK_ANNOUNCEMENTS_CHANNEL!, text: `Today's events are: ${eventsList .map((event) => { return `${event.title}: ${DateTime.fromISO( - event.startDateLocalized + event.startDateLocalized, ).toFormat('EEEE, fff')}`; }) .join(', ')}`, @@ -94,14 +95,14 @@ const handler = async function (event, context) { unfurl_media: false, blocks: [ { - type: 'header', + type: 'header' as const, text: { - type: 'plain_text', + type: 'plain_text' as const, text: "📆 Today's Events Are:", emoji: true, }, }, - ...eventsList.reduce((list, event) => { + ...eventsList.reduce[]>((list, event) => { const eventDate = DateTime.fromISO(event.startDateLocalized); return [ ...list, @@ -112,7 +113,7 @@ const handler = async function (event, context) { text: `*${ event.title }*\n`, }, }, @@ -148,15 +149,13 @@ const handler = async function (event, context) { await postMessage(dailyMessage); } - return { - statusCode: 200, - }; + return new Response(null, { status: 200 }); } catch (e) { console.error(e); - return { - statusCode: 500, - }; + return new Response(null, { status: 500 }); } }; -module.exports.handler = schedule('0 12 * * *', handler); +export const config: Config = { + schedule: '0 12 * * *', +}; diff --git a/functions/event-reminders-hourly/index.ts b/functions/event-reminders-hourly/index.ts index d114368..598d265 100644 --- a/functions/event-reminders-hourly/index.ts +++ b/functions/event-reminders-hourly/index.ts @@ -1,9 +1,9 @@ -require('dotenv').config(); -const { GraphQLClient, gql } = require('graphql-request'); -const { DateTime } = require('luxon'); -const { postMessage } = require('../../util/slack'); -var slackify = require('slackify-html'); -const { schedule } = require('@netlify/functions'); +import { GraphQLClient, gql } from 'graphql-request'; +import { DateTime } from 'luxon'; +import { postMessage } from '../../util/slack.js'; +import slackify from 'slackify-html'; +import type { Config } from '@netlify/functions'; +import type { CalendarsResponse, EventsResponse } from '../../types/cms.js'; const SLACK_ANNOUNCEMENTS_CHANNEL = process.env.TEST_SLACK_ANNOUNCEMENTS_CHANNEL || @@ -25,7 +25,7 @@ const calendarsQuery = gql` } `; -function createEventsQuery(calendars) { +function createEventsQuery(calendars: CalendarsResponse) { return gql` query getEvents($rangeStart: String!, $rangeEnd: String!) { solspace_calendar { @@ -51,7 +51,7 @@ function createEventsQuery(calendars) { `; } -const handler = async function (event, context) { +export default async (req: Request) => { const graphQLClient = new GraphQLClient(`${process.env.CMS_URL}/api`, { headers: { Authorization: `bearer ${process.env.CMS_TOKEN}`, @@ -70,14 +70,15 @@ const handler = async function (event, context) { console.log('Fetching events', rangeStart, rangeEnd); try { - const calendarsResponse = await graphQLClient.request(calendarsQuery); + const calendarsResponse = + await graphQLClient.request(calendarsQuery); - const eventsResponse = await graphQLClient.request( + const eventsResponse = await graphQLClient.request( createEventsQuery(calendarsResponse), { rangeStart, rangeEnd, - } + }, ); const eventsList = eventsResponse.solspace_calendar.events; @@ -92,35 +93,25 @@ const handler = async function (event, context) { const hourlyMessages = filteredList.map((event) => { const eventDate = DateTime.fromISO(event.startDateLocalized); - const message = { - channel: - event.eventSlackAnnouncementsChannelId || - DEFAULT_SLACK_EVENT_CHANNEL, - text: `Starting soon: ${event.title}: ${eventDate.toFormat( - 'EEEE, fff' - )}`, - unfurl_links: false, - unfurl_media: false, - blocks: [ - { - type: 'header', - text: { - type: 'plain_text', - text: '⏰ Starting Soon:', - emoji: true, - }, + const blocks: Record[] = [ + { + type: 'header', + text: { + type: 'plain_text', + text: '⏰ Starting Soon:', + emoji: true, }, - ], - }; + }, + ]; - const titleBlock = { + const titleBlock: Record = { type: 'section', text: { type: 'mrkdwn', text: `*${ event.title }*\n`, }, }; @@ -142,13 +133,13 @@ const handler = async function (event, context) { }; } - message.blocks.push(titleBlock); + blocks.push(titleBlock); if ( event.eventJoinLink && event.eventJoinLink.substring(0, 4) !== 'http' ) { - message.blocks.push({ + blocks.push({ type: 'section', text: { type: 'mrkdwn', @@ -157,7 +148,7 @@ const handler = async function (event, context) { }); } - message.blocks.push( + blocks.push( { type: 'context', elements: [ @@ -169,18 +160,27 @@ const handler = async function (event, context) { }, { type: 'divider', - } + }, ); - return message; + return { + channel: (event.eventSlackAnnouncementsChannelId || + DEFAULT_SLACK_EVENT_CHANNEL)!, + text: `Starting soon: ${event.title}: ${eventDate.toFormat( + 'EEEE, fff', + )}`, + unfurl_links: false, + unfurl_media: false, + blocks, + }; }); const hourlyAdminMessage = { - channel: SLACK_EVENT_ADMIN_CHANNEL, + channel: SLACK_EVENT_ADMIN_CHANNEL!, text: `Starting soon: ${filteredList .map((event) => { return `${event.title}: ${DateTime.fromISO( - event.startDateLocalized + event.startDateLocalized, ).toFormat('EEEE, fff')}`; }) .join(', ')}`, @@ -188,101 +188,101 @@ const handler = async function (event, context) { unfurl_media: false, blocks: [ { - type: 'header', + type: 'header' as const, text: { - type: 'plain_text', + type: 'plain_text' as const, text: '⏰ Starting Soon:', emoji: true, }, }, - ...filteredList.reduce((list, event) => { - const eventDate = DateTime.fromISO(event.startDateLocalized); + ...filteredList.reduce[]>( + (list, event) => { + const eventDate = DateTime.fromISO(event.startDateLocalized); - const titleBlock = { - type: 'section', - text: { - type: 'mrkdwn', - text: `*${ - event.title - }*\n`, - }, - }; - - if ( - event.eventJoinLink && - event.eventJoinLink.substring(0, 4) === 'http' - ) { - titleBlock.accessory = { - type: 'button', + const titleBlock: Record = { + type: 'section', text: { - type: 'plain_text', - text: 'Join Event', - emoji: true, + type: 'mrkdwn', + text: `*${ + event.title + }*\n`, }, - value: `join_event_${event.id}`, - url: event.eventJoinLink, - action_id: 'button-join-event', }; - } - return [ - ...list, - titleBlock, - { - type: 'section', - text: { - type: 'mrkdwn', - text: `*Location:* ${event.eventJoinLink}`, + if ( + event.eventJoinLink && + event.eventJoinLink.substring(0, 4) === 'http' + ) { + titleBlock.accessory = { + type: 'button', + text: { + type: 'plain_text', + text: 'Join Event', + emoji: true, + }, + value: `join_event_${event.id}`, + url: event.eventJoinLink, + action_id: 'button-join-event', + }; + } + + return [ + ...list, + titleBlock, + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*Location:* ${event.eventJoinLink}`, + }, }, - }, - ...(event.eventZoomHostCode - ? [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: `*Host Code:* ${event.eventZoomHostCode}`, + ...(event.eventZoomHostCode + ? [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*Host Code:* ${event.eventZoomHostCode}`, + }, }, - }, - ] - : []), - { - type: 'section', - text: { - type: 'mrkdwn', - text: `*Announcement posted to:* <#${ - event.eventSlackAnnouncementsChannelId || - DEFAULT_SLACK_EVENT_CHANNEL - }>`, + ] + : []), + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*Announcement posted to:* <#${ + event.eventSlackAnnouncementsChannelId || + DEFAULT_SLACK_EVENT_CHANNEL + }>`, + }, }, - }, - { - type: 'divider', - }, - ]; - }, []), + { + type: 'divider', + }, + ]; + }, + [], + ), ], }; await postMessage(hourlyAdminMessage); await Promise.all( - hourlyMessages.map((message) => postMessage(message)) + hourlyMessages.map((message) => postMessage(message)), ); - // console.log(JSON.stringify(hourlyMessage, null, 2)); } } - return { - statusCode: 200, - }; + return new Response(null, { status: 200 }); } catch (e) { console.error(e); - return { - statusCode: 500, - }; + return new Response(null, { status: 500 }); } }; -module.exports.handler = schedule('50 * * * *', handler); +export const config: Config = { + schedule: '50 * * * *', +}; diff --git a/functions/event-reminders-weekly/index.ts b/functions/event-reminders-weekly/index.ts index 8d9b311..e7bae73 100644 --- a/functions/event-reminders-weekly/index.ts +++ b/functions/event-reminders-weekly/index.ts @@ -1,8 +1,8 @@ -require('dotenv').config(); -const { GraphQLClient, gql } = require('graphql-request'); -const { DateTime } = require('luxon'); -const { postMessage } = require('../../util/slack'); -const { schedule } = require('@netlify/functions'); +import { GraphQLClient, gql } from 'graphql-request'; +import { DateTime } from 'luxon'; +import { postMessage } from '../../util/slack.js'; +import type { Config } from '@netlify/functions'; +import type { CalendarsResponse, EventsResponse } from '../../types/cms.js'; const SLACK_ANNOUNCEMENTS_CHANNEL = process.env.TEST_SLACK_ANNOUNCEMENTS_CHANNEL || @@ -20,7 +20,7 @@ const calendarsQuery = gql` } `; -function createEventsQuery(calendars) { +function createEventsQuery(calendars: CalendarsResponse) { return gql` query getEvents($rangeStart: String!, $rangeEnd: String!) { solspace_calendar { @@ -46,7 +46,7 @@ function createEventsQuery(calendars) { `; } -const handler = async function (event, context) { +export default async (req: Request) => { const graphQLClient = new GraphQLClient(`${process.env.CMS_URL}/api`, { headers: { Authorization: `bearer ${process.env.CMS_TOKEN}`, @@ -65,24 +65,25 @@ const handler = async function (event, context) { console.log('Fetching events', rangeStart, rangeEnd); try { - const calendarsResponse = await graphQLClient.request(calendarsQuery); + const calendarsResponse = + await graphQLClient.request(calendarsQuery); - const eventsResponse = await graphQLClient.request( + const eventsResponse = await graphQLClient.request( createEventsQuery(calendarsResponse), { rangeStart, rangeEnd, - } + }, ); const eventsList = eventsResponse.solspace_calendar.events; if (eventsList && eventsList.length) { const weeklyMessage = { - channel: SLACK_ANNOUNCEMENTS_CHANNEL, + channel: SLACK_ANNOUNCEMENTS_CHANNEL!, text: `This weeks events are: ${eventsList .map((event) => { return `${event.title}: ${DateTime.fromISO( - event.startDateLocalized + event.startDateLocalized, ).toFormat('EEEE, fff')}`; }) .join(', ')}`, @@ -90,9 +91,9 @@ const handler = async function (event, context) { unfurl_media: false, blocks: [ { - type: 'header', + type: 'header' as const, text: { - type: 'plain_text', + type: 'plain_text' as const, text: "📆 This Week's Events Are:", emoji: true, }, @@ -101,11 +102,11 @@ const handler = async function (event, context) { const eventDate = DateTime.fromISO(event.startDateLocalized); // TODO - colate these by date return { - type: 'section', + type: 'section' as const, text: { - type: 'mrkdwn', + type: 'mrkdwn' as const, text: `** in <#${ event.eventSlackAnnouncementsChannelId || DEFAULT_SLACK_EVENT_CHANNEL @@ -114,22 +115,22 @@ const handler = async function (event, context) { }; }), { - type: 'context', + type: 'context' as const, elements: [ { - type: 'mrkdwn', + type: 'mrkdwn' as const, text: `â„šī¸ Links to join will be posted in the specified channel about 10 minutes before the event starts.`, }, ], }, { - type: 'divider', + type: 'divider' as const, }, { - type: 'context', + type: 'context' as const, elements: [ { - type: 'mrkdwn', + type: 'mrkdwn' as const, text: `See details and more events at !`, }, ], @@ -139,15 +140,13 @@ const handler = async function (event, context) { await postMessage(weeklyMessage); } - return { - statusCode: 200, - }; + return new Response(null, { status: 200 }); } catch (e) { console.error(e); - return { - statusCode: 500, - }; + return new Response(null, { status: 500 }); } }; -module.exports.handler = schedule('0 12 * * 1', handler); +export const config: Config = { + schedule: '0 12 * * 1', +}; diff --git a/types/cms.ts b/types/cms.ts new file mode 100644 index 0000000..cb2969d --- /dev/null +++ b/types/cms.ts @@ -0,0 +1,22 @@ +export interface CalendarsResponse { + solspace_calendar: { + calendars: Array<{ handle: string }>; + }; +} + +export interface CalendarEvent { + id: string; + title: string; + startDateLocalized: string; + endDateLocalized: string; + eventCalendarDescription: string; + eventJoinLink?: string; + eventZoomHostCode?: string; + eventSlackAnnouncementsChannelId?: string; +} + +export interface EventsResponse { + solspace_calendar: { + events: CalendarEvent[]; + }; +} From d12758e4f67d733f985a2f2e61237d5a8bb32cba Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:47:00 -0400 Subject: [PATCH 06/18] Migrate Slack handler functions to Netlify Functions v2 - slack-events: v2 handler with HMAC verification via req.text() - slack-interactivity: v2 handler (stub) - slack-send-message-background: v2 handler for async message dispatch - Convert CommonJS to ESM, add type annotations --- functions/slack-events/index.ts | 98 +++++++------------ functions/slack-interactivity/index.ts | 9 +- .../slack-send-message-background/index.ts | 29 +++--- 3 files changed, 53 insertions(+), 83 deletions(-) diff --git a/functions/slack-events/index.ts b/functions/slack-events/index.ts index 59b357a..c6e3fec 100644 --- a/functions/slack-events/index.ts +++ b/functions/slack-events/index.ts @@ -1,45 +1,43 @@ -require('dotenv').config(); - -const crypto = require('crypto'); -const messages = require('./messages'); - -const { postMessage, publishView } = require('../../util/slack'); +import crypto from 'node:crypto'; +import { welcome, appHome } from './messages.js'; +import { postMessage, publishView } from '../../util/slack.js'; const SLACK_SIGNING_SECRET = process.env.TEST_SLACK_SIGNING_SECRET || process.env.SLACK_SIGNING_SECRET; -function verify(event) { - const slackSignature = event.headers['x-slack-signature']; - const timestamp = event.headers['x-slack-request-timestamp']; +function verify(rawBody: string, headers: Headers) { + const slackSignature = headers.get('x-slack-signature'); + const timestamp = headers.get('x-slack-request-timestamp'); // convert current time from milliseconds to seconds const time = Math.floor(new Date().getTime() / 1000); - if (Math.abs(time - timestamp) > 300) { + if (!timestamp || Math.abs(time - Number(timestamp)) > 300) { return { - valid: false, + valid: false as const, reason: 'Ignore this request.', }; } - const verificationString = `v0:${timestamp}:${event.body}`; + const verificationString = `v0:${timestamp}:${rawBody}`; const mySignature = 'v0=' + crypto - .createHmac('sha256', SLACK_SIGNING_SECRET) + .createHmac('sha256', SLACK_SIGNING_SECRET!) .update(verificationString, 'utf8') .digest('hex'); if ( + slackSignature && crypto.timingSafeEqual( Buffer.from(mySignature, 'utf8'), - Buffer.from(slackSignature, 'utf8') + Buffer.from(slackSignature, 'utf8'), ) ) { return { - valid: true, + valid: true as const, }; } else { return { - valid: false, + valid: false as const, reason: 'Verification Failed.', }; } @@ -48,44 +46,37 @@ function verify(event) { const EVENT_TEAM_JOIN = 'team_join'; const EVENT_APP_HOME_OPENED = 'app_home_opened'; -const handler = async function (event, context) { +export default async (req: Request) => { try { - const request = JSON.parse(event.body); + const rawBody = await req.text(); + const request = JSON.parse(rawBody); switch (request.type) { case 'url_verification': if (request.challenge) { console.log('Valid url_verification'); - return { - statusCode: 200, - body: request.challenge, - // body: JSON.stringify({ identity, user, msg: data.value }), - }; + return new Response(request.challenge, { status: 200 }); } break; - case 'event_callback': - const isValid = verify(event); + case 'event_callback': { + const isValid = verify(rawBody, req.headers); if (!isValid.valid) { console.log('Failed validation: ', isValid.reason); - return { - statusCode: 400, - body: isValid.reason, - }; + return new Response(isValid.reason, { status: 400 }); } - // v0 - let result = null; + let result: { ok?: boolean } | null = null; switch (request.event.type) { case EVENT_TEAM_JOIN: console.log('Posting to slack-background for team join'); result = await postMessage( - messages.welcome({ event: request.event }), + welcome({ event: request.event }), { background: true, - } + }, ); break; @@ -94,10 +85,10 @@ const handler = async function (event, context) { console.log('Posting to slack-background for app home'); result = await publishView( - messages.appHome({ event: request.event }), + appHome({ event: request.event }), { background: true, - } + }, ); break; @@ -109,43 +100,30 @@ const handler = async function (event, context) { if (result.ok) { console.log(`Successfully posted to slack-background`); - return { - statusCode: 200, - body: JSON.stringify({ success: true }), - }; + return new Response(JSON.stringify({ success: true }), { + status: 200, + }); } else { console.log(`Error posting to slack-background`); - console.log(result); - return { - statusCode: 400, - body: JSON.stringify({ success: false }), - }; + return new Response(JSON.stringify({ success: false }), { + status: 400, + }); } } - + break; + } default: break; } console.log('Unknown action.'); - return { - statusCode: 400, - body: JSON.stringify({ message: 'Unknown action.' }), - }; + return new Response(JSON.stringify({ message: 'Unknown action.' }), { + status: 400, + }); } catch (error) { console.log(error); - return { - statusCode: 500, - body: '', - }; + return new Response('', { status: 500 }); } - - // return { - // statusCode: 200, - // body: '', - // }; }; - -module.exports = { handler }; diff --git a/functions/slack-interactivity/index.ts b/functions/slack-interactivity/index.ts index 4fc8316..2e579c6 100644 --- a/functions/slack-interactivity/index.ts +++ b/functions/slack-interactivity/index.ts @@ -1,8 +1,3 @@ -const handler = async function (event, context) { - return { - statusCode: 200, - body: '', - }; +export default async (req: Request) => { + return new Response('', { status: 200 }); }; - -module.exports = { handler }; diff --git a/functions/slack-send-message-background/index.ts b/functions/slack-send-message-background/index.ts index 832ed56..cb97013 100644 --- a/functions/slack-send-message-background/index.ts +++ b/functions/slack-send-message-background/index.ts @@ -1,26 +1,30 @@ -require('dotenv').config(); +import { postMessage, publishView } from '../../util/slack.js'; -const { postMessage, publishView } = require('../../util/slack'); +interface BackgroundRequest { + key: string; + action: string; + message: Record; +} -const handler = async function (event, context) { - const request = JSON.parse(event.body); +export default async (req: Request) => { + const request = (await req.json()) as BackgroundRequest; if (request.key !== process.env.WEBHOOKS_VERIFICATION) { console.log('Not Authorized'); throw new Error('Not Authorized'); } - let result; + let result: { ok?: boolean; ts?: string; message?: { username?: string } } | undefined; switch (request.action) { case 'postMessage': - result = await postMessage(request.message); + result = await postMessage(request.message as unknown as Parameters[0]); if (result.ok) { console.log( `Successfully posted message ${result.ts} to user ${ result.message && result.message.username - }` + }`, ); } else { console.log('Error posting message:'); @@ -30,11 +34,11 @@ const handler = async function (event, context) { break; case 'publishView': - result = await publishView(request.message); + result = await publishView(request.message as unknown as Parameters[0]); if (result.ok) { console.log( - `Successfully published view to ${request.message.user_id}` + `Successfully published view to ${(request.message as Record).user_id}`, ); } else { console.log('Error publishing view:'); @@ -47,11 +51,4 @@ const handler = async function (event, context) { console.log('No action'); break; } - - // return { - // statusCode: 200, - // body: '', - // }; }; - -module.exports = { handler }; From 04b65b3f525874ba38f35d65a087417ca07c0c2a Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:49:06 -0400 Subject: [PATCH 07/18] Migrate Zoom webhook handler to Netlify Functions v2 - Convert handler to v2 Request/Response pattern - Move lazy requires to top-level ESM imports - Add Room type and Zoom webhook request types - HMAC verification uses req.text() for raw body - Airtable helper with proper types --- .../zoom-meeting-webhook-handler/airtable.ts | 15 ++- .../zoom-meeting-webhook-handler/index.ts | 106 +++++++++--------- .../zoom-meeting-webhook-handler/slack.ts | 61 ++++++---- types/room.ts | 15 +++ 4 files changed, 115 insertions(+), 82 deletions(-) create mode 100644 types/room.ts diff --git a/functions/zoom-meeting-webhook-handler/airtable.ts b/functions/zoom-meeting-webhook-handler/airtable.ts index 1356e2b..3c7b541 100644 --- a/functions/zoom-meeting-webhook-handler/airtable.ts +++ b/functions/zoom-meeting-webhook-handler/airtable.ts @@ -1,6 +1,15 @@ +import type { Room } from '../../types/room.js'; +import type Airtable from 'airtable'; + +type AirtableBase = ReturnType['base']>; + // returns a roomInstance record, or undefined. // Will retry 5 times, pausing 1 second between tries. -async function findRoomInstance(room, base, instanceId) { +export async function findRoomInstance( + room: Room, + base: AirtableBase, + instanceId: string, +) { async function tryFind() { const resultArray = await base('room_instances') .select({ @@ -13,7 +22,7 @@ async function findRoomInstance(room, base, instanceId) { return resultArray[0]; } - function sleep(ms) { + function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -31,5 +40,3 @@ async function findRoomInstance(room, base, instanceId) { return roomInstance; } - -module.exports = { findRoomInstance }; diff --git a/functions/zoom-meeting-webhook-handler/index.ts b/functions/zoom-meeting-webhook-handler/index.ts index 76fa365..921c443 100644 --- a/functions/zoom-meeting-webhook-handler/index.ts +++ b/functions/zoom-meeting-webhook-handler/index.ts @@ -1,10 +1,12 @@ -require('dotenv').config(); +import crypto from 'node:crypto'; +import Airtable from 'airtable'; +import { updateMeetingStatus, updateMeetingAttendence } from './slack.js'; +import { findRoomInstance } from './airtable.js'; +import type { Room } from '../../types/room.js'; -const crypto = require('crypto'); - -const { updateMeetingStatus, updateMeetingAttendence } = require('./slack'); - -const rooms = require('../../data/rooms.json'); +// @ts-expect-error - rooms.json is generated by the build script before bundling +import rooms from '../../data/rooms.json' with { type: 'json' }; +const typedRooms = rooms as Room[]; const EVENT_MEETING_STARTED = 'meeting.started'; const EVENT_MEETING_ENDED = 'meeting.ended'; @@ -18,19 +20,23 @@ const ZOOM_SECRET = const ZOOM_AUTH = process.env.TEST_ZOOM_WEBHOOK_AUTH || process.env.ZOOM_WEBHOOK_AUTH; -const handler = async function (event, context) { +export default async (req: Request) => { try { + const rawBody = await req.text(); + /** * verification. zoom will either send an authorization header or a x-zm-signature header */ let authorized = false; - if (event.headers['x-zm-signature']) { - const message = `v0:${event.headers['x-zm-request-timestamp']}:${event.body}`; + const zmSignature = req.headers.get('x-zm-signature'); + if (zmSignature) { + const zmTimestamp = req.headers.get('x-zm-request-timestamp'); + const message = `v0:${zmTimestamp}:${rawBody}`; const hashForVerify = crypto - .createHmac('sha256', ZOOM_SECRET) + .createHmac('sha256', ZOOM_SECRET!) .update(message) .digest('hex'); @@ -41,45 +47,42 @@ const handler = async function (event, context) { console.log('signature'); console.log(signature); console.log('x-zm-signature'); - console.log(event.headers['x-zm-signature']); + console.log(zmSignature); - if (event.headers['x-zm-signature'] === signature) { + if (zmSignature === signature) { authorized = true; } } else { - if (event.headers.authorization === ZOOM_AUTH) { + if (req.headers.get('authorization') === ZOOM_AUTH) { authorized = true; } } if (!authorized) { - console.log('Unauthorized', event); - return { - statusCode: 401, - body: '', - }; + console.log('Unauthorized'); + return new Response('', { status: 401 }); } - const request = JSON.parse(event.body); + const request = JSON.parse(rawBody); if (request.event == 'endpoint.url_validation') { const hashForValidate = crypto - .createHmac('sha256', ZOOM_SECRET) + .createHmac('sha256', ZOOM_SECRET!) .update(request.payload.plainToken) .digest('hex'); - return { - statusCode: 200, - body: JSON.stringify({ + return new Response( + JSON.stringify({ plainToken: request.payload.plainToken, encryptedToken: hashForValidate, }), - }; + { status: 200 }, + ); } // check our meeting ID. The meeting ID never changes, but the uuid is different for each instance - const room = rooms.find( - (room) => room.ZoomMeetingId === request.payload.object.id + const room = typedRooms.find( + (room) => room.ZoomMeetingId === request.payload.object.id, ); console.log('incoming request'); console.log('request payload'); @@ -88,34 +91,32 @@ const handler = async function (event, context) { console.log(request.event); if (room) { - const Airtable = require('airtable'); - const base = new Airtable().base(process.env.AIRTABLE_COWORKING_BASE); - - const { findRoomInstance } = require('./airtable'); + const base = new Airtable().base(process.env.AIRTABLE_COWORKING_BASE!); switch (request.event) { case EVENT_PARTICIPANT_JOINED: - case EVENT_PARTICIPANT_LEFT: - let roomInstance = await findRoomInstance( + case EVENT_PARTICIPANT_LEFT: { + const roomInstance = await findRoomInstance( room, base, - request.payload.object.uuid + request.payload.object.uuid, ); if (roomInstance) { // create room event record console.log(`found room instance ${roomInstance.getId()}`); - const updatedMeeting = await updateMeetingAttendence( + await updateMeetingAttendence( room, - roomInstance.get('slack_thread_timestamp'), - request + roomInstance.get('slack_thread_timestamp') as string, + request, ); } break; + } - case EVENT_MEETING_STARTED: + case EVENT_MEETING_STARTED: { // post message to Slack and get result console.log('posting update'); const result = await updateMeetingStatus(room); @@ -136,27 +137,27 @@ const handler = async function (event, context) { console.log(`room_event created: ${created.getId()}`); break; + } - case EVENT_MEETING_ENDED: - let roomInstanceEnd = await findRoomInstance( + case EVENT_MEETING_ENDED: { + const roomInstanceEnd = await findRoomInstance( room, base, - request.payload.object.uuid + request.payload.object.uuid, ); if (roomInstanceEnd) { - const slackedEnd = await updateMeetingStatus( + await updateMeetingStatus( room, - roomInstanceEnd.get('slack_thread_timestamp') + roomInstanceEnd.get('slack_thread_timestamp') as string, ); // update room instance - // const updated = await base('room_instances').update( roomInstanceEnd.getId(), { end_time: request.payload.object.end_time, - } + }, ); if (!updated) { @@ -167,6 +168,7 @@ const handler = async function (event, context) { } break; + } default: break; @@ -175,19 +177,13 @@ const handler = async function (event, context) { console.log('meeting ID is not co-working meeting'); } - return { - statusCode: 200, - body: '', - }; + return new Response('', { status: 200 }); } catch (error) { // output to netlify function log console.log(error); - return { - statusCode: 500, - // Could be a custom message or object i.e. JSON.stringify(err) - body: JSON.stringify({ msg: error.message }), - }; + return new Response( + JSON.stringify({ msg: error instanceof Error ? error.message : String(error) }), + { status: 500 }, + ); } }; - -module.exports = { handler }; diff --git a/functions/zoom-meeting-webhook-handler/slack.ts b/functions/zoom-meeting-webhook-handler/slack.ts index fd56dc2..b07c8f5 100644 --- a/functions/zoom-meeting-webhook-handler/slack.ts +++ b/functions/zoom-meeting-webhook-handler/slack.ts @@ -1,11 +1,21 @@ -require('dotenv').config(); +import { postMessage, updateMessage } from '../../util/slack.js'; +import type { Room } from '../../types/room.js'; -const { postMessage, updateMessage } = require('../../util/slack'); +interface ZoomWebhookRequest { + event: string; + payload: { + object: { + participant: { + user_name: string; + }; + }; + }; +} // timestamp: if we have a timestamp, that means we've ended the meeting and are trying to update the message // otherwise, post a new message -async function updateMeetingStatus(room, timestamp) { +export async function updateMeetingStatus(room: Room, timestamp?: string) { const message = { channel: room.SlackChannelId, text: timestamp ? room.MessageSessionEnded : room.MessageSessionStarted, @@ -13,39 +23,39 @@ async function updateMeetingStatus(room, timestamp) { unfurl_media: false, blocks: [ { - type: 'section', + type: 'section' as const, text: { - type: 'mrkdwn', + type: 'mrkdwn' as const, text: timestamp ? room.MessageSessionEnded : room.MessageSessionStarted, }, accessory: { - type: 'button', + type: 'button' as const, text: { - type: 'plain_text', + type: 'plain_text' as const, text: timestamp ? room.ButtonStartNew : room.ButtonJoin, emoji: true, }, value: 'join_meeting', url: room.ZoomMeetingInviteUrl, action_id: 'button-action', - style: 'primary', + style: 'primary' as const, confirm: { title: { - type: 'plain_text', + type: 'plain_text' as const, text: room.NoticeTitle, }, text: { - type: 'mrkdwn', + type: 'mrkdwn' as const, text: room.NoticeBody, }, confirm: { - type: 'plain_text', + type: 'plain_text' as const, text: room.NoticeConfirm, }, deny: { - type: 'plain_text', + type: 'plain_text' as const, text: room.NoticeCancel, }, }, @@ -54,10 +64,10 @@ async function updateMeetingStatus(room, timestamp) { ...(room.ContextBody ? [ { - type: 'context', + type: 'context' as const, elements: [ { - type: 'mrkdwn', + type: 'mrkdwn' as const, text: room.ContextBody, }, ], @@ -67,20 +77,25 @@ async function updateMeetingStatus(room, timestamp) { ], }; - // console.log(JSON.stringify(message)); - + // These calls never use background mode, so the result is always a Slack API response const result = timestamp ? await updateMessage({ ...message, ts: timestamp }) : await postMessage(message); + const slackResult = result as { ts?: string }; + console.log( - `Successfully send message ${result.ts} in conversation ${room.SlackChannelId}` + `Successfully send message ${slackResult.ts} in conversation ${room.SlackChannelId}`, ); - return result; + return slackResult; } -async function updateMeetingAttendence(room, thread_ts, zoomRequest) { +export async function updateMeetingAttendence( + room: Room, + thread_ts: string, + zoomRequest: ZoomWebhookRequest, +) { const username = zoomRequest.payload.object.participant.user_name; const result = await postMessage({ thread_ts, @@ -91,11 +106,11 @@ async function updateMeetingAttendence(room, thread_ts, zoomRequest) { channel: room.SlackChannelId, }); + const slackResult = result as { ts?: string }; + console.log( - `Successfully send message ${result.ts} in conversation ${room.SlackChannelId}` + `Successfully send message ${slackResult.ts} in conversation ${room.SlackChannelId}`, ); - return result; + return slackResult; } - -module.exports = { updateMeetingStatus, updateMeetingAttendence }; diff --git a/types/room.ts b/types/room.ts new file mode 100644 index 0000000..d4ef3c4 --- /dev/null +++ b/types/room.ts @@ -0,0 +1,15 @@ +export interface Room { + ZoomMeetingId: number; + SlackChannelId: string; + ZoomMeetingInviteUrl: string; + MessageSessionStarted: string; + MessageSessionEnded: string; + ButtonJoin: string; + ButtonStartNew: string; + NoticeTitle: string; + NoticeBody: string; + NoticeConfirm: string; + NoticeCancel: string; + ContextBody?: string; + record_id: string; +} From 3756cdcebda54d9da2a65cc1f1fc30352e2a976d Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:49:33 -0400 Subject: [PATCH 08/18] Migrate build script to TypeScript + ESM - Convert CommonJS requires to ESM imports - Replace __dirname with import.meta.url derivation - Keep dotenv/config for local development --- scripts/build.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/scripts/build.ts b/scripts/build.ts index 658576d..ede4c39 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -1,8 +1,11 @@ -require('dotenv').config(); -const Airtable = require('airtable'); -const base = new Airtable().base(process.env.AIRTABLE_COWORKING_BASE); -var fs = require('fs'); -const path = require('path'); +import 'dotenv/config'; +import Airtable from 'airtable'; +import { writeFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const base = new Airtable().base(process.env.AIRTABLE_COWORKING_BASE!); async function main() { console.log('Building rooms'); @@ -13,9 +16,9 @@ async function main() { record_id: record.id, })); - fs.writeFileSync( - path.resolve(__dirname, '..', 'data', 'rooms.json'), - JSON.stringify(rooms, null, 2) + writeFileSync( + resolve(__dirname, '..', 'data', 'rooms.json'), + JSON.stringify(rooms, null, 2), ); console.log(`Done building ${rooms.length} rooms`); From c554c85569fc5f92f2c75b1d795b5be0ca3ad3c3 Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:52:32 -0400 Subject: [PATCH 09/18] Validate AIRTABLE_COWORKING_BASE env var in build script --- scripts/build.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/build.ts b/scripts/build.ts index ede4c39..9651f09 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -5,7 +5,12 @@ import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const base = new Airtable().base(process.env.AIRTABLE_COWORKING_BASE!); + +if (!process.env.AIRTABLE_COWORKING_BASE) { + throw new Error('AIRTABLE_COWORKING_BASE environment variable is required'); +} + +const base = new Airtable().base(process.env.AIRTABLE_COWORKING_BASE); async function main() { console.log('Building rooms'); From a204a13a08cc93ee883fa6aadd62a4f716173dc6 Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:56:49 -0400 Subject: [PATCH 10/18] Switch to bundler module resolution and strip .js import extensions esbuild handles module resolution for Netlify Functions and tsx handles it for the build script, so nodenext's strict .js extension requirement is unnecessary. --- functions/event-reminders-daily/index.ts | 4 ++-- functions/event-reminders-hourly/index.ts | 4 ++-- functions/event-reminders-weekly/index.ts | 4 ++-- functions/slack-events/index.ts | 4 ++-- functions/slack-send-message-background/index.ts | 2 +- functions/zoom-meeting-webhook-handler/airtable.ts | 2 +- functions/zoom-meeting-webhook-handler/index.ts | 6 +++--- functions/zoom-meeting-webhook-handler/slack.ts | 4 ++-- tsconfig.json | 4 ++-- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/functions/event-reminders-daily/index.ts b/functions/event-reminders-daily/index.ts index 63a75ee..bd610bd 100644 --- a/functions/event-reminders-daily/index.ts +++ b/functions/event-reminders-daily/index.ts @@ -1,9 +1,9 @@ import { GraphQLClient, gql } from 'graphql-request'; import { DateTime } from 'luxon'; -import { postMessage } from '../../util/slack.js'; +import { postMessage } from '../../util/slack'; import slackify from 'slackify-html'; import type { Config } from '@netlify/functions'; -import type { CalendarsResponse, EventsResponse } from '../../types/cms.js'; +import type { CalendarsResponse, EventsResponse } from '../../types/cms'; const SLACK_ANNOUNCEMENTS_CHANNEL = process.env.TEST_SLACK_ANNOUNCEMENTS_CHANNEL || diff --git a/functions/event-reminders-hourly/index.ts b/functions/event-reminders-hourly/index.ts index 598d265..4180711 100644 --- a/functions/event-reminders-hourly/index.ts +++ b/functions/event-reminders-hourly/index.ts @@ -1,9 +1,9 @@ import { GraphQLClient, gql } from 'graphql-request'; import { DateTime } from 'luxon'; -import { postMessage } from '../../util/slack.js'; +import { postMessage } from '../../util/slack'; import slackify from 'slackify-html'; import type { Config } from '@netlify/functions'; -import type { CalendarsResponse, EventsResponse } from '../../types/cms.js'; +import type { CalendarsResponse, EventsResponse } from '../../types/cms'; const SLACK_ANNOUNCEMENTS_CHANNEL = process.env.TEST_SLACK_ANNOUNCEMENTS_CHANNEL || diff --git a/functions/event-reminders-weekly/index.ts b/functions/event-reminders-weekly/index.ts index e7bae73..af33b49 100644 --- a/functions/event-reminders-weekly/index.ts +++ b/functions/event-reminders-weekly/index.ts @@ -1,8 +1,8 @@ import { GraphQLClient, gql } from 'graphql-request'; import { DateTime } from 'luxon'; -import { postMessage } from '../../util/slack.js'; +import { postMessage } from '../../util/slack'; import type { Config } from '@netlify/functions'; -import type { CalendarsResponse, EventsResponse } from '../../types/cms.js'; +import type { CalendarsResponse, EventsResponse } from '../../types/cms'; const SLACK_ANNOUNCEMENTS_CHANNEL = process.env.TEST_SLACK_ANNOUNCEMENTS_CHANNEL || diff --git a/functions/slack-events/index.ts b/functions/slack-events/index.ts index c6e3fec..0f71bd4 100644 --- a/functions/slack-events/index.ts +++ b/functions/slack-events/index.ts @@ -1,6 +1,6 @@ import crypto from 'node:crypto'; -import { welcome, appHome } from './messages.js'; -import { postMessage, publishView } from '../../util/slack.js'; +import { welcome, appHome } from './messages'; +import { postMessage, publishView } from '../../util/slack'; const SLACK_SIGNING_SECRET = process.env.TEST_SLACK_SIGNING_SECRET || process.env.SLACK_SIGNING_SECRET; diff --git a/functions/slack-send-message-background/index.ts b/functions/slack-send-message-background/index.ts index cb97013..29627b5 100644 --- a/functions/slack-send-message-background/index.ts +++ b/functions/slack-send-message-background/index.ts @@ -1,4 +1,4 @@ -import { postMessage, publishView } from '../../util/slack.js'; +import { postMessage, publishView } from '../../util/slack'; interface BackgroundRequest { key: string; diff --git a/functions/zoom-meeting-webhook-handler/airtable.ts b/functions/zoom-meeting-webhook-handler/airtable.ts index 3c7b541..cfbc05f 100644 --- a/functions/zoom-meeting-webhook-handler/airtable.ts +++ b/functions/zoom-meeting-webhook-handler/airtable.ts @@ -1,4 +1,4 @@ -import type { Room } from '../../types/room.js'; +import type { Room } from '../../types/room'; import type Airtable from 'airtable'; type AirtableBase = ReturnType['base']>; diff --git a/functions/zoom-meeting-webhook-handler/index.ts b/functions/zoom-meeting-webhook-handler/index.ts index 921c443..0be0f20 100644 --- a/functions/zoom-meeting-webhook-handler/index.ts +++ b/functions/zoom-meeting-webhook-handler/index.ts @@ -1,8 +1,8 @@ import crypto from 'node:crypto'; import Airtable from 'airtable'; -import { updateMeetingStatus, updateMeetingAttendence } from './slack.js'; -import { findRoomInstance } from './airtable.js'; -import type { Room } from '../../types/room.js'; +import { updateMeetingStatus, updateMeetingAttendence } from './slack'; +import { findRoomInstance } from './airtable'; +import type { Room } from '../../types/room'; // @ts-expect-error - rooms.json is generated by the build script before bundling import rooms from '../../data/rooms.json' with { type: 'json' }; diff --git a/functions/zoom-meeting-webhook-handler/slack.ts b/functions/zoom-meeting-webhook-handler/slack.ts index b07c8f5..feafd02 100644 --- a/functions/zoom-meeting-webhook-handler/slack.ts +++ b/functions/zoom-meeting-webhook-handler/slack.ts @@ -1,5 +1,5 @@ -import { postMessage, updateMessage } from '../../util/slack.js'; -import type { Room } from '../../types/room.js'; +import { postMessage, updateMessage } from '../../util/slack'; +import type { Room } from '../../types/room'; interface ZoomWebhookRequest { event: string; diff --git a/tsconfig.json b/tsconfig.json index 7239331..bf964eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2024", - "module": "nodenext", - "moduleResolution": "nodenext", + "module": "esnext", + "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "skipLibCheck": true, From 3df2d3c08200c0490667485f3b8ce6b50b7eeaaf Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:59:02 -0400 Subject: [PATCH 11/18] Add env.d.ts for typed environment variables --- env.d.ts | 33 +++++++++++++++++++++++++++++++++ tsconfig.json | 1 + 2 files changed, 34 insertions(+) create mode 100644 env.d.ts diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..1f2af38 --- /dev/null +++ b/env.d.ts @@ -0,0 +1,33 @@ +declare namespace NodeJS { + interface ProcessEnv { + // CMS + CMS_URL?: string; + CMS_TOKEN?: string; + + // Slack + SLACK_BOT_TOKEN?: string; + SLACK_SIGNING_SECRET?: string; + SLACK_ANNOUNCEMENTS_CHANNEL?: string; + SLACK_EVENT_ADMIN_CHANNEL?: string; + + // Zoom + ZOOM_WEBHOOK_SECRET_TOKEN?: string; + ZOOM_WEBHOOK_AUTH?: string; + + // Airtable + AIRTABLE_COWORKING_BASE?: string; + + // App + APP_HOST?: string; + WEBHOOKS_VERIFICATION?: string; + + // Test overrides + TEST_SLACK_BOT_TOKEN?: string; + TEST_SLACK_SIGNING_SECRET?: string; + TEST_SLACK_ANNOUNCEMENTS_CHANNEL?: string; + TEST_SLACK_EVENT_ADMIN_CHANNEL?: string; + TEST_APP_HOST?: string; + TEST_ZOOM_WEBHOOK_SECRET_TOKEN?: string; + TEST_ZOOM_WEBHOOK_AUTH?: string; + } +} diff --git a/tsconfig.json b/tsconfig.json index bf964eb..92f27e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "types": ["node"] }, "include": [ + "env.d.ts", "functions/**/*.ts", "util/**/*.ts", "scripts/**/*.ts" From b0aace97d30cd8f11618d1d3bf1670d44e32823b Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:02:47 -0400 Subject: [PATCH 12/18] Replace non-null assertions with runtime env var validation Add util/env.ts with requireEnv() helper that throws a clear error if a required environment variable is missing. Applied to all env vars that were previously using the ! operator. --- functions/event-reminders-daily/index.ts | 5 +++-- functions/event-reminders-hourly/index.ts | 11 ++++++----- functions/event-reminders-weekly/index.ts | 5 +++-- functions/slack-events/index.ts | 6 ++++-- functions/zoom-meeting-webhook-handler/index.ts | 11 ++++++----- util/env.ts | 7 +++++++ 6 files changed, 29 insertions(+), 16 deletions(-) create mode 100644 util/env.ts diff --git a/functions/event-reminders-daily/index.ts b/functions/event-reminders-daily/index.ts index bd610bd..c1cd3a8 100644 --- a/functions/event-reminders-daily/index.ts +++ b/functions/event-reminders-daily/index.ts @@ -2,12 +2,13 @@ import { GraphQLClient, gql } from 'graphql-request'; import { DateTime } from 'luxon'; import { postMessage } from '../../util/slack'; import slackify from 'slackify-html'; +import { requireEnv } from '../../util/env'; import type { Config } from '@netlify/functions'; import type { CalendarsResponse, EventsResponse } from '../../types/cms'; const SLACK_ANNOUNCEMENTS_CHANNEL = process.env.TEST_SLACK_ANNOUNCEMENTS_CHANNEL || - process.env.SLACK_ANNOUNCEMENTS_CHANNEL; + requireEnv('SLACK_ANNOUNCEMENTS_CHANNEL'); const DEFAULT_SLACK_EVENT_CHANNEL = 'C017WAKN883'; @@ -83,7 +84,7 @@ export default async (req: Request) => { } const dailyMessage = { - channel: SLACK_ANNOUNCEMENTS_CHANNEL!, + channel: SLACK_ANNOUNCEMENTS_CHANNEL, text: `Today's events are: ${eventsList .map((event) => { return `${event.title}: ${DateTime.fromISO( diff --git a/functions/event-reminders-hourly/index.ts b/functions/event-reminders-hourly/index.ts index 4180711..e2238a5 100644 --- a/functions/event-reminders-hourly/index.ts +++ b/functions/event-reminders-hourly/index.ts @@ -2,16 +2,17 @@ import { GraphQLClient, gql } from 'graphql-request'; import { DateTime } from 'luxon'; import { postMessage } from '../../util/slack'; import slackify from 'slackify-html'; +import { requireEnv } from '../../util/env'; import type { Config } from '@netlify/functions'; import type { CalendarsResponse, EventsResponse } from '../../types/cms'; const SLACK_ANNOUNCEMENTS_CHANNEL = process.env.TEST_SLACK_ANNOUNCEMENTS_CHANNEL || - process.env.SLACK_ANNOUNCEMENTS_CHANNEL; + requireEnv('SLACK_ANNOUNCEMENTS_CHANNEL'); const SLACK_EVENT_ADMIN_CHANNEL = process.env.TEST_SLACK_EVENT_ADMIN_CHANNEL || - process.env.SLACK_EVENT_ADMIN_CHANNEL; + requireEnv('SLACK_EVENT_ADMIN_CHANNEL'); const DEFAULT_SLACK_EVENT_CHANNEL = 'C017WAKN883'; @@ -164,8 +165,8 @@ export default async (req: Request) => { ); return { - channel: (event.eventSlackAnnouncementsChannelId || - DEFAULT_SLACK_EVENT_CHANNEL)!, + channel: event.eventSlackAnnouncementsChannelId || + DEFAULT_SLACK_EVENT_CHANNEL, text: `Starting soon: ${event.title}: ${eventDate.toFormat( 'EEEE, fff', )}`, @@ -176,7 +177,7 @@ export default async (req: Request) => { }); const hourlyAdminMessage = { - channel: SLACK_EVENT_ADMIN_CHANNEL!, + channel: SLACK_EVENT_ADMIN_CHANNEL, text: `Starting soon: ${filteredList .map((event) => { return `${event.title}: ${DateTime.fromISO( diff --git a/functions/event-reminders-weekly/index.ts b/functions/event-reminders-weekly/index.ts index af33b49..53121f5 100644 --- a/functions/event-reminders-weekly/index.ts +++ b/functions/event-reminders-weekly/index.ts @@ -1,12 +1,13 @@ import { GraphQLClient, gql } from 'graphql-request'; import { DateTime } from 'luxon'; import { postMessage } from '../../util/slack'; +import { requireEnv } from '../../util/env'; import type { Config } from '@netlify/functions'; import type { CalendarsResponse, EventsResponse } from '../../types/cms'; const SLACK_ANNOUNCEMENTS_CHANNEL = process.env.TEST_SLACK_ANNOUNCEMENTS_CHANNEL || - process.env.SLACK_ANNOUNCEMENTS_CHANNEL; + requireEnv('SLACK_ANNOUNCEMENTS_CHANNEL'); const DEFAULT_SLACK_EVENT_CHANNEL = 'C017WAKN883'; @@ -79,7 +80,7 @@ export default async (req: Request) => { const eventsList = eventsResponse.solspace_calendar.events; if (eventsList && eventsList.length) { const weeklyMessage = { - channel: SLACK_ANNOUNCEMENTS_CHANNEL!, + channel: SLACK_ANNOUNCEMENTS_CHANNEL, text: `This weeks events are: ${eventsList .map((event) => { return `${event.title}: ${DateTime.fromISO( diff --git a/functions/slack-events/index.ts b/functions/slack-events/index.ts index 0f71bd4..b2f1684 100644 --- a/functions/slack-events/index.ts +++ b/functions/slack-events/index.ts @@ -1,9 +1,11 @@ import crypto from 'node:crypto'; import { welcome, appHome } from './messages'; import { postMessage, publishView } from '../../util/slack'; +import { requireEnv } from '../../util/env'; const SLACK_SIGNING_SECRET = - process.env.TEST_SLACK_SIGNING_SECRET || process.env.SLACK_SIGNING_SECRET; + process.env.TEST_SLACK_SIGNING_SECRET || + requireEnv('SLACK_SIGNING_SECRET'); function verify(rawBody: string, headers: Headers) { const slackSignature = headers.get('x-slack-signature'); @@ -21,7 +23,7 @@ function verify(rawBody: string, headers: Headers) { const mySignature = 'v0=' + crypto - .createHmac('sha256', SLACK_SIGNING_SECRET!) + .createHmac('sha256', SLACK_SIGNING_SECRET) .update(verificationString, 'utf8') .digest('hex'); diff --git a/functions/zoom-meeting-webhook-handler/index.ts b/functions/zoom-meeting-webhook-handler/index.ts index 0be0f20..b61dd7d 100644 --- a/functions/zoom-meeting-webhook-handler/index.ts +++ b/functions/zoom-meeting-webhook-handler/index.ts @@ -2,6 +2,7 @@ import crypto from 'node:crypto'; import Airtable from 'airtable'; import { updateMeetingStatus, updateMeetingAttendence } from './slack'; import { findRoomInstance } from './airtable'; +import { requireEnv } from '../../util/env'; import type { Room } from '../../types/room'; // @ts-expect-error - rooms.json is generated by the build script before bundling @@ -15,10 +16,10 @@ const EVENT_PARTICIPANT_LEFT = 'meeting.participant_left'; const ZOOM_SECRET = process.env.TEST_ZOOM_WEBHOOK_SECRET_TOKEN || - process.env.ZOOM_WEBHOOK_SECRET_TOKEN; + requireEnv('ZOOM_WEBHOOK_SECRET_TOKEN'); const ZOOM_AUTH = - process.env.TEST_ZOOM_WEBHOOK_AUTH || process.env.ZOOM_WEBHOOK_AUTH; + process.env.TEST_ZOOM_WEBHOOK_AUTH || requireEnv('ZOOM_WEBHOOK_AUTH'); export default async (req: Request) => { try { @@ -36,7 +37,7 @@ export default async (req: Request) => { const message = `v0:${zmTimestamp}:${rawBody}`; const hashForVerify = crypto - .createHmac('sha256', ZOOM_SECRET!) + .createHmac('sha256', ZOOM_SECRET) .update(message) .digest('hex'); @@ -67,7 +68,7 @@ export default async (req: Request) => { if (request.event == 'endpoint.url_validation') { const hashForValidate = crypto - .createHmac('sha256', ZOOM_SECRET!) + .createHmac('sha256', ZOOM_SECRET) .update(request.payload.plainToken) .digest('hex'); return new Response( @@ -91,7 +92,7 @@ export default async (req: Request) => { console.log(request.event); if (room) { - const base = new Airtable().base(process.env.AIRTABLE_COWORKING_BASE!); + const base = new Airtable().base(requireEnv('AIRTABLE_COWORKING_BASE')); switch (request.event) { case EVENT_PARTICIPANT_JOINED: diff --git a/util/env.ts b/util/env.ts new file mode 100644 index 0000000..597a010 --- /dev/null +++ b/util/env.ts @@ -0,0 +1,7 @@ +export function requireEnv(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Missing required environment variable: ${name}`); + } + return value; +} From c9c8675a75c38790c08c1f7f0f03d93ee38c0428 Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:06:38 -0400 Subject: [PATCH 13/18] Extract webhook HMAC verification into shared util/verify.ts - verifyHmacSignature: core timing-safe HMAC-SHA256 comparison - verifySlackRequest: Slack-specific wrapper with timestamp staleness check - verifyZoomSignature: Zoom-specific wrapper for x-zm-signature header --- functions/slack-events/index.ts | 42 +---------- .../zoom-meeting-webhook-handler/index.ts | 33 ++------- util/verify.ts | 69 +++++++++++++++++++ 3 files changed, 75 insertions(+), 69 deletions(-) create mode 100644 util/verify.ts diff --git a/functions/slack-events/index.ts b/functions/slack-events/index.ts index b2f1684..63e0459 100644 --- a/functions/slack-events/index.ts +++ b/functions/slack-events/index.ts @@ -1,50 +1,12 @@ -import crypto from 'node:crypto'; import { welcome, appHome } from './messages'; import { postMessage, publishView } from '../../util/slack'; import { requireEnv } from '../../util/env'; +import { verifySlackRequest } from '../../util/verify'; const SLACK_SIGNING_SECRET = process.env.TEST_SLACK_SIGNING_SECRET || requireEnv('SLACK_SIGNING_SECRET'); -function verify(rawBody: string, headers: Headers) { - const slackSignature = headers.get('x-slack-signature'); - const timestamp = headers.get('x-slack-request-timestamp'); - // convert current time from milliseconds to seconds - const time = Math.floor(new Date().getTime() / 1000); - if (!timestamp || Math.abs(time - Number(timestamp)) > 300) { - return { - valid: false as const, - reason: 'Ignore this request.', - }; - } - - const verificationString = `v0:${timestamp}:${rawBody}`; - const mySignature = - 'v0=' + - crypto - .createHmac('sha256', SLACK_SIGNING_SECRET) - .update(verificationString, 'utf8') - .digest('hex'); - - if ( - slackSignature && - crypto.timingSafeEqual( - Buffer.from(mySignature, 'utf8'), - Buffer.from(slackSignature, 'utf8'), - ) - ) { - return { - valid: true as const, - }; - } else { - return { - valid: false as const, - reason: 'Verification Failed.', - }; - } -} - const EVENT_TEAM_JOIN = 'team_join'; const EVENT_APP_HOME_OPENED = 'app_home_opened'; @@ -61,7 +23,7 @@ export default async (req: Request) => { } break; case 'event_callback': { - const isValid = verify(rawBody, req.headers); + const isValid = verifySlackRequest(rawBody, req.headers, SLACK_SIGNING_SECRET); if (!isValid.valid) { console.log('Failed validation: ', isValid.reason); diff --git a/functions/zoom-meeting-webhook-handler/index.ts b/functions/zoom-meeting-webhook-handler/index.ts index b61dd7d..423d9df 100644 --- a/functions/zoom-meeting-webhook-handler/index.ts +++ b/functions/zoom-meeting-webhook-handler/index.ts @@ -3,6 +3,7 @@ import Airtable from 'airtable'; import { updateMeetingStatus, updateMeetingAttendence } from './slack'; import { findRoomInstance } from './airtable'; import { requireEnv } from '../../util/env'; +import { verifyZoomSignature } from '../../util/verify'; import type { Room } from '../../types/room'; // @ts-expect-error - rooms.json is generated by the build script before bundling @@ -29,35 +30,9 @@ export default async (req: Request) => { * verification. zoom will either send an authorization header or a x-zm-signature header */ - let authorized = false; - - const zmSignature = req.headers.get('x-zm-signature'); - if (zmSignature) { - const zmTimestamp = req.headers.get('x-zm-request-timestamp'); - const message = `v0:${zmTimestamp}:${rawBody}`; - - const hashForVerify = crypto - .createHmac('sha256', ZOOM_SECRET) - .update(message) - .digest('hex'); - - const signature = `v0=${hashForVerify}`; - - console.log('message'); - console.log(message); - console.log('signature'); - console.log(signature); - console.log('x-zm-signature'); - console.log(zmSignature); - - if (zmSignature === signature) { - authorized = true; - } - } else { - if (req.headers.get('authorization') === ZOOM_AUTH) { - authorized = true; - } - } + const authorized = + verifyZoomSignature(rawBody, req.headers, ZOOM_SECRET) || + req.headers.get('authorization') === ZOOM_AUTH; if (!authorized) { console.log('Unauthorized'); diff --git a/util/verify.ts b/util/verify.ts new file mode 100644 index 0000000..b1d187c --- /dev/null +++ b/util/verify.ts @@ -0,0 +1,69 @@ +import crypto from 'node:crypto'; + +/** + * Core HMAC-SHA256 verification. Computes `v0=HMAC(secret, message)` and + * performs a timing-safe comparison against the expected signature. + */ +export function verifyHmacSignature( + secret: string, + message: string, + expectedSignature: string, +): boolean { + const computed = + 'v0=' + + crypto.createHmac('sha256', secret).update(message, 'utf8').digest('hex'); + + if (computed.length !== expectedSignature.length) { + return false; + } + + return crypto.timingSafeEqual( + Buffer.from(computed, 'utf8'), + Buffer.from(expectedSignature, 'utf8'), + ); +} + +/** + * Verifies a Slack request signature. Checks timestamp staleness (>300s) + * then validates the HMAC signature. + */ +export function verifySlackRequest( + rawBody: string, + headers: Headers, + secret: string, +): { valid: true } | { valid: false; reason: string } { + const slackSignature = headers.get('x-slack-signature'); + const timestamp = headers.get('x-slack-request-timestamp'); + + const time = Math.floor(Date.now() / 1000); + if (!timestamp || Math.abs(time - Number(timestamp)) > 300) { + return { valid: false, reason: 'Ignore this request.' }; + } + + const message = `v0:${timestamp}:${rawBody}`; + + if (slackSignature && verifyHmacSignature(secret, message, slackSignature)) { + return { valid: true }; + } + + return { valid: false, reason: 'Verification Failed.' }; +} + +/** + * Verifies a Zoom webhook signature using the x-zm-signature header. + */ +export function verifyZoomSignature( + rawBody: string, + headers: Headers, + secret: string, +): boolean { + const zmSignature = headers.get('x-zm-signature'); + const zmTimestamp = headers.get('x-zm-request-timestamp'); + + if (!zmSignature || !zmTimestamp) { + return false; + } + + const message = `v0:${zmTimestamp}:${rawBody}`; + return verifyHmacSignature(secret, message, zmSignature); +} From cac30f0bc1e4e91fcad4ad34f34785f1eadc449f Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:28:47 -0400 Subject: [PATCH 14/18] narrow type --- util/env.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/env.ts b/util/env.ts index 597a010..8bd47cb 100644 --- a/util/env.ts +++ b/util/env.ts @@ -1,4 +1,4 @@ -export function requireEnv(name: string): string { +export function requireEnv ( name: keyof typeof process.env ): string { const value = process.env[name]; if (!value) { throw new Error(`Missing required environment variable: ${name}`); From 78cf2166b60320e7d104d353bf71bcc8e5c75c32 Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:30:52 -0400 Subject: [PATCH 15/18] Use namespace import for slack-events messages module --- functions/slack-events/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/functions/slack-events/index.ts b/functions/slack-events/index.ts index 63e0459..7556b3a 100644 --- a/functions/slack-events/index.ts +++ b/functions/slack-events/index.ts @@ -1,4 +1,4 @@ -import { welcome, appHome } from './messages'; +import * as messages from './messages'; import { postMessage, publishView } from '../../util/slack'; import { requireEnv } from '../../util/env'; import { verifySlackRequest } from '../../util/verify'; @@ -37,7 +37,7 @@ export default async (req: Request) => { console.log('Posting to slack-background for team join'); result = await postMessage( - welcome({ event: request.event }), + messages.welcome({ event: request.event }), { background: true, }, @@ -49,7 +49,7 @@ export default async (req: Request) => { console.log('Posting to slack-background for app home'); result = await publishView( - appHome({ event: request.event }), + messages.appHome({ event: request.event }), { background: true, }, From 5832d0c71df8c0362136a34df4fed56121ce2bac Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:34:28 -0400 Subject: [PATCH 16/18] Move background request key verification to util/verify.ts --- functions/slack-send-message-background/index.ts | 7 +++---- util/verify.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/functions/slack-send-message-background/index.ts b/functions/slack-send-message-background/index.ts index 29627b5..d6041eb 100644 --- a/functions/slack-send-message-background/index.ts +++ b/functions/slack-send-message-background/index.ts @@ -1,4 +1,6 @@ import { postMessage, publishView } from '../../util/slack'; +import { requireEnv } from '../../util/env'; +import { verifyBackgroundRequest } from '../../util/verify'; interface BackgroundRequest { key: string; @@ -9,10 +11,7 @@ interface BackgroundRequest { export default async (req: Request) => { const request = (await req.json()) as BackgroundRequest; - if (request.key !== process.env.WEBHOOKS_VERIFICATION) { - console.log('Not Authorized'); - throw new Error('Not Authorized'); - } + verifyBackgroundRequest(request.key, requireEnv('WEBHOOKS_VERIFICATION')); let result: { ok?: boolean; ts?: string; message?: { username?: string } } | undefined; diff --git a/util/verify.ts b/util/verify.ts index b1d187c..26d7f89 100644 --- a/util/verify.ts +++ b/util/verify.ts @@ -67,3 +67,16 @@ export function verifyZoomSignature( const message = `v0:${zmTimestamp}:${rawBody}`; return verifyHmacSignature(secret, message, zmSignature); } + +/** + * Verifies a background function request by comparing the provided key + * against the expected verification key. + */ +export function verifyBackgroundRequest( + key: string, + expected: string, +): void { + if (key !== expected) { + throw new Error('Not Authorized'); + } +} From 3e7c7aaff20024ccef5ce8baa00b9101d4992263 Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:36:48 -0400 Subject: [PATCH 17/18] Centralize all crypto usage in util/verify.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add hmacSha256Hex() for Zoom's endpoint URL validation challenge. Remove node:crypto import from zoom handler — all crypto operations now go through the shared verify module. --- functions/zoom-meeting-webhook-handler/index.ts | 8 ++------ util/verify.ts | 8 ++++++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/functions/zoom-meeting-webhook-handler/index.ts b/functions/zoom-meeting-webhook-handler/index.ts index 423d9df..46c9d62 100644 --- a/functions/zoom-meeting-webhook-handler/index.ts +++ b/functions/zoom-meeting-webhook-handler/index.ts @@ -1,9 +1,8 @@ -import crypto from 'node:crypto'; import Airtable from 'airtable'; import { updateMeetingStatus, updateMeetingAttendence } from './slack'; import { findRoomInstance } from './airtable'; import { requireEnv } from '../../util/env'; -import { verifyZoomSignature } from '../../util/verify'; +import { verifyZoomSignature, hmacSha256Hex } from '../../util/verify'; import type { Room } from '../../types/room'; // @ts-expect-error - rooms.json is generated by the build script before bundling @@ -42,10 +41,7 @@ export default async (req: Request) => { const request = JSON.parse(rawBody); if (request.event == 'endpoint.url_validation') { - const hashForValidate = crypto - .createHmac('sha256', ZOOM_SECRET) - .update(request.payload.plainToken) - .digest('hex'); + const hashForValidate = hmacSha256Hex(ZOOM_SECRET, request.payload.plainToken); return new Response( JSON.stringify({ plainToken: request.payload.plainToken, diff --git a/util/verify.ts b/util/verify.ts index 26d7f89..ff6e9d4 100644 --- a/util/verify.ts +++ b/util/verify.ts @@ -68,6 +68,14 @@ export function verifyZoomSignature( return verifyHmacSignature(secret, message, zmSignature); } +/** + * Computes an HMAC-SHA256 hex digest. Used for Zoom's endpoint URL + * validation challenge response. + */ +export function hmacSha256Hex(secret: string, data: string): string { + return crypto.createHmac('sha256', secret).update(data).digest('hex'); +} + /** * Verifies a background function request by comparing the provided key * against the expected verification key. From 922054bd85f40044a22c86fd14cbc375d5bbcfa9 Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 1 Apr 2026 20:04:52 -0400 Subject: [PATCH 18/18] include json during build --- netlify.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/netlify.toml b/netlify.toml index 0d175dc..784a270 100644 --- a/netlify.toml +++ b/netlify.toml @@ -3,6 +3,7 @@ [functions] directory = "functions" + included_files = ["data/*.json"] [[redirects]] from = "/zoom-meeting-webhook-handler"