adport 0.2.0 → 0.3.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.
@@ -1,3 +1,8 @@
1
+ import {
2
+ printLocalConnectionIntro,
3
+ printLocalConnectionSaved
4
+ } from "./chunk-OPRADP2V.js";
5
+
1
6
  // src/connect/apple.ts
2
7
  import { promises as fs } from "fs";
3
8
  import os from "os";
@@ -9,7 +14,7 @@ async function connectApple({ io }) {
9
14
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
10
15
  const store = new CredentialStore();
11
16
  try {
12
- io.out("");
17
+ printLocalConnectionIntro(io, "Apple Ads");
13
18
  io.out("Connecting Apple Ads (Apple Search Ads). One-time setup (~10 min, no approval):");
14
19
  io.out(" 1. In ads.apple.com: Account Settings \u2192 API \u2192 create an API user (API role).");
15
20
  io.out(" 2. Generate an EC key pair locally:");
@@ -18,6 +23,7 @@ async function connectApple({ io }) {
18
23
  io.out(" 3. Upload public-key.pem in Account Settings \u2192 API; Apple shows your");
19
24
  io.out(" clientId, teamId, and keyId (all needed below).");
20
25
  io.out(" Note: access tokens last 1h and are re-minted automatically; no rotation chores.");
26
+ io.out(" Your private key stays on this machine; upload only the public key to Apple.");
21
27
  io.out("");
22
28
  const clientId = (await rl.question("clientId (SEARCHADS.xxxx): ")).trim();
23
29
  const teamId = (await rl.question(`teamId [${clientId}]: `)).trim() || clientId;
@@ -42,6 +48,7 @@ async function connectApple({ io }) {
42
48
  for (const account of accounts) {
43
49
  io.out(` ${account.id} ${account.name} ${account.currency ?? ""} ${account.status ?? ""}`);
44
50
  }
51
+ printLocalConnectionSaved(io);
45
52
  io.out("");
46
53
  io.out("Heads-up: the Campaign Management API v5 sunsets 2027-01-26; adport will migrate");
47
54
  io.out("to the new Ads Platform API when its docs go live \u2014 your credentials carry over.");
@@ -57,4 +64,4 @@ function expandHome(p) {
57
64
  export {
58
65
  connectApple
59
66
  };
60
- //# sourceMappingURL=apple-RHO3KWJ4.js.map
67
+ //# sourceMappingURL=apple-ZLY4ZTQ3.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';\nimport { printLocalConnectionIntro, printLocalConnectionSaved } from './local.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 printLocalConnectionIntro(io, 'Apple Ads');\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(' Your private key stays on this machine; upload only the public key to Apple.');\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 printLocalConnectionSaved(io);\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;AAQjD,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,8BAA0B,IAAI,WAAW;AACzC,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,gFAAgF;AACvF,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,8BAA0B,EAAE;AAC5B,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,26 @@
1
+ // src/connect/local.ts
2
+ import path from "path";
3
+ import { adportHome } from "@adport/core";
4
+ function credentialFilePath() {
5
+ return path.join(adportHome(), "credentials.json");
6
+ }
7
+ function printLocalConnectionIntro(io, provider) {
8
+ io.out("");
9
+ io.out(`Local / bring-your-own ${provider} connection`);
10
+ io.out(" \u2022 You own the provider app, API access, and credentials used below.");
11
+ io.out(" \u2022 Adport Cloud and its hosted OAuth broker are not used.");
12
+ io.out(" \u2022 Adport connects from this machine directly to the provider API.");
13
+ io.out(` \u2022 Secrets are stored locally in ${credentialFilePath()} (mode 0600).`);
14
+ io.out("");
15
+ }
16
+ function printLocalConnectionSaved(io) {
17
+ io.out(`Credentials saved locally: ${credentialFilePath()} (mode 0600).`);
18
+ io.out("Adport Cloud received no credentials. Remove the local copy with `adport disconnect <provider>`.");
19
+ io.out("To invalidate an issued token or key, revoke it separately at the provider.");
20
+ }
21
+
22
+ export {
23
+ printLocalConnectionIntro,
24
+ printLocalConnectionSaved
25
+ };
26
+ //# sourceMappingURL=chunk-OPRADP2V.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connect/local.ts"],"sourcesContent":["import path from 'node:path';\nimport { adportHome } from '@adport/core';\nimport type { ProgramIO } from '../program.js';\n\nexport function credentialFilePath(): string {\n return path.join(adportHome(), 'credentials.json');\n}\n\nexport function printLocalConnectionIntro(io: ProgramIO, provider: string): void {\n io.out('');\n io.out(`Local / bring-your-own ${provider} connection`);\n io.out(' • You own the provider app, API access, and credentials used below.');\n io.out(' • Adport Cloud and its hosted OAuth broker are not used.');\n io.out(' • Adport connects from this machine directly to the provider API.');\n io.out(` • Secrets are stored locally in ${credentialFilePath()} (mode 0600).`);\n io.out('');\n}\n\nexport function printLocalConnectionSaved(io: ProgramIO): void {\n io.out(`Credentials saved locally: ${credentialFilePath()} (mode 0600).`);\n io.out('Adport Cloud received no credentials. Remove the local copy with `adport disconnect <provider>`.');\n io.out('To invalidate an issued token or key, revoke it separately at the provider.');\n}\n"],"mappings":";AAAA,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAGpB,SAAS,qBAA6B;AAC3C,SAAO,KAAK,KAAK,WAAW,GAAG,kBAAkB;AACnD;AAEO,SAAS,0BAA0B,IAAe,UAAwB;AAC/E,KAAG,IAAI,EAAE;AACT,KAAG,IAAI,0BAA0B,QAAQ,aAAa;AACtD,KAAG,IAAI,4EAAuE;AAC9E,KAAG,IAAI,iEAA4D;AACnE,KAAG,IAAI,0EAAqE;AAC5E,KAAG,IAAI,0CAAqC,mBAAmB,CAAC,eAAe;AAC/E,KAAG,IAAI,EAAE;AACX;AAEO,SAAS,0BAA0B,IAAqB;AAC7D,KAAG,IAAI,8BAA8B,mBAAmB,CAAC,eAAe;AACxE,KAAG,IAAI,kGAAkG;AACzG,KAAG,IAAI,6EAA6E;AACtF;","names":[]}
@@ -6,14 +6,17 @@ import { AdportError } from "@adport/core";
6
6
  var GOOGLE_ADS_SCOPE = "https://www.googleapis.com/auth/adwords";
7
7
  var AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
8
8
  var TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
9
- function buildGoogleAuthUrl(clientId, redirectUri) {
9
+ function buildGoogleAuthUrl(clientId, redirectUri, codeChallenge, state) {
10
10
  const params = new URLSearchParams({
11
11
  client_id: clientId,
12
12
  redirect_uri: redirectUri,
13
13
  response_type: "code",
14
14
  scope: GOOGLE_ADS_SCOPE,
15
15
  access_type: "offline",
16
- prompt: "consent"
16
+ prompt: "consent",
17
+ code_challenge: codeChallenge,
18
+ code_challenge_method: "S256",
19
+ state
17
20
  });
18
21
  return `${AUTH_ENDPOINT}?${params}`;
19
22
  }
@@ -26,7 +29,8 @@ async function exchangeCodeForTokens(input, fetchImpl = fetch) {
26
29
  client_secret: input.clientSecret,
27
30
  code: input.code,
28
31
  redirect_uri: input.redirectUri,
29
- grant_type: "authorization_code"
32
+ grant_type: "authorization_code",
33
+ code_verifier: input.codeVerifier
30
34
  })
31
35
  });
32
36
  const data = await response.json();
@@ -47,7 +51,7 @@ function parseClientSecretJson(text) {
47
51
  }
48
52
  return { clientId: entry.client_id, clientSecret: entry.client_secret };
49
53
  }
50
- async function startLoopbackServer(redirectHostname = "127.0.0.1") {
54
+ async function startLoopbackServer(redirectHostname = "127.0.0.1", expectedState) {
51
55
  let resolveCode;
52
56
  let rejectCode;
53
57
  const waitForCode = new Promise((resolve, reject) => {
@@ -58,9 +62,16 @@ async function startLoopbackServer(redirectHostname = "127.0.0.1") {
58
62
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
59
63
  const code = url.searchParams.get("code");
60
64
  const error = url.searchParams.get("error");
61
- res.writeHead(200, { "content-type": "text/html" });
65
+ const state = url.searchParams.get("state");
66
+ if (expectedState && state !== expectedState) {
67
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
68
+ res.end("<h2>Authorization failed</h2><p>OAuth state validation failed. Return to the terminal and retry.</p>");
69
+ rejectCode(new AdportError("PROVIDER_ERROR", "OAuth state validation failed. Retry the connection."));
70
+ return;
71
+ }
72
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
62
73
  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>`
74
+ code ? "<h2>adport is connected.</h2><p>You can close this tab and return to the terminal.</p>" : `<h2>Authorization failed${error ? `: ${escapeHtml(error)}` : ""}</h2><p>Return to the terminal and retry.</p>`
64
75
  );
65
76
  if (code) resolveCode(code);
66
77
  else if (error) rejectCode(new Error(`OAuth authorization failed: ${error}`));
@@ -82,7 +93,10 @@ function generatePkce() {
82
93
  const challenge = createHash("sha256").update(verifier).digest("base64url");
83
94
  return { verifier, challenge };
84
95
  }
85
- function buildMicrosoftAuthUrl(clientId, redirectUri, challenge) {
96
+ function generateOAuthState() {
97
+ return randomBytes(32).toString("base64url");
98
+ }
99
+ function buildMicrosoftAuthUrl(clientId, redirectUri, challenge, state) {
86
100
  const params = new URLSearchParams({
87
101
  client_id: clientId,
88
102
  response_type: "code",
@@ -90,7 +104,8 @@ function buildMicrosoftAuthUrl(clientId, redirectUri, challenge) {
90
104
  scope: MS_ADS_SCOPE,
91
105
  code_challenge: challenge,
92
106
  code_challenge_method: "S256",
93
- prompt: "login"
107
+ prompt: "login",
108
+ state
94
109
  });
95
110
  return `${MS_AUTHORIZE}?${params}`;
96
111
  }
@@ -121,6 +136,15 @@ function openInBrowser(url) {
121
136
  const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
122
137
  spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
123
138
  }
139
+ function escapeHtml(value) {
140
+ return value.replace(/[&<>"']/g, (char) => ({
141
+ "&": "&amp;",
142
+ "<": "&lt;",
143
+ ">": "&gt;",
144
+ '"': "&quot;",
145
+ "'": "&#39;"
146
+ })[char]);
147
+ }
124
148
 
125
149
  export {
126
150
  buildGoogleAuthUrl,
@@ -128,8 +152,9 @@ export {
128
152
  parseClientSecretJson,
129
153
  startLoopbackServer,
130
154
  generatePkce,
155
+ generateOAuthState,
131
156
  buildMicrosoftAuthUrl,
132
157
  exchangeMicrosoftCode,
133
158
  openInBrowser
134
159
  };
135
- //# sourceMappingURL=chunk-CVLEVRS4.js.map
160
+ //# sourceMappingURL=chunk-ZBNODOHE.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(\n clientId: string,\n redirectUri: string,\n codeChallenge: string,\n state: string,\n): 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 code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n state,\n });\n return `${AUTH_ENDPOINT}?${params}`;\n}\n\nexport async function exchangeCodeForTokens(\n input: { clientId: string; clientSecret: string; code: string; redirectUri: string; codeVerifier: 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 code_verifier: input.codeVerifier,\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(\n redirectHostname: '127.0.0.1' | 'localhost' = '127.0.0.1',\n expectedState?: string,\n): 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 const state = url.searchParams.get('state');\n if (expectedState && state !== expectedState) {\n res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' });\n res.end('<h2>Authorization failed</h2><p>OAuth state validation failed. Return to the terminal and retry.</p>');\n rejectCode(new AdportError('PROVIDER_ERROR', 'OAuth state validation failed. Retry the connection.'));\n return;\n }\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\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 ? `: ${escapeHtml(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://${redirectHostname}:${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 generateOAuthState(): string {\n return randomBytes(32).toString('base64url');\n}\n\nexport function buildMicrosoftAuthUrl(clientId: string, redirectUri: string, challenge: string, state: 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 state,\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\nfunction escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (char) => ({\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n })[char]!);\n}\n"],"mappings":"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","names":[]}
@@ -1,10 +1,16 @@
1
1
  import {
2
2
  buildGoogleAuthUrl,
3
3
  exchangeCodeForTokens,
4
+ generateOAuthState,
5
+ generatePkce,
4
6
  openInBrowser,
5
7
  parseClientSecretJson,
6
8
  startLoopbackServer
7
- } from "./chunk-CVLEVRS4.js";
9
+ } from "./chunk-ZBNODOHE.js";
10
+ import {
11
+ printLocalConnectionIntro,
12
+ printLocalConnectionSaved
13
+ } from "./chunk-OPRADP2V.js";
8
14
 
9
15
  // src/connect/google.ts
10
16
  import { promises as fs } from "fs";
@@ -18,6 +24,11 @@ async function connectGoogle({ openBrowser, io }) {
18
24
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
19
25
  const store = new CredentialStore();
20
26
  try {
27
+ printLocalConnectionIntro(io, "Google Ads");
28
+ io.out('Google may show "Google has not verified this app" for a private/unverified');
29
+ io.out("OAuth project. The developer shown will be your project contact. That warning");
30
+ io.out("is expected for BYO personal use; a verified Adport screen is Cloud-only.");
31
+ io.out("");
21
32
  const existing = await store.get("google");
22
33
  if (existing?.data.developer_token && existing.data.client_id && existing.data.client_secret) {
23
34
  const answer = (await rl.question(
@@ -44,12 +55,12 @@ async function connectGoogle({ openBrowser, io }) {
44
55
  return;
45
56
  }
46
57
  }
47
- io.out("");
48
58
  io.out("Connecting Google Ads. You need (the wizard guides each step):");
49
59
  io.out(" 1. A manager account (MCC) \u2014 free, instant: https://ads.google.com/home/tools/manager-accounts/");
50
60
  io.out(" 2. A developer token from the MCC API Center (Explorer access is automatic): https://ads.google.com/aw/apicenter");
51
61
  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)');
62
+ io.out(" Enable Google Ads API; choose External; add yourself as a test user.");
63
+ io.out(' Suggested private app name: "Adport Local \u2013 <your organization>".');
53
64
  io.out("");
54
65
  const imported = await tryImportGoogleAdsYaml(rl, io);
55
66
  let creds;
@@ -68,6 +79,11 @@ async function connectGoogle({ openBrowser, io }) {
68
79
  clientId = (await rl.question("OAuth client id: ")).trim();
69
80
  clientSecret = (await rl.question("OAuth client secret: ")).trim();
70
81
  }
82
+ if (!developerToken || !clientId || !clientSecret) {
83
+ io.err("Missing developer token or OAuth client credentials \u2014 aborting.");
84
+ process.exitCode = 1;
85
+ return;
86
+ }
71
87
  const loginCustomerId = (await rl.question("Manager (MCC) customer id for login-customer-id (Enter to skip): ")).trim() || void 0;
72
88
  const refreshToken = await runOAuthFlow(clientId, clientSecret, openBrowser, io);
73
89
  creds = { developerToken, clientId, clientSecret, refreshToken, loginCustomerId };
@@ -81,8 +97,10 @@ async function runOAuthFlow(clientId, clientSecret, openBrowser, io) {
81
97
  io.out("");
82
98
  io.out("Starting the OAuth flow (scope: Google Ads). A browser window should open;");
83
99
  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);
100
+ const pkce = generatePkce();
101
+ const state = generateOAuthState();
102
+ const loopback = await startLoopbackServer("127.0.0.1", state);
103
+ const authUrl = buildGoogleAuthUrl(clientId, loopback.redirectUri, pkce.challenge, state);
86
104
  if (openBrowser) {
87
105
  openInBrowser(authUrl);
88
106
  io.out(`If the browser did not open, visit:
@@ -93,7 +111,13 @@ async function runOAuthFlow(clientId, clientSecret, openBrowser, io) {
93
111
  }
94
112
  try {
95
113
  const code = await loopback.waitForCode;
96
- const tokens = await exchangeCodeForTokens({ clientId, clientSecret, code, redirectUri: loopback.redirectUri });
114
+ const tokens = await exchangeCodeForTokens({
115
+ clientId,
116
+ clientSecret,
117
+ code,
118
+ redirectUri: loopback.redirectUri,
119
+ codeVerifier: pkce.verifier
120
+ });
97
121
  return tokens.refreshToken;
98
122
  } finally {
99
123
  loopback.close();
@@ -116,6 +140,7 @@ async function verifyAndSave(creds, store, io) {
116
140
  });
117
141
  io.out("");
118
142
  io.out(`\u2713 Connected. ${customers.length} accessible customer id(s): ${customers.join(", ")}`);
143
+ printLocalConnectionSaved(io);
119
144
  io.out("");
120
145
  io.out('If this token expires again in ~7 days: your OAuth consent screen is in "Testing"');
121
146
  io.out('status \u2014 set it to "In production" at console.cloud.google.com/apis/credentials/consent');
@@ -163,4 +188,4 @@ function expandHome(p) {
163
188
  export {
164
189
  connectGoogle
165
190
  };
166
- //# sourceMappingURL=google-2FRVX6RS.js.map
191
+ //# sourceMappingURL=google-35WAUFUS.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 generateOAuthState,\n generatePkce,\n openInBrowser,\n parseClientSecretJson,\n startLoopbackServer,\n} from './oauth.js';\nimport { printLocalConnectionIntro, printLocalConnectionSaved } from './local.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 printLocalConnectionIntro(io, 'Google Ads');\n io.out('Google may show \"Google has not verified this app\" for a private/unverified');\n io.out('OAuth project. The developer shown will be your project contact. That warning');\n io.out('is expected for BYO personal use; a verified Adport screen is Cloud-only.');\n io.out('');\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('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 Google Ads API; choose External; add yourself as a test user.');\n io.out(' Suggested private app name: \"Adport Local – <your organization>\".');\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 if (!developerToken || !clientId || !clientSecret) {\n io.err('Missing developer token or OAuth client credentials — aborting.');\n process.exitCode = 1;\n return;\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 pkce = generatePkce();\n const state = generateOAuthState();\n const loopback = await startLoopbackServer('127.0.0.1', state);\n const authUrl = buildGoogleAuthUrl(clientId, loopback.redirectUri, pkce.challenge, state);\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({\n clientId,\n clientSecret,\n code,\n redirectUri: loopback.redirectUri,\n codeVerifier: pkce.verifier,\n });\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 printLocalConnectionSaved(io);\n io.out('');\n io.out('If this token expires again in ~7 days: your OAuth consent screen is in \"Testing\"');\n io.out('status — set it to \"In production\" at console.cloud.google.com/apis/credentials/consent');\n io.out('(no verification needed for your own use; refresh tokens then stop expiring weekly).');\n io.out('');\n io.out('Try: adport accounts · adport report --provider google · adport mcp');\n}\n\nasync function tryImportGoogleAdsYaml(\n rl: readline.Interface,\n io: ProgramIO,\n): Promise<GoogleCredentials | undefined> {\n const candidates = [\n process.env.GOOGLE_ADS_CONFIGURATION_FILE_PATH,\n path.join(os.homedir(), 'google-ads.yaml'),\n ].filter((p): p is string => Boolean(p));\n for (const candidate of candidates) {\n let raw: string;\n try {\n raw = await fs.readFile(candidate, 'utf8');\n } catch {\n continue;\n }\n const parsed = YAML.parse(raw) as Record<string, unknown> | null;\n const developerToken = str(parsed?.developer_token);\n const clientId = str(parsed?.client_id);\n const clientSecret = str(parsed?.client_secret);\n const refreshToken = str(parsed?.refresh_token);\n if (!developerToken || !clientId || !clientSecret || !refreshToken) continue;\n const answer = (await rl.question(`Found ${candidate} — import it? [Y/n] `)).trim().toLowerCase();\n if (answer === 'n' || answer === 'no') continue;\n io.out(`Importing credentials from ${candidate}.`);\n return {\n developerToken,\n clientId,\n clientSecret,\n refreshToken,\n loginCustomerId: str(parsed?.login_customer_id) || undefined,\n };\n }\n return undefined;\n}\n\nfunction str(value: unknown): string {\n return value === undefined || value === null ? '' : String(value);\n}\n\nfunction expandHome(p: string): string {\n return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p;\n}\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  // src/program.ts
4
4
  import { Command } from "commander";
5
+
6
+ // package.json
7
+ var package_default = {
8
+ name: "adport",
9
+ version: "0.3.0",
10
+ description: "adport CLI \u2014 the open control plane for paid media",
11
+ license: "Apache-2.0",
12
+ type: "module",
13
+ bin: {
14
+ adport: "./dist/index.js"
15
+ },
16
+ files: ["dist"],
17
+ scripts: {
18
+ build: "tsup src/index.ts --format esm --sourcemap --clean",
19
+ test: "vitest run",
20
+ typecheck: "tsc --noEmit",
21
+ clean: "rm -rf dist"
22
+ },
23
+ dependencies: {
24
+ "@adport/core": "workspace:*",
25
+ "@adport/mcp": "workspace:*",
26
+ "@adport/provider-apple": "workspace:*",
27
+ "@adport/provider-google": "workspace:*",
28
+ "@adport/provider-meta": "workspace:*",
29
+ "@adport/provider-microsoft": "workspace:*",
30
+ "@adport/provider-tiktok": "workspace:*",
31
+ commander: "^13.0.0",
32
+ yaml: "^2.6.0",
33
+ zod: "^4.4.3"
34
+ },
35
+ devDependencies: {
36
+ "@types/node": "^26.1.2",
37
+ tsup: "^8.3.0",
38
+ typescript: "^5.8.0",
39
+ vitest: "^4.1.10"
40
+ }
41
+ };
42
+
43
+ // src/program.ts
5
44
  import {
6
45
  AdportError,
7
46
  AuditLog,
@@ -21,16 +60,17 @@ function table(rows) {
21
60
  const line = (values) => values.map((v, i) => v.padEnd(widths[i])).join(" ");
22
61
  return [line(keys), line(widths.map((w) => "-".repeat(w))), ...rows.map((r) => line(keys.map((k) => String(r[k] ?? ""))))].join("\n");
23
62
  }
24
- async function runtime() {
63
+ async function runtime(includeMock) {
25
64
  const { assembleRuntime } = await import("@adport/mcp");
26
- return assembleRuntime();
65
+ return assembleRuntime({ includeMock });
27
66
  }
28
67
  function buildProgram(io = defaultIO) {
29
68
  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()) });
69
+ program.description("The open control plane for paid media (MCP + CLI)").option("--demo", "Use synthetic mock accounts and tools (never real provider data)").version(package_default.version).configureOutput({ writeOut: (s) => io.out(s.trimEnd()), writeErr: (s) => io.err(s.trimEnd()) });
70
+ const commandRuntime = () => runtime(program.opts().demo === true ? true : void 0);
31
71
  const tools = program.command("tools").description("Inspect and invoke the shared tool registry");
32
72
  tools.command("list").description("List available tools").option("--json", "JSON output").action(async (opts) => {
33
- const { registry } = await runtime();
73
+ const { registry } = await commandRuntime();
34
74
  const defs = registry.list();
35
75
  if (opts.json) {
36
76
  io.out(
@@ -56,17 +96,17 @@ function buildProgram(io = defaultIO) {
56
96
  );
57
97
  });
58
98
  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();
99
+ const rt = await commandRuntime();
60
100
  const result = await rt.registry.call(name, JSON.parse(opts.input), rt.ctx);
61
101
  io.out(json(result));
62
102
  });
63
103
  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();
104
+ const rt = await commandRuntime();
65
105
  const result = await rt.registry.call("accounts_list", { provider: opts.provider }, rt.ctx);
66
106
  io.out(opts.json ? json(result.accounts) : table(result.accounts));
67
107
  });
68
108
  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();
109
+ const rt = await commandRuntime();
70
110
  const dateRange = opts.range.includes("..") ? { start: opts.range.split("..")[0], end: opts.range.split("..")[1] } : opts.range;
71
111
  const result = await rt.registry.call(
72
112
  "report",
@@ -97,7 +137,7 @@ function buildProgram(io = defaultIO) {
97
137
  if (result.truncated) io.err("(truncated \u2014 raise --limit for more rows)");
98
138
  });
99
139
  program.command("policy").description("Show the active write policy and where it was loaded from").action(async () => {
100
- const rt = await runtime();
140
+ const rt = await commandRuntime();
101
141
  io.out(json({ source: rt.policySource, policy: rt.ctx.engine.policy }));
102
142
  });
103
143
  const audit = program.command("audit").description("Inspect or extend the write-audit trail");
@@ -109,8 +149,19 @@ function buildProgram(io = defaultIO) {
109
149
  }
110
150
  io.out(entries.map((e) => JSON.stringify(e)).join("\n"));
111
151
  });
152
+ audit.command("export").description("Export write-audit entries to stdout for archival or compliance ingestion").option("--limit <n>", "Max entries", "10000").option("--format <format>", "jsonl | json", "jsonl").action(async (opts) => {
153
+ const limit = Number(opts.limit);
154
+ if (!Number.isInteger(limit) || limit < 1) {
155
+ throw new AdportError("INVALID_INPUT", "--limit must be a positive integer");
156
+ }
157
+ if (opts.format !== "jsonl" && opts.format !== "json") {
158
+ throw new AdportError("INVALID_INPUT", "--format must be jsonl or json");
159
+ }
160
+ const entries = await new AuditLog().read(limit);
161
+ io.out(opts.format === "json" ? json(entries) : entries.map((e) => JSON.stringify(e)).join("\n"));
162
+ });
112
163
  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();
164
+ const rt = await commandRuntime();
114
165
  const dateRange = opts.range.includes("..") ? { start: opts.range.split("..")[0], end: opts.range.split("..")[1] } : opts.range;
115
166
  const result = await rt.registry.call(
116
167
  "audit_run",
@@ -136,40 +187,54 @@ function buildProgram(io = defaultIO) {
136
187
  });
137
188
  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
189
  if (provider === "google") {
139
- const { connectGoogle } = await import("./google-2FRVX6RS.js");
190
+ const { connectGoogle } = await import("./google-35WAUFUS.js");
140
191
  await connectGoogle({ openBrowser: opts.browser, io });
141
192
  return;
142
193
  }
143
194
  if (provider === "meta") {
144
- const { connectMeta } = await import("./meta-IXPXEBI2.js");
195
+ const { connectMeta } = await import("./meta-4WOW7YBE.js");
145
196
  await connectMeta({ io });
146
197
  return;
147
198
  }
148
199
  if (provider === "tiktok") {
149
- const { connectTikTok } = await import("./tiktok-TL7Z64OV.js");
200
+ const { connectTikTok } = await import("./tiktok-462KBGJL.js");
150
201
  await connectTikTok({ io });
151
202
  return;
152
203
  }
153
204
  if (provider === "apple") {
154
- const { connectApple } = await import("./apple-RHO3KWJ4.js");
205
+ const { connectApple } = await import("./apple-ZLY4ZTQ3.js");
155
206
  await connectApple({ io });
156
207
  return;
157
208
  }
158
209
  if (provider === "microsoft") {
159
- const { connectMicrosoft } = await import("./microsoft-ZZALD3EH.js");
210
+ const { connectMicrosoft } = await import("./microsoft-GXB6A6JC.js");
160
211
  await connectMicrosoft({ openBrowser: opts.browser, io });
161
212
  return;
162
213
  }
163
214
  if (provider === "mock") {
164
- io.out("The mock provider needs no credentials \u2014 it is available out of the box. Try: adport accounts");
215
+ io.out("The mock provider is explicit demo mode, not a connection. Try: adport --demo accounts");
165
216
  return;
166
217
  }
167
218
  io.err(`Provider "${provider}" is not supported. Available: google, meta, tiktok, apple, microsoft, mock.`);
168
219
  process.exitCode = 1;
169
220
  });
221
+ program.command("disconnect <provider>").description("Remove a provider connection from this machine").action(async (provider) => {
222
+ if (!["google", "meta", "tiktok", "apple", "microsoft"].includes(provider)) {
223
+ io.err(`Provider "${provider}" is not supported. Available: google, meta, tiktok, apple, microsoft.`);
224
+ process.exitCode = 1;
225
+ return;
226
+ }
227
+ const removed = await new CredentialStore().delete(provider);
228
+ if (!removed) {
229
+ io.out(`No local ${provider} credentials were stored.`);
230
+ return;
231
+ }
232
+ io.out(`Removed local ${provider} credentials.`);
233
+ io.out("Provider-side access was not revoked. Revoke the token or key at the provider if needed.");
234
+ });
170
235
  const recommendations = program.command("recommendations").description("Review and act on audit findings");
171
236
  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();
237
+ const rt = await commandRuntime();
173
238
  const result = await rt.registry.call("recommendations_list", { status: opts.status }, rt.ctx);
174
239
  if (opts.json) {
175
240
  io.out(json(result.findings));
@@ -186,7 +251,7 @@ function buildProgram(io = defaultIO) {
186
251
  }
187
252
  });
188
253
  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();
254
+ const rt = await commandRuntime();
190
255
  const result = await rt.registry.call(
191
256
  "recommendation_apply",
192
257
  { finding_id: findingId, pending_operation_id: opts.pending },
@@ -195,14 +260,14 @@ function buildProgram(io = defaultIO) {
195
260
  io.out(json(result));
196
261
  });
197
262
  recommendations.command("dismiss <findingId>").description("Dismiss a finding (never reopened by future runs)").action(async (findingId) => {
198
- const rt = await runtime();
263
+ const rt = await commandRuntime();
199
264
  const result = await rt.registry.call("recommendation_dismiss", { finding_id: findingId }, rt.ctx);
200
265
  io.out(json(result));
201
266
  });
202
267
  program.command("doctor").description("Check credential and connection health for every provider").action(async () => {
203
268
  const store = new CredentialStore();
204
269
  const records = await store.list();
205
- const rt = await runtime();
270
+ const rt = await commandRuntime();
206
271
  io.out(`policy: ${rt.policySource}`);
207
272
  io.out(`providers loaded: ${rt.ctx.providers.list().map((p) => p.id).join(", ") || "(none)"}`);
208
273
  for (const record of records) {
@@ -225,7 +290,7 @@ function buildProgram(io = defaultIO) {
225
290
  });
226
291
  program.command("mcp").description("Start the adport MCP server on stdio").action(async () => {
227
292
  const { runStdioServer } = await import("@adport/mcp");
228
- await runStdioServer(await runtime());
293
+ await runStdioServer(await commandRuntime());
229
294
  await new Promise(() => {
230
295
  });
231
296
  });
package/dist/index.js.map CHANGED
@@ -1 +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":[]}
1
+ {"version":3,"sources":["../src/program.ts","../package.json","../src/index.ts"],"sourcesContent":["import { Command } from 'commander';\nimport packageJson from '../package.json';\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(includeMock?: boolean): Promise<AdportRuntime> {\n const { assembleRuntime } = await import('@adport/mcp');\n return assembleRuntime({ includeMock });\n}\n\nexport function buildProgram(io: ProgramIO = defaultIO): Command {\n const program = new Command('adport');\n program\n .description('The open control plane for paid media (MCP + CLI)')\n .option('--demo', 'Use synthetic mock accounts and tools (never real provider data)')\n .version(packageJson.version)\n .configureOutput({ writeOut: (s) => io.out(s.trimEnd()), writeErr: (s) => io.err(s.trimEnd()) });\n const commandRuntime = () => runtime(program.opts<{ demo?: boolean }>().demo === true ? true : undefined);\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 commandRuntime();\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 commandRuntime();\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 commandRuntime();\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 commandRuntime();\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 commandRuntime();\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('export')\n .description('Export write-audit entries to stdout for archival or compliance ingestion')\n .option('--limit <n>', 'Max entries', '10000')\n .option('--format <format>', 'jsonl | json', 'jsonl')\n .action(async (opts: { limit: string; format: string }) => {\n const limit = Number(opts.limit);\n if (!Number.isInteger(limit) || limit < 1) {\n throw new AdportError('INVALID_INPUT', '--limit must be a positive integer');\n }\n if (opts.format !== 'jsonl' && opts.format !== 'json') {\n throw new AdportError('INVALID_INPUT', '--format must be jsonl or json');\n }\n const entries = await new AuditLog().read(limit);\n io.out(opts.format === 'json' ? json(entries) : 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 commandRuntime();\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 is explicit demo mode, not a connection. Try: adport --demo 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 program\n .command('disconnect <provider>')\n .description('Remove a provider connection from this machine')\n .action(async (provider: string) => {\n if (!['google', 'meta', 'tiktok', 'apple', 'microsoft'].includes(provider)) {\n io.err(`Provider \"${provider}\" is not supported. Available: google, meta, tiktok, apple, microsoft.`);\n process.exitCode = 1;\n return;\n }\n const removed = await new CredentialStore().delete(provider);\n if (!removed) {\n io.out(`No local ${provider} credentials were stored.`);\n return;\n }\n io.out(`Removed local ${provider} credentials.`);\n io.out('Provider-side access was not revoked. Revoke the token or key at the provider if needed.');\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 commandRuntime();\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 commandRuntime();\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 commandRuntime();\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 commandRuntime();\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 commandRuntime());\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","{\n \"name\": \"adport\",\n \"version\": \"0.3.0\",\n \"description\": \"adport CLI — the open control plane for paid media\",\n \"license\": \"Apache-2.0\",\n \"type\": \"module\",\n \"bin\": {\n \"adport\": \"./dist/index.js\"\n },\n \"files\": [\"dist\"],\n \"scripts\": {\n \"build\": \"tsup src/index.ts --format esm --sourcemap --clean\",\n \"test\": \"vitest run\",\n \"typecheck\": \"tsc --noEmit\",\n \"clean\": \"rm -rf dist\"\n },\n \"dependencies\": {\n \"@adport/core\": \"workspace:*\",\n \"@adport/mcp\": \"workspace:*\",\n \"@adport/provider-apple\": \"workspace:*\",\n \"@adport/provider-google\": \"workspace:*\",\n \"@adport/provider-meta\": \"workspace:*\",\n \"@adport/provider-microsoft\": \"workspace:*\",\n \"@adport/provider-tiktok\": \"workspace:*\",\n \"commander\": \"^13.0.0\",\n \"yaml\": \"^2.6.0\",\n \"zod\": \"^4.4.3\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^26.1.2\",\n \"tsup\": \"^8.3.0\",\n \"typescript\": \"^5.8.0\",\n \"vitest\": \"^4.1.10\"\n }\n}\n","#!/usr/bin/env node\nimport { main } from './program.js';\n\nawait main(process.argv);\n"],"mappings":"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","names":[]}
@@ -1,3 +1,8 @@
1
+ import {
2
+ printLocalConnectionIntro,
3
+ printLocalConnectionSaved
4
+ } from "./chunk-OPRADP2V.js";
5
+
1
6
  // src/connect/meta.ts
2
7
  import readline from "readline/promises";
3
8
  import { CredentialStore } from "@adport/core";
@@ -6,7 +11,7 @@ async function connectMeta({ io }) {
6
11
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
7
12
  const store = new CredentialStore();
8
13
  try {
9
- io.out("");
14
+ printLocalConnectionIntro(io, "Meta Ads");
10
15
  io.out("Connecting Meta Ads. One-time setup (~20-30 min, no review for your own accounts):");
11
16
  io.out(" 1. Create a Business-type app (dev mode is fine): https://developers.facebook.com/apps/");
12
17
  io.out(' Add the "Marketing API" product to it.');
@@ -16,6 +21,8 @@ async function connectMeta({ io }) {
16
21
  io.out(" https://business.facebook.com/settings/system-users");
17
22
  io.out(" 3. Alternative \u2014 a user token from Graph API Explorer (expires after ~60 days");
18
23
  io.out(" when extended; adport will warn you, but system-user tokens avoid this).");
24
+ io.out(" Meta App Review/business verification can be required for assets owned by");
25
+ io.out(" other businesses; your local Adport installation does not bypass that.");
19
26
  io.out("");
20
27
  const accessToken = (await rl.question("Paste your access token: ")).trim();
21
28
  if (!accessToken) {
@@ -49,6 +56,7 @@ async function connectMeta({ io }) {
49
56
  for (const account of accounts) {
50
57
  io.out(` ${account.id} ${account.name} ${account.currency ?? ""} ${account.status ?? ""}`);
51
58
  }
59
+ printLocalConnectionSaved(io);
52
60
  if (debug?.expiresAt) {
53
61
  const days = Math.round((debug.expiresAt * 1e3 - Date.now()) / 864e5);
54
62
  io.out(
@@ -66,4 +74,4 @@ async function connectMeta({ io }) {
66
74
  export {
67
75
  connectMeta
68
76
  };
69
- //# sourceMappingURL=meta-IXPXEBI2.js.map
77
+ //# sourceMappingURL=meta-4WOW7YBE.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';\nimport { printLocalConnectionIntro, printLocalConnectionSaved } from './local.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 printLocalConnectionIntro(io, 'Meta Ads');\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(' Meta App Review/business verification can be required for assets owned by');\n io.out(' other businesses; your local Adport installation does not bypass that.');\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 printLocalConnectionSaved(io);\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;AAYvE,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,8BAA0B,IAAI,UAAU;AACxC,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,6EAA6E;AACpF,OAAG,IAAI,0EAA0E;AACjF,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,8BAA0B,EAAE;AAC5B,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":[]}
@@ -1,10 +1,15 @@
1
1
  import {
2
2
  buildMicrosoftAuthUrl,
3
3
  exchangeMicrosoftCode,
4
+ generateOAuthState,
4
5
  generatePkce,
5
6
  openInBrowser,
6
7
  startLoopbackServer
7
- } from "./chunk-CVLEVRS4.js";
8
+ } from "./chunk-ZBNODOHE.js";
9
+ import {
10
+ printLocalConnectionIntro,
11
+ printLocalConnectionSaved
12
+ } from "./chunk-OPRADP2V.js";
8
13
 
9
14
  // src/connect/microsoft.ts
10
15
  import readline from "readline/promises";
@@ -14,13 +19,15 @@ async function connectMicrosoft({ openBrowser, io }) {
14
19
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
15
20
  const store = new CredentialStore();
16
21
  try {
17
- io.out("");
22
+ printLocalConnectionIntro(io, "Microsoft Advertising");
18
23
  io.out("Connecting Microsoft Advertising. Setup:");
19
24
  io.out(' 1. Azure Portal \u2192 App registrations \u2192 New: "Mobile and desktop applications"');
20
25
  io.out(" platform with redirect URI http://localhost (public client, no secret).");
21
26
  io.out(" 2. Developer token (self-serve, no review): sign in as Super Admin at");
22
27
  io.out(" https://ads.microsoft.com/cc/Settings/DevSettings \u2192 Request Token.");
23
28
  io.out(` 3. Sandbox alternative: universal token ${SANDBOX_DEVELOPER_TOKEN} works for everyone.`);
29
+ io.out(" Microsoft may show an unverified-publisher or admin-consent notice for your");
30
+ io.out(" Entra app; its branding and tenant policy belong to you, not Adport Cloud.");
24
31
  io.out("");
25
32
  const sandboxAnswer = (await rl.question("Use the SANDBOX environment? [y/N] ")).trim().toLowerCase();
26
33
  const sandbox = sandboxAnswer === "y" || sandboxAnswer === "yes";
@@ -34,8 +41,9 @@ async function connectMicrosoft({ openBrowser, io }) {
34
41
  io.out("");
35
42
  io.out("Starting the Microsoft sign-in (PKCE, no client secret)\u2026");
36
43
  const pkce = generatePkce();
37
- const loopback = await startLoopbackServer("localhost");
38
- const authUrl = buildMicrosoftAuthUrl(clientId, loopback.redirectUri, pkce.challenge);
44
+ const state = generateOAuthState();
45
+ const loopback = await startLoopbackServer("localhost", state);
46
+ const authUrl = buildMicrosoftAuthUrl(clientId, loopback.redirectUri, pkce.challenge, state);
39
47
  if (openBrowser) {
40
48
  openInBrowser(authUrl);
41
49
  io.out(`If the browser did not open, visit:
@@ -76,6 +84,7 @@ async function connectMicrosoft({ openBrowser, io }) {
76
84
  for (const account of accounts) {
77
85
  io.out(` ${account.id} ${account.name} ${account.currency ?? ""} ${account.status ?? ""}`);
78
86
  }
87
+ printLocalConnectionSaved(io);
79
88
  io.out("Note: public-client refresh tokens expire after ~90 days of disuse; adport");
80
89
  io.out("persists rotated tokens automatically on every use.");
81
90
  io.out("");
@@ -87,4 +96,4 @@ async function connectMicrosoft({ openBrowser, io }) {
87
96
  export {
88
97
  connectMicrosoft
89
98
  };
90
- //# sourceMappingURL=microsoft-ZZALD3EH.js.map
99
+ //# sourceMappingURL=microsoft-GXB6A6JC.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 generateOAuthState,\n generatePkce,\n openInBrowser,\n startLoopbackServer,\n} from './oauth.js';\nimport { printLocalConnectionIntro, printLocalConnectionSaved } from './local.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 printLocalConnectionIntro(io, 'Microsoft Advertising');\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(' Microsoft may show an unverified-publisher or admin-consent notice for your');\n io.out(' Entra app; its branding and tenant policy belong to you, not Adport Cloud.');\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 state = generateOAuthState();\n const loopback = await startLoopbackServer('localhost', state);\n const authUrl = buildMicrosoftAuthUrl(clientId, loopback.redirectUri, pkce.challenge, state);\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 printLocalConnectionSaved(io);\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;AAgBlF,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,8BAA0B,IAAI,uBAAuB;AACrD,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,+EAA+E;AACtF,OAAG,IAAI,8EAA8E;AACrF,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,QAAQ,mBAAmB;AACjC,UAAM,WAAW,MAAM,oBAAoB,aAAa,KAAK;AAC7D,UAAM,UAAU,sBAAsB,UAAU,SAAS,aAAa,KAAK,WAAW,KAAK;AAC3F,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,8BAA0B,EAAE;AAC5B,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":[]}
@@ -1,3 +1,8 @@
1
+ import {
2
+ printLocalConnectionIntro,
3
+ printLocalConnectionSaved
4
+ } from "./chunk-OPRADP2V.js";
5
+
1
6
  // src/connect/tiktok.ts
2
7
  import readline from "readline/promises";
3
8
  import { CredentialStore } from "@adport/core";
@@ -6,7 +11,7 @@ async function connectTikTok({ io }) {
6
11
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
7
12
  const store = new CredentialStore();
8
13
  try {
9
- io.out("");
14
+ printLocalConnectionIntro(io, "TikTok Ads");
10
15
  io.out("Connecting TikTok Ads (Marketing API). Setup:");
11
16
  io.out(" 1. Register at https://business-api.tiktok.com/portal and create an app.");
12
17
  io.out(" Production needs human review (2 days\u20132 weeks). SANDBOX works today:");
@@ -40,6 +45,7 @@ async function connectTikTok({ io }) {
40
45
  for (const account of accounts) {
41
46
  io.out(` ${account.id} ${account.name} ${account.currency ?? ""} ${account.status ?? ""}`);
42
47
  }
48
+ printLocalConnectionSaved(io);
43
49
  io.out("");
44
50
  io.out("Try: adport accounts \xB7 adport report --provider tiktok \xB7 adport mcp");
45
51
  } finally {
@@ -49,4 +55,4 @@ async function connectTikTok({ io }) {
49
55
  export {
50
56
  connectTikTok
51
57
  };
52
- //# sourceMappingURL=tiktok-TL7Z64OV.js.map
58
+ //# sourceMappingURL=tiktok-462KBGJL.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';\nimport { printLocalConnectionIntro, printLocalConnectionSaved } from './local.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 printLocalConnectionIntro(io, 'TikTok Ads');\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 printLocalConnectionSaved(io);\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;AAQhD,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,8BAA0B,IAAI,YAAY;AAC1C,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,8BAA0B,EAAE;AAC5B,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,oFAA8E;AAAA,EACvF,UAAE;AACA,OAAG,MAAM;AAAA,EACX;AACF;","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "adport",
3
- "version": "0.2.0",
4
- "description": "adport CLI — manage your ad accounts from the terminal or any AI agent",
3
+ "version": "0.3.0",
4
+ "description": "adport CLI — the open control plane for paid media",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "bin": {
@@ -13,20 +13,20 @@
13
13
  "dependencies": {
14
14
  "commander": "^13.0.0",
15
15
  "yaml": "^2.6.0",
16
- "zod": "^3.25.0",
17
- "@adport/core": "0.2.0",
18
- "@adport/mcp": "0.2.0",
19
- "@adport/provider-apple": "0.2.0",
20
- "@adport/provider-google": "0.2.0",
21
- "@adport/provider-meta": "0.2.0",
22
- "@adport/provider-microsoft": "0.2.0",
23
- "@adport/provider-tiktok": "0.2.0"
16
+ "zod": "^4.4.3",
17
+ "@adport/core": "0.3.0",
18
+ "@adport/mcp": "0.3.0",
19
+ "@adport/provider-google": "0.3.0",
20
+ "@adport/provider-apple": "0.3.0",
21
+ "@adport/provider-meta": "0.3.0",
22
+ "@adport/provider-tiktok": "0.3.0",
23
+ "@adport/provider-microsoft": "0.3.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@types/node": "^22.10.0",
26
+ "@types/node": "^26.1.2",
27
27
  "tsup": "^8.3.0",
28
28
  "typescript": "^5.8.0",
29
- "vitest": "^3.0.0"
29
+ "vitest": "^4.1.10"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "tsup src/index.ts --format esm --sourcemap --clean",
@@ -1 +0,0 @@
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":[]}
@@ -1 +0,0 @@
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(\n redirectHostname: '127.0.0.1' | 'localhost' = '127.0.0.1',\n): 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://${redirectHostname}:${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":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/connect/google.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport readline from 'node:readline/promises';\nimport YAML from 'yaml';\nimport { CredentialStore } from '@adport/core';\nimport { GoogleAdsRestClient, type GoogleCredentials } from '@adport/provider-google';\nimport type { ProgramIO } from '../program.js';\nimport {\n buildGoogleAuthUrl,\n exchangeCodeForTokens,\n openInBrowser,\n parseClientSecretJson,\n startLoopbackServer,\n} from './oauth.js';\n\nexport interface ConnectGoogleOptions {\n openBrowser: boolean;\n io: ProgramIO;\n}\n\n/**\n * Guided Google Ads connection. Since Google's Explorer access tier (Oct 2025),\n * a fresh developer token works on production accounts the same day — the wizard's\n * job is navigation, not waiting.\n */\nexport async function connectGoogle({ openBrowser, io }: ConnectGoogleOptions): Promise<void> {\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n const store = new CredentialStore();\n try {\n // Re-auth fast path: keep the stored developer token + OAuth client and\n // only redo the browser consent (covers expired/revoked refresh tokens).\n const existing = await store.get('google');\n if (existing?.data.developer_token && existing.data.client_id && existing.data.client_secret) {\n const answer = (\n await rl.question(\n 'Existing Google connection found. Re-authorize with the same developer token and OAuth client? [Y/n] ',\n )\n )\n .trim()\n .toLowerCase();\n if (answer !== 'n' && answer !== 'no') {\n const refreshToken = await runOAuthFlow(\n existing.data.client_id,\n existing.data.client_secret,\n openBrowser,\n io,\n );\n await verifyAndSave(\n {\n developerToken: existing.data.developer_token,\n clientId: existing.data.client_id,\n clientSecret: existing.data.client_secret,\n refreshToken,\n loginCustomerId: existing.data.login_customer_id || undefined,\n },\n store,\n io,\n );\n return;\n }\n }\n\n io.out('');\n io.out('Connecting Google Ads. You need (the wizard guides each step):');\n io.out(' 1. A manager account (MCC) — free, instant: https://ads.google.com/home/tools/manager-accounts/');\n io.out(' 2. A developer token from the MCC API Center (Explorer access is automatic): https://ads.google.com/aw/apicenter');\n io.out(' 3. A Google Cloud OAuth \"Desktop app\" client: https://console.cloud.google.com/apis/credentials');\n io.out(' (enable the \"Google Ads API\" for the project, consent screen: External, add yourself as test user)');\n io.out('');\n\n // Fast path: import an existing google-ads.yaml (the ecosystem convention).\n const imported = await tryImportGoogleAdsYaml(rl, io);\n let creds: GoogleCredentials;\n if (imported) {\n creds = imported;\n } else {\n const developerToken = (await rl.question('Developer token (from the MCC API Center): ')).trim();\n\n let clientId = '';\n let clientSecret = '';\n const secretPath = (\n await rl.question('Path to downloaded client_secret_*.json (or press Enter to type id/secret manually): ')\n ).trim();\n if (secretPath) {\n const parsed = parseClientSecretJson(await fs.readFile(expandHome(secretPath), 'utf8'));\n clientId = parsed.clientId;\n clientSecret = parsed.clientSecret;\n } else {\n clientId = (await rl.question('OAuth client id: ')).trim();\n clientSecret = (await rl.question('OAuth client secret: ')).trim();\n }\n\n const loginCustomerId =\n (await rl.question('Manager (MCC) customer id for login-customer-id (Enter to skip): ')).trim() || undefined;\n\n const refreshToken = await runOAuthFlow(clientId, clientSecret, openBrowser, io);\n creds = { developerToken, clientId, clientSecret, refreshToken, loginCustomerId };\n }\n\n await verifyAndSave(creds, store, io);\n } finally {\n rl.close();\n }\n}\n\nasync function runOAuthFlow(\n clientId: string,\n clientSecret: string,\n openBrowser: boolean,\n io: ProgramIO,\n): Promise<string> {\n io.out('');\n io.out('Starting the OAuth flow (scope: Google Ads). A browser window should open;');\n io.out('sign in with the Google account that can access your ad accounts.');\n const loopback = await startLoopbackServer();\n const authUrl = buildGoogleAuthUrl(clientId, loopback.redirectUri);\n if (openBrowser) {\n openInBrowser(authUrl);\n io.out(`If the browser did not open, visit:\\n ${authUrl}`);\n } else {\n io.out(`Open this URL in a browser on this machine (or SSH port-forward the shown port):\\n ${authUrl}`);\n }\n try {\n const code = await loopback.waitForCode;\n const tokens = await exchangeCodeForTokens({ clientId, clientSecret, code, redirectUri: loopback.redirectUri });\n return tokens.refreshToken;\n } finally {\n loopback.close();\n }\n}\n\nasync function verifyAndSave(creds: GoogleCredentials, store: CredentialStore, io: ProgramIO): Promise<void> {\n io.out('Verifying access…');\n const client = new GoogleAdsRestClient(creds);\n const customers = await client.listAccessibleCustomers();\n await store.set({\n provider: 'google',\n source: 'byo',\n data: {\n developer_token: creds.developerToken,\n client_id: creds.clientId,\n client_secret: creds.clientSecret,\n refresh_token: creds.refreshToken,\n ...(creds.loginCustomerId ? { login_customer_id: creds.loginCustomerId } : {}),\n },\n });\n io.out('');\n io.out(`✓ Connected. ${customers.length} accessible customer id(s): ${customers.join(', ')}`);\n io.out('');\n io.out('If this token expires again in ~7 days: your OAuth consent screen is in \"Testing\"');\n io.out('status — set it to \"In production\" at console.cloud.google.com/apis/credentials/consent');\n io.out('(no verification needed for your own use; refresh tokens then stop expiring weekly).');\n io.out('');\n io.out('Try: adport accounts · adport report --provider google · adport mcp');\n}\n\nasync function tryImportGoogleAdsYaml(\n rl: readline.Interface,\n io: ProgramIO,\n): Promise<GoogleCredentials | undefined> {\n const candidates = [\n process.env.GOOGLE_ADS_CONFIGURATION_FILE_PATH,\n path.join(os.homedir(), 'google-ads.yaml'),\n ].filter((p): p is string => Boolean(p));\n for (const candidate of candidates) {\n let raw: string;\n try {\n raw = await fs.readFile(candidate, 'utf8');\n } catch {\n continue;\n }\n const parsed = YAML.parse(raw) as Record<string, unknown> | null;\n const developerToken = str(parsed?.developer_token);\n const clientId = str(parsed?.client_id);\n const clientSecret = str(parsed?.client_secret);\n const refreshToken = str(parsed?.refresh_token);\n if (!developerToken || !clientId || !clientSecret || !refreshToken) continue;\n const answer = (await rl.question(`Found ${candidate} — import it? [Y/n] `)).trim().toLowerCase();\n if (answer === 'n' || answer === 'no') continue;\n io.out(`Importing credentials from ${candidate}.`);\n return {\n developerToken,\n clientId,\n clientSecret,\n refreshToken,\n loginCustomerId: str(parsed?.login_customer_id) || undefined,\n };\n }\n return undefined;\n}\n\nfunction str(value: unknown): string {\n return value === undefined || value === null ? '' : String(value);\n}\n\nfunction expandHome(p: string): string {\n return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p;\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
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":[]}
@@ -1 +0,0 @@
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('localhost');\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,WAAW;AACtD,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":[]}
@@ -1 +0,0 @@
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":[]}