adport 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Yannick Westermann
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,60 @@
1
+ // src/connect/apple.ts
2
+ import { promises as fs } from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import readline from "readline/promises";
6
+ import { CredentialStore } from "@adport/core";
7
+ import { AppleAdsClient, AppleAdsProvider } from "@adport/provider-apple";
8
+ async function connectApple({ io }) {
9
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
10
+ const store = new CredentialStore();
11
+ try {
12
+ io.out("");
13
+ io.out("Connecting Apple Ads (Apple Search Ads). One-time setup (~10 min, no approval):");
14
+ io.out(" 1. In ads.apple.com: Account Settings \u2192 API \u2192 create an API user (API role).");
15
+ io.out(" 2. Generate an EC key pair locally:");
16
+ io.out(" openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem");
17
+ io.out(" openssl ec -in private-key.pem -pubout -out public-key.pem");
18
+ io.out(" 3. Upload public-key.pem in Account Settings \u2192 API; Apple shows your");
19
+ io.out(" clientId, teamId, and keyId (all needed below).");
20
+ io.out(" Note: access tokens last 1h and are re-minted automatically; no rotation chores.");
21
+ io.out("");
22
+ const clientId = (await rl.question("clientId (SEARCHADS.xxxx): ")).trim();
23
+ const teamId = (await rl.question(`teamId [${clientId}]: `)).trim() || clientId;
24
+ const keyId = (await rl.question("keyId: ")).trim();
25
+ const keyPath = (await rl.question("Path to private-key.pem: ")).trim();
26
+ if (!clientId || !keyId || !keyPath) {
27
+ io.err("Missing values \u2014 aborting.");
28
+ process.exitCode = 1;
29
+ return;
30
+ }
31
+ const privateKeyPem = await fs.readFile(expandHome(keyPath), "utf8");
32
+ io.out("Verifying access\u2026");
33
+ const provider = new AppleAdsProvider(new AppleAdsClient({ clientId, teamId, keyId, privateKeyPem }));
34
+ const accounts = await provider.listAccounts();
35
+ await store.set({
36
+ provider: "apple",
37
+ source: "byo",
38
+ data: { client_id: clientId, team_id: teamId, key_id: keyId, private_key: privateKeyPem }
39
+ });
40
+ io.out("");
41
+ io.out(`\u2713 Connected. ${accounts.length} organization(s):`);
42
+ for (const account of accounts) {
43
+ io.out(` ${account.id} ${account.name} ${account.currency ?? ""} ${account.status ?? ""}`);
44
+ }
45
+ io.out("");
46
+ io.out("Heads-up: the Campaign Management API v5 sunsets 2027-01-26; adport will migrate");
47
+ io.out("to the new Ads Platform API when its docs go live \u2014 your credentials carry over.");
48
+ io.out("");
49
+ io.out("Try: adport accounts \xB7 adport report --provider apple \xB7 adport mcp");
50
+ } finally {
51
+ rl.close();
52
+ }
53
+ }
54
+ function expandHome(p) {
55
+ return p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
56
+ }
57
+ export {
58
+ connectApple
59
+ };
60
+ //# sourceMappingURL=apple-RHO3KWJ4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connect/apple.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport readline from 'node:readline/promises';\nimport { CredentialStore } from '@adport/core';\nimport { AppleAdsClient, AppleAdsProvider } from '@adport/provider-apple';\nimport type { ProgramIO } from '../program.js';\n\n/**\n * Apple Ads is fully self-serve for your own org: create an API user, generate\n * an EC key pair locally, upload the public key — no Apple approval involved.\n */\nexport async function connectApple({ io }: { io: ProgramIO }): Promise<void> {\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n const store = new CredentialStore();\n try {\n io.out('');\n io.out('Connecting Apple Ads (Apple Search Ads). One-time setup (~10 min, no approval):');\n io.out(' 1. In ads.apple.com: Account Settings → API → create an API user (API role).');\n io.out(' 2. Generate an EC key pair locally:');\n io.out(' openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem');\n io.out(' openssl ec -in private-key.pem -pubout -out public-key.pem');\n io.out(' 3. Upload public-key.pem in Account Settings → API; Apple shows your');\n io.out(' clientId, teamId, and keyId (all needed below).');\n io.out(' Note: access tokens last 1h and are re-minted automatically; no rotation chores.');\n io.out('');\n\n const clientId = (await rl.question('clientId (SEARCHADS.xxxx): ')).trim();\n const teamId = (await rl.question(`teamId [${clientId}]: `)).trim() || clientId;\n const keyId = (await rl.question('keyId: ')).trim();\n const keyPath = (await rl.question('Path to private-key.pem: ')).trim();\n if (!clientId || !keyId || !keyPath) {\n io.err('Missing values — aborting.');\n process.exitCode = 1;\n return;\n }\n const privateKeyPem = await fs.readFile(expandHome(keyPath), 'utf8');\n\n io.out('Verifying access…');\n const provider = new AppleAdsProvider(new AppleAdsClient({ clientId, teamId, keyId, privateKeyPem }));\n const accounts = await provider.listAccounts();\n await store.set({\n provider: 'apple',\n source: 'byo',\n data: { client_id: clientId, team_id: teamId, key_id: keyId, private_key: privateKeyPem },\n });\n io.out('');\n io.out(`✓ Connected. ${accounts.length} organization(s):`);\n for (const account of accounts) {\n io.out(` ${account.id} ${account.name} ${account.currency ?? ''} ${account.status ?? ''}`);\n }\n io.out('');\n io.out('Heads-up: the Campaign Management API v5 sunsets 2027-01-26; adport will migrate');\n io.out('to the new Ads Platform API when its docs go live — your credentials carry over.');\n io.out('');\n io.out('Try: adport accounts · adport report --provider apple · adport mcp');\n } finally {\n rl.close();\n }\n}\n\nfunction expandHome(p: string): string {\n return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p;\n}\n"],"mappings":";AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,cAAc;AACrB,SAAS,uBAAuB;AAChC,SAAS,gBAAgB,wBAAwB;AAOjD,eAAsB,aAAa,EAAE,GAAG,GAAqC;AAC3E,QAAM,KAAK,SAAS,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AACpF,QAAM,QAAQ,IAAI,gBAAgB;AAClC,MAAI;AACF,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,iFAAiF;AACxF,OAAG,IAAI,0FAAgF;AACvF,OAAG,IAAI,uCAAuC;AAC9C,OAAG,IAAI,6EAA6E;AACpF,OAAG,IAAI,mEAAmE;AAC1E,OAAG,IAAI,6EAAwE;AAC/E,OAAG,IAAI,sDAAsD;AAC7D,OAAG,IAAI,oFAAoF;AAC3F,OAAG,IAAI,EAAE;AAET,UAAM,YAAY,MAAM,GAAG,SAAS,6BAA6B,GAAG,KAAK;AACzE,UAAM,UAAU,MAAM,GAAG,SAAS,WAAW,QAAQ,KAAK,GAAG,KAAK,KAAK;AACvE,UAAM,SAAS,MAAM,GAAG,SAAS,SAAS,GAAG,KAAK;AAClD,UAAM,WAAW,MAAM,GAAG,SAAS,2BAA2B,GAAG,KAAK;AACtE,QAAI,CAAC,YAAY,CAAC,SAAS,CAAC,SAAS;AACnC,SAAG,IAAI,iCAA4B;AACnC,cAAQ,WAAW;AACnB;AAAA,IACF;AACA,UAAM,gBAAgB,MAAM,GAAG,SAAS,WAAW,OAAO,GAAG,MAAM;AAEnE,OAAG,IAAI,wBAAmB;AAC1B,UAAM,WAAW,IAAI,iBAAiB,IAAI,eAAe,EAAE,UAAU,QAAQ,OAAO,cAAc,CAAC,CAAC;AACpG,UAAM,WAAW,MAAM,SAAS,aAAa;AAC7C,UAAM,MAAM,IAAI;AAAA,MACd,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,MAAM,EAAE,WAAW,UAAU,SAAS,QAAQ,QAAQ,OAAO,aAAa,cAAc;AAAA,IAC1F,CAAC;AACD,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,qBAAgB,SAAS,MAAM,mBAAmB;AACzD,eAAW,WAAW,UAAU;AAC9B,SAAG,IAAI,KAAK,QAAQ,EAAE,KAAK,QAAQ,IAAI,KAAK,QAAQ,YAAY,EAAE,KAAK,QAAQ,UAAU,EAAE,EAAE;AAAA,IAC/F;AACA,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,kFAAkF;AACzF,OAAG,IAAI,uFAAkF;AACzF,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,mFAA6E;AAAA,EACtF,UAAE;AACA,OAAG,MAAM;AAAA,EACX;AACF;AAEA,SAAS,WAAW,GAAmB;AACrC,SAAO,EAAE,WAAW,IAAI,IAAI,KAAK,KAAK,GAAG,QAAQ,GAAG,EAAE,MAAM,CAAC,CAAC,IAAI;AACpE;","names":[]}
@@ -0,0 +1,135 @@
1
+ // src/connect/oauth.ts
2
+ import { createHash, randomBytes } from "crypto";
3
+ import http from "http";
4
+ import { spawn } from "child_process";
5
+ import { AdportError } from "@adport/core";
6
+ var GOOGLE_ADS_SCOPE = "https://www.googleapis.com/auth/adwords";
7
+ var AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
8
+ var TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
9
+ function buildGoogleAuthUrl(clientId, redirectUri) {
10
+ const params = new URLSearchParams({
11
+ client_id: clientId,
12
+ redirect_uri: redirectUri,
13
+ response_type: "code",
14
+ scope: GOOGLE_ADS_SCOPE,
15
+ access_type: "offline",
16
+ prompt: "consent"
17
+ });
18
+ return `${AUTH_ENDPOINT}?${params}`;
19
+ }
20
+ async function exchangeCodeForTokens(input, fetchImpl = fetch) {
21
+ const response = await fetchImpl(TOKEN_ENDPOINT, {
22
+ method: "POST",
23
+ headers: { "content-type": "application/x-www-form-urlencoded" },
24
+ body: new URLSearchParams({
25
+ client_id: input.clientId,
26
+ client_secret: input.clientSecret,
27
+ code: input.code,
28
+ redirect_uri: input.redirectUri,
29
+ grant_type: "authorization_code"
30
+ })
31
+ });
32
+ const data = await response.json();
33
+ if (!response.ok || !data.refresh_token || !data.access_token) {
34
+ throw new AdportError(
35
+ "PROVIDER_ERROR",
36
+ `OAuth code exchange failed${data.error ? ` (${data.error})` : ""}. If no refresh_token was returned, revoke the app at myaccount.google.com/permissions and retry.`,
37
+ data
38
+ );
39
+ }
40
+ return { refreshToken: data.refresh_token, accessToken: data.access_token };
41
+ }
42
+ function parseClientSecretJson(text) {
43
+ const parsed = JSON.parse(text);
44
+ const entry = parsed.installed ?? parsed.web;
45
+ if (!entry?.client_id || !entry?.client_secret) {
46
+ throw new AdportError("INVALID_INPUT", 'Not a valid OAuth client JSON (expected "installed" or "web" key).');
47
+ }
48
+ return { clientId: entry.client_id, clientSecret: entry.client_secret };
49
+ }
50
+ async function startLoopbackServer() {
51
+ let resolveCode;
52
+ let rejectCode;
53
+ const waitForCode = new Promise((resolve, reject) => {
54
+ resolveCode = resolve;
55
+ rejectCode = reject;
56
+ });
57
+ const server = http.createServer((req, res) => {
58
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
59
+ const code = url.searchParams.get("code");
60
+ const error = url.searchParams.get("error");
61
+ res.writeHead(200, { "content-type": "text/html" });
62
+ res.end(
63
+ code ? "<h2>adport is connected.</h2><p>You can close this tab and return to the terminal.</p>" : `<h2>Authorization failed${error ? `: ${error}` : ""}</h2><p>Return to the terminal and retry.</p>`
64
+ );
65
+ if (code) resolveCode(code);
66
+ else if (error) rejectCode(new Error(`OAuth authorization failed: ${error}`));
67
+ });
68
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
69
+ const address = server.address();
70
+ if (!address || typeof address === "string") throw new Error("Loopback server failed to bind");
71
+ return {
72
+ redirectUri: `http://127.0.0.1:${address.port}`,
73
+ waitForCode,
74
+ close: () => server.close()
75
+ };
76
+ }
77
+ var MS_AUTHORIZE = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
78
+ var MS_TOKEN = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
79
+ var MS_ADS_SCOPE = "openid profile https://ads.microsoft.com/msads.manage offline_access";
80
+ function generatePkce() {
81
+ const verifier = randomBytes(32).toString("base64url");
82
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
83
+ return { verifier, challenge };
84
+ }
85
+ function buildMicrosoftAuthUrl(clientId, redirectUri, challenge) {
86
+ const params = new URLSearchParams({
87
+ client_id: clientId,
88
+ response_type: "code",
89
+ redirect_uri: redirectUri,
90
+ scope: MS_ADS_SCOPE,
91
+ code_challenge: challenge,
92
+ code_challenge_method: "S256",
93
+ prompt: "login"
94
+ });
95
+ return `${MS_AUTHORIZE}?${params}`;
96
+ }
97
+ async function exchangeMicrosoftCode(input, fetchImpl = fetch) {
98
+ const response = await fetchImpl(MS_TOKEN, {
99
+ method: "POST",
100
+ headers: { "content-type": "application/x-www-form-urlencoded" },
101
+ body: new URLSearchParams({
102
+ client_id: input.clientId,
103
+ scope: "https://ads.microsoft.com/msads.manage offline_access",
104
+ code: input.code,
105
+ redirect_uri: input.redirectUri,
106
+ grant_type: "authorization_code",
107
+ code_verifier: input.codeVerifier
108
+ })
109
+ });
110
+ const data = await response.json();
111
+ if (!response.ok || !data.refresh_token) {
112
+ throw new AdportError(
113
+ "PROVIDER_ERROR",
114
+ `Microsoft OAuth exchange failed${data.error ? ` (${data.error})` : ""}: ${data.error_description ?? ""}`,
115
+ data
116
+ );
117
+ }
118
+ return { refreshToken: data.refresh_token };
119
+ }
120
+ function openInBrowser(url) {
121
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
122
+ spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
123
+ }
124
+
125
+ export {
126
+ buildGoogleAuthUrl,
127
+ exchangeCodeForTokens,
128
+ parseClientSecretJson,
129
+ startLoopbackServer,
130
+ generatePkce,
131
+ buildMicrosoftAuthUrl,
132
+ exchangeMicrosoftCode,
133
+ openInBrowser
134
+ };
135
+ //# sourceMappingURL=chunk-JPHFJ6HN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connect/oauth.ts"],"sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\nimport http from 'node:http';\nimport { spawn } from 'node:child_process';\nimport { AdportError } from '@adport/core';\n\nexport const GOOGLE_ADS_SCOPE = 'https://www.googleapis.com/auth/adwords';\nconst AUTH_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth';\nconst TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token';\n\nexport function buildGoogleAuthUrl(clientId: string, redirectUri: string): string {\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: 'code',\n scope: GOOGLE_ADS_SCOPE,\n access_type: 'offline',\n prompt: 'consent',\n });\n return `${AUTH_ENDPOINT}?${params}`;\n}\n\nexport async function exchangeCodeForTokens(\n input: { clientId: string; clientSecret: string; code: string; redirectUri: string },\n fetchImpl: typeof fetch = fetch,\n): Promise<{ refreshToken: string; accessToken: string }> {\n const response = await fetchImpl(TOKEN_ENDPOINT, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n client_id: input.clientId,\n client_secret: input.clientSecret,\n code: input.code,\n redirect_uri: input.redirectUri,\n grant_type: 'authorization_code',\n }),\n });\n const data = (await response.json()) as { refresh_token?: string; access_token?: string; error?: string };\n if (!response.ok || !data.refresh_token || !data.access_token) {\n throw new AdportError(\n 'PROVIDER_ERROR',\n `OAuth code exchange failed${data.error ? ` (${data.error})` : ''}. ` +\n 'If no refresh_token was returned, revoke the app at myaccount.google.com/permissions and retry.',\n data,\n );\n }\n return { refreshToken: data.refresh_token, accessToken: data.access_token };\n}\n\n/** Parse a downloaded OAuth client JSON (client_secret_*.json, \"installed\" or \"web\"). */\nexport function parseClientSecretJson(text: string): { clientId: string; clientSecret: string } {\n const parsed = JSON.parse(text) as {\n installed?: { client_id: string; client_secret: string };\n web?: { client_id: string; client_secret: string };\n };\n const entry = parsed.installed ?? parsed.web;\n if (!entry?.client_id || !entry?.client_secret) {\n throw new AdportError('INVALID_INPUT', 'Not a valid OAuth client JSON (expected \"installed\" or \"web\" key).');\n }\n return { clientId: entry.client_id, clientSecret: entry.client_secret };\n}\n\nexport interface LoopbackServer {\n redirectUri: string;\n /** Resolves with the authorization code once Google redirects back. */\n waitForCode: Promise<string>;\n close: () => void;\n}\n\n/** Loopback listener for the OAuth redirect — the desktop-app flow's return path. */\nexport async function startLoopbackServer(): Promise<LoopbackServer> {\n let resolveCode!: (code: string) => void;\n let rejectCode!: (err: Error) => void;\n const waitForCode = new Promise<string>((resolve, reject) => {\n resolveCode = resolve;\n rejectCode = reject;\n });\n\n const server = http.createServer((req, res) => {\n const url = new URL(req.url ?? '/', 'http://127.0.0.1');\n const code = url.searchParams.get('code');\n const error = url.searchParams.get('error');\n res.writeHead(200, { 'content-type': 'text/html' });\n res.end(\n code\n ? '<h2>adport is connected.</h2><p>You can close this tab and return to the terminal.</p>'\n : `<h2>Authorization failed${error ? `: ${error}` : ''}</h2><p>Return to the terminal and retry.</p>`,\n );\n if (code) resolveCode(code);\n else if (error) rejectCode(new Error(`OAuth authorization failed: ${error}`));\n });\n\n await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));\n const address = server.address();\n if (!address || typeof address === 'string') throw new Error('Loopback server failed to bind');\n return {\n redirectUri: `http://127.0.0.1:${address.port}`,\n waitForCode,\n close: () => server.close(),\n };\n}\n\n// ---- Microsoft identity platform (public client + PKCE) ---------------------\n\nconst MS_AUTHORIZE = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize';\nconst MS_TOKEN = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';\nexport const MS_ADS_SCOPE = 'openid profile https://ads.microsoft.com/msads.manage offline_access';\n\nexport function generatePkce(): { verifier: string; challenge: string } {\n const verifier = randomBytes(32).toString('base64url');\n const challenge = createHash('sha256').update(verifier).digest('base64url');\n return { verifier, challenge };\n}\n\nexport function buildMicrosoftAuthUrl(clientId: string, redirectUri: string, challenge: string): string {\n const params = new URLSearchParams({\n client_id: clientId,\n response_type: 'code',\n redirect_uri: redirectUri,\n scope: MS_ADS_SCOPE,\n code_challenge: challenge,\n code_challenge_method: 'S256',\n prompt: 'login',\n });\n return `${MS_AUTHORIZE}?${params}`;\n}\n\nexport async function exchangeMicrosoftCode(\n input: { clientId: string; code: string; redirectUri: string; codeVerifier: string },\n fetchImpl: typeof fetch = fetch,\n): Promise<{ refreshToken: string }> {\n const response = await fetchImpl(MS_TOKEN, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n client_id: input.clientId,\n scope: 'https://ads.microsoft.com/msads.manage offline_access',\n code: input.code,\n redirect_uri: input.redirectUri,\n grant_type: 'authorization_code',\n code_verifier: input.codeVerifier,\n }),\n });\n const data = (await response.json()) as { refresh_token?: string; error?: string; error_description?: string };\n if (!response.ok || !data.refresh_token) {\n throw new AdportError(\n 'PROVIDER_ERROR',\n `Microsoft OAuth exchange failed${data.error ? ` (${data.error})` : ''}: ${data.error_description ?? ''}`,\n data,\n );\n }\n return { refreshToken: data.refresh_token };\n}\n\nexport function openInBrowser(url: string): void {\n const [cmd, args] =\n process.platform === 'darwin'\n ? ['open', [url]]\n : process.platform === 'win32'\n ? ['cmd', ['/c', 'start', '', url]]\n : ['xdg-open', [url]];\n spawn(cmd as string, args as string[], { detached: true, stdio: 'ignore' }).unref();\n}\n"],"mappings":";AAAA,SAAS,YAAY,mBAAmB;AACxC,OAAO,UAAU;AACjB,SAAS,aAAa;AACtB,SAAS,mBAAmB;AAErB,IAAM,mBAAmB;AAChC,IAAM,gBAAgB;AACtB,IAAM,iBAAiB;AAEhB,SAAS,mBAAmB,UAAkB,aAA6B;AAChF,QAAM,SAAS,IAAI,gBAAgB;AAAA,IACjC,WAAW;AAAA,IACX,cAAc;AAAA,IACd,eAAe;AAAA,IACf,OAAO;AAAA,IACP,aAAa;AAAA,IACb,QAAQ;AAAA,EACV,CAAC;AACD,SAAO,GAAG,aAAa,IAAI,MAAM;AACnC;AAEA,eAAsB,sBACpB,OACA,YAA0B,OAC8B;AACxD,QAAM,WAAW,MAAM,UAAU,gBAAgB;AAAA,IAC/C,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,IAC/D,MAAM,IAAI,gBAAgB;AAAA,MACxB,WAAW,MAAM;AAAA,MACjB,eAAe,MAAM;AAAA,MACrB,MAAM,MAAM;AAAA,MACZ,cAAc,MAAM;AAAA,MACpB,YAAY;AAAA,IACd,CAAC;AAAA,EACH,CAAC;AACD,QAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,MAAI,CAAC,SAAS,MAAM,CAAC,KAAK,iBAAiB,CAAC,KAAK,cAAc;AAC7D,UAAM,IAAI;AAAA,MACR;AAAA,MACA,6BAA6B,KAAK,QAAQ,KAAK,KAAK,KAAK,MAAM,EAAE;AAAA,MAEjE;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,cAAc,KAAK,eAAe,aAAa,KAAK,aAAa;AAC5E;AAGO,SAAS,sBAAsB,MAA0D;AAC9F,QAAM,SAAS,KAAK,MAAM,IAAI;AAI9B,QAAM,QAAQ,OAAO,aAAa,OAAO;AACzC,MAAI,CAAC,OAAO,aAAa,CAAC,OAAO,eAAe;AAC9C,UAAM,IAAI,YAAY,iBAAiB,oEAAoE;AAAA,EAC7G;AACA,SAAO,EAAE,UAAU,MAAM,WAAW,cAAc,MAAM,cAAc;AACxE;AAUA,eAAsB,sBAA+C;AACnE,MAAI;AACJ,MAAI;AACJ,QAAM,cAAc,IAAI,QAAgB,CAAC,SAAS,WAAW;AAC3D,kBAAc;AACd,iBAAa;AAAA,EACf,CAAC;AAED,QAAM,SAAS,KAAK,aAAa,CAAC,KAAK,QAAQ;AAC7C,UAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,kBAAkB;AACtD,UAAM,OAAO,IAAI,aAAa,IAAI,MAAM;AACxC,UAAM,QAAQ,IAAI,aAAa,IAAI,OAAO;AAC1C,QAAI,UAAU,KAAK,EAAE,gBAAgB,YAAY,CAAC;AAClD,QAAI;AAAA,MACF,OACI,2FACA,2BAA2B,QAAQ,KAAK,KAAK,KAAK,EAAE;AAAA,IAC1D;AACA,QAAI,KAAM,aAAY,IAAI;AAAA,aACjB,MAAO,YAAW,IAAI,MAAM,+BAA+B,KAAK,EAAE,CAAC;AAAA,EAC9E,CAAC;AAED,QAAM,IAAI,QAAc,CAAC,YAAY,OAAO,OAAO,GAAG,aAAa,OAAO,CAAC;AAC3E,QAAM,UAAU,OAAO,QAAQ;AAC/B,MAAI,CAAC,WAAW,OAAO,YAAY,SAAU,OAAM,IAAI,MAAM,gCAAgC;AAC7F,SAAO;AAAA,IACL,aAAa,oBAAoB,QAAQ,IAAI;AAAA,IAC7C;AAAA,IACA,OAAO,MAAM,OAAO,MAAM;AAAA,EAC5B;AACF;AAIA,IAAM,eAAe;AACrB,IAAM,WAAW;AACV,IAAM,eAAe;AAErB,SAAS,eAAwD;AACtE,QAAM,WAAW,YAAY,EAAE,EAAE,SAAS,WAAW;AACrD,QAAM,YAAY,WAAW,QAAQ,EAAE,OAAO,QAAQ,EAAE,OAAO,WAAW;AAC1E,SAAO,EAAE,UAAU,UAAU;AAC/B;AAEO,SAAS,sBAAsB,UAAkB,aAAqB,WAA2B;AACtG,QAAM,SAAS,IAAI,gBAAgB;AAAA,IACjC,WAAW;AAAA,IACX,eAAe;AAAA,IACf,cAAc;AAAA,IACd,OAAO;AAAA,IACP,gBAAgB;AAAA,IAChB,uBAAuB;AAAA,IACvB,QAAQ;AAAA,EACV,CAAC;AACD,SAAO,GAAG,YAAY,IAAI,MAAM;AAClC;AAEA,eAAsB,sBACpB,OACA,YAA0B,OACS;AACnC,QAAM,WAAW,MAAM,UAAU,UAAU;AAAA,IACzC,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,IAC/D,MAAM,IAAI,gBAAgB;AAAA,MACxB,WAAW,MAAM;AAAA,MACjB,OAAO;AAAA,MACP,MAAM,MAAM;AAAA,MACZ,cAAc,MAAM;AAAA,MACpB,YAAY;AAAA,MACZ,eAAe,MAAM;AAAA,IACvB,CAAC;AAAA,EACH,CAAC;AACD,QAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,MAAI,CAAC,SAAS,MAAM,CAAC,KAAK,eAAe;AACvC,UAAM,IAAI;AAAA,MACR;AAAA,MACA,kCAAkC,KAAK,QAAQ,KAAK,KAAK,KAAK,MAAM,EAAE,KAAK,KAAK,qBAAqB,EAAE;AAAA,MACvG;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,cAAc,KAAK,cAAc;AAC5C;AAEO,SAAS,cAAc,KAAmB;AAC/C,QAAM,CAAC,KAAK,IAAI,IACd,QAAQ,aAAa,WACjB,CAAC,QAAQ,CAAC,GAAG,CAAC,IACd,QAAQ,aAAa,UACnB,CAAC,OAAO,CAAC,MAAM,SAAS,IAAI,GAAG,CAAC,IAChC,CAAC,YAAY,CAAC,GAAG,CAAC;AAC1B,QAAM,KAAe,MAAkB,EAAE,UAAU,MAAM,OAAO,SAAS,CAAC,EAAE,MAAM;AACpF;","names":[]}
@@ -0,0 +1,166 @@
1
+ import {
2
+ buildGoogleAuthUrl,
3
+ exchangeCodeForTokens,
4
+ openInBrowser,
5
+ parseClientSecretJson,
6
+ startLoopbackServer
7
+ } from "./chunk-JPHFJ6HN.js";
8
+
9
+ // src/connect/google.ts
10
+ import { promises as fs } from "fs";
11
+ import os from "os";
12
+ import path from "path";
13
+ import readline from "readline/promises";
14
+ import YAML from "yaml";
15
+ import { CredentialStore } from "@adport/core";
16
+ import { GoogleAdsRestClient } from "@adport/provider-google";
17
+ async function connectGoogle({ openBrowser, io }) {
18
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
19
+ const store = new CredentialStore();
20
+ try {
21
+ const existing = await store.get("google");
22
+ if (existing?.data.developer_token && existing.data.client_id && existing.data.client_secret) {
23
+ const answer = (await rl.question(
24
+ "Existing Google connection found. Re-authorize with the same developer token and OAuth client? [Y/n] "
25
+ )).trim().toLowerCase();
26
+ if (answer !== "n" && answer !== "no") {
27
+ const refreshToken = await runOAuthFlow(
28
+ existing.data.client_id,
29
+ existing.data.client_secret,
30
+ openBrowser,
31
+ io
32
+ );
33
+ await verifyAndSave(
34
+ {
35
+ developerToken: existing.data.developer_token,
36
+ clientId: existing.data.client_id,
37
+ clientSecret: existing.data.client_secret,
38
+ refreshToken,
39
+ loginCustomerId: existing.data.login_customer_id || void 0
40
+ },
41
+ store,
42
+ io
43
+ );
44
+ return;
45
+ }
46
+ }
47
+ io.out("");
48
+ io.out("Connecting Google Ads. You need (the wizard guides each step):");
49
+ io.out(" 1. A manager account (MCC) \u2014 free, instant: https://ads.google.com/home/tools/manager-accounts/");
50
+ io.out(" 2. A developer token from the MCC API Center (Explorer access is automatic): https://ads.google.com/aw/apicenter");
51
+ io.out(' 3. A Google Cloud OAuth "Desktop app" client: https://console.cloud.google.com/apis/credentials');
52
+ io.out(' (enable the "Google Ads API" for the project, consent screen: External, add yourself as test user)');
53
+ io.out("");
54
+ const imported = await tryImportGoogleAdsYaml(rl, io);
55
+ let creds;
56
+ if (imported) {
57
+ creds = imported;
58
+ } else {
59
+ const developerToken = (await rl.question("Developer token (from the MCC API Center): ")).trim();
60
+ let clientId = "";
61
+ let clientSecret = "";
62
+ const secretPath = (await rl.question("Path to downloaded client_secret_*.json (or press Enter to type id/secret manually): ")).trim();
63
+ if (secretPath) {
64
+ const parsed = parseClientSecretJson(await fs.readFile(expandHome(secretPath), "utf8"));
65
+ clientId = parsed.clientId;
66
+ clientSecret = parsed.clientSecret;
67
+ } else {
68
+ clientId = (await rl.question("OAuth client id: ")).trim();
69
+ clientSecret = (await rl.question("OAuth client secret: ")).trim();
70
+ }
71
+ const loginCustomerId = (await rl.question("Manager (MCC) customer id for login-customer-id (Enter to skip): ")).trim() || void 0;
72
+ const refreshToken = await runOAuthFlow(clientId, clientSecret, openBrowser, io);
73
+ creds = { developerToken, clientId, clientSecret, refreshToken, loginCustomerId };
74
+ }
75
+ await verifyAndSave(creds, store, io);
76
+ } finally {
77
+ rl.close();
78
+ }
79
+ }
80
+ async function runOAuthFlow(clientId, clientSecret, openBrowser, io) {
81
+ io.out("");
82
+ io.out("Starting the OAuth flow (scope: Google Ads). A browser window should open;");
83
+ io.out("sign in with the Google account that can access your ad accounts.");
84
+ const loopback = await startLoopbackServer();
85
+ const authUrl = buildGoogleAuthUrl(clientId, loopback.redirectUri);
86
+ if (openBrowser) {
87
+ openInBrowser(authUrl);
88
+ io.out(`If the browser did not open, visit:
89
+ ${authUrl}`);
90
+ } else {
91
+ io.out(`Open this URL in a browser on this machine (or SSH port-forward the shown port):
92
+ ${authUrl}`);
93
+ }
94
+ try {
95
+ const code = await loopback.waitForCode;
96
+ const tokens = await exchangeCodeForTokens({ clientId, clientSecret, code, redirectUri: loopback.redirectUri });
97
+ return tokens.refreshToken;
98
+ } finally {
99
+ loopback.close();
100
+ }
101
+ }
102
+ async function verifyAndSave(creds, store, io) {
103
+ io.out("Verifying access\u2026");
104
+ const client = new GoogleAdsRestClient(creds);
105
+ const customers = await client.listAccessibleCustomers();
106
+ await store.set({
107
+ provider: "google",
108
+ source: "byo",
109
+ data: {
110
+ developer_token: creds.developerToken,
111
+ client_id: creds.clientId,
112
+ client_secret: creds.clientSecret,
113
+ refresh_token: creds.refreshToken,
114
+ ...creds.loginCustomerId ? { login_customer_id: creds.loginCustomerId } : {}
115
+ }
116
+ });
117
+ io.out("");
118
+ io.out(`\u2713 Connected. ${customers.length} accessible customer id(s): ${customers.join(", ")}`);
119
+ io.out("");
120
+ io.out('If this token expires again in ~7 days: your OAuth consent screen is in "Testing"');
121
+ io.out('status \u2014 set it to "In production" at console.cloud.google.com/apis/credentials/consent');
122
+ io.out("(no verification needed for your own use; refresh tokens then stop expiring weekly).");
123
+ io.out("");
124
+ io.out("Try: adport accounts \xB7 adport report --provider google \xB7 adport mcp");
125
+ }
126
+ async function tryImportGoogleAdsYaml(rl, io) {
127
+ const candidates = [
128
+ process.env.GOOGLE_ADS_CONFIGURATION_FILE_PATH,
129
+ path.join(os.homedir(), "google-ads.yaml")
130
+ ].filter((p) => Boolean(p));
131
+ for (const candidate of candidates) {
132
+ let raw;
133
+ try {
134
+ raw = await fs.readFile(candidate, "utf8");
135
+ } catch {
136
+ continue;
137
+ }
138
+ const parsed = YAML.parse(raw);
139
+ const developerToken = str(parsed?.developer_token);
140
+ const clientId = str(parsed?.client_id);
141
+ const clientSecret = str(parsed?.client_secret);
142
+ const refreshToken = str(parsed?.refresh_token);
143
+ if (!developerToken || !clientId || !clientSecret || !refreshToken) continue;
144
+ const answer = (await rl.question(`Found ${candidate} \u2014 import it? [Y/n] `)).trim().toLowerCase();
145
+ if (answer === "n" || answer === "no") continue;
146
+ io.out(`Importing credentials from ${candidate}.`);
147
+ return {
148
+ developerToken,
149
+ clientId,
150
+ clientSecret,
151
+ refreshToken,
152
+ loginCustomerId: str(parsed?.login_customer_id) || void 0
153
+ };
154
+ }
155
+ return void 0;
156
+ }
157
+ function str(value) {
158
+ return value === void 0 || value === null ? "" : String(value);
159
+ }
160
+ function expandHome(p) {
161
+ return p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
162
+ }
163
+ export {
164
+ connectGoogle
165
+ };
166
+ //# sourceMappingURL=google-ZQQS6JUM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connect/google.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport readline from 'node:readline/promises';\nimport YAML from 'yaml';\nimport { CredentialStore } from '@adport/core';\nimport { GoogleAdsRestClient, type GoogleCredentials } from '@adport/provider-google';\nimport type { ProgramIO } from '../program.js';\nimport {\n buildGoogleAuthUrl,\n exchangeCodeForTokens,\n openInBrowser,\n parseClientSecretJson,\n startLoopbackServer,\n} from './oauth.js';\n\nexport interface ConnectGoogleOptions {\n openBrowser: boolean;\n io: ProgramIO;\n}\n\n/**\n * Guided Google Ads connection. Since Google's Explorer access tier (Oct 2025),\n * a fresh developer token works on production accounts the same day — the wizard's\n * job is navigation, not waiting.\n */\nexport async function connectGoogle({ openBrowser, io }: ConnectGoogleOptions): Promise<void> {\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n const store = new CredentialStore();\n try {\n // Re-auth fast path: keep the stored developer token + OAuth client and\n // only redo the browser consent (covers expired/revoked refresh tokens).\n const existing = await store.get('google');\n if (existing?.data.developer_token && existing.data.client_id && existing.data.client_secret) {\n const answer = (\n await rl.question(\n 'Existing Google connection found. Re-authorize with the same developer token and OAuth client? [Y/n] ',\n )\n )\n .trim()\n .toLowerCase();\n if (answer !== 'n' && answer !== 'no') {\n const refreshToken = await runOAuthFlow(\n existing.data.client_id,\n existing.data.client_secret,\n openBrowser,\n io,\n );\n await verifyAndSave(\n {\n developerToken: existing.data.developer_token,\n clientId: existing.data.client_id,\n clientSecret: existing.data.client_secret,\n refreshToken,\n loginCustomerId: existing.data.login_customer_id || undefined,\n },\n store,\n io,\n );\n return;\n }\n }\n\n io.out('');\n io.out('Connecting Google Ads. You need (the wizard guides each step):');\n io.out(' 1. A manager account (MCC) — free, instant: https://ads.google.com/home/tools/manager-accounts/');\n io.out(' 2. A developer token from the MCC API Center (Explorer access is automatic): https://ads.google.com/aw/apicenter');\n io.out(' 3. A Google Cloud OAuth \"Desktop app\" client: https://console.cloud.google.com/apis/credentials');\n io.out(' (enable the \"Google Ads API\" for the project, consent screen: External, add yourself as test user)');\n io.out('');\n\n // Fast path: import an existing google-ads.yaml (the ecosystem convention).\n const imported = await tryImportGoogleAdsYaml(rl, io);\n let creds: GoogleCredentials;\n if (imported) {\n creds = imported;\n } else {\n const developerToken = (await rl.question('Developer token (from the MCC API Center): ')).trim();\n\n let clientId = '';\n let clientSecret = '';\n const secretPath = (\n await rl.question('Path to downloaded client_secret_*.json (or press Enter to type id/secret manually): ')\n ).trim();\n if (secretPath) {\n const parsed = parseClientSecretJson(await fs.readFile(expandHome(secretPath), 'utf8'));\n clientId = parsed.clientId;\n clientSecret = parsed.clientSecret;\n } else {\n clientId = (await rl.question('OAuth client id: ')).trim();\n clientSecret = (await rl.question('OAuth client secret: ')).trim();\n }\n\n const loginCustomerId =\n (await rl.question('Manager (MCC) customer id for login-customer-id (Enter to skip): ')).trim() || undefined;\n\n const refreshToken = await runOAuthFlow(clientId, clientSecret, openBrowser, io);\n creds = { developerToken, clientId, clientSecret, refreshToken, loginCustomerId };\n }\n\n await verifyAndSave(creds, store, io);\n } finally {\n rl.close();\n }\n}\n\nasync function runOAuthFlow(\n clientId: string,\n clientSecret: string,\n openBrowser: boolean,\n io: ProgramIO,\n): Promise<string> {\n io.out('');\n io.out('Starting the OAuth flow (scope: Google Ads). A browser window should open;');\n io.out('sign in with the Google account that can access your ad accounts.');\n const loopback = await startLoopbackServer();\n const authUrl = buildGoogleAuthUrl(clientId, loopback.redirectUri);\n if (openBrowser) {\n openInBrowser(authUrl);\n io.out(`If the browser did not open, visit:\\n ${authUrl}`);\n } else {\n io.out(`Open this URL in a browser on this machine (or SSH port-forward the shown port):\\n ${authUrl}`);\n }\n try {\n const code = await loopback.waitForCode;\n const tokens = await exchangeCodeForTokens({ clientId, clientSecret, code, redirectUri: loopback.redirectUri });\n return tokens.refreshToken;\n } finally {\n loopback.close();\n }\n}\n\nasync function verifyAndSave(creds: GoogleCredentials, store: CredentialStore, io: ProgramIO): Promise<void> {\n io.out('Verifying access…');\n const client = new GoogleAdsRestClient(creds);\n const customers = await client.listAccessibleCustomers();\n await store.set({\n provider: 'google',\n source: 'byo',\n data: {\n developer_token: creds.developerToken,\n client_id: creds.clientId,\n client_secret: creds.clientSecret,\n refresh_token: creds.refreshToken,\n ...(creds.loginCustomerId ? { login_customer_id: creds.loginCustomerId } : {}),\n },\n });\n io.out('');\n io.out(`✓ Connected. ${customers.length} accessible customer id(s): ${customers.join(', ')}`);\n io.out('');\n io.out('If this token expires again in ~7 days: your OAuth consent screen is in \"Testing\"');\n io.out('status — set it to \"In production\" at console.cloud.google.com/apis/credentials/consent');\n io.out('(no verification needed for your own use; refresh tokens then stop expiring weekly).');\n io.out('');\n io.out('Try: adport accounts · adport report --provider google · adport mcp');\n}\n\nasync function tryImportGoogleAdsYaml(\n rl: readline.Interface,\n io: ProgramIO,\n): Promise<GoogleCredentials | undefined> {\n const candidates = [\n process.env.GOOGLE_ADS_CONFIGURATION_FILE_PATH,\n path.join(os.homedir(), 'google-ads.yaml'),\n ].filter((p): p is string => Boolean(p));\n for (const candidate of candidates) {\n let raw: string;\n try {\n raw = await fs.readFile(candidate, 'utf8');\n } catch {\n continue;\n }\n const parsed = YAML.parse(raw) as Record<string, unknown> | null;\n const developerToken = str(parsed?.developer_token);\n const clientId = str(parsed?.client_id);\n const clientSecret = str(parsed?.client_secret);\n const refreshToken = str(parsed?.refresh_token);\n if (!developerToken || !clientId || !clientSecret || !refreshToken) continue;\n const answer = (await rl.question(`Found ${candidate} — import it? [Y/n] `)).trim().toLowerCase();\n if (answer === 'n' || answer === 'no') continue;\n io.out(`Importing credentials from ${candidate}.`);\n return {\n developerToken,\n clientId,\n clientSecret,\n refreshToken,\n loginCustomerId: str(parsed?.login_customer_id) || undefined,\n };\n }\n return undefined;\n}\n\nfunction str(value: unknown): string {\n return value === undefined || value === null ? '' : String(value);\n}\n\nfunction expandHome(p: string): string {\n return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p;\n}\n"],"mappings":"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","names":[]}
package/dist/index.js ADDED
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/program.ts
4
+ import { Command } from "commander";
5
+ import {
6
+ AdportError,
7
+ AuditLog,
8
+ CredentialStore
9
+ } from "@adport/core";
10
+ var defaultIO = {
11
+ out: (line) => console.log(line),
12
+ err: (line) => console.error(line)
13
+ };
14
+ function json(value) {
15
+ return JSON.stringify(value, null, 2);
16
+ }
17
+ function table(rows) {
18
+ if (rows.length === 0) return "(empty)";
19
+ const keys = Object.keys(rows[0]);
20
+ const widths = keys.map((k) => Math.max(k.length, ...rows.map((r) => String(r[k] ?? "").length)));
21
+ const line = (values) => values.map((v, i) => v.padEnd(widths[i])).join(" ");
22
+ return [line(keys), line(widths.map((w) => "-".repeat(w))), ...rows.map((r) => line(keys.map((k) => String(r[k] ?? ""))))].join("\n");
23
+ }
24
+ async function runtime() {
25
+ const { assembleRuntime } = await import("@adport/mcp");
26
+ return assembleRuntime();
27
+ }
28
+ function buildProgram(io = defaultIO) {
29
+ const program = new Command("adport");
30
+ program.description("Open-source multi-platform ads management for AI agents (MCP + CLI)").version("0.1.0").configureOutput({ writeOut: (s) => io.out(s.trimEnd()), writeErr: (s) => io.err(s.trimEnd()) });
31
+ const tools = program.command("tools").description("Inspect and invoke the shared tool registry");
32
+ tools.command("list").description("List available tools").option("--json", "JSON output").action(async (opts) => {
33
+ const { registry } = await runtime();
34
+ const defs = registry.list();
35
+ if (opts.json) {
36
+ io.out(
37
+ json(
38
+ defs.map((t) => ({
39
+ name: t.name,
40
+ namespace: t.namespace,
41
+ readOnly: t.annotations.readOnly ?? false,
42
+ destructive: t.annotations.destructive ?? false
43
+ }))
44
+ )
45
+ );
46
+ return;
47
+ }
48
+ io.out(
49
+ table(
50
+ defs.map((t) => ({
51
+ name: t.name,
52
+ namespace: t.namespace,
53
+ mode: t.annotations.readOnly ? "read" : t.annotations.destructive ? "DESTRUCTIVE" : "write"
54
+ }))
55
+ )
56
+ );
57
+ });
58
+ tools.command("run <name>").description("Invoke a tool with JSON input (the same tools the MCP server exposes)").option("--input <json>", "JSON object with the tool arguments", "{}").action(async (name, opts) => {
59
+ const rt = await runtime();
60
+ const result = await rt.registry.call(name, JSON.parse(opts.input), rt.ctx);
61
+ io.out(json(result));
62
+ });
63
+ program.command("accounts").description("List connected ad accounts across providers").option("--provider <id>", "Limit to one provider").option("--json", "JSON output").action(async (opts) => {
64
+ const rt = await runtime();
65
+ const result = await rt.registry.call("accounts_list", { provider: opts.provider }, rt.ctx);
66
+ io.out(opts.json ? json(result.accounts) : table(result.accounts));
67
+ });
68
+ program.command("report").description("Normalized cross-platform performance report").option("--provider <id>", "Limit to one provider").option("--level <level>", "account | campaign | ad_group | ad", "campaign").option("--metrics <list>", "Comma-separated metrics", "spend,impressions,clicks,conversions").option("--range <range>", "Preset (last_7_days, ...) or START..END", "last_7_days").option("--limit <n>", "Max rows", "100").option("--json", "JSON output").action(async (opts) => {
69
+ const rt = await runtime();
70
+ const dateRange = opts.range.includes("..") ? { start: opts.range.split("..")[0], end: opts.range.split("..")[1] } : opts.range;
71
+ const result = await rt.registry.call(
72
+ "report",
73
+ {
74
+ provider: opts.provider,
75
+ level: opts.level,
76
+ metrics: opts.metrics.split(",").map((m) => m.trim()),
77
+ date_range: dateRange,
78
+ limit: Number(opts.limit)
79
+ },
80
+ rt.ctx
81
+ );
82
+ if (opts.json) {
83
+ io.out(json(result));
84
+ return;
85
+ }
86
+ io.out(
87
+ table(
88
+ result.rows.map((r) => ({
89
+ provider: r.provider,
90
+ account: r.accountId,
91
+ entity: r.entity.name,
92
+ status: r.entity.status ?? "",
93
+ ...r.metrics
94
+ }))
95
+ )
96
+ );
97
+ if (result.truncated) io.err("(truncated \u2014 raise --limit for more rows)");
98
+ });
99
+ program.command("policy").description("Show the active write policy and where it was loaded from").action(async () => {
100
+ const rt = await runtime();
101
+ io.out(json({ source: rt.policySource, policy: rt.ctx.engine.policy }));
102
+ });
103
+ const audit = program.command("audit").description("Inspect or extend the write-audit trail");
104
+ audit.command("show", { isDefault: true }).description("Show recent write-audit entries").option("--limit <n>", "Max entries", "20").action(async (opts) => {
105
+ const entries = await new AuditLog().read(Number(opts.limit));
106
+ if (entries.length === 0) {
107
+ io.out("No audit entries yet.");
108
+ return;
109
+ }
110
+ io.out(entries.map((e) => JSON.stringify(e)).join("\n"));
111
+ });
112
+ audit.command("run").description("Run the audit rule packs over connected accounts and persist findings").option("--provider <id>", "Limit to one provider").option("--range <range>", "Preset or START..END", "last_30_days").action(async (opts) => {
113
+ const rt = await runtime();
114
+ const dateRange = opts.range.includes("..") ? { start: opts.range.split("..")[0], end: opts.range.split("..")[1] } : opts.range;
115
+ const result = await rt.registry.call(
116
+ "audit_run",
117
+ { provider: opts.provider, date_range: dateRange },
118
+ rt.ctx
119
+ );
120
+ io.out(`Evaluated ${result.evaluatedAccounts} account(s): ${result.counts.critical} critical, ${result.counts.warn} warn, ${result.counts.info} info`);
121
+ for (const finding of result.findings) {
122
+ io.out(` [${finding.severity.toUpperCase()}] ${finding.id}
123
+ ${finding.title}`);
124
+ }
125
+ if (result.findings.length > 0) io.out("Next: adport recommendations \xB7 adport recommendations apply <id>");
126
+ });
127
+ audit.command("note <text>").description("Record an external/manual change in the audit trail (e.g. a change made in the platform UI)").option("--provider <id>", "Provider the change concerns", "external").option("--account <id>", "Account id the change concerns", "-").action(async (text, opts) => {
128
+ await new AuditLog().append({
129
+ event: "note",
130
+ provider: opts.provider,
131
+ tool: "audit_note",
132
+ accountId: opts.account,
133
+ summary: text
134
+ });
135
+ io.out("Noted.");
136
+ });
137
+ program.command("connect <provider>").description("Connect an ad account (guided wizard)").option("--no-browser", "Print auth URLs instead of opening a browser").action(async (provider, opts) => {
138
+ if (provider === "google") {
139
+ const { connectGoogle } = await import("./google-ZQQS6JUM.js");
140
+ await connectGoogle({ openBrowser: opts.browser, io });
141
+ return;
142
+ }
143
+ if (provider === "meta") {
144
+ const { connectMeta } = await import("./meta-IXPXEBI2.js");
145
+ await connectMeta({ io });
146
+ return;
147
+ }
148
+ if (provider === "tiktok") {
149
+ const { connectTikTok } = await import("./tiktok-TL7Z64OV.js");
150
+ await connectTikTok({ io });
151
+ return;
152
+ }
153
+ if (provider === "apple") {
154
+ const { connectApple } = await import("./apple-RHO3KWJ4.js");
155
+ await connectApple({ io });
156
+ return;
157
+ }
158
+ if (provider === "microsoft") {
159
+ const { connectMicrosoft } = await import("./microsoft-QXMPCWJ2.js");
160
+ await connectMicrosoft({ openBrowser: opts.browser, io });
161
+ return;
162
+ }
163
+ if (provider === "mock") {
164
+ io.out("The mock provider needs no credentials \u2014 it is available out of the box. Try: adport accounts");
165
+ return;
166
+ }
167
+ io.err(`Provider "${provider}" is not supported. Available: google, meta, tiktok, apple, microsoft, mock.`);
168
+ process.exitCode = 1;
169
+ });
170
+ const recommendations = program.command("recommendations").description("Review and act on audit findings");
171
+ recommendations.command("list", { isDefault: true }).description("List findings (default: open), most severe first").option("--status <status>", "open | dismissed | applied", "open").option("--json", "JSON output").action(async (opts) => {
172
+ const rt = await runtime();
173
+ const result = await rt.registry.call("recommendations_list", { status: opts.status }, rt.ctx);
174
+ if (opts.json) {
175
+ io.out(json(result.findings));
176
+ return;
177
+ }
178
+ if (result.findings.length === 0) {
179
+ io.out(`No ${opts.status} findings. Run: adport audit run`);
180
+ return;
181
+ }
182
+ for (const f of result.findings) {
183
+ io.out(`[${f.severity.toUpperCase()}] ${f.id}`);
184
+ io.out(` ${f.title}`);
185
+ io.out(` \u2192 ${f.recommendation}${f.proposedAction ? " (has ready-to-apply action)" : ""}`);
186
+ }
187
+ });
188
+ recommendations.command("apply <findingId>").description("Run a finding's proposed action through the two-step validate\u2192apply flow").option("--pending <id>", "pending_operation_id from the validation step").action(async (findingId, opts) => {
189
+ const rt = await runtime();
190
+ const result = await rt.registry.call(
191
+ "recommendation_apply",
192
+ { finding_id: findingId, pending_operation_id: opts.pending },
193
+ rt.ctx
194
+ );
195
+ io.out(json(result));
196
+ });
197
+ recommendations.command("dismiss <findingId>").description("Dismiss a finding (never reopened by future runs)").action(async (findingId) => {
198
+ const rt = await runtime();
199
+ const result = await rt.registry.call("recommendation_dismiss", { finding_id: findingId }, rt.ctx);
200
+ io.out(json(result));
201
+ });
202
+ program.command("doctor").description("Check credential and connection health for every provider").action(async () => {
203
+ const store = new CredentialStore();
204
+ const records = await store.list();
205
+ const rt = await runtime();
206
+ io.out(`policy: ${rt.policySource}`);
207
+ io.out(`providers loaded: ${rt.ctx.providers.list().map((p) => p.id).join(", ") || "(none)"}`);
208
+ for (const record of records) {
209
+ io.out(`${record.provider}: credentials stored (source: ${record.source}, updated ${record.updatedAt})`);
210
+ }
211
+ for (const id of ["google", "meta", "tiktok", "apple", "microsoft"]) {
212
+ const provider = rt.ctx.providers.list().find((p) => p.id === id);
213
+ if (!provider) {
214
+ io.out(`${id}: not connected (run \`adport connect ${id}\`)`);
215
+ continue;
216
+ }
217
+ try {
218
+ const accounts = await provider.listAccounts();
219
+ io.out(`${id}: OK \u2014 ${accounts.length} accessible account(s)`);
220
+ } catch (err) {
221
+ io.err(`${id}: FAILED \u2014 ${err instanceof Error ? err.message : String(err)}`);
222
+ process.exitCode = 1;
223
+ }
224
+ }
225
+ });
226
+ program.command("mcp").description("Start the adport MCP server on stdio").action(async () => {
227
+ const { runStdioServer } = await import("@adport/mcp");
228
+ await runStdioServer(await runtime());
229
+ await new Promise(() => {
230
+ });
231
+ });
232
+ return program;
233
+ }
234
+ async function main(argv) {
235
+ const program = buildProgram();
236
+ try {
237
+ await program.parseAsync(argv);
238
+ } catch (err) {
239
+ if (err instanceof AdportError) {
240
+ console.error(JSON.stringify(err.toJSON(), null, 2));
241
+ process.exitCode = 1;
242
+ return;
243
+ }
244
+ throw err;
245
+ }
246
+ }
247
+
248
+ // src/index.ts
249
+ await main(process.argv);
250
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/program.ts","../src/index.ts"],"sourcesContent":["import { Command } from 'commander';\nimport {\n AdportError,\n AuditLog,\n CredentialStore,\n type AdportRuntime,\n type AnyToolDefinition,\n} from '@adport/core';\n\nexport interface ProgramIO {\n out: (line: string) => void;\n err: (line: string) => void;\n}\n\nconst defaultIO: ProgramIO = {\n out: (line) => console.log(line),\n err: (line) => console.error(line),\n};\n\nfunction json(value: unknown): string {\n return JSON.stringify(value, null, 2);\n}\n\nfunction table(rows: Array<Record<string, unknown>>): string {\n if (rows.length === 0) return '(empty)';\n const keys = Object.keys(rows[0]!);\n const widths = keys.map((k) => Math.max(k.length, ...rows.map((r) => String(r[k] ?? '').length)));\n const line = (values: string[]) => values.map((v, i) => v.padEnd(widths[i]!)).join(' ');\n return [line(keys), line(widths.map((w) => '-'.repeat(w))), ...rows.map((r) => line(keys.map((k) => String(r[k] ?? ''))))].join('\\n');\n}\n\nasync function runtime(): Promise<AdportRuntime> {\n const { assembleRuntime } = await import('@adport/mcp');\n return assembleRuntime();\n}\n\nexport function buildProgram(io: ProgramIO = defaultIO): Command {\n const program = new Command('adport');\n program\n .description('Open-source multi-platform ads management for AI agents (MCP + CLI)')\n .version('0.1.0')\n .configureOutput({ writeOut: (s) => io.out(s.trimEnd()), writeErr: (s) => io.err(s.trimEnd()) });\n\n const tools = program.command('tools').description('Inspect and invoke the shared tool registry');\n\n tools\n .command('list')\n .description('List available tools')\n .option('--json', 'JSON output')\n .action(async (opts: { json?: boolean }) => {\n const { registry } = await runtime();\n const defs = registry.list();\n if (opts.json) {\n io.out(\n json(\n defs.map((t: AnyToolDefinition) => ({\n name: t.name,\n namespace: t.namespace,\n readOnly: t.annotations.readOnly ?? false,\n destructive: t.annotations.destructive ?? false,\n })),\n ),\n );\n return;\n }\n io.out(\n table(\n defs.map((t) => ({\n name: t.name,\n namespace: t.namespace,\n mode: t.annotations.readOnly ? 'read' : t.annotations.destructive ? 'DESTRUCTIVE' : 'write',\n })),\n ),\n );\n });\n\n tools\n .command('run <name>')\n .description('Invoke a tool with JSON input (the same tools the MCP server exposes)')\n .option('--input <json>', 'JSON object with the tool arguments', '{}')\n .action(async (name: string, opts: { input: string }) => {\n const rt = await runtime();\n const result = await rt.registry.call(name, JSON.parse(opts.input), rt.ctx);\n io.out(json(result));\n });\n\n program\n .command('accounts')\n .description('List connected ad accounts across providers')\n .option('--provider <id>', 'Limit to one provider')\n .option('--json', 'JSON output')\n .action(async (opts: { provider?: string; json?: boolean }) => {\n const rt = await runtime();\n const result = (await rt.registry.call('accounts_list', { provider: opts.provider }, rt.ctx)) as {\n accounts: Array<Record<string, unknown>>;\n };\n io.out(opts.json ? json(result.accounts) : table(result.accounts));\n });\n\n program\n .command('report')\n .description('Normalized cross-platform performance report')\n .option('--provider <id>', 'Limit to one provider')\n .option('--level <level>', 'account | campaign | ad_group | ad', 'campaign')\n .option('--metrics <list>', 'Comma-separated metrics', 'spend,impressions,clicks,conversions')\n .option('--range <range>', 'Preset (last_7_days, ...) or START..END', 'last_7_days')\n .option('--limit <n>', 'Max rows', '100')\n .option('--json', 'JSON output')\n .action(async (opts: { provider?: string; level: string; metrics: string; range: string; limit: string; json?: boolean }) => {\n const rt = await runtime();\n const dateRange = opts.range.includes('..')\n ? { start: opts.range.split('..')[0]!, end: opts.range.split('..')[1]! }\n : opts.range;\n const result = (await rt.registry.call(\n 'report',\n {\n provider: opts.provider,\n level: opts.level,\n metrics: opts.metrics.split(',').map((m) => m.trim()),\n date_range: dateRange,\n limit: Number(opts.limit),\n },\n rt.ctx,\n )) as { rows: Array<{ provider: string; accountId: string; entity: { name: string; status?: string }; metrics: Record<string, number> }>; truncated: boolean };\n if (opts.json) {\n io.out(json(result));\n return;\n }\n io.out(\n table(\n result.rows.map((r) => ({\n provider: r.provider,\n account: r.accountId,\n entity: r.entity.name,\n status: r.entity.status ?? '',\n ...r.metrics,\n })),\n ),\n );\n if (result.truncated) io.err('(truncated — raise --limit for more rows)');\n });\n\n program\n .command('policy')\n .description('Show the active write policy and where it was loaded from')\n .action(async () => {\n const rt = await runtime();\n io.out(json({ source: rt.policySource, policy: rt.ctx.engine.policy }));\n });\n\n const audit = program.command('audit').description('Inspect or extend the write-audit trail');\n\n audit\n .command('show', { isDefault: true })\n .description('Show recent write-audit entries')\n .option('--limit <n>', 'Max entries', '20')\n .action(async (opts: { limit: string }) => {\n const entries = await new AuditLog().read(Number(opts.limit));\n if (entries.length === 0) {\n io.out('No audit entries yet.');\n return;\n }\n io.out(entries.map((e) => JSON.stringify(e)).join('\\n'));\n });\n\n audit\n .command('run')\n .description('Run the audit rule packs over connected accounts and persist findings')\n .option('--provider <id>', 'Limit to one provider')\n .option('--range <range>', 'Preset or START..END', 'last_30_days')\n .action(async (opts: { provider?: string; range: string }) => {\n const rt = await runtime();\n const dateRange = opts.range.includes('..')\n ? { start: opts.range.split('..')[0]!, end: opts.range.split('..')[1]! }\n : opts.range;\n const result = (await rt.registry.call(\n 'audit_run',\n { provider: opts.provider, date_range: dateRange },\n rt.ctx,\n )) as { findings: Array<{ id: string; severity: string; title: string }>; counts: Record<string, number>; evaluatedAccounts: number };\n io.out(`Evaluated ${result.evaluatedAccounts} account(s): ${result.counts.critical} critical, ${result.counts.warn} warn, ${result.counts.info} info`);\n for (const finding of result.findings) {\n io.out(` [${finding.severity.toUpperCase()}] ${finding.id}\\n ${finding.title}`);\n }\n if (result.findings.length > 0) io.out('Next: adport recommendations · adport recommendations apply <id>');\n });\n\n audit\n .command('note <text>')\n .description('Record an external/manual change in the audit trail (e.g. a change made in the platform UI)')\n .option('--provider <id>', 'Provider the change concerns', 'external')\n .option('--account <id>', 'Account id the change concerns', '-')\n .action(async (text: string, opts: { provider: string; account: string }) => {\n await new AuditLog().append({\n event: 'note',\n provider: opts.provider,\n tool: 'audit_note',\n accountId: opts.account,\n summary: text,\n });\n io.out('Noted.');\n });\n\n program\n .command('connect <provider>')\n .description('Connect an ad account (guided wizard)')\n .option('--no-browser', 'Print auth URLs instead of opening a browser')\n .action(async (provider: string, opts: { browser: boolean }) => {\n if (provider === 'google') {\n const { connectGoogle } = await import('./connect/google.js');\n await connectGoogle({ openBrowser: opts.browser, io });\n return;\n }\n if (provider === 'meta') {\n const { connectMeta } = await import('./connect/meta.js');\n await connectMeta({ io });\n return;\n }\n if (provider === 'tiktok') {\n const { connectTikTok } = await import('./connect/tiktok.js');\n await connectTikTok({ io });\n return;\n }\n if (provider === 'apple') {\n const { connectApple } = await import('./connect/apple.js');\n await connectApple({ io });\n return;\n }\n if (provider === 'microsoft') {\n const { connectMicrosoft } = await import('./connect/microsoft.js');\n await connectMicrosoft({ openBrowser: opts.browser, io });\n return;\n }\n if (provider === 'mock') {\n io.out('The mock provider needs no credentials — it is available out of the box. Try: adport accounts');\n return;\n }\n io.err(`Provider \"${provider}\" is not supported. Available: google, meta, tiktok, apple, microsoft, mock.`);\n process.exitCode = 1;\n });\n\n const recommendations = program.command('recommendations').description('Review and act on audit findings');\n\n recommendations\n .command('list', { isDefault: true })\n .description('List findings (default: open), most severe first')\n .option('--status <status>', 'open | dismissed | applied', 'open')\n .option('--json', 'JSON output')\n .action(async (opts: { status: string; json?: boolean }) => {\n const rt = await runtime();\n const result = (await rt.registry.call('recommendations_list', { status: opts.status }, rt.ctx)) as {\n findings: Array<{ id: string; severity: string; title: string; recommendation: string; proposedAction?: unknown }>;\n };\n if (opts.json) {\n io.out(json(result.findings));\n return;\n }\n if (result.findings.length === 0) {\n io.out(`No ${opts.status} findings. Run: adport audit run`);\n return;\n }\n for (const f of result.findings) {\n io.out(`[${f.severity.toUpperCase()}] ${f.id}`);\n io.out(` ${f.title}`);\n io.out(` → ${f.recommendation}${f.proposedAction ? ' (has ready-to-apply action)' : ''}`);\n }\n });\n\n recommendations\n .command('apply <findingId>')\n .description('Run a finding\\'s proposed action through the two-step validate→apply flow')\n .option('--pending <id>', 'pending_operation_id from the validation step')\n .action(async (findingId: string, opts: { pending?: string }) => {\n const rt = await runtime();\n const result = await rt.registry.call(\n 'recommendation_apply',\n { finding_id: findingId, pending_operation_id: opts.pending },\n rt.ctx,\n );\n io.out(json(result));\n });\n\n recommendations\n .command('dismiss <findingId>')\n .description('Dismiss a finding (never reopened by future runs)')\n .action(async (findingId: string) => {\n const rt = await runtime();\n const result = await rt.registry.call('recommendation_dismiss', { finding_id: findingId }, rt.ctx);\n io.out(json(result));\n });\n\n program\n .command('doctor')\n .description('Check credential and connection health for every provider')\n .action(async () => {\n const store = new CredentialStore();\n const records = await store.list();\n const rt = await runtime();\n io.out(`policy: ${rt.policySource}`);\n io.out(`providers loaded: ${rt.ctx.providers.list().map((p) => p.id).join(', ') || '(none)'}`);\n for (const record of records) {\n io.out(`${record.provider}: credentials stored (source: ${record.source}, updated ${record.updatedAt})`);\n }\n for (const id of ['google', 'meta', 'tiktok', 'apple', 'microsoft'] as const) {\n const provider = rt.ctx.providers.list().find((p) => p.id === id);\n if (!provider) {\n io.out(`${id}: not connected (run \\`adport connect ${id}\\`)`);\n continue;\n }\n try {\n const accounts = await provider.listAccounts();\n io.out(`${id}: OK — ${accounts.length} accessible account(s)`);\n } catch (err) {\n io.err(`${id}: FAILED — ${err instanceof Error ? err.message : String(err)}`);\n process.exitCode = 1;\n }\n }\n });\n\n program\n .command('mcp')\n .description('Start the adport MCP server on stdio')\n .action(async () => {\n const { runStdioServer } = await import('@adport/mcp');\n await runStdioServer(await runtime());\n // Keep the process alive for the stdio transport.\n await new Promise(() => {});\n });\n\n return program;\n}\n\nexport async function main(argv: string[]): Promise<void> {\n const program = buildProgram();\n try {\n await program.parseAsync(argv);\n } catch (err) {\n if (err instanceof AdportError) {\n console.error(JSON.stringify(err.toJSON(), null, 2));\n process.exitCode = 1;\n return;\n }\n throw err;\n }\n}\n","#!/usr/bin/env node\nimport { main } from './program.js';\n\nawait main(process.argv);\n"],"mappings":"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","names":[]}
@@ -0,0 +1,69 @@
1
+ // src/connect/meta.ts
2
+ import readline from "readline/promises";
3
+ import { CredentialStore } from "@adport/core";
4
+ import { MetaAdsProvider, MetaGraphClient } from "@adport/provider-meta";
5
+ async function connectMeta({ io }) {
6
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
7
+ const store = new CredentialStore();
8
+ try {
9
+ io.out("");
10
+ io.out("Connecting Meta Ads. One-time setup (~20-30 min, no review for your own accounts):");
11
+ io.out(" 1. Create a Business-type app (dev mode is fine): https://developers.facebook.com/apps/");
12
+ io.out(' Add the "Marketing API" product to it.');
13
+ io.out(" 2. RECOMMENDED token \u2014 system user (never expires):");
14
+ io.out(" Business Settings \u2192 Users \u2192 System users \u2192 Add \u2192 generate token");
15
+ io.out(" with ads_read + ads_management, and assign your ad account(s) to it.");
16
+ io.out(" https://business.facebook.com/settings/system-users");
17
+ io.out(" 3. Alternative \u2014 a user token from Graph API Explorer (expires after ~60 days");
18
+ io.out(" when extended; adport will warn you, but system-user tokens avoid this).");
19
+ io.out("");
20
+ const accessToken = (await rl.question("Paste your access token: ")).trim();
21
+ if (!accessToken) {
22
+ io.err("No token provided \u2014 aborting.");
23
+ process.exitCode = 1;
24
+ return;
25
+ }
26
+ const appId = (await rl.question("App id (optional, enables token-expiry checks \u2014 Enter to skip): ")).trim();
27
+ const appSecret = appId ? (await rl.question("App secret (optional, stored locally with mode 0600): ")).trim() : "";
28
+ const credentials = {
29
+ accessToken,
30
+ appId: appId || void 0,
31
+ appSecret: appSecret || void 0
32
+ };
33
+ io.out("Verifying access\u2026");
34
+ const client = new MetaGraphClient(credentials);
35
+ const provider = new MetaAdsProvider(client);
36
+ const accounts = await provider.listAccounts();
37
+ const debug = await client.debugToken();
38
+ await store.set({
39
+ provider: "meta",
40
+ source: "byo",
41
+ data: {
42
+ access_token: accessToken,
43
+ ...appId ? { app_id: appId } : {},
44
+ ...appSecret ? { app_secret: appSecret } : {}
45
+ }
46
+ });
47
+ io.out("");
48
+ io.out(`\u2713 Connected. ${accounts.length} ad account(s):`);
49
+ for (const account of accounts) {
50
+ io.out(` ${account.id} ${account.name} ${account.currency ?? ""} ${account.status ?? ""}`);
51
+ }
52
+ if (debug?.expiresAt) {
53
+ const days = Math.round((debug.expiresAt * 1e3 - Date.now()) / 864e5);
54
+ io.out(
55
+ days > 0 ? `\u26A0 This token expires in ~${days} days. Switch to a system-user token to avoid silent breakage.` : "\u26A0 This token is expired or expiring \u2014 generate a system-user token."
56
+ );
57
+ } else if (debug) {
58
+ io.out("Token has no expiry (system-user token) \u2014 ideal.");
59
+ }
60
+ io.out("");
61
+ io.out("Try: adport accounts \xB7 adport report --provider meta \xB7 adport mcp");
62
+ } finally {
63
+ rl.close();
64
+ }
65
+ }
66
+ export {
67
+ connectMeta
68
+ };
69
+ //# sourceMappingURL=meta-IXPXEBI2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connect/meta.ts"],"sourcesContent":["import readline from 'node:readline/promises';\nimport { CredentialStore } from '@adport/core';\nimport { MetaAdsProvider, MetaGraphClient, type MetaCredentials } from '@adport/provider-meta';\nimport type { ProgramIO } from '../program.js';\n\nexport interface ConnectMetaOptions {\n io: ProgramIO;\n}\n\n/**\n * Guided Meta connection. For your own ad accounts no App Review is needed:\n * a dev-mode Business app + a system-user token (non-expiring) is the whole setup.\n */\nexport async function connectMeta({ io }: ConnectMetaOptions): Promise<void> {\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n const store = new CredentialStore();\n try {\n io.out('');\n io.out('Connecting Meta Ads. One-time setup (~20-30 min, no review for your own accounts):');\n io.out(' 1. Create a Business-type app (dev mode is fine): https://developers.facebook.com/apps/');\n io.out(' Add the \"Marketing API\" product to it.');\n io.out(' 2. RECOMMENDED token — system user (never expires):');\n io.out(' Business Settings → Users → System users → Add → generate token');\n io.out(' with ads_read + ads_management, and assign your ad account(s) to it.');\n io.out(' https://business.facebook.com/settings/system-users');\n io.out(' 3. Alternative — a user token from Graph API Explorer (expires after ~60 days');\n io.out(' when extended; adport will warn you, but system-user tokens avoid this).');\n io.out('');\n\n const accessToken = (await rl.question('Paste your access token: ')).trim();\n if (!accessToken) {\n io.err('No token provided — aborting.');\n process.exitCode = 1;\n return;\n }\n const appId = (await rl.question('App id (optional, enables token-expiry checks — Enter to skip): ')).trim();\n const appSecret = appId\n ? (await rl.question('App secret (optional, stored locally with mode 0600): ')).trim()\n : '';\n\n const credentials: MetaCredentials = {\n accessToken,\n appId: appId || undefined,\n appSecret: appSecret || undefined,\n };\n\n io.out('Verifying access…');\n const client = new MetaGraphClient(credentials);\n const provider = new MetaAdsProvider(client);\n const accounts = await provider.listAccounts();\n\n const debug = await client.debugToken();\n await store.set({\n provider: 'meta',\n source: 'byo',\n data: {\n access_token: accessToken,\n ...(appId ? { app_id: appId } : {}),\n ...(appSecret ? { app_secret: appSecret } : {}),\n },\n });\n\n io.out('');\n io.out(`✓ Connected. ${accounts.length} ad account(s):`);\n for (const account of accounts) {\n io.out(` ${account.id} ${account.name} ${account.currency ?? ''} ${account.status ?? ''}`);\n }\n if (debug?.expiresAt) {\n const days = Math.round((debug.expiresAt * 1000 - Date.now()) / 86_400_000);\n io.out(\n days > 0\n ? `⚠ This token expires in ~${days} days. Switch to a system-user token to avoid silent breakage.`\n : '⚠ This token is expired or expiring — generate a system-user token.',\n );\n } else if (debug) {\n io.out('Token has no expiry (system-user token) — ideal.');\n }\n io.out('');\n io.out('Try: adport accounts · adport report --provider meta · adport mcp');\n } finally {\n rl.close();\n }\n}\n"],"mappings":";AAAA,OAAO,cAAc;AACrB,SAAS,uBAAuB;AAChC,SAAS,iBAAiB,uBAA6C;AAWvE,eAAsB,YAAY,EAAE,GAAG,GAAsC;AAC3E,QAAM,KAAK,SAAS,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AACpF,QAAM,QAAQ,IAAI,gBAAgB;AAClC,MAAI;AACF,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,oFAAoF;AAC3F,OAAG,IAAI,2FAA2F;AAClG,OAAG,IAAI,6CAA6C;AACpD,OAAG,IAAI,4DAAuD;AAC9D,OAAG,IAAI,0FAAsE;AAC7E,OAAG,IAAI,2EAA2E;AAClF,OAAG,IAAI,0DAA0D;AACjE,OAAG,IAAI,sFAAiF;AACxF,OAAG,IAAI,+EAA+E;AACtF,OAAG,IAAI,EAAE;AAET,UAAM,eAAe,MAAM,GAAG,SAAS,2BAA2B,GAAG,KAAK;AAC1E,QAAI,CAAC,aAAa;AAChB,SAAG,IAAI,oCAA+B;AACtC,cAAQ,WAAW;AACnB;AAAA,IACF;AACA,UAAM,SAAS,MAAM,GAAG,SAAS,uEAAkE,GAAG,KAAK;AAC3G,UAAM,YAAY,SACb,MAAM,GAAG,SAAS,wDAAwD,GAAG,KAAK,IACnF;AAEJ,UAAM,cAA+B;AAAA,MACnC;AAAA,MACA,OAAO,SAAS;AAAA,MAChB,WAAW,aAAa;AAAA,IAC1B;AAEA,OAAG,IAAI,wBAAmB;AAC1B,UAAM,SAAS,IAAI,gBAAgB,WAAW;AAC9C,UAAM,WAAW,IAAI,gBAAgB,MAAM;AAC3C,UAAM,WAAW,MAAM,SAAS,aAAa;AAE7C,UAAM,QAAQ,MAAM,OAAO,WAAW;AACtC,UAAM,MAAM,IAAI;AAAA,MACd,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,cAAc;AAAA,QACd,GAAI,QAAQ,EAAE,QAAQ,MAAM,IAAI,CAAC;AAAA,QACjC,GAAI,YAAY,EAAE,YAAY,UAAU,IAAI,CAAC;AAAA,MAC/C;AAAA,IACF,CAAC;AAED,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,qBAAgB,SAAS,MAAM,iBAAiB;AACvD,eAAW,WAAW,UAAU;AAC9B,SAAG,IAAI,KAAK,QAAQ,EAAE,KAAK,QAAQ,IAAI,KAAK,QAAQ,YAAY,EAAE,KAAK,QAAQ,UAAU,EAAE,EAAE;AAAA,IAC/F;AACA,QAAI,OAAO,WAAW;AACpB,YAAM,OAAO,KAAK,OAAO,MAAM,YAAY,MAAO,KAAK,IAAI,KAAK,KAAU;AAC1E,SAAG;AAAA,QACD,OAAO,IACH,iCAA4B,IAAI,mEAChC;AAAA,MACN;AAAA,IACF,WAAW,OAAO;AAChB,SAAG,IAAI,uDAAkD;AAAA,IAC3D;AACA,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,kFAA4E;AAAA,EACrF,UAAE;AACA,OAAG,MAAM;AAAA,EACX;AACF;","names":[]}
@@ -0,0 +1,90 @@
1
+ import {
2
+ buildMicrosoftAuthUrl,
3
+ exchangeMicrosoftCode,
4
+ generatePkce,
5
+ openInBrowser,
6
+ startLoopbackServer
7
+ } from "./chunk-JPHFJ6HN.js";
8
+
9
+ // src/connect/microsoft.ts
10
+ import readline from "readline/promises";
11
+ import { CredentialStore } from "@adport/core";
12
+ import { MicrosoftAdsClient, MicrosoftAdsProvider, SANDBOX_DEVELOPER_TOKEN } from "@adport/provider-microsoft";
13
+ async function connectMicrosoft({ openBrowser, io }) {
14
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
15
+ const store = new CredentialStore();
16
+ try {
17
+ io.out("");
18
+ io.out("Connecting Microsoft Advertising. Setup:");
19
+ io.out(' 1. Azure Portal \u2192 App registrations \u2192 New: "Mobile and desktop applications"');
20
+ io.out(" platform with redirect URI http://localhost (public client, no secret).");
21
+ io.out(" 2. Developer token (self-serve, no review): sign in as Super Admin at");
22
+ io.out(" https://ads.microsoft.com/cc/Settings/DevSettings \u2192 Request Token.");
23
+ io.out(` 3. Sandbox alternative: universal token ${SANDBOX_DEVELOPER_TOKEN} works for everyone.`);
24
+ io.out("");
25
+ const sandboxAnswer = (await rl.question("Use the SANDBOX environment? [y/N] ")).trim().toLowerCase();
26
+ const sandbox = sandboxAnswer === "y" || sandboxAnswer === "yes";
27
+ const clientId = (await rl.question("Azure application (client) id: ")).trim();
28
+ const developerToken = sandbox ? SANDBOX_DEVELOPER_TOKEN : (await rl.question("Developer token: ")).trim();
29
+ if (!clientId || !developerToken) {
30
+ io.err("Missing values \u2014 aborting.");
31
+ process.exitCode = 1;
32
+ return;
33
+ }
34
+ io.out("");
35
+ io.out("Starting the Microsoft sign-in (PKCE, no client secret)\u2026");
36
+ const pkce = generatePkce();
37
+ const loopback = await startLoopbackServer();
38
+ const authUrl = buildMicrosoftAuthUrl(clientId, loopback.redirectUri, pkce.challenge);
39
+ if (openBrowser) {
40
+ openInBrowser(authUrl);
41
+ io.out(`If the browser did not open, visit:
42
+ ${authUrl}`);
43
+ } else {
44
+ io.out(`Open this URL in a browser on this machine:
45
+ ${authUrl}`);
46
+ }
47
+ let refreshToken;
48
+ try {
49
+ const code = await loopback.waitForCode;
50
+ ({ refreshToken } = await exchangeMicrosoftCode({
51
+ clientId,
52
+ code,
53
+ redirectUri: loopback.redirectUri,
54
+ codeVerifier: pkce.verifier
55
+ }));
56
+ } finally {
57
+ loopback.close();
58
+ }
59
+ io.out("Verifying access\u2026");
60
+ const provider = new MicrosoftAdsProvider(
61
+ new MicrosoftAdsClient({ developerToken, clientId, refreshToken, sandbox })
62
+ );
63
+ const accounts = await provider.listAccounts();
64
+ await store.set({
65
+ provider: "microsoft",
66
+ source: "byo",
67
+ data: {
68
+ developer_token: developerToken,
69
+ client_id: clientId,
70
+ refresh_token: refreshToken,
71
+ ...sandbox ? { sandbox: "true" } : {}
72
+ }
73
+ });
74
+ io.out("");
75
+ io.out(`\u2713 Connected${sandbox ? " (sandbox)" : ""}. ${accounts.length} ad account(s):`);
76
+ for (const account of accounts) {
77
+ io.out(` ${account.id} ${account.name} ${account.currency ?? ""} ${account.status ?? ""}`);
78
+ }
79
+ io.out("Note: public-client refresh tokens expire after ~90 days of disuse; adport");
80
+ io.out("persists rotated tokens automatically on every use.");
81
+ io.out("");
82
+ io.out("Try: adport accounts \xB7 adport report --provider microsoft \xB7 adport mcp");
83
+ } finally {
84
+ rl.close();
85
+ }
86
+ }
87
+ export {
88
+ connectMicrosoft
89
+ };
90
+ //# sourceMappingURL=microsoft-QXMPCWJ2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connect/microsoft.ts"],"sourcesContent":["import readline from 'node:readline/promises';\nimport { CredentialStore } from '@adport/core';\nimport { MicrosoftAdsClient, MicrosoftAdsProvider, SANDBOX_DEVELOPER_TOKEN } from '@adport/provider-microsoft';\nimport type { ProgramIO } from '../program.js';\nimport {\n buildMicrosoftAuthUrl,\n exchangeMicrosoftCode,\n generatePkce,\n openInBrowser,\n startLoopbackServer,\n} from './oauth.js';\n\n/**\n * Microsoft Advertising is the friendliest setup of all providers: the dev\n * token is self-serve (no review), and the sandbox has a public universal token.\n */\nexport async function connectMicrosoft({ openBrowser, io }: { openBrowser: boolean; io: ProgramIO }): Promise<void> {\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n const store = new CredentialStore();\n try {\n io.out('');\n io.out('Connecting Microsoft Advertising. Setup:');\n io.out(' 1. Azure Portal → App registrations → New: \"Mobile and desktop applications\"');\n io.out(' platform with redirect URI http://localhost (public client, no secret).');\n io.out(' 2. Developer token (self-serve, no review): sign in as Super Admin at');\n io.out(' https://ads.microsoft.com/cc/Settings/DevSettings → Request Token.');\n io.out(` 3. Sandbox alternative: universal token ${SANDBOX_DEVELOPER_TOKEN} works for everyone.`);\n io.out('');\n\n const sandboxAnswer = (await rl.question('Use the SANDBOX environment? [y/N] ')).trim().toLowerCase();\n const sandbox = sandboxAnswer === 'y' || sandboxAnswer === 'yes';\n const clientId = (await rl.question('Azure application (client) id: ')).trim();\n const developerToken = sandbox\n ? SANDBOX_DEVELOPER_TOKEN\n : (await rl.question('Developer token: ')).trim();\n if (!clientId || !developerToken) {\n io.err('Missing values — aborting.');\n process.exitCode = 1;\n return;\n }\n\n io.out('');\n io.out('Starting the Microsoft sign-in (PKCE, no client secret)…');\n const pkce = generatePkce();\n const loopback = await startLoopbackServer();\n const authUrl = buildMicrosoftAuthUrl(clientId, loopback.redirectUri, pkce.challenge);\n if (openBrowser) {\n openInBrowser(authUrl);\n io.out(`If the browser did not open, visit:\\n ${authUrl}`);\n } else {\n io.out(`Open this URL in a browser on this machine:\\n ${authUrl}`);\n }\n let refreshToken: string;\n try {\n const code = await loopback.waitForCode;\n ({ refreshToken } = await exchangeMicrosoftCode({\n clientId,\n code,\n redirectUri: loopback.redirectUri,\n codeVerifier: pkce.verifier,\n }));\n } finally {\n loopback.close();\n }\n\n io.out('Verifying access…');\n const provider = new MicrosoftAdsProvider(\n new MicrosoftAdsClient({ developerToken, clientId, refreshToken, sandbox }),\n );\n const accounts = await provider.listAccounts();\n await store.set({\n provider: 'microsoft',\n source: 'byo',\n data: {\n developer_token: developerToken,\n client_id: clientId,\n refresh_token: refreshToken,\n ...(sandbox ? { sandbox: 'true' } : {}),\n },\n });\n io.out('');\n io.out(`✓ Connected${sandbox ? ' (sandbox)' : ''}. ${accounts.length} ad account(s):`);\n for (const account of accounts) {\n io.out(` ${account.id} ${account.name} ${account.currency ?? ''} ${account.status ?? ''}`);\n }\n io.out('Note: public-client refresh tokens expire after ~90 days of disuse; adport');\n io.out('persists rotated tokens automatically on every use.');\n io.out('');\n io.out('Try: adport accounts · adport report --provider microsoft · adport mcp');\n } finally {\n rl.close();\n }\n}\n"],"mappings":";;;;;;;;;AAAA,OAAO,cAAc;AACrB,SAAS,uBAAuB;AAChC,SAAS,oBAAoB,sBAAsB,+BAA+B;AAclF,eAAsB,iBAAiB,EAAE,aAAa,GAAG,GAA2D;AAClH,QAAM,KAAK,SAAS,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AACpF,QAAM,QAAQ,IAAI,gBAAgB;AAClC,MAAI;AACF,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,0CAA0C;AACjD,OAAG,IAAI,0FAAgF;AACvF,OAAG,IAAI,8EAA8E;AACrF,OAAG,IAAI,yEAAyE;AAChF,OAAG,IAAI,8EAAyE;AAChF,OAAG,IAAI,6CAA6C,uBAAuB,sBAAsB;AACjG,OAAG,IAAI,EAAE;AAET,UAAM,iBAAiB,MAAM,GAAG,SAAS,qCAAqC,GAAG,KAAK,EAAE,YAAY;AACpG,UAAM,UAAU,kBAAkB,OAAO,kBAAkB;AAC3D,UAAM,YAAY,MAAM,GAAG,SAAS,iCAAiC,GAAG,KAAK;AAC7E,UAAM,iBAAiB,UACnB,2BACC,MAAM,GAAG,SAAS,mBAAmB,GAAG,KAAK;AAClD,QAAI,CAAC,YAAY,CAAC,gBAAgB;AAChC,SAAG,IAAI,iCAA4B;AACnC,cAAQ,WAAW;AACnB;AAAA,IACF;AAEA,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,+DAA0D;AACjE,UAAM,OAAO,aAAa;AAC1B,UAAM,WAAW,MAAM,oBAAoB;AAC3C,UAAM,UAAU,sBAAsB,UAAU,SAAS,aAAa,KAAK,SAAS;AACpF,QAAI,aAAa;AACf,oBAAc,OAAO;AACrB,SAAG,IAAI;AAAA,IAA0C,OAAO,EAAE;AAAA,IAC5D,OAAO;AACL,SAAG,IAAI;AAAA,IAAkD,OAAO,EAAE;AAAA,IACpE;AACA,QAAI;AACJ,QAAI;AACF,YAAM,OAAO,MAAM,SAAS;AAC5B,OAAC,EAAE,aAAa,IAAI,MAAM,sBAAsB;AAAA,QAC9C;AAAA,QACA;AAAA,QACA,aAAa,SAAS;AAAA,QACtB,cAAc,KAAK;AAAA,MACrB,CAAC;AAAA,IACH,UAAE;AACA,eAAS,MAAM;AAAA,IACjB;AAEA,OAAG,IAAI,wBAAmB;AAC1B,UAAM,WAAW,IAAI;AAAA,MACnB,IAAI,mBAAmB,EAAE,gBAAgB,UAAU,cAAc,QAAQ,CAAC;AAAA,IAC5E;AACA,UAAM,WAAW,MAAM,SAAS,aAAa;AAC7C,UAAM,MAAM,IAAI;AAAA,MACd,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,iBAAiB;AAAA,QACjB,WAAW;AAAA,QACX,eAAe;AAAA,QACf,GAAI,UAAU,EAAE,SAAS,OAAO,IAAI,CAAC;AAAA,MACvC;AAAA,IACF,CAAC;AACD,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,mBAAc,UAAU,eAAe,EAAE,KAAK,SAAS,MAAM,iBAAiB;AACrF,eAAW,WAAW,UAAU;AAC9B,SAAG,IAAI,KAAK,QAAQ,EAAE,KAAK,QAAQ,IAAI,KAAK,QAAQ,YAAY,EAAE,KAAK,QAAQ,UAAU,EAAE,EAAE;AAAA,IAC/F;AACA,OAAG,IAAI,4EAA4E;AACnF,OAAG,IAAI,qDAAqD;AAC5D,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,uFAAiF;AAAA,EAC1F,UAAE;AACA,OAAG,MAAM;AAAA,EACX;AACF;","names":[]}
@@ -0,0 +1,52 @@
1
+ // src/connect/tiktok.ts
2
+ import readline from "readline/promises";
3
+ import { CredentialStore } from "@adport/core";
4
+ import { TikTokAdsProvider, TikTokClient } from "@adport/provider-tiktok";
5
+ async function connectTikTok({ io }) {
6
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
7
+ const store = new CredentialStore();
8
+ try {
9
+ io.out("");
10
+ io.out("Connecting TikTok Ads (Marketing API). Setup:");
11
+ io.out(" 1. Register at https://business-api.tiktok.com/portal and create an app.");
12
+ io.out(" Production needs human review (2 days\u20132 weeks). SANDBOX works today:");
13
+ io.out(' My Apps \u2192 your app \u2192 "Create a Sandbox Ad Account" \u2192 generate token there.');
14
+ io.out(" 2. For production: authorize your advertiser account via the app's auth URL,");
15
+ io.out(" then exchange the auth_code (the portal shows the flow). Tokens never expire.");
16
+ io.out("");
17
+ const sandboxAnswer = (await rl.question("Use the SANDBOX environment? [y/N] ")).trim().toLowerCase();
18
+ const sandbox = sandboxAnswer === "y" || sandboxAnswer === "yes";
19
+ const appId = (await rl.question("App id: ")).trim();
20
+ const secret = (await rl.question("App secret: ")).trim();
21
+ const accessToken = (await rl.question("Access token: ")).trim();
22
+ if (!appId || !secret || !accessToken) {
23
+ io.err("Missing values \u2014 aborting.");
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+ io.out("Verifying access\u2026");
28
+ const provider = new TikTokAdsProvider(new TikTokClient({ accessToken, appId, secret, sandbox }), {
29
+ appId,
30
+ secret
31
+ });
32
+ const accounts = await provider.listAccounts();
33
+ await store.set({
34
+ provider: "tiktok",
35
+ source: "byo",
36
+ data: { access_token: accessToken, app_id: appId, secret, ...sandbox ? { sandbox: "true" } : {} }
37
+ });
38
+ io.out("");
39
+ io.out(`\u2713 Connected${sandbox ? " (sandbox)" : ""}. ${accounts.length} advertiser account(s):`);
40
+ for (const account of accounts) {
41
+ io.out(` ${account.id} ${account.name} ${account.currency ?? ""} ${account.status ?? ""}`);
42
+ }
43
+ io.out("");
44
+ io.out("Try: adport accounts \xB7 adport report --provider tiktok \xB7 adport mcp");
45
+ } finally {
46
+ rl.close();
47
+ }
48
+ }
49
+ export {
50
+ connectTikTok
51
+ };
52
+ //# sourceMappingURL=tiktok-TL7Z64OV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connect/tiktok.ts"],"sourcesContent":["import readline from 'node:readline/promises';\nimport { CredentialStore } from '@adport/core';\nimport { TikTokAdsProvider, TikTokClient } from '@adport/provider-tiktok';\nimport type { ProgramIO } from '../program.js';\n\n/**\n * TikTok requires app review before production access (2 days–2 weeks) — but\n * the sandbox works the same day. The wizard supports both.\n */\nexport async function connectTikTok({ io }: { io: ProgramIO }): Promise<void> {\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n const store = new CredentialStore();\n try {\n io.out('');\n io.out('Connecting TikTok Ads (Marketing API). Setup:');\n io.out(' 1. Register at https://business-api.tiktok.com/portal and create an app.');\n io.out(' Production needs human review (2 days–2 weeks). SANDBOX works today:');\n io.out(' My Apps → your app → \"Create a Sandbox Ad Account\" → generate token there.');\n io.out(' 2. For production: authorize your advertiser account via the app\\'s auth URL,');\n io.out(' then exchange the auth_code (the portal shows the flow). Tokens never expire.');\n io.out('');\n\n const sandboxAnswer = (await rl.question('Use the SANDBOX environment? [y/N] ')).trim().toLowerCase();\n const sandbox = sandboxAnswer === 'y' || sandboxAnswer === 'yes';\n const appId = (await rl.question('App id: ')).trim();\n const secret = (await rl.question('App secret: ')).trim();\n const accessToken = (await rl.question('Access token: ')).trim();\n if (!appId || !secret || !accessToken) {\n io.err('Missing values — aborting.');\n process.exitCode = 1;\n return;\n }\n\n io.out('Verifying access…');\n const provider = new TikTokAdsProvider(new TikTokClient({ accessToken, appId, secret, sandbox }), {\n appId,\n secret,\n });\n const accounts = await provider.listAccounts();\n await store.set({\n provider: 'tiktok',\n source: 'byo',\n data: { access_token: accessToken, app_id: appId, secret, ...(sandbox ? { sandbox: 'true' } : {}) },\n });\n io.out('');\n io.out(`✓ Connected${sandbox ? ' (sandbox)' : ''}. ${accounts.length} advertiser account(s):`);\n for (const account of accounts) {\n io.out(` ${account.id} ${account.name} ${account.currency ?? ''} ${account.status ?? ''}`);\n }\n io.out('');\n io.out('Try: adport accounts · adport report --provider tiktok · adport mcp');\n } finally {\n rl.close();\n }\n}\n"],"mappings":";AAAA,OAAO,cAAc;AACrB,SAAS,uBAAuB;AAChC,SAAS,mBAAmB,oBAAoB;AAOhD,eAAsB,cAAc,EAAE,GAAG,GAAqC;AAC5E,QAAM,KAAK,SAAS,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AACpF,QAAM,QAAQ,IAAI,gBAAgB;AAClC,MAAI;AACF,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,+CAA+C;AACtD,OAAG,IAAI,4EAA4E;AACnF,OAAG,IAAI,gFAA2E;AAClF,OAAG,IAAI,gGAAiF;AACxF,OAAG,IAAI,gFAAiF;AACxF,OAAG,IAAI,oFAAoF;AAC3F,OAAG,IAAI,EAAE;AAET,UAAM,iBAAiB,MAAM,GAAG,SAAS,qCAAqC,GAAG,KAAK,EAAE,YAAY;AACpG,UAAM,UAAU,kBAAkB,OAAO,kBAAkB;AAC3D,UAAM,SAAS,MAAM,GAAG,SAAS,UAAU,GAAG,KAAK;AACnD,UAAM,UAAU,MAAM,GAAG,SAAS,cAAc,GAAG,KAAK;AACxD,UAAM,eAAe,MAAM,GAAG,SAAS,gBAAgB,GAAG,KAAK;AAC/D,QAAI,CAAC,SAAS,CAAC,UAAU,CAAC,aAAa;AACrC,SAAG,IAAI,iCAA4B;AACnC,cAAQ,WAAW;AACnB;AAAA,IACF;AAEA,OAAG,IAAI,wBAAmB;AAC1B,UAAM,WAAW,IAAI,kBAAkB,IAAI,aAAa,EAAE,aAAa,OAAO,QAAQ,QAAQ,CAAC,GAAG;AAAA,MAChG;AAAA,MACA;AAAA,IACF,CAAC;AACD,UAAM,WAAW,MAAM,SAAS,aAAa;AAC7C,UAAM,MAAM,IAAI;AAAA,MACd,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,MAAM,EAAE,cAAc,aAAa,QAAQ,OAAO,QAAQ,GAAI,UAAU,EAAE,SAAS,OAAO,IAAI,CAAC,EAAG;AAAA,IACpG,CAAC;AACD,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,mBAAc,UAAU,eAAe,EAAE,KAAK,SAAS,MAAM,yBAAyB;AAC7F,eAAW,WAAW,UAAU;AAC9B,SAAG,IAAI,KAAK,QAAQ,EAAE,KAAK,QAAQ,IAAI,KAAK,QAAQ,YAAY,EAAE,KAAK,QAAQ,UAAU,EAAE,EAAE;AAAA,IAC/F;AACA,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,oFAA8E;AAAA,EACvF,UAAE;AACA,OAAG,MAAM;AAAA,EACX;AACF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "adport",
3
+ "version": "0.1.0",
4
+ "description": "adport CLI — manage your ad accounts from the terminal or any AI agent",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "bin": {
8
+ "adport": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "dependencies": {
14
+ "commander": "^13.0.0",
15
+ "yaml": "^2.6.0",
16
+ "zod": "^3.25.0",
17
+ "@adport/core": "0.1.0",
18
+ "@adport/mcp": "0.1.0",
19
+ "@adport/provider-apple": "0.1.0",
20
+ "@adport/provider-google": "0.1.0",
21
+ "@adport/provider-meta": "0.1.0",
22
+ "@adport/provider-microsoft": "0.1.0",
23
+ "@adport/provider-tiktok": "0.1.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^22.10.0",
27
+ "tsup": "^8.3.0",
28
+ "typescript": "^5.8.0",
29
+ "vitest": "^3.0.0"
30
+ },
31
+ "scripts": {
32
+ "build": "tsup src/index.ts --format esm --sourcemap --clean",
33
+ "test": "vitest run",
34
+ "typecheck": "tsc --noEmit",
35
+ "clean": "rm -rf dist"
36
+ }
37
+ }