adport 0.1.0 → 0.2.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.
@@ -47,7 +47,7 @@ function parseClientSecretJson(text) {
47
47
  }
48
48
  return { clientId: entry.client_id, clientSecret: entry.client_secret };
49
49
  }
50
- async function startLoopbackServer() {
50
+ async function startLoopbackServer(redirectHostname = "127.0.0.1") {
51
51
  let resolveCode;
52
52
  let rejectCode;
53
53
  const waitForCode = new Promise((resolve, reject) => {
@@ -69,7 +69,7 @@ async function startLoopbackServer() {
69
69
  const address = server.address();
70
70
  if (!address || typeof address === "string") throw new Error("Loopback server failed to bind");
71
71
  return {
72
- redirectUri: `http://127.0.0.1:${address.port}`,
72
+ redirectUri: `http://${redirectHostname}:${address.port}`,
73
73
  waitForCode,
74
74
  close: () => server.close()
75
75
  };
@@ -132,4 +132,4 @@ export {
132
132
  exchangeMicrosoftCode,
133
133
  openInBrowser
134
134
  };
135
- //# sourceMappingURL=chunk-JPHFJ6HN.js.map
135
+ //# sourceMappingURL=chunk-CVLEVRS4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connect/oauth.ts"],"sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\nimport http from 'node:http';\nimport { spawn } from 'node:child_process';\nimport { AdportError } from '@adport/core';\n\nexport const GOOGLE_ADS_SCOPE = 'https://www.googleapis.com/auth/adwords';\nconst AUTH_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth';\nconst TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token';\n\nexport function buildGoogleAuthUrl(clientId: string, redirectUri: string): string {\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: 'code',\n scope: GOOGLE_ADS_SCOPE,\n access_type: 'offline',\n prompt: 'consent',\n });\n return `${AUTH_ENDPOINT}?${params}`;\n}\n\nexport async function exchangeCodeForTokens(\n input: { clientId: string; clientSecret: string; code: string; redirectUri: string },\n fetchImpl: typeof fetch = fetch,\n): Promise<{ refreshToken: string; accessToken: string }> {\n const response = await fetchImpl(TOKEN_ENDPOINT, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n client_id: input.clientId,\n client_secret: input.clientSecret,\n code: input.code,\n redirect_uri: input.redirectUri,\n grant_type: 'authorization_code',\n }),\n });\n const data = (await response.json()) as { refresh_token?: string; access_token?: string; error?: string };\n if (!response.ok || !data.refresh_token || !data.access_token) {\n throw new AdportError(\n 'PROVIDER_ERROR',\n `OAuth code exchange failed${data.error ? ` (${data.error})` : ''}. ` +\n 'If no refresh_token was returned, revoke the app at myaccount.google.com/permissions and retry.',\n data,\n );\n }\n return { refreshToken: data.refresh_token, accessToken: data.access_token };\n}\n\n/** Parse a downloaded OAuth client JSON (client_secret_*.json, \"installed\" or \"web\"). */\nexport function parseClientSecretJson(text: string): { clientId: string; clientSecret: string } {\n const parsed = JSON.parse(text) as {\n installed?: { client_id: string; client_secret: string };\n web?: { client_id: string; client_secret: string };\n };\n const entry = parsed.installed ?? parsed.web;\n if (!entry?.client_id || !entry?.client_secret) {\n throw new AdportError('INVALID_INPUT', 'Not a valid OAuth client JSON (expected \"installed\" or \"web\" key).');\n }\n return { clientId: entry.client_id, clientSecret: entry.client_secret };\n}\n\nexport interface LoopbackServer {\n redirectUri: string;\n /** Resolves with the authorization code once Google redirects back. */\n waitForCode: Promise<string>;\n close: () => void;\n}\n\n/** Loopback listener for the OAuth redirect — the desktop-app flow's return path. */\nexport async function startLoopbackServer(\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":[]}
@@ -4,7 +4,7 @@ import {
4
4
  openInBrowser,
5
5
  parseClientSecretJson,
6
6
  startLoopbackServer
7
- } from "./chunk-JPHFJ6HN.js";
7
+ } from "./chunk-CVLEVRS4.js";
8
8
 
9
9
  // src/connect/google.ts
10
10
  import { promises as fs } from "fs";
@@ -163,4 +163,4 @@ function expandHome(p) {
163
163
  export {
164
164
  connectGoogle
165
165
  };
166
- //# sourceMappingURL=google-ZQQS6JUM.js.map
166
+ //# sourceMappingURL=google-2FRVX6RS.js.map
package/dist/index.js CHANGED
@@ -136,7 +136,7 @@ function buildProgram(io = defaultIO) {
136
136
  });
137
137
  program.command("connect <provider>").description("Connect an ad account (guided wizard)").option("--no-browser", "Print auth URLs instead of opening a browser").action(async (provider, opts) => {
138
138
  if (provider === "google") {
139
- const { connectGoogle } = await import("./google-ZQQS6JUM.js");
139
+ const { connectGoogle } = await import("./google-2FRVX6RS.js");
140
140
  await connectGoogle({ openBrowser: opts.browser, io });
141
141
  return;
142
142
  }
@@ -156,7 +156,7 @@ function buildProgram(io = defaultIO) {
156
156
  return;
157
157
  }
158
158
  if (provider === "microsoft") {
159
- const { connectMicrosoft } = await import("./microsoft-QXMPCWJ2.js");
159
+ const { connectMicrosoft } = await import("./microsoft-ZZALD3EH.js");
160
160
  await connectMicrosoft({ openBrowser: opts.browser, io });
161
161
  return;
162
162
  }
@@ -4,7 +4,7 @@ import {
4
4
  generatePkce,
5
5
  openInBrowser,
6
6
  startLoopbackServer
7
- } from "./chunk-JPHFJ6HN.js";
7
+ } from "./chunk-CVLEVRS4.js";
8
8
 
9
9
  // src/connect/microsoft.ts
10
10
  import readline from "readline/promises";
@@ -34,7 +34,7 @@ async function connectMicrosoft({ openBrowser, io }) {
34
34
  io.out("");
35
35
  io.out("Starting the Microsoft sign-in (PKCE, no client secret)\u2026");
36
36
  const pkce = generatePkce();
37
- const loopback = await startLoopbackServer();
37
+ const loopback = await startLoopbackServer("localhost");
38
38
  const authUrl = buildMicrosoftAuthUrl(clientId, loopback.redirectUri, pkce.challenge);
39
39
  if (openBrowser) {
40
40
  openInBrowser(authUrl);
@@ -87,4 +87,4 @@ async function connectMicrosoft({ openBrowser, io }) {
87
87
  export {
88
88
  connectMicrosoft
89
89
  };
90
- //# sourceMappingURL=microsoft-QXMPCWJ2.js.map
90
+ //# sourceMappingURL=microsoft-ZZALD3EH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connect/microsoft.ts"],"sourcesContent":["import readline from 'node:readline/promises';\nimport { CredentialStore } from '@adport/core';\nimport { MicrosoftAdsClient, MicrosoftAdsProvider, SANDBOX_DEVELOPER_TOKEN } from '@adport/provider-microsoft';\nimport type { ProgramIO } from '../program.js';\nimport {\n buildMicrosoftAuthUrl,\n exchangeMicrosoftCode,\n generatePkce,\n openInBrowser,\n startLoopbackServer,\n} from './oauth.js';\n\n/**\n * Microsoft Advertising is the friendliest setup of all providers: the dev\n * token is self-serve (no review), and the sandbox has a public universal token.\n */\nexport async function connectMicrosoft({ openBrowser, io }: { openBrowser: boolean; io: ProgramIO }): Promise<void> {\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n const store = new CredentialStore();\n try {\n io.out('');\n io.out('Connecting Microsoft Advertising. Setup:');\n io.out(' 1. Azure Portal → App registrations → New: \"Mobile and desktop applications\"');\n io.out(' platform with redirect URI http://localhost (public client, no secret).');\n io.out(' 2. Developer token (self-serve, no review): sign in as Super Admin at');\n io.out(' https://ads.microsoft.com/cc/Settings/DevSettings → Request Token.');\n io.out(` 3. Sandbox alternative: universal token ${SANDBOX_DEVELOPER_TOKEN} works for everyone.`);\n io.out('');\n\n const sandboxAnswer = (await rl.question('Use the SANDBOX environment? [y/N] ')).trim().toLowerCase();\n const sandbox = sandboxAnswer === 'y' || sandboxAnswer === 'yes';\n const clientId = (await rl.question('Azure application (client) id: ')).trim();\n const developerToken = sandbox\n ? SANDBOX_DEVELOPER_TOKEN\n : (await rl.question('Developer token: ')).trim();\n if (!clientId || !developerToken) {\n io.err('Missing values — aborting.');\n process.exitCode = 1;\n return;\n }\n\n io.out('');\n io.out('Starting the Microsoft sign-in (PKCE, no client secret)…');\n const pkce = generatePkce();\n const loopback = await startLoopbackServer('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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adport",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "adport CLI — manage your ad accounts from the terminal or any AI agent",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -14,13 +14,13 @@
14
14
  "commander": "^13.0.0",
15
15
  "yaml": "^2.6.0",
16
16
  "zod": "^3.25.0",
17
- "@adport/core": "0.1.0",
18
- "@adport/mcp": "0.1.0",
19
- "@adport/provider-apple": "0.1.0",
20
- "@adport/provider-google": "0.1.0",
21
- "@adport/provider-meta": "0.1.0",
22
- "@adport/provider-microsoft": "0.1.0",
23
- "@adport/provider-tiktok": "0.1.0"
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"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^22.10.0",
@@ -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(): Promise<LoopbackServer> {\n let resolveCode!: (code: string) => void;\n let rejectCode!: (err: Error) => void;\n const waitForCode = new Promise<string>((resolve, reject) => {\n resolveCode = resolve;\n rejectCode = reject;\n });\n\n const server = http.createServer((req, res) => {\n const url = new URL(req.url ?? '/', 'http://127.0.0.1');\n const code = url.searchParams.get('code');\n const error = url.searchParams.get('error');\n res.writeHead(200, { 'content-type': 'text/html' });\n res.end(\n code\n ? '<h2>adport is connected.</h2><p>You can close this tab and return to the terminal.</p>'\n : `<h2>Authorization failed${error ? `: ${error}` : ''}</h2><p>Return to the terminal and retry.</p>`,\n );\n if (code) resolveCode(code);\n else if (error) rejectCode(new Error(`OAuth authorization failed: ${error}`));\n });\n\n await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));\n const address = server.address();\n if (!address || typeof address === 'string') throw new Error('Loopback server failed to bind');\n return {\n redirectUri: `http://127.0.0.1:${address.port}`,\n waitForCode,\n close: () => server.close(),\n };\n}\n\n// ---- Microsoft identity platform (public client + PKCE) ---------------------\n\nconst MS_AUTHORIZE = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize';\nconst MS_TOKEN = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';\nexport const MS_ADS_SCOPE = 'openid profile https://ads.microsoft.com/msads.manage offline_access';\n\nexport function generatePkce(): { verifier: string; challenge: string } {\n const verifier = randomBytes(32).toString('base64url');\n const challenge = createHash('sha256').update(verifier).digest('base64url');\n return { verifier, challenge };\n}\n\nexport function buildMicrosoftAuthUrl(clientId: string, redirectUri: string, challenge: string): string {\n const params = new URLSearchParams({\n client_id: clientId,\n response_type: 'code',\n redirect_uri: redirectUri,\n scope: MS_ADS_SCOPE,\n code_challenge: challenge,\n code_challenge_method: 'S256',\n prompt: 'login',\n });\n return `${MS_AUTHORIZE}?${params}`;\n}\n\nexport async function exchangeMicrosoftCode(\n input: { clientId: string; code: string; redirectUri: string; codeVerifier: string },\n fetchImpl: typeof fetch = fetch,\n): Promise<{ refreshToken: string }> {\n const response = await fetchImpl(MS_TOKEN, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n client_id: input.clientId,\n scope: 'https://ads.microsoft.com/msads.manage offline_access',\n code: input.code,\n redirect_uri: input.redirectUri,\n grant_type: 'authorization_code',\n code_verifier: input.codeVerifier,\n }),\n });\n const data = (await response.json()) as { refresh_token?: string; error?: string; error_description?: string };\n if (!response.ok || !data.refresh_token) {\n throw new AdportError(\n 'PROVIDER_ERROR',\n `Microsoft OAuth exchange failed${data.error ? ` (${data.error})` : ''}: ${data.error_description ?? ''}`,\n data,\n );\n }\n return { refreshToken: data.refresh_token };\n}\n\nexport function openInBrowser(url: string): void {\n const [cmd, args] =\n process.platform === 'darwin'\n ? ['open', [url]]\n : process.platform === 'win32'\n ? ['cmd', ['/c', 'start', '', url]]\n : ['xdg-open', [url]];\n spawn(cmd as string, args as string[], { detached: true, stdio: 'ignore' }).unref();\n}\n"],"mappings":"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","names":[]}
@@ -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();\n const authUrl = buildMicrosoftAuthUrl(clientId, loopback.redirectUri, pkce.challenge);\n if (openBrowser) {\n openInBrowser(authUrl);\n io.out(`If the browser did not open, visit:\\n ${authUrl}`);\n } else {\n io.out(`Open this URL in a browser on this machine:\\n ${authUrl}`);\n }\n let refreshToken: string;\n try {\n const code = await loopback.waitForCode;\n ({ refreshToken } = await exchangeMicrosoftCode({\n clientId,\n code,\n redirectUri: loopback.redirectUri,\n codeVerifier: pkce.verifier,\n }));\n } finally {\n loopback.close();\n }\n\n io.out('Verifying access…');\n const provider = new MicrosoftAdsProvider(\n new MicrosoftAdsClient({ developerToken, clientId, refreshToken, sandbox }),\n );\n const accounts = await provider.listAccounts();\n await store.set({\n provider: 'microsoft',\n source: 'byo',\n data: {\n developer_token: developerToken,\n client_id: clientId,\n refresh_token: refreshToken,\n ...(sandbox ? { sandbox: 'true' } : {}),\n },\n });\n io.out('');\n io.out(`✓ Connected${sandbox ? ' (sandbox)' : ''}. ${accounts.length} ad account(s):`);\n for (const account of accounts) {\n io.out(` ${account.id} ${account.name} ${account.currency ?? ''} ${account.status ?? ''}`);\n }\n io.out('Note: public-client refresh tokens expire after ~90 days of disuse; adport');\n io.out('persists rotated tokens automatically on every use.');\n io.out('');\n io.out('Try: adport accounts · adport report --provider microsoft · adport mcp');\n } finally {\n rl.close();\n }\n}\n"],"mappings":";;;;;;;;;AAAA,OAAO,cAAc;AACrB,SAAS,uBAAuB;AAChC,SAAS,oBAAoB,sBAAsB,+BAA+B;AAclF,eAAsB,iBAAiB,EAAE,aAAa,GAAG,GAA2D;AAClH,QAAM,KAAK,SAAS,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AACpF,QAAM,QAAQ,IAAI,gBAAgB;AAClC,MAAI;AACF,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,0CAA0C;AACjD,OAAG,IAAI,0FAAgF;AACvF,OAAG,IAAI,8EAA8E;AACrF,OAAG,IAAI,yEAAyE;AAChF,OAAG,IAAI,8EAAyE;AAChF,OAAG,IAAI,6CAA6C,uBAAuB,sBAAsB;AACjG,OAAG,IAAI,EAAE;AAET,UAAM,iBAAiB,MAAM,GAAG,SAAS,qCAAqC,GAAG,KAAK,EAAE,YAAY;AACpG,UAAM,UAAU,kBAAkB,OAAO,kBAAkB;AAC3D,UAAM,YAAY,MAAM,GAAG,SAAS,iCAAiC,GAAG,KAAK;AAC7E,UAAM,iBAAiB,UACnB,2BACC,MAAM,GAAG,SAAS,mBAAmB,GAAG,KAAK;AAClD,QAAI,CAAC,YAAY,CAAC,gBAAgB;AAChC,SAAG,IAAI,iCAA4B;AACnC,cAAQ,WAAW;AACnB;AAAA,IACF;AAEA,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,+DAA0D;AACjE,UAAM,OAAO,aAAa;AAC1B,UAAM,WAAW,MAAM,oBAAoB;AAC3C,UAAM,UAAU,sBAAsB,UAAU,SAAS,aAAa,KAAK,SAAS;AACpF,QAAI,aAAa;AACf,oBAAc,OAAO;AACrB,SAAG,IAAI;AAAA,IAA0C,OAAO,EAAE;AAAA,IAC5D,OAAO;AACL,SAAG,IAAI;AAAA,IAAkD,OAAO,EAAE;AAAA,IACpE;AACA,QAAI;AACJ,QAAI;AACF,YAAM,OAAO,MAAM,SAAS;AAC5B,OAAC,EAAE,aAAa,IAAI,MAAM,sBAAsB;AAAA,QAC9C;AAAA,QACA;AAAA,QACA,aAAa,SAAS;AAAA,QACtB,cAAc,KAAK;AAAA,MACrB,CAAC;AAAA,IACH,UAAE;AACA,eAAS,MAAM;AAAA,IACjB;AAEA,OAAG,IAAI,wBAAmB;AAC1B,UAAM,WAAW,IAAI;AAAA,MACnB,IAAI,mBAAmB,EAAE,gBAAgB,UAAU,cAAc,QAAQ,CAAC;AAAA,IAC5E;AACA,UAAM,WAAW,MAAM,SAAS,aAAa;AAC7C,UAAM,MAAM,IAAI;AAAA,MACd,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,iBAAiB;AAAA,QACjB,WAAW;AAAA,QACX,eAAe;AAAA,QACf,GAAI,UAAU,EAAE,SAAS,OAAO,IAAI,CAAC;AAAA,MACvC;AAAA,IACF,CAAC;AACD,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,mBAAc,UAAU,eAAe,EAAE,KAAK,SAAS,MAAM,iBAAiB;AACrF,eAAW,WAAW,UAAU;AAC9B,SAAG,IAAI,KAAK,QAAQ,EAAE,KAAK,QAAQ,IAAI,KAAK,QAAQ,YAAY,EAAE,KAAK,QAAQ,UAAU,EAAE,EAAE;AAAA,IAC/F;AACA,OAAG,IAAI,4EAA4E;AACnF,OAAG,IAAI,qDAAqD;AAC5D,OAAG,IAAI,EAAE;AACT,OAAG,IAAI,uFAAiF;AAAA,EAC1F,UAAE;AACA,OAAG,MAAM;AAAA,EACX;AACF;","names":[]}