@slates-integrations/google-ads 0.2.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/docs/SPEC.md +98 -0
- package/logo.png +0 -0
- package/package.json +21 -0
- package/slate.json +21 -0
- package/src/auth.contract.test.ts +174 -0
- package/src/auth.ts +150 -0
- package/src/config.ts +13 -0
- package/src/index.ts +34 -0
- package/src/lib/client.ts +245 -0
- package/src/lib/helpers.ts +20 -0
- package/src/provider.contract.test.ts +75 -0
- package/src/scopes.ts +22 -0
- package/src/spec.ts +13 -0
- package/src/tools/generate-keyword-ideas.ts +114 -0
- package/src/tools/index.ts +11 -0
- package/src/tools/list-accounts.ts +90 -0
- package/src/tools/manage-ad-groups.ts +125 -0
- package/src/tools/manage-ads.ts +162 -0
- package/src/tools/manage-audience-lists.ts +134 -0
- package/src/tools/manage-bidding-strategies.ts +153 -0
- package/src/tools/manage-campaigns.ts +275 -0
- package/src/tools/manage-conversion-actions.ts +224 -0
- package/src/tools/manage-keywords.ts +167 -0
- package/src/tools/search-reports.ts +71 -0
- package/src/tools/upload-offline-conversions.ts +93 -0
- package/src/triggers/index.ts +1 -0
- package/src/triggers/lead-form-submit.ts +135 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# <img src="https://provider-logos.metorial-cdn.com/googleads.png" height="20"> Google Ads
|
|
2
|
+
|
|
3
|
+
Create, manage, and optimize advertising campaigns across Google Search, Display Network, YouTube, and Shopping. Configure ad groups, keywords, bidding strategies, audience targeting, and geographic targeting. Track and import conversions including offline and enhanced conversions. Generate reporting and analytics using Google Ads Query Language (GAQL). Plan keywords with historical metrics and forecasts. Manage account hierarchies and multi-account operations. Receive lead form webhook data from campaigns.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
### Generate Keyword Ideas
|
|
8
|
+
|
|
9
|
+
Generates keyword suggestions based on seed keywords, a URL, or both. Returns keyword ideas with historical metrics including average monthly searches, competition level, and suggested bid ranges. Similar to the Keyword Planner tool in the Google Ads UI. Useful for keyword research, discovering new targeting opportunities, and estimating traffic potential.
|
|
10
|
+
|
|
11
|
+
### List Accounts
|
|
12
|
+
|
|
13
|
+
Lists all Google Ads customer accounts accessible to the authenticated user. Returns account IDs, names, currency, timezone, and status for each account. Useful for discovering which accounts can be managed and obtaining customer IDs needed for other operations.
|
|
14
|
+
|
|
15
|
+
### Manage Ad Groups
|
|
16
|
+
|
|
17
|
+
Create, update, or remove ad groups within a Google Ads campaign. Ad groups organize ads and keywords within a campaign. Supports setting the ad group name, status, type, CPC bid, and targeting URL.
|
|
18
|
+
|
|
19
|
+
### Manage Ads
|
|
20
|
+
|
|
21
|
+
Create, update, or remove ads within an ad group. Supports responsive search ads, expanded text ads, responsive display ads, and other ad formats. For responsive search ads, provide headlines and descriptions. Google will automatically test combinations. Pin headlines/descriptions to specific positions if needed.
|
|
22
|
+
|
|
23
|
+
### Manage Audience Lists
|
|
24
|
+
|
|
25
|
+
Create, update, or remove user lists (audience segments) for targeting. Supports CRM-based customer lists, rule-based lists, and remarketing lists. User lists can be applied to campaigns or ad groups for audience targeting, bid adjustments, or exclusions.
|
|
26
|
+
|
|
27
|
+
### Manage Bidding Strategies
|
|
28
|
+
|
|
29
|
+
Create, update, or remove portfolio bidding strategies that can be shared across multiple campaigns. Portfolio strategies centralize bid management and enable cross-campaign optimization. For campaign-level bidding, use the Manage Campaigns tool instead. This tool is specifically for shared/portfolio bidding strategies.
|
|
30
|
+
|
|
31
|
+
### Manage Campaigns
|
|
32
|
+
|
|
33
|
+
Create, update, or remove Google Ads campaigns. Supports setting campaign name, status, type, budget, start/end dates, bidding strategy, and network settings. When creating a campaign, a campaign budget is automatically created if \
|
|
34
|
+
|
|
35
|
+
### Manage Conversion Actions
|
|
36
|
+
|
|
37
|
+
Create, update, or remove conversion actions for tracking valuable customer actions. Conversion actions track events like purchases, sign-ups, phone calls, or app installs. Supports configuring conversion counting, attribution models, value settings, and conversion windows.
|
|
38
|
+
|
|
39
|
+
### Manage Keywords
|
|
40
|
+
|
|
41
|
+
Add, update, or remove keywords in an ad group. Also supports managing negative keywords at both the ad group and campaign levels. Keywords determine when ads are shown based on user search queries. Each keyword has a match type controlling how broadly it matches search terms.
|
|
42
|
+
|
|
43
|
+
### Run GAQL Query
|
|
44
|
+
|
|
45
|
+
Executes a Google Ads Query Language (GAQL) query to retrieve reporting data, resource details, or metrics from a Google Ads account. Supports querying any resource type including campaigns, ad groups, ads, keywords, conversions, and more. Use this tool to build custom reports, fetch performance metrics, or look up specific resources. The query follows the GAQL syntax: \
|
|
46
|
+
|
|
47
|
+
### Upload Offline Conversions
|
|
48
|
+
|
|
49
|
+
Upload offline click conversions to Google Ads. Imports real-world transaction data like in-store purchases, qualified phone leads, or CRM events to measure full-funnel conversion impact. Each conversion requires a Google Click ID (gclid) to link the offline event back to the original ad click.
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
This integration is licensed under the [FSL-1.1](https://github.com/metorial/metorial-platform/blob/dev/LICENSE).
|
|
54
|
+
|
|
55
|
+
<div align="center">
|
|
56
|
+
<sub>Built with ❤️ by <a href="https://metorial.com">Metorial</a></sub>
|
|
57
|
+
</div>
|
package/docs/SPEC.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Slates Specification for Google Ads
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Google Ads is Google's online advertising platform that allows advertisers to create and manage ad campaigns across Google Search, Display Network, YouTube, and other Google properties. The Google Ads API enables programmatic management of advertising accounts, campaigns, ads, keywords, bidding strategies, audience targeting, conversion tracking, and reporting.
|
|
6
|
+
|
|
7
|
+
## Authentication
|
|
8
|
+
|
|
9
|
+
Google Ads API utilizes the OAuth 2.0 protocol for authentication and authorization, allowing your app to access user accounts without handling login information. In addition to OAuth 2.0 credentials, a developer token is also required to make Google Ads API calls.
|
|
10
|
+
|
|
11
|
+
### Prerequisites
|
|
12
|
+
|
|
13
|
+
1. **Developer Token**: A developer token is a 22-character alphanumeric string obtained from the API Center page of your Google Ads manager account. Each developer token has an access level (Test, Basic, or Standard) which determines the number of API calls and the environment (test or production) it can be used in. You need a Google Ads manager account to obtain a developer token.
|
|
14
|
+
|
|
15
|
+
2. **Google Cloud Project**: Enable the Google Ads API in your Google Cloud Console project and create OAuth 2.0 client IDs under "API & Services" > "Credentials".
|
|
16
|
+
|
|
17
|
+
### OAuth 2.0 Flows
|
|
18
|
+
|
|
19
|
+
The API supports two main OAuth 2.0 flows:
|
|
20
|
+
|
|
21
|
+
- **Web/Desktop Application Flow**: For apps that allow users to sign in and authorize your app to manage their Google Ads accounts on their behalf. Your app builds and manages the OAuth 2.0 user credentials using a Client ID, Client Secret, and Refresh Token along with the Developer Token and Login Customer ID.
|
|
22
|
+
|
|
23
|
+
- **Service Account Flow**: For workflows that don't require human interaction. This requires a configuration step where the user adds a service account to their Google Ads account, and the app can then use the service account's credentials to manage the user's Google Ads account.
|
|
24
|
+
|
|
25
|
+
### OAuth 2.0 Scope
|
|
26
|
+
|
|
27
|
+
The scope for the Google Ads API is `https://www.googleapis.com/auth/adwords`.
|
|
28
|
+
|
|
29
|
+
### Required Headers
|
|
30
|
+
|
|
31
|
+
When calling the Google Ads API, you need both OAuth 2.0 application credentials and a developer token. If making API calls with a Google Ads manager account, you must specify a `login-customer-id` header.
|
|
32
|
+
|
|
33
|
+
### Token Endpoint
|
|
34
|
+
|
|
35
|
+
Refresh tokens can be exchanged for access tokens at: `https://www.googleapis.com/oauth2/v3/token`
|
|
36
|
+
|
|
37
|
+
### Important Notes
|
|
38
|
+
|
|
39
|
+
- The Google Ads API does not support simultaneous sign-in with data access request (hybrid) or domain-wide delegation of authority (2LO).
|
|
40
|
+
- Your developer token needs approval to work with production Google Ads accounts but can be used immediately with a test manager account.
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
### Campaign Management
|
|
45
|
+
|
|
46
|
+
Create, update, pause, and remove advertising campaigns across multiple campaign types including Search, Display, Video, Shopping, Performance Max, and App campaigns. Configure campaign-level settings such as budgets, start/end dates, geographic targeting, language targeting, and ad scheduling.
|
|
47
|
+
|
|
48
|
+
### Ad Group and Ad Management
|
|
49
|
+
|
|
50
|
+
Automatically generate keywords, ad text, landing pages, and custom reports. Create and manage ad groups within campaigns, and configure ads of various formats including text ads, responsive ads, image ads, and video ads. Update ad copy in real-time based on business data.
|
|
51
|
+
|
|
52
|
+
### Keyword Management
|
|
53
|
+
|
|
54
|
+
Add, update, and remove keywords within ad groups. Configure match types (broad, phrase, exact) and set keyword-level bids. Manage negative keywords at both campaign and ad group levels.
|
|
55
|
+
|
|
56
|
+
### Bidding Strategy Management
|
|
57
|
+
|
|
58
|
+
The Google Ads API offers a range of customizable bidding strategies that allow you to automate your bidding process and achieve your campaign goals. Available strategies include Manual CPC, Manual CPM, Enhanced CPC, Maximize Conversions, Maximize Conversion Value, Target CPA, Target ROAS, and more.
|
|
59
|
+
|
|
60
|
+
### Keyword Planning
|
|
61
|
+
|
|
62
|
+
Keyword Planning is a process for getting keyword metrics and forecasts as well as searching for new keywords to add to campaigns. The functionality is similar to the Keyword Planner tool in the Google Ads UI. Historical metrics provide data on how keywords have previously performed on Google Search. Forecast metrics provide a more exact estimate of future campaign performance.
|
|
63
|
+
|
|
64
|
+
### Audience Targeting
|
|
65
|
+
|
|
66
|
+
The API provides tools to implement sophisticated targeting strategies. One method is "Audience segments", also known as user lists, which are groups of people with specific interests, intents, or demographic information. Supports CRM-based user lists, rule-based user lists, remarketing lists, and similar audiences. Also supports geo-targeting and location-based targeting.
|
|
67
|
+
|
|
68
|
+
### Conversion Tracking and Management
|
|
69
|
+
|
|
70
|
+
The Google Ads API allows for programmatic management of the entire conversion management workflow, covering creating, importing, adjusting, monitoring, and grouping conversion actions. Import offline conversions to measure real-world transactions such as qualified leads over the phone or in-office payments. Enhanced conversions for web can improve the accuracy of your conversion measurement by supplementing existing conversion data with hashed first-party customer data in a privacy-safe way.
|
|
71
|
+
|
|
72
|
+
### Reporting and Analytics
|
|
73
|
+
|
|
74
|
+
Report on conversions by retrieving data about specific conversion actions or retrieving conversion metrics for other resources like campaigns, ad groups, and ads. Use Google Ads Query Language (GAQL) to build custom queries across all resource types. Compile your Google Ads data to use with other systems like inventories, generate regular reports, and make campaign adjustments in bulk.
|
|
75
|
+
|
|
76
|
+
### Account Management
|
|
77
|
+
|
|
78
|
+
Using a manager account, you can control client accounts and run operations without logging into each account separately. List accessible customer accounts, manage account hierarchies, and handle multi-account operations.
|
|
79
|
+
|
|
80
|
+
### Reach and Frequency Forecasting
|
|
81
|
+
|
|
82
|
+
Plan video and display campaigns by generating reach and frequency forecasts. Developer tokens must be allowlisted specifically for the ReachPlanService, even if approved for other Google Ads API services.
|
|
83
|
+
|
|
84
|
+
## Events
|
|
85
|
+
|
|
86
|
+
Webhook is not supported in the Google Ads API for general account or campaign changes.
|
|
87
|
+
|
|
88
|
+
However, Google Ads does support one specific webhook mechanism:
|
|
89
|
+
|
|
90
|
+
### Lead Form Webhook
|
|
91
|
+
|
|
92
|
+
POST requests to the webhook are sent in JSON format, adhering to a specific schema for user lead data. The JSON payload includes fields for lead identification, user-submitted data, form and campaign details, and verification keys. User-submitted data is contained within `user_column_data`, using `column_id` and `string_value` to represent the data type and its value.
|
|
93
|
+
|
|
94
|
+
- Configured per lead form extension within Google Ads campaigns.
|
|
95
|
+
- Requires a webhook URL and a verification key.
|
|
96
|
+
- Lead handlers should use the `lead_id` for deduplication and respond with specific HTTP codes indicating success or different types of errors.
|
|
97
|
+
- The `asset_group_id` field is only populated for Performance Max campaigns.
|
|
98
|
+
- Payload includes campaign ID, ad group ID, creative ID, click ID (gclid), and a test lead indicator.
|
package/logo.png
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@slates-integrations/google-ads",
|
|
3
|
+
"main": "src/index.ts",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "bunx @vercel/ncc build src/index.ts -o dist -m -s",
|
|
7
|
+
"test": "vitest run --passWithNoTests",
|
|
8
|
+
"typecheck": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@types/node": "^20",
|
|
12
|
+
"slates": "1.0.0-rc.9",
|
|
13
|
+
"zod": "^4.2"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@slates/test": "1.0.0-rc.4",
|
|
17
|
+
"typescript": "^5",
|
|
18
|
+
"vitest": "^3.1.2"
|
|
19
|
+
},
|
|
20
|
+
"version": "0.2.0-rc.2"
|
|
21
|
+
}
|
package/slate.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@google/google-ads",
|
|
3
|
+
"description": "Create, manage, and optimize advertising campaigns across Google Search, Display Network, YouTube, and Shopping. Configure ad groups, keywords, bidding strategies, audience targeting, and geographic targeting. Track and import conversions including offline and enhanced conversions. Generate reporting and analytics using Google Ads Query Language (GAQL). Plan keywords with historical metrics and forecasts. Manage account hierarchies and multi-account operations. Receive lead form webhook data from campaigns.",
|
|
4
|
+
"categories": [
|
|
5
|
+
"apis-and-http-requests",
|
|
6
|
+
"crm-and-sales-tools"
|
|
7
|
+
],
|
|
8
|
+
"skills": [
|
|
9
|
+
"manage ad campaigns",
|
|
10
|
+
"configure keyword targeting",
|
|
11
|
+
"set bidding strategies",
|
|
12
|
+
"track conversions",
|
|
13
|
+
"generate ad reports",
|
|
14
|
+
"manage audience targeting",
|
|
15
|
+
"plan keyword forecasts",
|
|
16
|
+
"manage ad groups and ads",
|
|
17
|
+
"import offline conversions",
|
|
18
|
+
"manage account hierarchies"
|
|
19
|
+
],
|
|
20
|
+
"logoUrl": "https://provider-logos.metorial-cdn.com/googleads.png"
|
|
21
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { createLocalSlateTestClient, expectSlateError } from '@slates/test';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { googleAdsScopes } from './scopes';
|
|
4
|
+
|
|
5
|
+
let oauthPost = vi.fn();
|
|
6
|
+
let profileGet = vi.fn();
|
|
7
|
+
|
|
8
|
+
let loadProviderClient = async () => {
|
|
9
|
+
vi.resetModules();
|
|
10
|
+
oauthPost.mockReset();
|
|
11
|
+
profileGet.mockReset();
|
|
12
|
+
|
|
13
|
+
vi.doMock('slates', async () => {
|
|
14
|
+
let actual = await vi.importActual<typeof import('slates')>('slates');
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
createAxios: vi.fn((config?: { baseURL?: string }) => {
|
|
19
|
+
if (config?.baseURL === 'https://oauth2.googleapis.com') {
|
|
20
|
+
return {
|
|
21
|
+
post: oauthPost
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (config?.baseURL === 'https://www.googleapis.com') {
|
|
26
|
+
return {
|
|
27
|
+
get: profileGet
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return actual.createAxios(config);
|
|
32
|
+
})
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
let { provider } = await import('./index');
|
|
37
|
+
return createLocalSlateTestClient({ slate: provider as any });
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
vi.doUnmock('slates');
|
|
42
|
+
vi.resetModules();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('google-ads auth contract', () => {
|
|
46
|
+
it('builds the expected OAuth authorization URL', async () => {
|
|
47
|
+
let client = await loadProviderClient();
|
|
48
|
+
let result = await client.getAuthorizationUrl({
|
|
49
|
+
authenticationMethodId: 'google_oauth',
|
|
50
|
+
redirectUri: 'https://example.com/callback',
|
|
51
|
+
state: 'state-123',
|
|
52
|
+
input: { developerToken: 'dev-token-22charslong00' },
|
|
53
|
+
clientId: 'client-id',
|
|
54
|
+
clientSecret: 'client-secret',
|
|
55
|
+
scopes: [googleAdsScopes.adwords]
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let url = new URL(result.authorizationUrl);
|
|
59
|
+
expect(`${url.origin}${url.pathname}`).toBe(
|
|
60
|
+
'https://accounts.google.com/o/oauth2/v2/auth'
|
|
61
|
+
);
|
|
62
|
+
expect(url.searchParams.get('scope')).toBe(googleAdsScopes.adwords);
|
|
63
|
+
expect(result.input).toEqual({ developerToken: 'dev-token-22charslong00' });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('maps callback and refresh token responses into the stored auth shape', async () => {
|
|
67
|
+
let client = await loadProviderClient();
|
|
68
|
+
|
|
69
|
+
oauthPost.mockResolvedValueOnce({
|
|
70
|
+
data: {
|
|
71
|
+
access_token: 'access-token',
|
|
72
|
+
refresh_token: 'refresh-token',
|
|
73
|
+
expires_in: 3600,
|
|
74
|
+
scope: `${googleAdsScopes.adwords} openid`
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let callbackResult = await client.handleAuthorizationCallback({
|
|
79
|
+
authenticationMethodId: 'google_oauth',
|
|
80
|
+
code: 'auth-code',
|
|
81
|
+
state: 'state-123',
|
|
82
|
+
redirectUri: 'https://example.com/callback',
|
|
83
|
+
input: { developerToken: 'dev-token-22charslong00' },
|
|
84
|
+
clientId: 'client-id',
|
|
85
|
+
clientSecret: 'client-secret',
|
|
86
|
+
scopes: [googleAdsScopes.adwords]
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(callbackResult.output).toMatchObject({
|
|
90
|
+
token: 'access-token',
|
|
91
|
+
refreshToken: 'refresh-token',
|
|
92
|
+
developerToken: 'dev-token-22charslong00'
|
|
93
|
+
});
|
|
94
|
+
expect(callbackResult.scopes).toEqual([googleAdsScopes.adwords, 'openid']);
|
|
95
|
+
expect(Date.parse(String(callbackResult.output.expiresAt))).toBeGreaterThan(Date.now());
|
|
96
|
+
|
|
97
|
+
oauthPost.mockResolvedValueOnce({
|
|
98
|
+
data: {
|
|
99
|
+
access_token: 'refreshed-token',
|
|
100
|
+
expires_in: 1800
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
let refreshResult = await client.refreshToken({
|
|
105
|
+
authenticationMethodId: 'google_oauth',
|
|
106
|
+
output: {
|
|
107
|
+
token: 'stale-token',
|
|
108
|
+
refreshToken: 'refresh-token',
|
|
109
|
+
developerToken: 'dev-token-22charslong00'
|
|
110
|
+
},
|
|
111
|
+
input: { developerToken: 'dev-token-22charslong00' },
|
|
112
|
+
clientId: 'client-id',
|
|
113
|
+
clientSecret: 'client-secret',
|
|
114
|
+
scopes: [googleAdsScopes.adwords]
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(refreshResult.output).toMatchObject({
|
|
118
|
+
token: 'refreshed-token',
|
|
119
|
+
refreshToken: 'refresh-token',
|
|
120
|
+
developerToken: 'dev-token-22charslong00'
|
|
121
|
+
});
|
|
122
|
+
expect(Date.parse(String(refreshResult.output.expiresAt))).toBeGreaterThan(Date.now());
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('fails refreshes cleanly when no refresh token is stored', async () => {
|
|
126
|
+
let client = await loadProviderClient();
|
|
127
|
+
|
|
128
|
+
await expectSlateError(
|
|
129
|
+
() =>
|
|
130
|
+
client.refreshToken({
|
|
131
|
+
authenticationMethodId: 'google_oauth',
|
|
132
|
+
output: {
|
|
133
|
+
token: 'stale-token',
|
|
134
|
+
developerToken: 'dev-token-22charslong00'
|
|
135
|
+
},
|
|
136
|
+
input: { developerToken: 'dev-token-22charslong00' },
|
|
137
|
+
clientId: 'client-id',
|
|
138
|
+
clientSecret: 'client-secret',
|
|
139
|
+
scopes: [googleAdsScopes.adwords]
|
|
140
|
+
}),
|
|
141
|
+
{ code: 'internal.unexpected', kind: 'internal', status: 500 }
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('maps the Google profile payload into the Slate profile shape', async () => {
|
|
146
|
+
let client = await loadProviderClient();
|
|
147
|
+
|
|
148
|
+
profileGet.mockResolvedValueOnce({
|
|
149
|
+
data: {
|
|
150
|
+
id: 'user-123',
|
|
151
|
+
email: 'ads-test@example.com',
|
|
152
|
+
name: 'Ads Test User',
|
|
153
|
+
picture: 'https://example.com/avatar.png'
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
let result = await client.getAuthProfile({
|
|
158
|
+
authenticationMethodId: 'google_oauth',
|
|
159
|
+
output: {
|
|
160
|
+
token: 'profile-token',
|
|
161
|
+
developerToken: 'dev-token-22charslong00'
|
|
162
|
+
},
|
|
163
|
+
input: { developerToken: 'dev-token-22charslong00' },
|
|
164
|
+
scopes: [googleAdsScopes.adwords]
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(result.profile).toEqual({
|
|
168
|
+
id: 'user-123',
|
|
169
|
+
email: 'ads-test@example.com',
|
|
170
|
+
name: 'Ads Test User',
|
|
171
|
+
imageUrl: 'https://example.com/avatar.png'
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { SlateAuth, createAxios } from 'slates';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { googleAdsScopes } from './scopes';
|
|
4
|
+
|
|
5
|
+
let googleOAuthAxios = createAxios({
|
|
6
|
+
baseURL: 'https://oauth2.googleapis.com'
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
let profileAxios = createAxios({
|
|
10
|
+
baseURL: 'https://www.googleapis.com'
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export let auth = SlateAuth.create()
|
|
14
|
+
.output(
|
|
15
|
+
z.object({
|
|
16
|
+
token: z.string(),
|
|
17
|
+
refreshToken: z.string().optional(),
|
|
18
|
+
expiresAt: z.string().optional(),
|
|
19
|
+
developerToken: z.string()
|
|
20
|
+
})
|
|
21
|
+
)
|
|
22
|
+
.addOauth({
|
|
23
|
+
type: 'auth.oauth',
|
|
24
|
+
name: 'Google OAuth',
|
|
25
|
+
key: 'google_oauth',
|
|
26
|
+
|
|
27
|
+
scopes: [
|
|
28
|
+
{
|
|
29
|
+
title: 'Google Ads',
|
|
30
|
+
description:
|
|
31
|
+
'Full access to manage Google Ads accounts, campaigns, ads, and reporting',
|
|
32
|
+
scope: googleAdsScopes.adwords
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
|
|
36
|
+
inputSchema: z.object({
|
|
37
|
+
developerToken: z
|
|
38
|
+
.string()
|
|
39
|
+
.describe(
|
|
40
|
+
'Your Google Ads API developer token (22-character alphanumeric string from the API Center in your manager account)'
|
|
41
|
+
)
|
|
42
|
+
}),
|
|
43
|
+
|
|
44
|
+
getAuthorizationUrl: async ctx => {
|
|
45
|
+
let params = new URLSearchParams({
|
|
46
|
+
client_id: ctx.clientId,
|
|
47
|
+
redirect_uri: ctx.redirectUri,
|
|
48
|
+
response_type: 'code',
|
|
49
|
+
scope: ctx.scopes.join(' '),
|
|
50
|
+
access_type: 'offline',
|
|
51
|
+
prompt: 'consent',
|
|
52
|
+
state: ctx.state
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
url: `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`,
|
|
57
|
+
input: ctx.input
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
handleCallback: async ctx => {
|
|
62
|
+
let response = await googleOAuthAxios.post(
|
|
63
|
+
'/token',
|
|
64
|
+
new URLSearchParams({
|
|
65
|
+
code: ctx.code,
|
|
66
|
+
client_id: ctx.clientId,
|
|
67
|
+
client_secret: ctx.clientSecret,
|
|
68
|
+
redirect_uri: ctx.redirectUri,
|
|
69
|
+
grant_type: 'authorization_code'
|
|
70
|
+
}).toString(),
|
|
71
|
+
{
|
|
72
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
let data = response.data;
|
|
77
|
+
let expiresAt = data.expires_in
|
|
78
|
+
? new Date(Date.now() + data.expires_in * 1000).toISOString()
|
|
79
|
+
: undefined;
|
|
80
|
+
let grantedScopes =
|
|
81
|
+
typeof data.scope === 'string' ? data.scope.split(' ').filter(Boolean) : undefined;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
output: {
|
|
85
|
+
token: data.access_token,
|
|
86
|
+
refreshToken: data.refresh_token,
|
|
87
|
+
expiresAt,
|
|
88
|
+
developerToken: ctx.input.developerToken
|
|
89
|
+
},
|
|
90
|
+
input: ctx.input,
|
|
91
|
+
scopes: grantedScopes
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
handleTokenRefresh: async ctx => {
|
|
96
|
+
if (!ctx.output.refreshToken) {
|
|
97
|
+
throw new Error('No refresh token available');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let response = await googleOAuthAxios.post(
|
|
101
|
+
'/token',
|
|
102
|
+
new URLSearchParams({
|
|
103
|
+
refresh_token: ctx.output.refreshToken,
|
|
104
|
+
client_id: ctx.clientId,
|
|
105
|
+
client_secret: ctx.clientSecret,
|
|
106
|
+
grant_type: 'refresh_token'
|
|
107
|
+
}).toString(),
|
|
108
|
+
{
|
|
109
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
let data = response.data;
|
|
114
|
+
let expiresAt = data.expires_in
|
|
115
|
+
? new Date(Date.now() + data.expires_in * 1000).toISOString()
|
|
116
|
+
: undefined;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
output: {
|
|
120
|
+
token: data.access_token,
|
|
121
|
+
refreshToken: ctx.output.refreshToken,
|
|
122
|
+
expiresAt,
|
|
123
|
+
developerToken: ctx.output.developerToken
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
getProfile: async (ctx: {
|
|
129
|
+
output: { token: string; refreshToken?: string; expiresAt?: string; developerToken: string };
|
|
130
|
+
input: { developerToken: string };
|
|
131
|
+
scopes: string[];
|
|
132
|
+
}) => {
|
|
133
|
+
let response = await profileAxios.get('/oauth2/v2/userinfo', {
|
|
134
|
+
headers: {
|
|
135
|
+
Authorization: `Bearer ${ctx.output.token}`
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
let data = response.data;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
profile: {
|
|
143
|
+
id: data.id,
|
|
144
|
+
email: data.email,
|
|
145
|
+
name: data.name,
|
|
146
|
+
imageUrl: data.picture
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SlateConfig } from 'slates';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export let config = SlateConfig.create(
|
|
5
|
+
z.object({
|
|
6
|
+
loginCustomerId: z
|
|
7
|
+
.string()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe(
|
|
10
|
+
'The Google Ads manager account ID (without hyphens) used when making API calls on behalf of a client account. Required when using a manager account.'
|
|
11
|
+
)
|
|
12
|
+
})
|
|
13
|
+
);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Slate } from 'slates';
|
|
2
|
+
import { spec } from './spec';
|
|
3
|
+
import {
|
|
4
|
+
listAccounts,
|
|
5
|
+
searchReports,
|
|
6
|
+
manageCampaigns,
|
|
7
|
+
manageAdGroups,
|
|
8
|
+
manageAds,
|
|
9
|
+
manageKeywords,
|
|
10
|
+
manageBiddingStrategies,
|
|
11
|
+
manageConversionActions,
|
|
12
|
+
generateKeywordIdeas,
|
|
13
|
+
uploadOfflineConversions,
|
|
14
|
+
manageAudienceLists
|
|
15
|
+
} from './tools';
|
|
16
|
+
import { leadFormSubmit } from './triggers';
|
|
17
|
+
|
|
18
|
+
export let provider = Slate.create({
|
|
19
|
+
spec,
|
|
20
|
+
tools: [
|
|
21
|
+
listAccounts,
|
|
22
|
+
searchReports,
|
|
23
|
+
manageCampaigns,
|
|
24
|
+
manageAdGroups,
|
|
25
|
+
manageAds,
|
|
26
|
+
manageKeywords,
|
|
27
|
+
manageBiddingStrategies,
|
|
28
|
+
manageConversionActions,
|
|
29
|
+
generateKeywordIdeas,
|
|
30
|
+
uploadOfflineConversions,
|
|
31
|
+
manageAudienceLists
|
|
32
|
+
] as any,
|
|
33
|
+
triggers: [leadFormSubmit] as any
|
|
34
|
+
});
|