diff --git a/.nvmrc b/.nvmrc index 209e3ef..a45fd52 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +24 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/functions/event-reminders-daily/index.js b/functions/event-reminders-daily/index.ts similarity index 76% rename from functions/event-reminders-daily/index.js rename to functions/event-reminders-daily/index.ts index 7ad9e36..c1cd3a8 100644 --- a/functions/event-reminders-daily/index.js +++ b/functions/event-reminders-daily/index.ts @@ -1,13 +1,14 @@ -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'; +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'; @@ -21,7 +22,7 @@ const calendarsQuery = gql` } `; -function createEventsQuery(calendars) { +function createEventsQuery(calendars: CalendarsResponse) { return gql` query getEvents($rangeStart: String!, $rangeEnd: String!) { solspace_calendar { @@ -47,7 +48,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 +64,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; @@ -86,7 +88,7 @@ const handler = async function (event, context) { text: `Today's events are: ${eventsList .map((event) => { return `${event.title}: ${DateTime.fromISO( - event.startDateLocalized + event.startDateLocalized, ).toFormat('EEEE, fff')}`; }) .join(', ')}`, @@ -94,14 +96,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 +114,7 @@ const handler = async function (event, context) { text: `*${ event.title }*\n`, }, }, @@ -148,15 +150,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.js b/functions/event-reminders-hourly/index.ts similarity index 54% rename from functions/event-reminders-hourly/index.js rename to functions/event-reminders-hourly/index.ts index d114368..e2238a5 100644 --- a/functions/event-reminders-hourly/index.js +++ b/functions/event-reminders-hourly/index.ts @@ -1,17 +1,18 @@ -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'; +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'; @@ -25,7 +26,7 @@ const calendarsQuery = gql` } `; -function createEventsQuery(calendars) { +function createEventsQuery(calendars: CalendarsResponse) { return gql` query getEvents($rangeStart: String!, $rangeEnd: String!) { solspace_calendar { @@ -51,7 +52,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 +71,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 +94,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 +134,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 +149,7 @@ const handler = async function (event, context) { }); } - message.blocks.push( + blocks.push( { type: 'context', elements: [ @@ -169,10 +161,19 @@ 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 = { @@ -180,7 +181,7 @@ const handler = async function (event, context) { text: `Starting soon: ${filteredList .map((event) => { return `${event.title}: ${DateTime.fromISO( - event.startDateLocalized + event.startDateLocalized, ).toFormat('EEEE, fff')}`; }) .join(', ')}`, @@ -188,101 +189,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); - - const titleBlock = { - type: 'section', - text: { - type: 'mrkdwn', - text: `*${ - event.title - }*\n`, - }, - }; + ...filteredList.reduce[]>( + (list, event) => { + const eventDate = DateTime.fromISO(event.startDateLocalized); - 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.js b/functions/event-reminders-weekly/index.ts similarity index 71% rename from functions/event-reminders-weekly/index.js rename to functions/event-reminders-weekly/index.ts index 8d9b311..53121f5 100644 --- a/functions/event-reminders-weekly/index.js +++ b/functions/event-reminders-weekly/index.ts @@ -1,12 +1,13 @@ -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'; +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'; @@ -20,7 +21,7 @@ const calendarsQuery = gql` } `; -function createEventsQuery(calendars) { +function createEventsQuery(calendars: CalendarsResponse) { return gql` query getEvents($rangeStart: String!, $rangeEnd: String!) { solspace_calendar { @@ -46,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}`, @@ -65,14 +66,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,7 +84,7 @@ const handler = async function (event, context) { text: `This weeks events are: ${eventsList .map((event) => { return `${event.title}: ${DateTime.fromISO( - event.startDateLocalized + event.startDateLocalized, ).toFormat('EEEE, fff')}`; }) .join(', ')}`, @@ -90,9 +92,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 +103,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 +116,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 +141,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/functions/slack-events/index.js b/functions/slack-events/index.js deleted file mode 100644 index 59b357a..0000000 --- a/functions/slack-events/index.js +++ /dev/null @@ -1,151 +0,0 @@ -require('dotenv').config(); - -const crypto = require('crypto'); -const messages = require('./messages'); - -const { postMessage, publishView } = require('../../util/slack'); - -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']; - // convert current time from milliseconds to seconds - const time = Math.floor(new Date().getTime() / 1000); - if (Math.abs(time - timestamp) > 300) { - return { - valid: false, - reason: 'Ignore this request.', - }; - } - - const verificationString = `v0:${timestamp}:${event.body}`; - const mySignature = - 'v0=' + - crypto - .createHmac('sha256', SLACK_SIGNING_SECRET) - .update(verificationString, 'utf8') - .digest('hex'); - - if ( - crypto.timingSafeEqual( - Buffer.from(mySignature, 'utf8'), - Buffer.from(slackSignature, 'utf8') - ) - ) { - return { - valid: true, - }; - } else { - return { - valid: false, - reason: 'Verification Failed.', - }; - } -} - -const EVENT_TEAM_JOIN = 'team_join'; -const EVENT_APP_HOME_OPENED = 'app_home_opened'; - -const handler = async function (event, context) { - try { - const request = JSON.parse(event.body); - - 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 }), - }; - } - break; - case 'event_callback': - const isValid = verify(event); - - if (!isValid.valid) { - console.log('Failed validation: ', isValid.reason); - return { - statusCode: 400, - body: isValid.reason, - }; - } - // v0 - - let result = 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 }), - { - background: true, - } - ); - - break; - - case EVENT_APP_HOME_OPENED: - console.log('Posting to slack-background for app home'); - - result = await publishView( - messages.appHome({ event: request.event }), - { - background: true, - } - ); - break; - - default: - break; - } - - if (result) { - if (result.ok) { - console.log(`Successfully posted to slack-background`); - - return { - statusCode: 200, - body: JSON.stringify({ success: true }), - }; - } else { - console.log(`Error posting to slack-background`); - - console.log(result); - - return { - statusCode: 400, - body: JSON.stringify({ success: false }), - }; - } - } - - default: - break; - } - - console.log('Unknown action.'); - return { - statusCode: 400, - body: JSON.stringify({ message: 'Unknown action.' }), - }; - } catch (error) { - console.log(error); - return { - statusCode: 500, - body: '', - }; - } - - // return { - // statusCode: 200, - // body: '', - // }; -}; - -module.exports = { handler }; diff --git a/functions/slack-events/index.ts b/functions/slack-events/index.ts new file mode 100644 index 0000000..7556b3a --- /dev/null +++ b/functions/slack-events/index.ts @@ -0,0 +1,93 @@ +import * as messages 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'); + +const EVENT_TEAM_JOIN = 'team_join'; +const EVENT_APP_HOME_OPENED = 'app_home_opened'; + +export default async (req: Request) => { + try { + 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 new Response(request.challenge, { status: 200 }); + } + break; + case 'event_callback': { + const isValid = verifySlackRequest(rawBody, req.headers, SLACK_SIGNING_SECRET); + + if (!isValid.valid) { + console.log('Failed validation: ', isValid.reason); + return new Response(isValid.reason, { status: 400 }); + } + + 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 }), + { + background: true, + }, + ); + + break; + + case EVENT_APP_HOME_OPENED: + console.log('Posting to slack-background for app home'); + + result = await publishView( + messages.appHome({ event: request.event }), + { + background: true, + }, + ); + break; + + default: + break; + } + + if (result) { + if (result.ok) { + console.log(`Successfully posted to slack-background`); + + return new Response(JSON.stringify({ success: true }), { + status: 200, + }); + } else { + console.log(`Error posting to slack-background`); + console.log(result); + + return new Response(JSON.stringify({ success: false }), { + status: 400, + }); + } + } + break; + } + default: + break; + } + + console.log('Unknown action.'); + return new Response(JSON.stringify({ message: 'Unknown action.' }), { + status: 400, + }); + } catch (error) { + console.log(error); + return new Response('', { status: 500 }); + } +}; diff --git a/functions/slack-events/messages.js b/functions/slack-events/messages.ts similarity index 91% rename from functions/slack-events/messages.js rename to functions/slack-events/messages.ts index 42e9518..9388d54 100644 --- a/functions/slack-events/messages.js +++ 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/functions/slack-interactivity/index.js b/functions/slack-interactivity/index.js deleted file mode 100644 index 4fc8316..0000000 --- a/functions/slack-interactivity/index.js +++ /dev/null @@ -1,8 +0,0 @@ -const handler = async function (event, context) { - return { - statusCode: 200, - body: '', - }; -}; - -module.exports = { handler }; diff --git a/functions/slack-interactivity/index.ts b/functions/slack-interactivity/index.ts new file mode 100644 index 0000000..2e579c6 --- /dev/null +++ b/functions/slack-interactivity/index.ts @@ -0,0 +1,3 @@ +export default async (req: Request) => { + return new Response('', { status: 200 }); +}; diff --git a/functions/slack-send-message-background/index.js b/functions/slack-send-message-background/index.js deleted file mode 100644 index 832ed56..0000000 --- a/functions/slack-send-message-background/index.js +++ /dev/null @@ -1,57 +0,0 @@ -require('dotenv').config(); - -const { postMessage, publishView } = require('../../util/slack'); - -const handler = async function (event, context) { - const request = JSON.parse(event.body); - - if (request.key !== process.env.WEBHOOKS_VERIFICATION) { - console.log('Not Authorized'); - throw new Error('Not Authorized'); - } - - let result; - - switch (request.action) { - case 'postMessage': - result = await postMessage(request.message); - - if (result.ok) { - console.log( - `Successfully posted message ${result.ts} to user ${ - result.message && result.message.username - }` - ); - } else { - console.log('Error posting message:'); - console.log(result); - } - - break; - - case 'publishView': - result = await publishView(request.message); - - if (result.ok) { - console.log( - `Successfully published view to ${request.message.user_id}` - ); - } else { - console.log('Error publishing view:'); - console.log(result); - } - - break; - - default: - console.log('No action'); - break; - } - - // return { - // statusCode: 200, - // body: '', - // }; -}; - -module.exports = { handler }; diff --git a/functions/slack-send-message-background/index.ts b/functions/slack-send-message-background/index.ts new file mode 100644 index 0000000..d6041eb --- /dev/null +++ b/functions/slack-send-message-background/index.ts @@ -0,0 +1,53 @@ +import { postMessage, publishView } from '../../util/slack'; +import { requireEnv } from '../../util/env'; +import { verifyBackgroundRequest } from '../../util/verify'; + +interface BackgroundRequest { + key: string; + action: string; + message: Record; +} + +export default async (req: Request) => { + const request = (await req.json()) as BackgroundRequest; + + verifyBackgroundRequest(request.key, requireEnv('WEBHOOKS_VERIFICATION')); + + let result: { ok?: boolean; ts?: string; message?: { username?: string } } | undefined; + + switch (request.action) { + case 'postMessage': + 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:'); + console.log(result); + } + + break; + + case 'publishView': + result = await publishView(request.message as unknown as Parameters[0]); + + if (result.ok) { + console.log( + `Successfully published view to ${(request.message as Record).user_id}`, + ); + } else { + console.log('Error publishing view:'); + console.log(result); + } + + break; + + default: + console.log('No action'); + break; + } +}; diff --git a/functions/zoom-meeting-webhook-handler/airtable.js b/functions/zoom-meeting-webhook-handler/airtable.ts similarity index 74% rename from functions/zoom-meeting-webhook-handler/airtable.js rename to functions/zoom-meeting-webhook-handler/airtable.ts index 1356e2b..cfbc05f 100644 --- a/functions/zoom-meeting-webhook-handler/airtable.js +++ b/functions/zoom-meeting-webhook-handler/airtable.ts @@ -1,6 +1,15 @@ +import type { Room } from '../../types/room'; +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.js b/functions/zoom-meeting-webhook-handler/index.ts similarity index 52% rename from functions/zoom-meeting-webhook-handler/index.js rename to functions/zoom-meeting-webhook-handler/index.ts index 76fa365..46c9d62 100644 --- a/functions/zoom-meeting-webhook-handler/index.js +++ b/functions/zoom-meeting-webhook-handler/index.ts @@ -1,10 +1,13 @@ -require('dotenv').config(); +import Airtable from 'airtable'; +import { updateMeetingStatus, updateMeetingAttendence } from './slack'; +import { findRoomInstance } from './airtable'; +import { requireEnv } from '../../util/env'; +import { verifyZoomSignature, hmacSha256Hex } from '../../util/verify'; +import type { Room } from '../../types/room'; -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'; @@ -13,73 +16,45 @@ 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'); -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 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(event.headers['x-zm-signature']); - - if (event.headers['x-zm-signature'] === signature) { - authorized = true; - } - } else { - if (event.headers.authorization === ZOOM_AUTH) { - authorized = true; - } - } + const authorized = + verifyZoomSignature(rawBody, req.headers, ZOOM_SECRET) || + req.headers.get('authorization') === ZOOM_AUTH; 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) - .update(request.payload.plainToken) - .digest('hex'); - return { - statusCode: 200, - body: JSON.stringify({ + const hashForValidate = hmacSha256Hex(ZOOM_SECRET, request.payload.plainToken); + 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 +63,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(requireEnv('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 +109,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 +140,7 @@ const handler = async function (event, context) { } break; + } default: break; @@ -175,19 +149,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.js b/functions/zoom-meeting-webhook-handler/slack.ts similarity index 58% rename from functions/zoom-meeting-webhook-handler/slack.js rename to functions/zoom-meeting-webhook-handler/slack.ts index fd56dc2..feafd02 100644 --- a/functions/zoom-meeting-webhook-handler/slack.js +++ b/functions/zoom-meeting-webhook-handler/slack.ts @@ -1,11 +1,21 @@ -require('dotenv').config(); +import { postMessage, updateMessage } from '../../util/slack'; +import type { Room } from '../../types/room'; -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/netlify.toml b/netlify.toml index 287f05a..784a270 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,6 +1,9 @@ [build] - functions = "functions" - command = "yarn run build" + command = "pnpm run build" + +[functions] + directory = "functions" + included_files = ["data/*.json"] [[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..7d4ec14 --- /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: ^24.12.0 + version: 24.12.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@24.12.0': + resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} + + '@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.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + 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': 24.12.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': 24.12.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@24.12.0': + dependencies: + undici-types: 7.16.0 + + '@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.16.0: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 diff --git a/scripts/build.js b/scripts/build.js deleted file mode 100644 index 658576d..0000000 --- a/scripts/build.js +++ /dev/null @@ -1,24 +0,0 @@ -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'); - -async function main() { - console.log('Building rooms'); - const results = await base('rooms').select().all(); - - const rooms = results.map((record) => ({ - ...record.fields, - record_id: record.id, - })); - - fs.writeFileSync( - path.resolve(__dirname, '..', 'data', 'rooms.json'), - JSON.stringify(rooms, null, 2) - ); - - console.log(`Done building ${rooms.length} rooms`); -} - -main(); diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 0000000..9651f09 --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,32 @@ +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)); + +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'); + const results = await base('rooms').select().all(); + + const rooms = results.map((record) => ({ + ...record.fields, + record_id: record.id, + })); + + writeFileSync( + resolve(__dirname, '..', 'data', 'rooms.json'), + JSON.stringify(rooms, null, 2), + ); + + console.log(`Done building ${rooms.length} rooms`); +} + +main(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..92f27e6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "esnext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noEmit": true, + "types": ["node"] + }, + "include": [ + "env.d.ts", + "functions/**/*.ts", + "util/**/*.ts", + "scripts/**/*.ts" + ], + "exclude": ["node_modules"] +} 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[]; + }; +} 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; +} diff --git a/util/env.ts b/util/env.ts new file mode 100644 index 0000000..8bd47cb --- /dev/null +++ b/util/env.ts @@ -0,0 +1,7 @@ +export function requireEnv ( name: keyof typeof process.env ): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Missing required environment variable: ${name}`); + } + return value; +} diff --git a/util/slack.js b/util/slack.ts similarity index 61% rename from util/slack.js rename to util/slack.ts index 333b504..2859e0a 100644 --- a/util/slack.js +++ 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 }; diff --git a/util/verify.ts b/util/verify.ts new file mode 100644 index 0000000..ff6e9d4 --- /dev/null +++ b/util/verify.ts @@ -0,0 +1,90 @@ +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); +} + +/** + * 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. + */ +export function verifyBackgroundRequest( + key: string, + expected: string, +): void { + if (key !== expected) { + throw new Error('Not Authorized'); + } +} 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"