diff --git a/packages/db/src/credentials.test.ts b/packages/db/src/credentials.test.ts index 1f125e9f..d13235e4 100644 --- a/packages/db/src/credentials.test.ts +++ b/packages/db/src/credentials.test.ts @@ -1552,7 +1552,7 @@ describe("credentials schemas", () => { }); describe("GoogleSearchConsoleCredentialsSchema", () => { - it("validates Google Search Console credentials", () => { + it("validates Google Search Console access token credentials", () => { const credentials: GoogleSearchConsoleCredentials = { accessToken: "ya29.token", siteUrl: "https://www.example.com/", @@ -1564,6 +1564,24 @@ describe("credentials schemas", () => { expect(result.success).toBe(true); }); + it("validates Google Search Console OAuth credentials", () => { + const credentials: GoogleSearchConsoleCredentials = { + accessToken: "ya29.token", + authType: "oauth", + expiresAt: Date.now() + 3_600_000, + refreshToken: "1//gsc", + siteUrl: "https://www.example.com/", + type: "google_search_console", + }; + + const result = + GoogleSearchConsoleCredentialsSchema.safeParse(credentials); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(credentials); + } + }); + it("rejects missing access token", () => { const result = GoogleSearchConsoleCredentialsSchema.safeParse({ siteUrl: "https://www.example.com/", @@ -2480,6 +2498,17 @@ describe("credentials schemas", () => { apiKey: "lin_api_xxx", type: "linear", }, + { + accessToken: "ya29.gsc", + siteUrl: "https://www.example.com/", + type: "google_search_console", + }, + { + accessToken: "ya29.gsc-oauth", + expiresAt: Date.now() + 3_600_000, + refreshToken: "1//gsc", + type: "google_search_console", + }, { accessToken: "ya29.youtube", expiresAt: Date.now() + 3_600_000, @@ -2490,7 +2519,12 @@ describe("credentials schemas", () => { const oauthCreds = allCredentials.filter(isOAuthCredentials); expect(oauthCreds.map((c) => c.type).toSorted()).toEqual( - ["bigquery", "ga", "youtube_analytics"].toSorted() + [ + "bigquery", + "ga", + "google_search_console", + "youtube_analytics", + ].toSorted() ); const dbCreds = allCredentials.filter(isDatabaseCredentials); diff --git a/packages/db/src/credentials.ts b/packages/db/src/credentials.ts index 360453e0..bb41375c 100644 --- a/packages/db/src/credentials.ts +++ b/packages/db/src/credentials.ts @@ -348,16 +348,37 @@ export const GranolaCredentialsSchema = z.object({ export type GranolaCredentials = z.infer; -export const GoogleSearchConsoleCredentialsSchema = z.object({ +export const GoogleSearchConsoleOAuthCredentialsSchema = z.object({ + accessToken: requiredOpaqueString("Access token is required"), + apiBaseUrl: optionalTrimmedUrl("API base URL must be a valid URL"), + authType: z.literal("oauth").optional(), + expiresAt: z.number().int().min(0, "Expiration time must be positive"), + refreshToken: requiredOpaqueString("Refresh token is required"), + siteUrl: optionalTrimmedString("Site URL is required"), + type: z.literal("google_search_console"), +}); + +export const GoogleSearchConsoleAccessTokenCredentialsSchema = z.object({ accessToken: requiredOpaqueString("Access token is required"), apiBaseUrl: optionalTrimmedUrl("API base URL must be a valid URL"), siteUrl: optionalTrimmedString("Site URL is required"), type: z.literal("google_search_console"), }); +export const GoogleSearchConsoleCredentialsSchema = z.union([ + GoogleSearchConsoleOAuthCredentialsSchema, + GoogleSearchConsoleAccessTokenCredentialsSchema, +]); + export type GoogleSearchConsoleCredentials = z.infer< typeof GoogleSearchConsoleCredentialsSchema >; +export type GoogleSearchConsoleOAuthCredentials = z.infer< + typeof GoogleSearchConsoleOAuthCredentialsSchema +>; +export type GoogleSearchConsoleAccessTokenCredentials = z.infer< + typeof GoogleSearchConsoleAccessTokenCredentialsSchema +>; export const ConfluenceCredentialsSchema = z.object({ apiToken: requiredOpaqueString("API token is required"), @@ -680,7 +701,11 @@ export function isOAuthCredentials( ): credentials is | GoogleAnalyticsOAuthCredentials | BigQueryOAuthCredentials + | GoogleSearchConsoleOAuthCredentials | YouTubeAnalyticsCredentials { + if (credentials.type === "google_search_console") { + return "refreshToken" in credentials && "expiresAt" in credentials; + } if ( credentials.type !== "ga" && credentials.type !== "bigquery" && diff --git a/packages/db/src/source-providers.ts b/packages/db/src/source-providers.ts index 827d30cb..24e8d764 100644 --- a/packages/db/src/source-providers.ts +++ b/packages/db/src/source-providers.ts @@ -761,7 +761,7 @@ export const SOURCE_PROVIDER_REGISTRY = { sourceApiInterface: true, testable: false, dashboardConnectable: true, - dashboardCredentialForm: "json", + dashboardCredentialForm: "google_oauth", publicCategory: "Marketing", guide: { summary: