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 +201 -0
- package/dist/apple-RHO3KWJ4.js +60 -0
- package/dist/apple-RHO3KWJ4.js.map +1 -0
- package/dist/chunk-JPHFJ6HN.js +135 -0
- package/dist/chunk-JPHFJ6HN.js.map +1 -0
- package/dist/google-ZQQS6JUM.js +166 -0
- package/dist/google-ZQQS6JUM.js.map +1 -0
- package/dist/index.js +250 -0
- package/dist/index.js.map +1 -0
- package/dist/meta-IXPXEBI2.js +69 -0
- package/dist/meta-IXPXEBI2.js.map +1 -0
- package/dist/microsoft-QXMPCWJ2.js +90 -0
- package/dist/microsoft-QXMPCWJ2.js.map +1 -0
- package/dist/tiktok-TL7Z64OV.js +52 -0
- package/dist/tiktok-TL7Z64OV.js.map +1 -0
- package/package.json +37 -0
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":"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","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":";;;;;;;;;AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,cAAc;AACrB,OAAO,UAAU;AACjB,SAAS,uBAAuB;AAChC,SAAS,2BAAmD;AAoB5D,eAAsB,cAAc,EAAE,aAAa,GAAG,GAAwC;AAC5F,QAAM,KAAK,SAAS,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AACpF,QAAM,QAAQ,IAAI,gBAAgB;AAClC,MAAI;AAGF,UAAM,WAAW,MAAM,MAAM,IAAI,QAAQ;AACzC,QAAI,UAAU,KAAK,mBAAmB,SAAS,KAAK,aAAa,SAAS,KAAK,eAAe;AAC5F,YAAM,UACJ,MAAM,GAAG;AAAA,QACP;AAAA,MACF,GAEC,KAAK,EACL,YAAY;AACf,UAAI,WAAW,OAAO,WAAW,MAAM;AACrC,cAAM,eAAe,MAAM;AAAA,UACzB,SAAS,KAAK;AAAA,UACd,SAAS,KAAK;AAAA,UACd;AAAA,UACA;AAAA,QACF;AACA,cAAM;AAAA,UACJ;AAAA,YACE,gBAAgB,SAAS,KAAK;AAAA,YAC9B,UAAU,SAAS,KAAK;AAAA,YACxB,cAAc,SAAS,KAAK;AAAA,YAC5B;AAAA,YACA,iBAAiB,SAAS,KAAK,qBAAqB;AAAA,UACtD;AAAA,UACA;AAAA,UACA;AAAA,QACF;AACA;AAAA,MACF;AAAA,IACF;AAEA,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,gEAAgE;AACvE,OAAG,IAAI,yGAAoG;AAC3G,OAAG,IAAI,oHAAoH;AAC3H,OAAG,IAAI,oGAAoG;AAC3G,OAAG,IAAI,yGAAyG;AAChH,OAAG,IAAI,EAAE;AAGT,UAAM,WAAW,MAAM,uBAAuB,IAAI,EAAE;AACpD,QAAI;AACJ,QAAI,UAAU;AACZ,cAAQ;AAAA,IACV,OAAO;AACL,YAAM,kBAAkB,MAAM,GAAG,SAAS,6CAA6C,GAAG,KAAK;AAE/F,UAAI,WAAW;AACf,UAAI,eAAe;AACnB,YAAM,cACJ,MAAM,GAAG,SAAS,uFAAuF,GACzG,KAAK;AACP,UAAI,YAAY;AACd,cAAM,SAAS,sBAAsB,MAAM,GAAG,SAAS,WAAW,UAAU,GAAG,MAAM,CAAC;AACtF,mBAAW,OAAO;AAClB,uBAAe,OAAO;AAAA,MACxB,OAAO;AACL,oBAAY,MAAM,GAAG,SAAS,mBAAmB,GAAG,KAAK;AACzD,wBAAgB,MAAM,GAAG,SAAS,uBAAuB,GAAG,KAAK;AAAA,MACnE;AAEA,YAAM,mBACH,MAAM,GAAG,SAAS,mEAAmE,GAAG,KAAK,KAAK;AAErG,YAAM,eAAe,MAAM,aAAa,UAAU,cAAc,aAAa,EAAE;AAC/E,cAAQ,EAAE,gBAAgB,UAAU,cAAc,cAAc,gBAAgB;AAAA,IAClF;AAEA,UAAM,cAAc,OAAO,OAAO,EAAE;AAAA,EACtC,UAAE;AACA,OAAG,MAAM;AAAA,EACX;AACF;AAEA,eAAe,aACb,UACA,cACA,aACA,IACiB;AACjB,KAAG,IAAI,EAAE;AACT,KAAG,IAAI,4EAA4E;AACnF,KAAG,IAAI,mEAAmE;AAC1E,QAAM,WAAW,MAAM,oBAAoB;AAC3C,QAAM,UAAU,mBAAmB,UAAU,SAAS,WAAW;AACjE,MAAI,aAAa;AACf,kBAAc,OAAO;AACrB,OAAG,IAAI;AAAA,IAA0C,OAAO,EAAE;AAAA,EAC5D,OAAO;AACL,OAAG,IAAI;AAAA,IAAuF,OAAO,EAAE;AAAA,EACzG;AACA,MAAI;AACF,UAAM,OAAO,MAAM,SAAS;AAC5B,UAAM,SAAS,MAAM,sBAAsB,EAAE,UAAU,cAAc,MAAM,aAAa,SAAS,YAAY,CAAC;AAC9G,WAAO,OAAO;AAAA,EAChB,UAAE;AACA,aAAS,MAAM;AAAA,EACjB;AACF;AAEA,eAAe,cAAc,OAA0B,OAAwB,IAA8B;AAC3G,KAAG,IAAI,wBAAmB;AAC1B,QAAM,SAAS,IAAI,oBAAoB,KAAK;AAC5C,QAAM,YAAY,MAAM,OAAO,wBAAwB;AACvD,QAAM,MAAM,IAAI;AAAA,IACd,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,MAAM;AAAA,MACJ,iBAAiB,MAAM;AAAA,MACvB,WAAW,MAAM;AAAA,MACjB,eAAe,MAAM;AAAA,MACrB,eAAe,MAAM;AAAA,MACrB,GAAI,MAAM,kBAAkB,EAAE,mBAAmB,MAAM,gBAAgB,IAAI,CAAC;AAAA,IAC9E;AAAA,EACF,CAAC;AACD,KAAG,IAAI,EAAE;AACT,KAAG,IAAI,qBAAgB,UAAU,MAAM,+BAA+B,UAAU,KAAK,IAAI,CAAC,EAAE;AAC5F,KAAG,IAAI,EAAE;AACT,KAAG,IAAI,mFAAmF;AAC1F,KAAG,IAAI,8FAAyF;AAChG,KAAG,IAAI,sFAAsF;AAC7F,KAAG,IAAI,EAAE;AACT,KAAG,IAAI,oFAA8E;AACvF;AAEA,eAAe,uBACb,IACA,IACwC;AACxC,QAAM,aAAa;AAAA,IACjB,QAAQ,IAAI;AAAA,IACZ,KAAK,KAAK,GAAG,QAAQ,GAAG,iBAAiB;AAAA,EAC3C,EAAE,OAAO,CAAC,MAAmB,QAAQ,CAAC,CAAC;AACvC,aAAW,aAAa,YAAY;AAClC,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,GAAG,SAAS,WAAW,MAAM;AAAA,IAC3C,QAAQ;AACN;AAAA,IACF;AACA,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,UAAM,iBAAiB,IAAI,QAAQ,eAAe;AAClD,UAAM,WAAW,IAAI,QAAQ,SAAS;AACtC,UAAM,eAAe,IAAI,QAAQ,aAAa;AAC9C,UAAM,eAAe,IAAI,QAAQ,aAAa;AAC9C,QAAI,CAAC,kBAAkB,CAAC,YAAY,CAAC,gBAAgB,CAAC,aAAc;AACpE,UAAM,UAAU,MAAM,GAAG,SAAS,SAAS,SAAS,2BAAsB,GAAG,KAAK,EAAE,YAAY;AAChG,QAAI,WAAW,OAAO,WAAW,KAAM;AACvC,OAAG,IAAI,8BAA8B,SAAS,GAAG;AACjD,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,iBAAiB,IAAI,QAAQ,iBAAiB,KAAK;AAAA,IACrD;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,IAAI,OAAwB;AACnC,SAAO,UAAU,UAAa,UAAU,OAAO,KAAK,OAAO,KAAK;AAClE;AAEA,SAAS,WAAW,GAAmB;AACrC,SAAO,EAAE,WAAW,IAAI,IAAI,KAAK,KAAK,GAAG,QAAQ,GAAG,EAAE,MAAM,CAAC,CAAC,IAAI;AACpE;","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
|
+
}
|