diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts index 4a4f09f4f..0529ae844 100644 --- a/apps/server/src/config.ts +++ b/apps/server/src/config.ts @@ -6,8 +6,8 @@ const schema = z .object({ API_ENDPOINT: z.string().url().default(`http://localhost:${defaultPort}/api`), CHANNEL_SECRET: z.string().min(1).default('channelSecret'), - GOOGLE_APPLICATION_ID: z.string().min(1), - GOOGLE_APPLICATION_SECRET: z.string().min(1), + GOOGLE_LOGIN_APPLICATION_ID: z.string().min(1), + GOOGLE_LOGIN_APPLICATION_SECRET: z.string().min(1), EMAILABLE_API_KEY: z.string().min(1), SLACK_WEBHOOK_URL_PUBLIC_FEEDBACK: z.string().url().optional(), PORT: z diff --git a/apps/server/src/contexts/login-provider/google-login-provider.ts b/apps/server/src/contexts/login-provider/google-login-provider.ts index 157aadd4b..f9c5de890 100644 --- a/apps/server/src/contexts/login-provider/google-login-provider.ts +++ b/apps/server/src/contexts/login-provider/google-login-provider.ts @@ -18,8 +18,8 @@ interface GoogleJwtPayload extends JwtPayload { } const client = new OAuth2Client( - env.GOOGLE_APPLICATION_ID, - env.GOOGLE_APPLICATION_SECRET, + env.GOOGLE_LOGIN_APPLICATION_ID, + env.GOOGLE_LOGIN_APPLICATION_SECRET, `${env.API_ENDPOINT}${authLoginEndpoint}`, ); diff --git a/apps/server/src/schema/integrations/integration-operations.ts b/apps/server/src/schema/integrations/integration-operations.ts index 0145e949e..451a13531 100644 --- a/apps/server/src/schema/integrations/integration-operations.ts +++ b/apps/server/src/schema/integrations/integration-operations.ts @@ -117,6 +117,7 @@ const integrationStatus = (type: IntegrationTypeEnum, integrations: Integration[ IntegrationTypeEnum.META, IntegrationTypeEnum.TIKTOK, IntegrationTypeEnum.LINKEDIN, + IntegrationTypeEnum.GOOGLE, ]; if (!SUPPORTED_INTEGRATIONS.includes(type)) return IntegrationStatusEnum.ComingSoon; diff --git a/packages/channel-google/.eslintrc.cjs b/packages/channel-google/.eslintrc.cjs new file mode 100644 index 000000000..da8b40ecd --- /dev/null +++ b/packages/channel-google/.eslintrc.cjs @@ -0,0 +1,8 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + extends: ["@repo/eslint-config/library.cjs"], + parser: "@typescript-eslint/parser", + parserOptions: { + project: true, + }, +}; diff --git a/packages/channel-google/package.json b/packages/channel-google/package.json new file mode 100644 index 000000000..b31cec022 --- /dev/null +++ b/packages/channel-google/package.json @@ -0,0 +1,46 @@ +{ + "name": "@repo/channel-google", + "version": "1.0.0", + "private": true, + "license": "UNLICENCED", + "description": "Google channel business logic", + "main": "./src/index.ts", + "types": "./src/index.ts", + "type": "module", + "exports": { + "javascript": "./dist/index.js", + "default": "./src/index.ts" + }, + "scripts": { + "build": "tsc", + "lint": "eslint . --max-warnings=0", + "lint:fix": "eslint . --fix --max-warnings 0", + "prettier": "prettier . --check", + "prettier:fix": "prettier . --write", + "clean": "rm -rf .turbo .next node_modules dist", + "update": "pnpm update --latest" + }, + "dependencies": { + "@lifeomic/attempt": "^3.1.0", + "@repo/channel-utils": "workspace:*", + "@repo/database": "workspace:*", + "@repo/logger": "workspace:*", + "@repo/utils": "workspace:*", + "facebook-nodejs-business-sdk": "^20.0.3", + "google-ads-api": "17.1.0-rest-beta", + "google-auth-library": "^9.14.1", + "jsonwebtoken": "^9.0.2", + "lodash": "^4.17.21", + "zod": "^3.23.8" + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/express": "^5.0.0", + "@types/facebook-nodejs-business-sdk": "^20.0.2", + "@types/jsonwebtoken": "^9.0.7", + "@types/lodash": "^4.17.9", + "@types/node": "^22.7.4", + "typescript": "^5.6.2" + } +} diff --git a/packages/channel-google/src/channel-google.ts b/packages/channel-google/src/channel-google.ts new file mode 100644 index 000000000..b1541bc8f --- /dev/null +++ b/packages/channel-google/src/channel-google.ts @@ -0,0 +1,1093 @@ +import { createHmac } from 'node:crypto'; +import { URLSearchParams } from 'node:url'; +import { OAuth2Client } from 'google-auth-library'; +import { + type AdAccount as DbAdAccount, + CurrencyEnum, + DeviceEnum, + type Integration, + IntegrationTypeEnum, + prisma, + PublisherEnum, +} from '@repo/database'; +import { AError, FireAndForget, formatYYYMMDDDate, isAError } from '@repo/utils'; +import { z, type ZodTypeAny } from 'zod'; +import { logger } from '@repo/logger'; +import { type Request as ExpressRequest, type Response as ExpressResponse } from 'express'; +import * as adsSdk from 'facebook-nodejs-business-sdk'; +import { + Ad, + AdAccount, + AdAccountAdVolume, + AdCreative, + AdPreview, + AdReportRun, + AdsInsights, + User, +} from 'facebook-nodejs-business-sdk'; +import type Cursor from 'facebook-nodejs-business-sdk/src/cursor'; +import { + type AdAccountWithIntegration, + adReportsStatusesToRedis, + type AdWithAdAccount, + authEndpoint, + type ChannelAd, + type ChannelAdAccount, + type ChannelAdSet, + type ChannelCampaign, + type ChannelCreative, + type ChannelIFrame, + type ChannelInsight, + type ChannelInterface, + deleteOldInsights, + formatDimensionsMap, + type GenerateAuthUrlResp, + getConnectedIntegrationByOrg, + getIFrame, + getIFrameAdFormat, + isMetaAdPosition, + JobStatusEnum, + markErrorIntegrationById, + // MetaError, + revokeIntegration, + saveAccounts, + saveInsightsAdsAdsSetsCampaigns, + type TokensResponse, +} from '@repo/channel-utils'; +import { retry } from '@lifeomic/attempt'; +// import { decode } from 'jsonwebtoken'; +import { decode, type JwtPayload } from 'jsonwebtoken'; +import { GoogleAdsApi } from 'google-ads-api'; +import { env } from './config'; + +const fireAndForget = new FireAndForget(); + +const apiVersion = 'v20.0'; +export const baseOauthFbUrl = `https://www.facebook.com/${apiVersion}`; +export const baseGraphFbUrl = `https://graph.facebook.com/${apiVersion}`; + +interface CustomerClient { + resourceName: string; + clientCustomer: string; + level: string; + manager: boolean; + descriptiveName: string; +} + +interface GoogleAdsResult { + customerClient: CustomerClient; +} + +interface GoogleAdsResponse { + results: GoogleAdsResult[]; + fieldMask: string; + requestId: string; + queryResourceConsumption: string; +} + + +const limit = 600; + +// const authLoginEndpoint = '/auth/login/callback'; + +const client = new OAuth2Client( + env.GOOGLE_CHANNEL_APPLICATION_ID, + env.GOOGLE_CHANNEL_APPLICATION_SECRET, + `${env.API_ENDPOINT}${authEndpoint}`, +); + +const adsAPi = new GoogleAdsApi({ + client_id: env.GOOGLE_CHANNEL_APPLICATION_ID, + client_secret: env.GOOGLE_CHANNEL_APPLICATION_SECRET, + developer_token: env.GOOGLE_CHANNEL_DEVELOPER_TOKEN, +}); + +interface GoogleJwtPayload extends JwtPayload { + azp: string; + email: string; + email_verified: boolean; + at_hash: string; + name: string; + picture: string; + given_name: string | undefined; + family_name: string | undefined; +} + +class Google implements ChannelInterface { + private readonly insightFields = [ + AdsInsights.Fields.account_id, + AdsInsights.Fields.ad_id, + AdsInsights.Fields.adset_name, + AdsInsights.Fields.adset_id, + AdsInsights.Fields.ad_name, + AdsInsights.Fields.campaign_name, + AdsInsights.Fields.campaign_id, + AdsInsights.Fields.date_start, + AdsInsights.Fields.impressions, + AdsInsights.Fields.inline_link_clicks, //clicks + AdsInsights.Fields.spend, + ]; + + generateAuthUrl(state: string): GenerateAuthUrlResp { + const url = client.generateAuthUrl({ + access_type: 'offline', + scope: [ + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/adwords', + ], + state, + prompt: 'consent', + }); + + return { + url, + }; + } + + async exchangeCodeForTokens(code: string): Promise { + const getTokenResponse = await client.getToken(code); + + const refreshToken = getTokenResponse.tokens.refresh_token; + if (!getTokenResponse.tokens.id_token) { + return new AError('No id_token in response'); + } + + const decoded = decode(getTokenResponse.tokens.id_token) as GoogleJwtPayload | null; + if (!decoded) { + return new AError('Could not decode id_token'); + } + if (!getTokenResponse.tokens.access_token) return new AError('Could not get access_token'); + if (!getTokenResponse.tokens.refresh_token) return new AError('Could not refresh token'); + + if (getTokenResponse.tokens.expiry_date) { + return { + accessToken: getTokenResponse.tokens.access_token, + accessTokenExpiresAt: new Date(Date.now() + getTokenResponse.tokens.expiry_date * 1000), + refreshToken: getTokenResponse.tokens.refresh_token, + }; + } + const accessTokenExpiresAt = await Google.getExpireAt(getTokenResponse.tokens.id_token); + if (isAError(accessTokenExpiresAt)) return accessTokenExpiresAt; + + return { + accessToken: getTokenResponse.tokens.access_token, + accessTokenExpiresAt, + refreshToken: getTokenResponse.tokens.refresh_token, + }; + } + + async getUserId(accessToken: string): Promise { + try { + const response = await fetch(`https://www.googleapis.com/oauth2/v2/userinfo`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + if (!response.ok) { + logger.error(`Failed to fetch Google user.`); + return new AError('Failed to fetch user'); + } + + const parsed = z.object({ id: z.string() }).safeParse(await response.json()); + if (!parsed.success) { + logger.error('Failed to parse Google user response', parsed.error); + return new AError('Failed to fetch user'); + } + + return parsed.data.id; // Return the user's Google ID (substitute for externalId) + } catch (error) { + logger.error('Error fetching Google user', error); + return new AError('Network error while fetching user info'); + } + } + + signOutCallback(req: ExpressRequest, res: ExpressResponse): void { + logger.info(`sign out callback body ${JSON.stringify(req.body)}`); + + const parsedBody = z.object({ signed_request: z.string() }).safeParse(req.body); + if (!parsedBody.success) { + res.status(400).send('Failed to parse sign out request'); + return; + } + const userId = Google.parseRequest(parsedBody.data.signed_request, env.GOOGLE_CHANNEL_APPLICATION_SECRET); + if (isAError(userId)) { + logger.error(userId.message); + res.status(400).send(userId.message); + return; + } + fireAndForget.add(() => revokeIntegration(userId, IntegrationTypeEnum.GOOGLE)); + res.status(200).send('OK'); + } + + async deAuthorize(organizationId: string): Promise { + const integration = await getConnectedIntegrationByOrg(organizationId, IntegrationTypeEnum.GOOGLE); + if (!integration) return new AError('No integration found'); + if (isAError(integration)) return integration; + + const response = await fetch( + `${baseGraphFbUrl}/${integration.externalId}/permissions?access_token=${integration.accessToken}`, + { + method: 'DELETE', + }, + ).catch((error: unknown) => { + logger.error('Failed to de-authorize %o', { error }); + return error instanceof Error ? error : new Error(JSON.stringify(error)); + }); + + if (response instanceof Error) return response; + // if (!response.ok) { + // // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Will check with zod + // const json = await response.json(); + // const fbErrorSchema = z.object({ + // error: z.object({ + // message: z.string(), + // code: z.number(), + // error_subcode: z.number(), + // fbtrace_id: z.string(), + // }), + // }); + // const parsed = fbErrorSchema.safeParse(json); + // if (!parsed.success) { + // logger.error('De-authorization request failed due to %o', json); + // return new AError('Failed to de-authorize'); + // } + // const googleError = new LinkedinError( + // parsed.data.error.message, + // parsed.data.error.code, + // parsed.data.error.error_subcode, + // parsed.data.error.fbtrace_id, + // ); + // logger.error(googleError, 'De-authorization request failed'); + // if (await disConnectIntegrationOnError(integration.id, googleError, false)) { + // return integration.externalId; + // } + // return googleError; + // } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Will check with zod + const data = await response.json(); + const parsed = z.object({ success: z.literal(true) }).safeParse(data); + if (!parsed.success) { + logger.error('Failed to de-authorize %o', data); + return new AError('Failed to de-authorize'); + } + return integration.externalId; + } + + async refreshAccessToken () { + const url = 'https://www.googleapis.com/oauth2/v3/token'; + + const params = new URLSearchParams(); + params.append('grant_type', 'refresh_token'); + params.append('client_id', env.GOOGLE_CHANNEL_APPLICATION_ID); + params.append('client_secret', env.GOOGLE_CHANNEL_APPLICATION_SECRET); + params.append('refresh_token', env.GOOGLE_CHANNEL_REFRESH_TOKEN); + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: params.toString(), + }); + + if (!response.ok) { + throw new Error(`Failed to refresh token: ${response.statusText}`); + } + + const data = await response.json(); + console.log('Access Token:', data.access_token); + return data.access_token; + } catch (error) { + console.error('Error refreshing access token:', error); + throw error; + } + }; + + + async fetchGoogleAdsData (): Promise { + const managerId = process.env.GOOGLE_CHANNEL_TEMP_CUSTOMER_ID; + + const url = `https://googleads.googleapis.com/v18/customers/${managerId}/googleAds:searchStream`; + + const accessToken: string = await this.refreshAccessToken() + console.log('Access Token:', accessToken); + + const query = ` + SELECT + customer_client.client_customer, + customer_client.level, + customer_client.manager, + customer_client.descriptive_name + FROM + customer_client + WHERE + customer_client.level <= 1 + `; + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'developer-token': env.GOOGLE_CHANNEL_DEVELOPER_TOKEN, + 'login-customer-id': managerId, + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ query }), + }); + + if (!response.ok) { + throw new Error(`Request failed: ${response.statusText}`); + } + + const data = await response.json(); + console.log('Google Ads API Response:', data); + return data; + } catch (error) { + console.error('Error fetching Google Ads data:', error); + throw error; + } + }; + + async fetchYoutubeAds (customerId: string) { + const url = `https://googleads.googleapis.com/v18/customers/${customerId}/googleAds:searchStream`; + const query = ` + SELECT + video.id, + video.title, + video.duration_millis, + ad_group_ad.ad.id, + ad_group_ad.ad.name, + ad_group_ad.ad.type, + ad_group.id, + ad_group.name, + ad_group.type, + campaign.id, + campaign.name, + campaign.advertising_channel_type, + campaign.advertising_channel_sub_type, + metrics.impressions, + metrics.clicks, + metrics.cost_micros, + metrics.video_quartile_p25_rate, + metrics.video_quartile_p50_rate, + metrics.video_quartile_p75_rate, + metrics.video_quartile_p100_rate, + metrics.video_view_rate, + metrics.video_views, + segments.ad_format_type, + segments.date + FROM video + WHERE segments.date BETWEEN '2024-09-01' AND '2024-09-02' + `; + + const authToken: string = await this.refreshAccessToken() + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'developer-token': env.GOOGLE_CHANNEL_DEVELOPER_TOKEN, + 'login-customer-id': process.env.GOOGLE_CHANNEL_TEMP_CUSTOMER_ID, + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify({ query }) + }); + + if (!response.ok) { + const error = await response.json(); + console.error('Error:', error); + throw new Error(`Request failed: ${response.statusText}`); + } + + const data = await response.json(); + console.log('Google Ads Data:', data); + return data; + } catch (error) { + console.error('Fetch Error:', error); + throw error; + } + }; + + async getChannelData(integration: Integration, initial: boolean): Promise { + const dbAccounts = await this.saveAdAccounts(integration); + if (isAError(dbAccounts)) return dbAccounts; + if (!integration.refreshToken) return new AError('Refresh token is required'); + + try { + + const customers = await this.fetchGoogleAdsData() + + customers[0].results.map(async (customer) => { + let youtubeData = await this.fetchYoutubeAds(customer.customerClient.clientCustomer.split('/')[1]) + console.log(youtubeData, 'YOUTUBE DATAAATA') + }) + + } catch (err) { + logger.error(err, 'GET CHANNEL DATA'); + } + + // for (const dbAccount of dbAccounts) { + // const ranges = await timeRanges(initial, dbAccount.id); + // for (const range of ranges) { + // const analytics = await this.getAdAnalytics(integration, range, dbAccount); + // if (isAError(analytics)) return analytics; + + // const campaigns = await this.getCampaignGroupsAsCampaigns(integration, analytics.campaignGroupIds, dbAccount); + // if (isAError(campaigns)) return campaigns; + + // const adSets = await this.getCampaignsAsAdSets( + // integration, + // new Set(Array.from(analytics.campaignIds).map((c) => c.externalCampaignId)), + // dbAccount, + // ); + // if (isAError(adSets)) return adSets; + // const ads: ChannelAd[] = Array.from(analytics.creativeIds).map((c) => ({ + // externalAdAccountId: dbAccount.externalId, + // externalId: c.externalCreativeId, + // externalAdSetId: c.externalCampaignId, + // })); + // await deleteOldInsights(dbAccount.id, range.since, range.until); + // await saveInsightsAdsAdsSetsCampaigns( + // campaigns, + // new Map(), + // dbAccount, + // adSets, + // new Map(), + // ads, + // new Map(), + // [], + // new Map(), + // analytics.insights, + // ); + // } + // } + return Promise.resolve(undefined); + } + + async getAdPreview( + integration: Integration, + adId: string, + publisher?: PublisherEnum, + device?: DeviceEnum, + position?: string, + ): Promise { + adsSdk.FacebookAdsApi.init(integration.accessToken); + if (!isMetaAdPosition(position)) return new AError('Invalid position'); + const { externalId } = await prisma.ad.findUniqueOrThrow({ where: { id: adId } }); + const ad = new Ad(externalId, {}, undefined, undefined); + const format = getIFrameAdFormat(publisher, device, position); + if (!format) { + logger.error('Invalid ad format %o', { publisher, device, position, adId }); + return new AError('Invalid ad format'); + } + const previewsFn = ad.getPreviews([AdPreview.Fields.body], { + ad_format: format, + }); + const previewSchema = z.object({ body: z.string() }); + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -- to complicated + const toBody = (preview: z.infer) => preview.body; + const parsedPreviews = await Google.handlePagination(integration, previewsFn, previewSchema, toBody); + if (isAError(parsedPreviews)) return parsedPreviews; + + if (parsedPreviews.length === 0) return new AError('No ad preview found'); + if (parsedPreviews.length > 1) return new AError('More than one ad previews found'); + const iFrame = getIFrame(parsedPreviews[0]); + if (isAError(iFrame)) return iFrame; + + const overrideDimensions = formatDimensionsMap.get(format); + if (overrideDimensions) { + iFrame.width = overrideDimensions.width; + iFrame.height = overrideDimensions.height; + } + + return iFrame; + } + + getDefaultPublisher(): PublisherEnum { + return PublisherEnum.Facebook; + } + + async saveAdAccounts(integration: Integration): Promise { + try { + if (!integration.refreshToken) return new AError('No refresh token found'); + + const customer = adsAPi.Customer({ + customer_id: process.env.GOOGLE_CHANNEL_TEMP_CUSTOMER_ID || '', + refresh_token: integration.refreshToken, + }); + const query = ` + SELECT + customer.resource_name, + customer.id, + customer.currency_code, + customer.descriptive_name + FROM + customer + LIMIT 50 + `; + + const youtubeResponse = await customer.query(query); + + const response = await customer.query(query); + + + const accountSchema = z.array( + z.object({ + customer: z.object({ + resource_name: z.string(), + id: z.string().or(z.number()), + currency_code: z.string(), + descriptive_name: z.string().optional(), + }), + }), + ); + + const parsed = accountSchema.safeParse(response); + + if (!parsed.success) { + return new AError('Failed to parse Google Ads accounts data'); + } + + const channelAccounts = parsed.data.map((account) => ({ + name: account.customer.descriptive_name ?? account.customer.resource_name, + currency: account.customer.currency_code, + externalId: account.customer.id.toString(), + })) satisfies ChannelAdAccount[]; + + return await saveAccounts(channelAccounts, integration); + } catch (err) { + console.log(err, 'THIS IS ERRORRR OF GOOGLE API CALL'); + logger.error(err); + } + } + + private async getCreatives( + accounts: ChannelAdAccount[], + integration: Integration, + ): Promise { + adsSdk.FacebookAdsApi.init(integration.accessToken); + const creatives: ChannelCreative[] = []; + const adsSchema = z.object({ + id: z.string(), + account_id: z.string(), + creative: z.object({ id: z.string(), name: z.string() }), + }); + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -- to complicated + const toCreative = (ad: z.infer) => ({ + externalAdId: ad.id, + externalId: ad.creative.id, + name: ad.creative.name, + externalAdAccountId: ad.account_id, + }); + + for (const acc of accounts) { + const account = new AdAccount(`act_${acc.externalId}`, {}, undefined, undefined); + const getAdsFn = account.getAds( + [Ad.Fields.id, Ad.Fields.account_id, `creative{${AdCreative.Fields.id}, ${AdCreative.Fields.name}}`], + { + limit, + }, + ); + const accountCreatives = await Google.handlePagination(integration, getAdsFn, adsSchema, toCreative); + if (!isAError(accountCreatives)) creatives.push(...accountCreatives); + } + return creatives; + } + + async getReportStatus({ id, integration }: AdAccountWithIntegration, taskId: string): Promise { + adsSdk.FacebookAdsApi.init(integration.accessToken); + const reportId = taskId; + const report = new AdReportRun(reportId, { report_run_id: reportId }, undefined, undefined); + const resp = await Google.sdk( + () => report.get([AdReportRun.Fields.async_status, AdReportRun.Fields.async_percent_completion]), + integration, + ); + const googleJobStatusEnum = z.enum([ + 'Job Not Started', + 'Job Started', + 'Job Running', + 'Job Completed', + 'Job Failed', + 'Job Skipped', + ]); + const reportSchema = z.object({ + [AdReportRun.Fields.async_status]: googleJobStatusEnum, + [AdReportRun.Fields.async_percent_completion]: z.number(), + }); + const parsed = reportSchema.safeParse(resp); + if (!parsed.success) { + logger.error(parsed.error, `Failed to parse google ad report for ${reportId} and ${id}`); + return JobStatusEnum.FAILED; + } + const goolgleJobStatusMap = new Map, JobStatusEnum>([ + ['Job Not Started', JobStatusEnum.QUEUING], + ['Job Started', JobStatusEnum.PROCESSING], + ['Job Running', JobStatusEnum.PROCESSING], + ['Job Completed', JobStatusEnum.SUCCESS], + ['Job Failed', JobStatusEnum.FAILED], + ['Job Skipped', JobStatusEnum.CANCELED], + ]); + return goolgleJobStatusMap.get(parsed.data.async_status) ?? JobStatusEnum.FAILED; + } + + async saveCreatives(integration: Integration, groupByAdAccount: Map): Promise { + adsSdk.FacebookAdsApi.init(integration.accessToken); + const creativeExternalIdMap = new Map(); + for (const [__, accountAds] of groupByAdAccount) { + const adExternalIdMap = new Map(accountAds.map((ad) => [ad.externalId, ad.id])); + const adAccount = accountAds[0].adAccount; + const chunkedAccountAds = _.chunk(accountAds, 100); + for (const chunk of chunkedAccountAds) { + const creatives = await this.getCreatives( + integration, + adAccount.id, + // adAccount.externalId, + // new Set(chunk.map((a) => a.externalId)), + ); + const newCreatives = creatives.filter((c) => !creativeExternalIdMap.has(c.externalId)); + await saveCreatives(newCreatives, adAccount.id, adExternalIdMap, creativeExternalIdMap); + } + } + } + + async processReport( + adAccount: AdAccountWithIntegration, + taskId: string, + since: Date, + until: Date, + ): Promise { + adsSdk.FacebookAdsApi.init(adAccount.integration.accessToken); + const reportId = taskId; + const adExternalIdMap = new Map(); + const externalAdSetToIdMap = new Map(); + const externalCampaignToIdMap = new Map(); + const insightSchema = z.object({ + account_id: z.string(), + adset_id: z.string(), + adset_name: z.string(), + ad_id: z.string(), + ad_name: z.string(), + campaign_id: z.string(), + campaign_name: z.string(), + date_start: z.coerce.date(), + impressions: z.coerce.number(), + spend: z.coerce.number(), + device_platform: z.string(), + publisher_platform: z.string(), + platform_position: z.string(), + inline_link_clicks: z.coerce.number().optional(), + }); + + const toInsightAndAd = ( + insight: z.infer, + ): { insight: ChannelInsight; ad: ChannelAd; adSet: ChannelAdSet; campaign: ChannelCampaign } => ({ + insight: { + clicks: insight.inline_link_clicks ?? 0, + externalAdId: insight.ad_id, + date: insight.date_start, + externalAccountId: insight.account_id, + impressions: insight.impressions, + spend: Math.trunc(insight.spend * 100), // converting to cents + device: Google.deviceEnumMap.get(insight.device_platform) ?? DeviceEnum.Unknown, + publisher: Google.publisherEnumMap.get(insight.publisher_platform) ?? PublisherEnum.Unknown, + position: insight.platform_position, + }, + ad: { + externalAdSetId: insight.adset_id, + externalAdAccountId: insight.account_id, + externalId: insight.ad_id, + name: insight.ad_name, + }, + campaign: { + externalAdAccountId: insight.account_id, + externalId: insight.campaign_id, + name: insight.campaign_name, + }, + adSet: { + externalCampaignId: insight.campaign_id, + externalId: insight.adset_id, + name: insight.adset_name, + }, + }); + + logger.info('Getting insights for report %s', reportId); + + const report = new AdReportRun(reportId, { report_run_id: reportId }, undefined, undefined); + const getInsightsFn = report.getInsights( + [ + ...this.insightFields, + AdsInsights.Breakdowns.device_platform, + AdsInsights.Breakdowns.publisher_platform, + AdsInsights.Breakdowns.platform_position, + ], + { + limit, + }, + ); + const insightsProcessFn = async ( + i: { + insight: ChannelInsight; + ad: ChannelAd; + adSet: ChannelAdSet; + campaign: ChannelCampaign; + }[], + ): Promise => { + await this.saveAdsAdSetsCampaignsAndInsights( + i, + adExternalIdMap, + externalAdSetToIdMap, + externalCampaignToIdMap, + adAccount, + ); + return undefined; + }; + await deleteOldInsights(adAccount.id, since, until); + const accountInsightsAndAds = await Google.handlePaginationFn( + adAccount.integration, + getInsightsFn, + insightSchema, + toInsightAndAd, + insightsProcessFn, + ); + if (isAError(accountInsightsAndAds)) return accountInsightsAndAds; + } + + async runAdInsightReport( + adAccount: DbAdAccount, + integration: Integration, + since: Date, + until: Date, + ): Promise { + adsSdk.FacebookAdsApi.init(integration.accessToken); + const adReportRunSchema = z.object({ id: z.string() }); + const account = new AdAccount(`act_${adAccount.externalId}`, {}, undefined, undefined); + const resp = await Google.sdk(async () => { + const mtimeRange = { since: formatYYYMMDDDate(since), until: formatYYYMMDDDate(until) }; + logger.info(`Running report for account ${adAccount.id} with time range ${JSON.stringify(mtimeRange)}`); + + return account.getInsightsAsync(this.insightFields, { + limit, + time_increment: 1, + filtering: [{ field: AdsInsights.Fields.spend, operator: 'GREATER_THAN', value: '0' }], + breakdowns: [ + AdsInsights.Breakdowns.device_platform, + AdsInsights.Breakdowns.publisher_platform, + AdsInsights.Breakdowns.platform_position, + ], + level: AdsInsights.Level.ad, + time_range: mtimeRange, + }); + }, integration); + if (isAError(resp)) { + logger.error(resp, 'Failed to run ad report'); + return resp; + } + const parsed = adReportRunSchema.safeParse(resp); + if (!parsed.success) { + logger.error('Failed to parse ad report run %o', resp); + return new AError('Failed to parse google ad report run'); + } + return parsed.data.id; + } + + async saveOldInsightsAdsAdsSetsCampaigns( + integration: Integration, + groupByAdAccount: Map, + ): Promise { + adsSdk.FacebookAdsApi.init(integration.accessToken); + for (const [_, accountAds] of groupByAdAccount) { + const adAccount = accountAds[0].adAccount; + + const schema = z.object({ + id: z.string(), + campaign_id: z.string(), + campaign: z.object({ name: z.string() }), + adset_id: z.string(), + adset: z.object({ name: z.string() }), + }); + const toAdSetAdAndCampaign = ( + ad: z.infer, + ): { + adId: string; + adSetId: string; + campaignId: string; + campaignName: string; + adSetName: string; + } => ({ + adId: ad.id, + adSetId: ad.adset_id, + campaignId: ad.campaign_id, + campaignName: ad.campaign.name, + adSetName: ad.adset.name, + }); + + const processFn = async ( + adSetAdAndCampaigns: { + adId: string; + adSetId: string; + campaignId: string; + campaignName: string; + adSetName: string; + }[], + ): Promise => { + const { campaigns, adSets, ads } = adSetAdAndCampaigns.reduce<{ + ads: ChannelAd[]; + adSets: ChannelAdSet[]; + campaigns: ChannelCampaign[]; + }>( + (acc, row) => { + acc.ads.push({ + externalAdSetId: String(row.adSetId), + externalAdAccountId: adAccount.externalId, + externalId: row.adId, + name: row.adSetName, + }); + acc.adSets.push({ + externalCampaignId: row.campaignId, + externalId: row.adSetId, + name: row.campaignName, + }); + acc.campaigns.push({ + externalAdAccountId: adAccount.externalId, + externalId: row.campaignId, + name: row.campaignName, + }); + return acc; + }, + { ads: [], adSets: [], campaigns: [] }, + ); + await saveInsightsAdsAdsSetsCampaigns(campaigns, new Map(), adAccount, adSets, new Map(), ads, new Map(), []); + }; + + let start = 0; + let smallAccountAds = accountAds.slice(start, start + limit); + while (smallAccountAds.length > 0) { + const account = new AdAccount(`act_${adAccount.externalId}`, {}, undefined, undefined); + const callFn = account.getAds( + [ + Ad.Fields.id, + Ad.Fields.campaign_id, + `${Ad.Fields.campaign}{name}`, + Ad.Fields.adset_id, + `${Ad.Fields.adset}{name}`, + ], + { + limit, + effective_status: [ + 'ACTIVE', + 'PAUSED', + 'DISAPPROVED', + 'PENDING_REVIEW', + 'CAMPAIGN_PAUSED', + 'ARCHIVED', + 'ADSET_PAUSED', + 'IN_PROCESS', + 'WITH_ISSUES', + ], + filtering: [{ field: Ad.Fields.id, operator: 'IN', value: smallAccountAds.map((a) => a.externalId) }], + }, + ); + await Google.handlePaginationFn(integration, callFn, schema, toAdSetAdAndCampaign, processFn); + start += limit; + smallAccountAds = accountAds.slice(start, start + limit); + } + } + return Promise.resolve(undefined); + } + + private async saveAdsAdSetsCampaignsAndInsights( + accountInsightsAndAds: { insight: ChannelInsight; ad: ChannelAd; adSet: ChannelAdSet; campaign: ChannelCampaign }[], + adExternalIdMap: Map, + externalAdSetToIdMap: Map, + externalCampaignToIdMap: Map, + dbAccount: AdAccountWithIntegration, + ): Promise { + const [insights, ads, adSets, campaigns] = accountInsightsAndAds.reduce( + (acc, item) => { + acc[0].push(item.insight); + acc[1].push(item.ad); + acc[2].push(item.adSet); + acc[3].push(item.campaign); + return acc; + }, + [[] as ChannelInsight[], [] as ChannelAd[], [] as ChannelAdSet[], [] as ChannelCampaign[]], + ); + await saveInsightsAdsAdsSetsCampaigns( + campaigns, + externalCampaignToIdMap, + dbAccount, + adSets, + externalAdSetToIdMap, + ads, + adExternalIdMap, + insights, + ); + } + + private static async handlePagination( + integration: Integration, + fn: Promise | Cursor | AError, + schema: U, + parseCallback: (result: z.infer) => T, + ): Promise { + const cursor = await this.sdk(() => fn, integration); + if (isAError(cursor)) return cursor; + const arraySchema = z.array(schema); + const parsed = arraySchema.safeParse(cursor); + if (!parsed.success) { + logger.error('Failed to parse %o', cursor); + return new AError('Failed to parse google paginated response'); + } + const results = parsed.data.map(parseCallback); + while (cursor.hasNext()) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Will catch error + const next = await this.sdk(() => cursor.next(), integration); + const parsedNext = arraySchema.safeParse(next); + if (parsedNext.success) { + results.push(...parsedNext.data.map(parseCallback)); + } else { + logger.error(parsedNext, 'Failed to parse paginated'); + } + } + return results; + } + + private static async handlePaginationFn( + integration: Integration, + fn: Promise | Cursor, + schema: U, + parseCallback: (result: z.infer) => T, + processCallback: (result: T[]) => Promise | V[] | undefined, + ): Promise { + const cursor = await this.sdk(() => fn, integration); + if (isAError(cursor)) return cursor; + const arraySchema = z.array(schema); + const parsed = arraySchema.safeParse(cursor); + if (!parsed.success) { + logger.error('Failed to parse %o', cursor); + return new AError('Failed to parse google paginated response'); + } + const resultsP = processCallback(parsed.data.map(parseCallback)); + while (cursor.hasNext()) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Will catch error + const next = await this.sdk(() => cursor.next(), integration); + const parsedNext = arraySchema.safeParse(next); + if (parsedNext.success) { + const processed = await processCallback(parsedNext.data.map(parseCallback)); + const results = await resultsP; + if (results && processed) results.push(...processed); + } else { + logger.error(parsedNext, 'Failed to parse paginated function'); + } + } + return resultsP; + } + + private static async sdk(fn: () => Promise | T | AError, integration: Integration): Promise { + try { + return await retry(async () => await fn()); + } catch (error) { + const msg = 'Failed to complete fb sdk call'; + logger.error(error, msg); + if (error instanceof Error) { + await disConnectIntegrationOnError(integration.id, error, true); + } + return new AError(msg); + } + } + + private static parseRequest(signedRequest: string, secret: string): string | AError { + const [encodedSig, payload] = signedRequest.split('.'); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Will check with zod + const data = JSON.parse(Buffer.from(payload, 'base64').toString()); + const signOutTokenSchema = z.object({ + user_id: z.string(), + algorithm: z.literal('HMAC-SHA256'), + issued_at: z.number(), + }); + const parsed = signOutTokenSchema.safeParse(data); + if (!parsed.success) { + return new AError('Failed to parse sign out token'); + } + if (parsed.data.algorithm.toUpperCase() !== 'HMAC-SHA256') + return new AError('Failed to verify sign out token, wrong algorithm'); + + const hmac = createHmac('sha256', secret); + const encodedPayload = hmac + .update(payload) + .digest('base64') + .replace(/\//g, '_') + .replace(/\+/g, '-') + .replace(/={1,2}$/, ''); + + if (encodedSig !== encodedPayload) return new AError('Failed to verify sign out token'); + + return parsed.data.user_id; + } + + private static async getExpireAt(accessToken: string): Promise { + const debugToken = await fetch( + `${baseGraphFbUrl}/debug_token?input_token=${accessToken}&access_token=${env.GOOGLE_CHANNEL_APPLICATION_ID}|${env.GOOGLE_CHANNEL_APPLICATION_SECRET}`, + ).catch((error: unknown) => { + logger.error('Failed to debug token %o', { error }); + return error instanceof Error ? error : new Error(JSON.stringify(error)); + }); + if (debugToken instanceof Error) return debugToken; + if (!debugToken.ok) { + logger.error(await debugToken.json(), `Failed to debug token for accessToken: ${accessToken}`); + return new AError(`Failed to debug token: ${debugToken.statusText}`); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Will check with zod + const debugTokenBody = await debugToken.json().catch((_e: unknown) => null); + const debugTokenParsed = z + .object({ data: z.object({ data_access_expires_at: z.number() }) }) + .safeParse(debugTokenBody); + if (!debugTokenParsed.success) { + return new AError('Failed to parse token response'); + } + return new Date(debugTokenParsed.data.data.data_access_expires_at * 1000); + } + + private static deviceEnumMap: Map = new Map([ + ['mobile_app', DeviceEnum.MobileApp], + ['mobile_web', DeviceEnum.MobileWeb], + ['desktop', DeviceEnum.Desktop], + ]); + + private static publisherEnumMap: Map = new Map([ + ['facebook', PublisherEnum.Facebook], + ['instagram', PublisherEnum.Instagram], + ['messenger', PublisherEnum.Messenger], + ['audience_network', PublisherEnum.AudienceNetwork], + ]); + + getType(): IntegrationTypeEnum { + return IntegrationTypeEnum.GOOGLE; + } +} + +const disConnectIntegrationOnError = async (integrationId: string, error: Error, notify: boolean): Promise => { + const metaErrorValidatingAccessTokenChangedSession = + 'Error validating access token: The session has been invalidated because the user changed their password or Facebook has changed the session for security reasons.'; + const metaErrorNotAuthenticated = 'Error validating access token: The user has not authorized application'; + const metaErrorFollowInstructions = + 'You cannot access the app till you log in to www.facebook.com and follow the instructions given.'; + if ( + error.message === metaErrorValidatingAccessTokenChangedSession || + error.message === metaErrorFollowInstructions || + error.message.startsWith(metaErrorNotAuthenticated) + ) { + await markErrorIntegrationById(integrationId, notify); + return true; + } + return false; +}; + +export const google = new Google(); diff --git a/packages/channel-google/src/config.ts b/packages/channel-google/src/config.ts new file mode 100644 index 000000000..4438f8c2e --- /dev/null +++ b/packages/channel-google/src/config.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; +import { apiEndpointSchema, createEnv } from '@repo/utils'; +import { channelsSchema } from '@repo/channel-utils'; + +const schema = z + .object({ + GOOGLE_CHANNEL_APPLICATION_ID: z.string(), + GOOGLE_CHANNEL_APPLICATION_SECRET: z.string(), + GOOGLE_CHANNEL_DEVELOPER_TOKEN: z.string(), + GOOGLE_CHANNEL_REFRESH_TOKEN: z.string(), + }) + .merge(apiEndpointSchema) + .merge(channelsSchema); + +export const env = createEnv(schema); diff --git a/packages/channel-google/src/index.ts b/packages/channel-google/src/index.ts new file mode 100644 index 000000000..56ba0e583 --- /dev/null +++ b/packages/channel-google/src/index.ts @@ -0,0 +1 @@ +export * from './channel-google'; diff --git a/packages/channel-google/tsconfig.json b/packages/channel-google/tsconfig.json new file mode 100644 index 000000000..5f6357490 --- /dev/null +++ b/packages/channel-google/tsconfig.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "allowJs": true + }, + "extends": "@repo/typescript-config/base.json", + "include": ["src/**.*js", "src/**.ts"] +} diff --git a/packages/channel/package.json b/packages/channel/package.json index 661edc588..3650f32b7 100644 --- a/packages/channel/package.json +++ b/packages/channel/package.json @@ -24,6 +24,7 @@ "dependencies": { "@aws-sdk/client-batch": "^3.658.1", "@aws-sdk/client-sqs": "^3.658.1", + "@repo/channel-google": "workspace:*", "@repo/channel-linkedin": "workspace:*", "@repo/channel-meta": "workspace:*", "@repo/channel-tiktok": "workspace:*", diff --git a/packages/channel/src/channel-helper.ts b/packages/channel/src/channel-helper.ts index b0f3981ca..b860812a4 100644 --- a/packages/channel/src/channel-helper.ts +++ b/packages/channel/src/channel-helper.ts @@ -3,6 +3,7 @@ import { meta } from '@repo/channel-meta'; import type { ChannelInterface } from '@repo/channel-utils'; import { linkedIn } from '@repo/channel-linkedin'; import { tiktok } from '@repo/channel-tiktok'; +import { google } from '@repo/channel-google'; export const getChannel = (channel: IntegrationTypeEnum): ChannelInterface => { switch (channel) { @@ -12,6 +13,8 @@ export const getChannel = (channel: IntegrationTypeEnum): ChannelInterface => { return linkedIn; case IntegrationTypeEnum.TIKTOK: return tiktok; + case IntegrationTypeEnum.GOOGLE: + return google; default: throw new Error('Channel not found'); } diff --git a/packages/channel/src/integration-helper.ts b/packages/channel/src/integration-helper.ts index 05c1bf2b9..a8ad64728 100644 --- a/packages/channel/src/integration-helper.ts +++ b/packages/channel/src/integration-helper.ts @@ -121,6 +121,7 @@ const completeIntegration = async ( logger.error(e, 'Failed to save tokens to database'); return new AError('Failed to save tokens to database'); }); + if (isAError(decryptedIntegration)) return decryptedIntegration; fireAndForget.add(async () => await invokeChannelIngress(false, [decryptedIntegration.id])); @@ -170,6 +171,14 @@ const saveTokens = async ( status: IntegrationStatus.CONNECTED, organizationId, }; + + function adjustDate(date: Date): Date { + const maxDate = new Date('9999-12-31T23:59:59.999Z'); // Prisma's max supported date + return date > maxDate ? maxDate : date; + } + integrationData.accessTokenExpiresAt = adjustDate(new Date(tokens.accessTokenExpiresAt ?? new Date())) + integrationData.refreshTokenExpiresAt = tokens.refreshTokenExpiresAt ? adjustDate(new Date(tokens.refreshTokenExpiresAt)) : undefined + const integration = await prisma.integration.upsert({ create: integrationData, update: integrationData, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37aec2b95..00ccb6aab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -981,6 +981,9 @@ importers: '@aws-sdk/client-sqs': specifier: ^3.658.1 version: 3.658.1 + '@repo/channel-google': + specifier: workspace:* + version: link:../channel-google '@repo/channel-linkedin': specifier: workspace:* version: link:../channel-linkedin @@ -1052,6 +1055,67 @@ importers: specifier: ^5.6.2 version: 5.6.2 + packages/channel-google: + dependencies: + '@lifeomic/attempt': + specifier: ^3.1.0 + version: 3.1.0 + '@repo/channel-utils': + specifier: workspace:* + version: link:../channel-utils + '@repo/database': + specifier: workspace:* + version: link:../database + '@repo/logger': + specifier: workspace:* + version: link:../logger + '@repo/utils': + specifier: workspace:* + version: link:../utils + facebook-nodejs-business-sdk: + specifier: ^20.0.3 + version: 20.0.3 + google-ads-api: + specifier: 17.1.0-rest-beta + version: 17.1.0-rest-beta + google-auth-library: + specifier: ^9.14.1 + version: 9.14.1 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + zod: + specifier: ^3.23.8 + version: 3.23.8 + devDependencies: + '@repo/eslint-config': + specifier: workspace:* + version: link:../config-eslint + '@repo/typescript-config': + specifier: workspace:* + version: link:../config-typescript + '@types/express': + specifier: ^5.0.0 + version: 5.0.0 + '@types/facebook-nodejs-business-sdk': + specifier: ^20.0.2 + version: 20.0.2 + '@types/jsonwebtoken': + specifier: ^9.0.7 + version: 9.0.7 + '@types/lodash': + specifier: ^4.17.9 + version: 4.17.9 + '@types/node': + specifier: ^22.7.4 + version: 22.7.4 + typescript: + specifier: ^5.6.2 + version: 5.6.2 + packages/channel-linkedin: dependencies: '@repo/channel-utils': @@ -2765,6 +2829,15 @@ packages: resolution: {integrity: sha512-w+liuBySifrstuHbFrHoHAEyVnDFVib+073q8AeAJ/qqJfvFvAwUPLLtNohR/WDVRgSasfXtl3dcNuVJWN+rjg==} engines: {node: '>=18.0.0'} + '@grpc/grpc-js@1.12.2': + resolution: {integrity: sha512-bgxdZmgTrJZX50OjyVwz3+mNEnCTNkh3cIqGPWVNeW9jX6bn1ZkU80uPd+67/ZpIJIjRQ9qaHCjhavyoWYxumg==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.13': + resolution: {integrity: sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==} + engines: {node: '>=6'} + hasBin: true + '@hookform/resolvers@3.9.0': resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==} peerDependencies: @@ -2895,6 +2968,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/ttlcache@1.4.1': + resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} + engines: {node: '>=12'} + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -2993,6 +3070,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@kamilkisiela/fast-url-parser@1.1.4': resolution: {integrity: sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew==} @@ -3625,6 +3705,36 @@ packages: '@prisma/instrumentation@5.19.1': resolution: {integrity: sha512-VLnzMQq7CWroL5AeaW0Py2huiNKeoMfCH3SUxstdzPrlWQi6UQ9UrfcbUkNHlVFqOMacqy8X/8YtE0kuKDpD9w==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@radix-ui/primitive@1.1.0': resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} @@ -4529,6 +4639,9 @@ packages: '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + '@types/caseless@0.12.5': + resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + '@types/connect@3.4.36': resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} @@ -4616,6 +4729,9 @@ packages: '@types/lodash@4.17.9': resolution: {integrity: sha512-w9iWudx1XWOHW5lQRS9iKpK/XuRhnN+0T7HvdCCd802FYkT1AMTnxndJHGrNJwRoRHkslGr4S29tjm1cT7x/7w==} + '@types/long@4.0.2': + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + '@types/luxon@3.4.2': resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} @@ -4661,6 +4777,9 @@ packages: '@types/react@18.3.10': resolution: {integrity: sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==} + '@types/request@2.48.12': + resolution: {integrity: sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==} + '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -5083,6 +5202,10 @@ packages: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} + arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -5328,6 +5451,9 @@ packages: resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==} engines: {node: '>=8'} + circ-json@1.0.4: + resolution: {integrity: sha512-/2NljkKlOCvNgs8jsIlJcCFhUfF6C1JJKM8PdJQ6HqIrqhJBouMcWFdeDJLNLZAadeTj0K9FMziH74hpEtAr1g==} + cjs-module-lexer@1.4.1: resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==} @@ -5821,6 +5947,9 @@ packages: resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} engines: {node: '>=4'} + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -6139,7 +6268,6 @@ packages: eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: @@ -6262,6 +6390,9 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-text-encoding@1.0.6: + resolution: {integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==} + fast-url-parser@1.1.3: resolution: {integrity: sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==} @@ -6342,6 +6473,10 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + form-data@2.5.2: + resolution: {integrity: sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==} + engines: {node: '>= 0.12'} + form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -6389,10 +6524,18 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gaxios@4.3.3: + resolution: {integrity: sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==} + engines: {node: '>=10'} + gaxios@6.7.1: resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} engines: {node: '>=14'} + gcp-metadata@4.3.1: + resolution: {integrity: sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==} + engines: {node: '>=10'} + gcp-metadata@6.1.0: resolution: {integrity: sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==} engines: {node: '>=14'} @@ -6501,10 +6644,30 @@ packages: peerDependencies: csstype: ^3.0.10 + google-ads-api@17.1.0-rest-beta: + resolution: {integrity: sha512-SgM8nECNkjUUVBtzpIySEsi1Ckl8MjiwAg9sh16UJgtnFeWX3318GSb1ThtSAWaXMjIhESai5LtaD8Dj03l2bw==} + + google-ads-node@14.0.0: + resolution: {integrity: sha512-rbgZkcaRSCQKnwRki4Jl2GY6Csd12a3ouX45xcK1ttFbJqtKrm8GPkctAIVnwAx0yVi8VMgpFWaEls8/i0SGJg==} + + google-auth-library@7.14.1: + resolution: {integrity: sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==} + engines: {node: '>=10'} + google-auth-library@9.14.1: resolution: {integrity: sha512-Rj+PMjoNFGFTmtItH7gHfbHpGVSb3vmnGK3nwNBqxQF9NoBpttSZI/rc0WiM63ma2uGDQtYEkMHkK9U6937NiA==} engines: {node: '>=14'} + google-gax@4.4.1: + resolution: {integrity: sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==} + engines: {node: '>=14'} + + google-p12-pem@3.1.4: + resolution: {integrity: sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==} + engines: {node: '>=10'} + deprecated: Package is no longer maintained + hasBin: true + gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} @@ -6563,6 +6726,10 @@ packages: resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + gtoken@5.3.2: + resolution: {integrity: sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==} + engines: {node: '>=10'} + gtoken@7.1.0: resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} engines: {node: '>=14.0.0'} @@ -7321,6 +7488,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} @@ -7369,6 +7539,12 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + long@4.0.0: + resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + + long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -7402,6 +7578,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + lucide-react@0.446.0: resolution: {integrity: sha512-BU7gy8MfBMqvEdDPH79VhOXSEgyG8TSPOKWaExWGCQVqnGH7wGgDngPbofu+KdtVjPQBWbEmnfMTq90CTiiDRg==} peerDependencies: @@ -7436,6 +7616,10 @@ packages: resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} engines: {node: '>=0.10.0'} + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + markdown-it@14.1.0: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true @@ -7620,6 +7804,10 @@ packages: encoding: optional: true + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + node-html-parser@6.1.13: resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} @@ -8214,6 +8402,14 @@ packages: prosemirror-view@1.34.3: resolution: {integrity: sha512-mKZ54PrX19sSaQye+sef+YjBbNu2voNwLS1ivb6aD2IRmxRGW64HU9B644+7OfJStGLyxvOreKqEgfvXa91WIA==} + proto3-json-serializer@2.0.2: + resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==} + engines: {node: '>=14.0.0'} + + protobufjs@7.4.0: + resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -8505,6 +8701,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry-request@7.0.2: + resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==} + engines: {node: '>=14'} + reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -8774,6 +8974,18 @@ packages: resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} engines: {node: '>= 0.4'} + stream-chain@2.2.5: + resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} + + stream-events@1.0.5: + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + + stream-json@1.8.0: + resolution: {integrity: sha512-HZfXngYHUAr1exT4fxlbc1IOce1RYxp2ldeaf97LYCOPSoOqY/1Psp7iGvpb+6JIOgkra9zDYnPX01hGAHzEPw==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -8860,6 +9072,9 @@ packages: strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + stubs@3.0.0: + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + styled-jsx@5.1.1: resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -8929,6 +9144,10 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + teeny-request@9.0.0: + resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} + engines: {node: '>=14'} + terraform@1.23.0: resolution: {integrity: sha512-7eLMXVQ5ZIMPeE0TrTw6NQiBZdDr3ylJ1+7JBVM9OlBdd+vaZLXLhwG2XGSDIyyIzilV5CWjhGuHDrC6w0nz0Q==} @@ -9493,6 +9712,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml-ast-parser@0.0.43: resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} @@ -11327,6 +11549,18 @@ snapshots: '@repeaterjs/repeater': 3.0.6 tslib: 2.7.0 + '@grpc/grpc-js@1.12.2': + dependencies: + '@grpc/proto-loader': 0.7.13 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.13': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.2.3 + protobufjs: 7.4.0 + yargs: 17.7.2 + '@hookform/resolvers@3.9.0(react-hook-form@7.53.0(react@18.3.1))': dependencies: react-hook-form: 7.53.0(react@18.3.1) @@ -11429,6 +11663,8 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/ttlcache@1.4.1': {} + '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 @@ -11628,6 +11864,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@js-sdsl/ordered-map@4.4.2': {} + '@kamilkisiela/fast-url-parser@1.1.4': {} '@lifeomic/attempt@3.1.0': {} @@ -12288,6 +12526,29 @@ snapshots: transitivePeerDependencies: - supports-color + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@radix-ui/primitive@1.1.0': {} '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -13371,6 +13632,8 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 22.7.4 + '@types/caseless@0.12.5': {} + '@types/connect@3.4.36': dependencies: '@types/node': 22.7.4 @@ -13464,6 +13727,8 @@ snapshots: '@types/lodash@4.17.9': {} + '@types/long@4.0.2': {} + '@types/luxon@3.4.2': {} '@types/markdown-it@14.1.2': @@ -13516,6 +13781,13 @@ snapshots: '@types/prop-types': 15.7.13 csstype: 3.1.3 + '@types/request@2.48.12': + dependencies: + '@types/caseless': 0.12.5 + '@types/node': 22.7.4 + '@types/tough-cookie': 4.0.5 + form-data: 2.5.2 + '@types/semver@7.5.8': {} '@types/send@0.17.4': @@ -13717,7 +13989,7 @@ snapshots: '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2) '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.6.2) eslint-config-prettier: 9.1.0(eslint@8.57.0) - eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.30.0) + eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)) eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.0) eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0) eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) @@ -14039,6 +14311,8 @@ snapshots: is-array-buffer: 3.0.4 is-shared-array-buffer: 1.0.3 + arrify@2.0.1: {} + asap@2.0.6: {} asn1js@3.0.5: @@ -14379,6 +14653,8 @@ snapshots: ci-info@4.0.0: {} + circ-json@1.0.4: {} + cjs-module-lexer@1.4.1: {} class-variance-authority@0.7.0: @@ -14824,6 +15100,13 @@ snapshots: dset@3.1.4: {} + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + eastasianwidth@0.2.0: {} ecdsa-sig-formatter@1.0.11: @@ -15066,7 +15349,7 @@ snapshots: eslint: 8.57.0 eslint-plugin-turbo: 2.1.3(eslint@8.57.0) - eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.30.0): + eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)): dependencies: eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) @@ -15084,7 +15367,7 @@ snapshots: debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 @@ -15097,7 +15380,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -15135,7 +15418,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -15481,6 +15764,8 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-text-encoding@1.0.6: {} + fast-url-parser@1.1.3: dependencies: punycode: 1.4.1 @@ -15574,6 +15859,13 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 + form-data@2.5.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + safe-buffer: 5.2.1 + form-data@4.0.0: dependencies: asynckit: 0.4.0 @@ -15610,6 +15902,17 @@ snapshots: functions-have-names@1.2.3: {} + gaxios@4.3.3: + dependencies: + abort-controller: 3.0.0 + extend: 3.0.2 + https-proxy-agent: 5.0.1 + is-stream: 2.0.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + - supports-color + gaxios@6.7.1: dependencies: extend: 3.0.2 @@ -15621,6 +15924,14 @@ snapshots: - encoding - supports-color + gcp-metadata@4.3.1: + dependencies: + gaxios: 4.3.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + gcp-metadata@6.1.0: dependencies: gaxios: 6.7.1 @@ -15750,6 +16061,45 @@ snapshots: dependencies: csstype: 3.1.3 + google-ads-api@17.1.0-rest-beta: + dependencies: + '@isaacs/ttlcache': 1.4.1 + axios: 1.7.7 + circ-json: 1.0.4 + google-ads-node: 14.0.0 + google-auth-library: 7.14.1 + google-gax: 4.4.1 + long: 4.0.0 + map-obj: 4.3.0 + stream-json: 1.8.0 + transitivePeerDependencies: + - debug + - encoding + - supports-color + + google-ads-node@14.0.0: + dependencies: + google-gax: 4.4.1 + lru-cache: 10.4.3 + transitivePeerDependencies: + - encoding + - supports-color + + google-auth-library@7.14.1: + dependencies: + arrify: 2.0.1 + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + fast-text-encoding: 1.0.6 + gaxios: 4.3.3 + gcp-metadata: 4.3.1 + gtoken: 5.3.2 + jws: 4.0.0 + lru-cache: 6.0.0 + transitivePeerDependencies: + - encoding + - supports-color + google-auth-library@9.14.1: dependencies: base64-js: 1.5.1 @@ -15762,6 +16112,28 @@ snapshots: - encoding - supports-color + google-gax@4.4.1: + dependencies: + '@grpc/grpc-js': 1.12.2 + '@grpc/proto-loader': 0.7.13 + '@types/long': 4.0.2 + abort-controller: 3.0.0 + duplexify: 4.1.3 + google-auth-library: 9.14.1 + node-fetch: 2.7.0 + object-hash: 3.0.0 + proto3-json-serializer: 2.0.2 + protobufjs: 7.4.0 + retry-request: 7.0.2 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + google-p12-pem@3.1.4: + dependencies: + node-forge: 1.3.1 + gopd@1.0.1: dependencies: get-intrinsic: 1.2.4 @@ -15834,6 +16206,15 @@ snapshots: graphql@16.9.0(patch_hash=jry3qelthzltp4b752bkujt7je): {} + gtoken@5.3.2: + dependencies: + gaxios: 4.3.3 + google-p12-pem: 3.1.4 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + gtoken@7.1.0: dependencies: gaxios: 6.7.1 @@ -16827,6 +17208,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} + lodash.defaults@4.2.0: {} lodash.includes@4.3.0: {} @@ -16871,6 +17254,10 @@ snapshots: strip-ansi: 7.1.0 wrap-ansi: 9.0.0 + long@4.0.0: {} + + long@5.2.3: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -16901,6 +17288,10 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + lucide-react@0.446.0(react@18.3.1): dependencies: react: 18.3.1 @@ -16929,6 +17320,8 @@ snapshots: map-cache@0.2.2: {} + map-obj@4.3.0: {} + markdown-it@14.1.0: dependencies: argparse: 2.0.1 @@ -17082,6 +17475,8 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-forge@1.3.1: {} + node-html-parser@6.1.13: dependencies: css-select: 5.1.0 @@ -17664,6 +18059,25 @@ snapshots: prosemirror-state: 1.4.3 prosemirror-transform: 1.10.0 + proto3-json-serializer@2.0.2: + dependencies: + protobufjs: 7.4.0 + + protobufjs@7.4.0: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 22.7.4 + long: 5.2.3 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -17972,6 +18386,15 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry-request@7.0.2: + dependencies: + '@types/request': 2.48.12 + extend: 3.0.2 + teeny-request: 9.0.0 + transitivePeerDependencies: + - encoding + - supports-color + reusify@1.0.4: {} rfdc@1.4.1: {} @@ -18300,6 +18723,18 @@ snapshots: dependencies: internal-slot: 1.0.7 + stream-chain@2.2.5: {} + + stream-events@1.0.5: + dependencies: + stubs: 3.0.0 + + stream-json@1.8.0: + dependencies: + stream-chain: 2.2.5 + + stream-shift@1.0.3: {} + streamsearch@1.1.0: {} string-argv@0.3.2: {} @@ -18401,6 +18836,8 @@ snapshots: strnum@1.0.5: {} + stubs@3.0.0: {} + styled-jsx@5.1.1(@babel/core@7.25.2)(react@18.3.1): dependencies: client-only: 0.0.1 @@ -18482,6 +18919,17 @@ snapshots: tapable@2.2.1: {} + teeny-request@9.0.0: + dependencies: + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + stream-events: 1.0.5 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + terraform@1.23.0: dependencies: ejs: 3.1.9 @@ -19074,6 +19522,8 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + yaml-ast-parser@0.0.43: {} yaml@2.5.1: {} diff --git a/turbo.json b/turbo.json index adb6271a5..2500e8e9e 100644 --- a/turbo.json +++ b/turbo.json @@ -15,8 +15,12 @@ "FB_APPLICATION_ID", "FB_APPLICATION_SECRET", "GRAPHQL_ENDPOINT", - "GOOGLE_APPLICATION_ID", - "GOOGLE_APPLICATION_SECRET", + "GOOGLE_LOGIN_APPLICATION_ID", + "GOOGLE_LOGIN_APPLICATION_SECRET", + "GOOGLE_CHANNEL_REFRESH_TOKEN", + "GOOGLE_CHANNEL_APPLICATION_ID", + "GOOGLE_CHANNEL_APPLICATION_SECRET", + "GOOGLE_CHANNEL_REFRESH_TOKEN", "LINKEDIN_APPLICATION_ID", "LINKEDIN_APPLICATION_SECRET", "SENTRY_AUTH_TOKEN",