@a8techads/cli 0.4.2 → 0.4.4

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.
Files changed (2) hide show
  1. package/dist/a8techads.js +895 -26
  2. package/package.json +1 -1
package/dist/a8techads.js CHANGED
@@ -4096,7 +4096,8 @@ function printSectionedCampaignDetail(campaign) {
4096
4096
  supplyProductCount: campaign.supplyProductCount ?? (Array.isArray(campaign.supplyProductIds) ? campaign.supplyProductIds.length : 0),
4097
4097
  supplyProducts: formatSupplyProducts(campaign.supplyProducts),
4098
4098
  sspPartnerIds: formatScalar(campaign.sspPartnerIds),
4099
- sourceMasking: formatScalar(campaign.sourceMasking)
4099
+ sourceMasking: formatScalar(campaign.sourceMasking),
4100
+ bidderAlgorithmId: campaign.bidderAlgorithmId
4100
4101
  }
4101
4102
  },
4102
4103
  {
@@ -4217,7 +4218,7 @@ Examples:
4217
4218
  }
4218
4219
  console.log(`Campaign created: ${json.data?.id ?? json.id}`);
4219
4220
  });
4220
- cmd.command("update").description("Update an existing campaign.").argument("<id>", "Campaign ID").option("--name <name>", "New name").option("--budget <amount>", "New daily budget").option("--max-bid <amount>", "Set priceSettings.maxBid without overwriting existing price settings").option("--clear-max-bid", "Remove priceSettings.maxBid without overwriting other price settings").option("--supply-products <ids>", "Comma-separated supply product bindings").option("--preferred-supply <ids>", "Deprecated alias for --supply-products").option("--clear-supply-products", "Remove all supply product bindings").option("--clear-preferred-supply", "Deprecated alias for --clear-supply-products").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
4221
+ cmd.command("update").description("Update an existing campaign.").argument("<id>", "Campaign ID").option("--name <name>", "New name").option("--budget <amount>", "New daily budget").option("--max-bid <amount>", "Set priceSettings.maxBid without overwriting existing price settings").option("--clear-max-bid", "Remove priceSettings.maxBid without overwriting other price settings").option("--supply-products <ids>", "Comma-separated supply product bindings").option("--preferred-supply <ids>", "Deprecated alias for --supply-products").option("--clear-supply-products", "Remove all supply product bindings").option("--clear-preferred-supply", "Deprecated alias for --clear-supply-products").option("--bidder-algorithm <id>", "Bind a bidder algorithm").option("--bidder-algorithm-id <id>", "Bind a bidder algorithm").option("--clear-bidder-algorithm", "Remove bidder algorithm binding").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
4221
4222
  let body;
4222
4223
  if (opts.fromJson) {
4223
4224
  const { readFileSync: readFileSync4 } = await import("fs");
@@ -4238,14 +4239,22 @@ Examples:
4238
4239
  console.error("Error: use either --supply-products or --clear-supply-products, not both.");
4239
4240
  process.exit(1);
4240
4241
  }
4242
+ const bidderAlgorithmId = opts.bidderAlgorithm ?? opts.bidderAlgorithmId;
4243
+ if (bidderAlgorithmId && opts.clearBidderAlgorithm) {
4244
+ console.error("Error: use either --bidder-algorithm or --clear-bidder-algorithm, not both.");
4245
+ process.exit(1);
4246
+ }
4241
4247
  const updatesSupplyBindings = Boolean(supplyProducts || clearSupplyProducts);
4242
- if (updatesSupplyBindings) {
4248
+ const updatesBidderAlgorithm = Boolean(bidderAlgorithmId || opts.clearBidderAlgorithm);
4249
+ if (updatesSupplyBindings || updatesBidderAlgorithm) {
4243
4250
  const campaign = await fetchCampaign(id);
4244
4251
  if (String(campaign.status || "").toUpperCase() === "ACTIVE") {
4245
- console.error("Error: active campaigns cannot change supply product bindings via update.");
4252
+ const field = updatesSupplyBindings ? "supply product bindings" : "bidder algorithm binding";
4253
+ const updateArg = updatesSupplyBindings ? supplyProducts ? `--supply-products ${supplyProducts}` : "--clear-supply-products" : bidderAlgorithmId ? `--bidder-algorithm ${bidderAlgorithmId}` : "--clear-bidder-algorithm";
4254
+ console.error(`Error: active campaigns cannot change ${field} via update.`);
4246
4255
  console.error(`Next steps:`);
4247
4256
  console.error(` a8techads campaigns pause ${id}`);
4248
- console.error(` a8techads campaigns update ${id} ${supplyProducts ? `--supply-products ${supplyProducts}` : "--clear-supply-products"}`);
4257
+ console.error(` a8techads campaigns update ${id} ${updateArg}`);
4249
4258
  console.error(` a8techads campaigns resume ${id}`);
4250
4259
  process.exit(1);
4251
4260
  }
@@ -4263,6 +4272,10 @@ Examples:
4263
4272
  body.supplyProductIds = parseCsvList(supplyProducts);
4264
4273
  if (clearSupplyProducts)
4265
4274
  body.supplyProductIds = [];
4275
+ if (bidderAlgorithmId)
4276
+ body.bidderAlgorithmId = bidderAlgorithmId;
4277
+ if (opts.clearBidderAlgorithm)
4278
+ body.bidderAlgorithmId = null;
4266
4279
  }
4267
4280
  const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/campaigns/${id}`, body });
4268
4281
  if (!resp.ok) {
@@ -5129,7 +5142,16 @@ Examples:
5129
5142
  }
5130
5143
  console.log(`Zone ${id} deleted.`);
5131
5144
  });
5132
- cmd.command("tag").description("Get the ad tag code for a zone (JS or VAST).").argument("<id>", "Zone ID").action(async (id) => {
5145
+ cmd.command("tag").description("Get or generate the ad tag code for a zone.").argument("<id>", "Zone ID").option("--local", "Generate tag locally without calling the API").option("--variant <variant>", "Tag variant: js or iframe", "js").option("--format <format>", "Zone format: banner, popunder, offerwall, rewarded_video, native, video").option("--ad-server <url>", "Bidder/ad server URL", "https://bidder.a8.tech").option("--loader-url <url>", "Loader script URL", "https://assets.a8.tech/tags/v1/loader.js").option("--sizes <sizes>", "Comma-separated sizes for display tags, e.g. 728x90,300x250").option("--theme <theme>", "Theme for offerwall/rewarded tags, e.g. light or dark").option("--sub <sub>", "Publisher player/user identifier macro", "{YOUR_PLAYER_ID}").option("--keywords <keywords>", "Comma-separated targeting keywords").option("--limit <n>", "Offerwall max offers", "20").option("--button-text <text>", "Rewarded video button text", "Watch and earn reward").addHelpText("after", `
5146
+ Examples:
5147
+ $ a8techads zones tag <id>
5148
+ $ a8techads zones tag <id> --local --format offerwall --theme dark --sub player-123
5149
+ $ a8techads zones tag <id> --local --format rewarded_video --button-text "Watch ad"
5150
+ $ a8techads zones tag <id> --local --variant iframe --format banner --sizes 728x90`).action(async (id, opts) => {
5151
+ if (shouldGenerateLocalTag(opts)) {
5152
+ console.log(buildZoneTag(id, opts));
5153
+ return;
5154
+ }
5133
5155
  const resp = await apiRequest({ path: `${sspPrefix()}/zones/${id}/tag` });
5134
5156
  const json = await resp.json();
5135
5157
  if (!resp.ok) {
@@ -5140,6 +5162,111 @@ Examples:
5140
5162
  });
5141
5163
  return cmd;
5142
5164
  }
5165
+ function shouldGenerateLocalTag(opts) {
5166
+ return Boolean(opts.local || opts.variant !== "js" || opts.format || opts.theme || opts.sub !== "{YOUR_PLAYER_ID}" || opts.keywords || opts.sizes || opts.limit !== "20" || opts.buttonText !== "Watch and earn reward");
5167
+ }
5168
+ function buildZoneTag(zoneId, opts) {
5169
+ const format = normalizeFormat(opts.format);
5170
+ if (!format) {
5171
+ console.error("Error: --format is required when generating a local tag.");
5172
+ process.exit(1);
5173
+ }
5174
+ const variant = String(opts.variant).trim().toLowerCase();
5175
+ if (variant === "iframe")
5176
+ return buildIframeTag(zoneId, format, opts);
5177
+ if (variant !== "js") {
5178
+ console.error("Error: --variant must be js or iframe.");
5179
+ process.exit(1);
5180
+ }
5181
+ return buildJavascriptTag(zoneId, format, opts);
5182
+ }
5183
+ function buildJavascriptTag(zoneId, format, opts) {
5184
+ const divId = `alpineads-${zoneId.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 20)}`;
5185
+ const attrs = {
5186
+ id: divId,
5187
+ "data-zone": zoneId,
5188
+ "data-format": format,
5189
+ "data-ad-server": cleanUrl(opts.adServer)
5190
+ };
5191
+ if (opts.sizes)
5192
+ attrs["data-sizes"] = opts.sizes;
5193
+ if (format === "offerwall") {
5194
+ attrs["data-limit"] = String(opts.limit ?? "20");
5195
+ attrs["data-theme"] = opts.theme ?? "dark";
5196
+ attrs["data-sub"] = opts.sub;
5197
+ }
5198
+ if (format === "rewarded_video") {
5199
+ attrs["data-theme"] = opts.theme ?? "dark";
5200
+ attrs["data-sub"] = opts.sub;
5201
+ attrs["data-button-text"] = opts.buttonText;
5202
+ }
5203
+ if (opts.keywords)
5204
+ attrs["data-keywords"] = opts.keywords;
5205
+ const attrLines = Object.entries(attrs).map(([key, value], index) => `${index === 0 ? "" : " "}${key}="${escapeHtml(String(value ?? ""))}"`).join(`
5206
+ `);
5207
+ return `<!-- AlpineAds Zone: ${zoneId} -->
5208
+ <div ${attrLines}>
5209
+ </div>
5210
+ <script async src="${escapeHtml(cleanUrl(opts.loaderUrl))}"></script>`;
5211
+ }
5212
+ function buildIframeTag(zoneId, format, opts) {
5213
+ const params = new URLSearchParams;
5214
+ params.set("format", format);
5215
+ if (opts.sizes)
5216
+ params.set("sizes", opts.sizes);
5217
+ if (shouldIncludeSub(format, opts.sub))
5218
+ params.set("sub", opts.sub);
5219
+ if (opts.keywords)
5220
+ params.set("keywords", opts.keywords);
5221
+ if (format === "offerwall") {
5222
+ params.set("limit", String(opts.limit ?? "20"));
5223
+ params.set("theme", opts.theme ?? "dark");
5224
+ }
5225
+ if (format === "rewarded_video") {
5226
+ params.set("theme", opts.theme ?? "dark");
5227
+ }
5228
+ const size = iframeSize(format, opts.sizes);
5229
+ const src = `${cleanUrl(opts.adServer)}/ad/${encodeURIComponent(zoneId)}?${params.toString()}`;
5230
+ return `<!-- AlpineAds Zone: ${zoneId} -->
5231
+ <iframe src="${escapeHtml(src)}"
5232
+ width="${size.width}" height="${size.height}"
5233
+ frameborder="0" scrolling="no"
5234
+ style="border:none;overflow:hidden"></iframe>`;
5235
+ }
5236
+ function normalizeFormat(value) {
5237
+ if (!value)
5238
+ return null;
5239
+ const normalized = String(value).trim().toLowerCase().replace(/-/g, "_");
5240
+ if (normalized === "rewarded" || normalized === "rewardedvideo")
5241
+ return "rewarded_video";
5242
+ if (normalized === "offer_wall")
5243
+ return "offerwall";
5244
+ return normalized;
5245
+ }
5246
+ function shouldIncludeSub(format, sub) {
5247
+ if (!sub)
5248
+ return false;
5249
+ if (format === "offerwall" || format === "rewarded_video")
5250
+ return true;
5251
+ return String(sub) !== "{YOUR_PLAYER_ID}";
5252
+ }
5253
+ function iframeSize(format, sizes) {
5254
+ const firstSize = String(sizes ?? "").split(",")[0]?.trim();
5255
+ const match = firstSize?.match(/^(\d+)x(\d+)$/);
5256
+ if (match)
5257
+ return { width: match[1], height: match[2] };
5258
+ if (format === "offerwall")
5259
+ return { width: "320", height: "480" };
5260
+ if (format === "rewarded_video")
5261
+ return { width: "640", height: "360" };
5262
+ return { width: "300", height: "250" };
5263
+ }
5264
+ function cleanUrl(value) {
5265
+ return String(value).replace(/\/+$/, "");
5266
+ }
5267
+ function escapeHtml(value) {
5268
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5269
+ }
5143
5270
 
5144
5271
  // src/commands/reports.ts
5145
5272
  function createReportsCommand() {
@@ -5148,6 +5275,7 @@ function createReportsCommand() {
5148
5275
  Routes based on current capability (DSP or SSP).`).addHelpText("after", `
5149
5276
  Examples:
5150
5277
  $ a8techads reports query --metrics spend,impressions --dimensions date
5278
+ $ a8techads reports ssp-pull --from 2026-06-14 --to 2026-06-15 --group-by zone,day,country
5151
5279
  $ a8techads reports list
5152
5280
  $ a8techads reports templates`);
5153
5281
  addFormatOption(cmd.command("query").description(`Execute an ad-hoc analytics query.
@@ -5195,6 +5323,54 @@ Summary:`);
5195
5323
  }
5196
5324
  }
5197
5325
  });
5326
+ addFormatOption(cmd.command("ssp-pull").description(`Pull authoritative SSP reporting data for reconciliation.
5327
+
5328
+ Requires: PUBLISHER capability and an SSP tenant context.`).requiredOption("--from <date>", "Start date/time, e.g. 2026-06-14 or 2026-06-14T00:00:00Z").requiredOption("--to <date>", "End date/time, e.g. 2026-06-15 or 2026-06-15T00:00:00Z").option("--group-by <list>", "Comma-separated group dimensions", "zone,day").option("--metrics <list>", "Comma-separated metrics", "requests,fills,impressions,clicks,revenue").option("--timezone <tz>", "Report timezone", "UTC").option("--zone <id>", "Filter by zone ID").option("--site <id>", "Filter by site ID").option("--country <code>", "Filter by ISO country code").option("--device <type>", "Filter by device type").option("--campaign <id>", "Filter by campaign ID").option("--variation <id>", "Filter by variation ID").option("--format-type <format>", "Filter by ad format/zone type").option("--sub-id1 <value>", "Filter by sub_id1").option("--limit <n>", "Max rows", "500").option("--offset <n>", "Result offset", "0").addHelpText("after", `
5329
+ Examples:
5330
+ $ a8techads reports ssp-pull --from 2026-06-14 --to 2026-06-15 --group-by zone,day,country
5331
+ $ a8techads reports ssp-pull --from 2026-06-14 --to 2026-06-15 --zone <zone-id> --metrics impressions,clicks,revenue --format json`)).action(async (opts) => {
5332
+ const params = new URLSearchParams;
5333
+ params.set("from", opts.from);
5334
+ params.set("to", opts.to);
5335
+ params.set("group_by", opts.groupBy);
5336
+ params.set("metrics", opts.metrics);
5337
+ params.set("timezone", opts.timezone);
5338
+ params.set("limit", opts.limit);
5339
+ params.set("offset", opts.offset);
5340
+ const filters = [
5341
+ ["zone_id", opts.zone],
5342
+ ["site_id", opts.site],
5343
+ ["country", opts.country],
5344
+ ["device", opts.device],
5345
+ ["campaign_id", opts.campaign],
5346
+ ["variation_id", opts.variation],
5347
+ ["format", opts.formatType],
5348
+ ["sub_id1", opts.subId1]
5349
+ ];
5350
+ for (const [key, value] of filters) {
5351
+ if (value !== undefined && value !== null && String(value) !== "")
5352
+ params.set(key, String(value));
5353
+ }
5354
+ const resp = await apiRequest({ path: `${sspPrefix()}/reports?${params}` });
5355
+ const json = await resp.json();
5356
+ if (!resp.ok) {
5357
+ console.error(`Error: ${json.error ?? json.message ?? resp.statusText}`);
5358
+ process.exit(1);
5359
+ }
5360
+ const data = json.data ?? json;
5361
+ const rows = data.rows ?? data.results ?? data;
5362
+ if (opts.format === "json") {
5363
+ console.log(JSON.stringify(data, null, 2));
5364
+ return;
5365
+ }
5366
+ if (!Array.isArray(rows) || rows.length === 0) {
5367
+ console.log("No results.");
5368
+ return;
5369
+ }
5370
+ const keys = Object.keys(rows[0]);
5371
+ const columns = keys.map((k) => ({ key: k, header: k.toUpperCase(), width: Math.max(k.length, 12) }));
5372
+ printData(rows, columns, opts.format);
5373
+ });
5198
5374
  addFormatOption(cmd.command("list").description("List saved reports.")).action(async (opts) => {
5199
5375
  const resp = await apiRequest({ path: `${getApiPrefix()}/reports/saved` });
5200
5376
  const json = await resp.json();
@@ -5614,7 +5790,7 @@ Examples:
5614
5790
  }
5615
5791
  printDetail(json.data ?? json, opts.format);
5616
5792
  });
5617
- tenants.command("create").description("Create a new tenant with admin user.").option("--name <name>", "Company name (required)").option("--email <email>", "Admin email (required)").option("--admin-name <name>", "Admin user name").option("--capabilities <caps>", "Comma-separated capabilities: ADVERTISER,PUBLISHER", "ADVERTISER").addHelpText("after", `
5793
+ tenants.command("create").description("Create a new tenant with admin user.").option("--name <name>", "Company name (required)").option("--email <email>", "Admin email (required unless --tenant-type is INTERNAL_SSP or EXTERNAL_SSP)").option("--admin-name <name>", "Admin user name").option("--tenant-type <type>", "Tenant type: ADVERTISER, PUBLISHER, INTERNAL_SSP, EXTERNAL_SSP").option("--capabilities <caps>", "Comma-separated capabilities: ADVERTISER,PUBLISHER", "ADVERTISER").addHelpText("after", `
5618
5794
  Examples:
5619
5795
  $ a8techads admin tenants create --name "Acme Corp" --email admin@acme.com
5620
5796
  $ a8techads admin tenants create --name "MediaCo" --email admin@media.co --capabilities ADVERTISER,PUBLISHER`).action(async (opts) => {
@@ -5622,17 +5798,20 @@ Examples:
5622
5798
  console.error("Error: --name is required.");
5623
5799
  process.exit(1);
5624
5800
  }
5625
- if (!opts.email) {
5801
+ const tenantType = opts.tenantType ? String(opts.tenantType).trim().toUpperCase() : undefined;
5802
+ const isSupplyPartner = tenantType === "INTERNAL_SSP" || tenantType === "EXTERNAL_SSP";
5803
+ if (!opts.email && !isSupplyPartner) {
5626
5804
  console.error("Error: --email is required.");
5627
5805
  process.exit(1);
5628
5806
  }
5629
- const capabilities = opts.capabilities.split(",").map((c) => c.trim().toUpperCase());
5807
+ const capabilities = isSupplyPartner ? [] : opts.capabilities.split(",").map((c) => c.trim().toUpperCase());
5630
5808
  const body = {
5631
5809
  tenant: {
5632
5810
  companyName: opts.name,
5811
+ tenantType,
5633
5812
  capabilities
5634
5813
  },
5635
- admin: {
5814
+ admin: isSupplyPartner ? undefined : {
5636
5815
  email: opts.email,
5637
5816
  name: opts.adminName ?? undefined
5638
5817
  }
@@ -5764,11 +5943,104 @@ Examples:
5764
5943
  { key: "time", header: "TIME", width: 22 }
5765
5944
  ], opts.format);
5766
5945
  });
5946
+ const rateLimits = cmd.command("rate-limits").description("Business rate-limit configuration.").addHelpText("after", `
5947
+ Examples:
5948
+ $ a8techads admin rate-limits list --format table
5949
+ $ a8techads admin rate-limits upsert-partner PROPELLERADS --qps 500 --yes
5950
+ $ a8techads admin rate-limits upsert-partner HILLTOPADS --qps 100 --yes`);
5951
+ function normalizeRateLimit(config) {
5952
+ return {
5953
+ id: config.id,
5954
+ name: config.name ?? config.targetName ?? config.target_name,
5955
+ configType: config.configType ?? config.config_type,
5956
+ targetId: config.targetId ?? config.target_id,
5957
+ qpsLimit: config.qpsLimit ?? config.qps_limit,
5958
+ windowSizeMs: config.windowSizeMs ?? config.window_size_ms,
5959
+ burstAllowance: config.burstAllowance ?? config.burst_allowance,
5960
+ strategy: config.strategy,
5961
+ isEnabled: config.isEnabled ?? config.is_enabled,
5962
+ updatedAt: config.updatedAt ?? config.updated_at
5963
+ };
5964
+ }
5965
+ addFormatOption(rateLimits.command("list").description("List business rate-limit configs.").option("--type <type>", "Filter by config type, e.g. global or partner").option("--limit <n>", "Max results", "100")).action(async (opts) => {
5966
+ const params = new URLSearchParams({ limit: opts.limit });
5967
+ if (opts.type)
5968
+ params.set("configType", opts.type);
5969
+ const resp = await apiRequest({ path: `${consolePrefix()}/rate-limits/config?${params}` });
5970
+ const json = await resp.json();
5971
+ if (!resp.ok) {
5972
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
5973
+ process.exit(1);
5974
+ }
5975
+ const rows = (json.data ?? json).map(normalizeRateLimit);
5976
+ printData(rows, [
5977
+ { key: "id", header: "ID", width: 36 },
5978
+ { key: "configType", header: "TYPE", width: 10 },
5979
+ { key: "name", header: "NAME", width: 20 },
5980
+ { key: "qpsLimit", header: "QPS", width: 8 },
5981
+ { key: "windowSizeMs", header: "WINDOW", width: 8 },
5982
+ { key: "burstAllowance", header: "BURST", width: 8 },
5983
+ { key: "strategy", header: "STRATEGY", width: 10 },
5984
+ { key: "isEnabled", header: "ENABLED", width: 8 }
5985
+ ], opts.format);
5986
+ });
5987
+ addFormatOption(rateLimits.command("upsert-partner").description("Create or update a partner business QPS limit by partner code.").argument("<partner-code>", "Partner code, e.g. PROPELLERADS or HILLTOPADS").requiredOption("--qps <n>", "QPS limit").option("--window-ms <n>", "Window size in milliseconds", "1000").option("--burst <n>", "Burst allowance multiplier", "1.5").option("--strategy <strategy>", "Limit strategy", "drop").option("--disable", "Create/update as disabled", false).option("--yes", "Skip confirmation", false)).action(async (partnerCode, opts) => {
5988
+ const normalizedPartner = String(partnerCode).trim().toUpperCase();
5989
+ const qps = Number.parseInt(opts.qps, 10);
5990
+ const windowMs = Number.parseInt(opts.windowMs, 10);
5991
+ const burst = Number.parseFloat(opts.burst);
5992
+ if (!normalizedPartner) {
5993
+ console.error("Error: partner code is required.");
5994
+ process.exit(1);
5995
+ }
5996
+ if (!Number.isFinite(qps) || qps < 0) {
5997
+ console.error("Error: --qps must be a non-negative integer.");
5998
+ process.exit(1);
5999
+ }
6000
+ if (!Number.isFinite(windowMs) || windowMs <= 0) {
6001
+ console.error("Error: --window-ms must be a positive integer.");
6002
+ process.exit(1);
6003
+ }
6004
+ if (!Number.isFinite(burst) || burst <= 0) {
6005
+ console.error("Error: --burst must be a positive number.");
6006
+ process.exit(1);
6007
+ }
6008
+ await confirmAction(`Set business rate limit for ${normalizedPartner} to ${qps} QPS?`, opts.yes);
6009
+ const listResp = await apiRequest({ path: `${consolePrefix()}/rate-limits/config?configType=partner&limit=100` });
6010
+ const listJson = await listResp.json();
6011
+ if (!listResp.ok) {
6012
+ console.error(`Error: ${listJson.error ?? listJson.errors ?? listResp.statusText}`);
6013
+ process.exit(1);
6014
+ }
6015
+ const existing = (listJson.data ?? listJson).find((config) => {
6016
+ const name = String(config.name ?? config.targetName ?? config.target_name ?? "").trim().toUpperCase();
6017
+ return name === normalizedPartner;
6018
+ });
6019
+ const body = {
6020
+ configType: "partner",
6021
+ name: normalizedPartner,
6022
+ targetName: normalizedPartner,
6023
+ qpsLimit: qps,
6024
+ windowSizeMs: windowMs,
6025
+ burstAllowance: burst,
6026
+ strategy: opts.strategy,
6027
+ dailyRequestLimit: 0,
6028
+ isEnabled: !opts.disable
6029
+ };
6030
+ const resp = existing ? await apiRequest({ method: "PATCH", path: `${consolePrefix()}/rate-limits/${existing.id}`, body }) : await apiRequest({ method: "POST", path: `${consolePrefix()}/rate-limits`, body });
6031
+ const json = await resp.json();
6032
+ if (!resp.ok) {
6033
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
6034
+ process.exit(1);
6035
+ }
6036
+ printDetail(normalizeRateLimit(json.data ?? json), opts.format);
6037
+ });
5767
6038
  const campaignReviews = cmd.command("campaign-reviews").description("Campaign review operations (Console).").addHelpText("after", `
5768
6039
  Examples:
5769
6040
  $ a8techads admin campaign-reviews pending
5770
6041
  $ a8techads admin campaign-reviews show <campaign-id>
5771
6042
  $ a8techads admin campaign-reviews approve-all <campaign-id> --yes
6043
+ $ a8techads admin campaign-reviews approve-variation <variation-id> --yes
5772
6044
  $ a8techads admin campaign-reviews decline <campaign-id> --reason-code policy --feedback "Needs changes" --yes`);
5773
6045
  const reviewColumns = [
5774
6046
  { key: "id", header: "ID", width: 36 },
@@ -5845,6 +6117,16 @@ Variations`);
5845
6117
  }
5846
6118
  printDetail(json.data ?? json, opts.format);
5847
6119
  });
6120
+ addFormatOption(campaignReviews.command("approve-variation").description("Approve one pending variation without changing the parent campaign status.").argument("<id>", "Variation ID").option("--yes", "Skip confirmation", false)).action(async (id, opts) => {
6121
+ await confirmAction(`Approve variation ${id}?`, opts.yes);
6122
+ const resp = await apiRequest({ method: "PATCH", path: `${consolePrefix()}/variations/${id}/approve` });
6123
+ const json = await resp.json();
6124
+ if (!resp.ok) {
6125
+ console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
6126
+ process.exit(1);
6127
+ }
6128
+ printDetail(json.data ?? json, opts.format);
6129
+ });
5848
6130
  addFormatOption(campaignReviews.command("decline").description("Decline all pending variations for a campaign atomically.").argument("<id>", "Campaign ID").option("--feedback <text>", "Review feedback").option("--reason-code <code>", "Review reason code").option("--yes", "Skip confirmation", false)).action(async (id, opts) => {
5849
6131
  await confirmAction(`Decline campaign ${id}?`, opts.yes);
5850
6132
  const body = {};
@@ -6910,9 +7192,11 @@ Examples:
6910
7192
  var COLUMNS9 = [
6911
7193
  { key: "id", header: "ID", width: 36 },
6912
7194
  { key: "name", header: "NAME", width: 24 },
7195
+ { key: "codes", header: "CODES", width: 24 },
6913
7196
  { key: "goalOrder", header: "G#", width: 4 },
6914
7197
  { key: "conversionType", header: "TYPE", width: 20 },
6915
7198
  { key: "valueType", header: "VALUE", width: 10 },
7199
+ { key: "signatureRequired", header: "HMAC", width: 8 },
6916
7200
  { key: "status", header: "STATUS", width: 10 }
6917
7201
  ];
6918
7202
  function createConversionGoalsCommand() {
@@ -6923,6 +7207,7 @@ Examples:
6923
7207
  $ a8techads conversion-goals list
6924
7208
  $ a8techads conversion-goals get <id>
6925
7209
  $ a8techads conversion-goals create --name "Purchase" --conversion-type PURCHASE_CC --goal-order 1
7210
+ $ a8techads conversion-goals integration <id> --status install
6926
7211
  $ a8techads conversion-goals pause <id>`);
6927
7212
  addFormatOption(cmd.command("list").description("List conversion goals.").option("--status <status>", "Filter by status (ACTIVE, PAUSED, ARCHIVED)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
6928
7213
  const params = new URLSearchParams;
@@ -6939,8 +7224,10 @@ Examples:
6939
7224
  id: g.id,
6940
7225
  name: g.name,
6941
7226
  goalOrder: `G${g.goalOrder ?? g.goal_order}`,
7227
+ codes: Array.isArray(g.codes) ? g.codes.join(",") : "",
6942
7228
  conversionType: g.conversionType ?? g.conversion_type,
6943
7229
  valueType: g.valueType ?? g.value_type,
7230
+ signatureRequired: g.signatureRequired ?? g.signature_required ? "required" : "optional",
6944
7231
  status: g.status
6945
7232
  }));
6946
7233
  printData(rows, COLUMNS9, opts.format);
@@ -6954,10 +7241,45 @@ Examples:
6954
7241
  }
6955
7242
  printDetail(json.data ?? json, opts.format);
6956
7243
  });
7244
+ cmd.command("integration").description("Print S2S postback and pixel integration examples for a conversion goal.").argument("<id>", "Goal ID").option("--base-url <url>", "Bidder/conversion base URL", "https://bidder.a8.tech").option("--click-id <macro>", "Click ID macro or test click ID", "{click_id}").option("--status <code>", "External code/status sample, e.g. install or registration").option("--value <amount>", "Conversion value sample").action(async (id, opts) => {
7245
+ const resp = await apiRequest({ path: `${dspPrefix()}/conversion-goals/${id}` });
7246
+ const json = await resp.json();
7247
+ if (!resp.ok) {
7248
+ console.error(`Error: ${json.error ?? resp.statusText}`);
7249
+ process.exit(1);
7250
+ }
7251
+ const goal = json.data ?? json;
7252
+ const goalId = goal.goalId ?? goal.goal_id ?? goal.id ?? id;
7253
+ const codes = Array.isArray(goal.codes) ? goal.codes : [];
7254
+ const base = cleanBaseUrl(opts.baseUrl);
7255
+ const baseUrl = buildConversionUrl(base, goalId, opts.clickId, undefined, opts.value);
7256
+ const statusUrl = opts.status ? buildConversionUrl(base, goalId, opts.clickId, opts.status, opts.value) : null;
7257
+ const pixelUrl = `${base}/conversion.gif?${conversionParams(goalId, opts.clickId, opts.status, opts.value)}`;
7258
+ console.log(`Goal: ${goal.name ?? id}`);
7259
+ console.log(`Goal ID: ${goalId}`);
7260
+ console.log(`External codes: ${codes.length > 0 ? codes.join(", ") : "(none)"}`);
7261
+ console.log("");
7262
+ console.log("S2S postback:");
7263
+ console.log(baseUrl);
7264
+ if (statusUrl) {
7265
+ console.log("");
7266
+ console.log(`Status/code postback sample (${opts.status}):`);
7267
+ console.log(statusUrl);
7268
+ }
7269
+ console.log("");
7270
+ console.log("Browser pixel sample:");
7271
+ console.log(`<!-- AlpineAds Conversion Pixel -->`);
7272
+ console.log(`<img src="${pixelUrl}" width="1" height="1" style="display:none" alt="" />`);
7273
+ console.log("");
7274
+ console.log("Notes:");
7275
+ console.log("- Replace {click_id} with the click ID captured from the campaign tracking URL.");
7276
+ console.log("- status=<code> is matched against this goal external codes, for tracker integrations such as install or registration.");
7277
+ console.log("- DYNAMIC value goals should use signed server-to-server postbacks; browser pixels are only for validation or lightweight fixed/no-value goals.");
7278
+ });
6957
7279
  cmd.command("create").description(`Create a conversion goal.
6958
7280
 
6959
7281
  Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
6960
- PURCHASE_CARRIER, SUBSCRIPTION_CC, SUBSCRIPTION_CARRIER, WEBSITE_INTERACTION, MULTIPLE, OTHER`).option("--name <name>", "Goal name (required)").option("--conversion-type <type>", "Conversion type (required)").option("--goal-order <n>", "Priority 1-10, maps to G1-G10 (required)").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC", "NO_VALUE").option("--fixed-value <amount>", "Fixed value per conversion (when value-type is FIXED)").option("--count-type <type>", "ONE per user or EVERY conversion", "EVERY").option("--window <hours>", "Attribution window in hours", "720").option("--description <text>", "Goal description").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
7282
+ PURCHASE_CARRIER, SUBSCRIPTION_CC, SUBSCRIPTION_CARRIER, WEBSITE_INTERACTION, MULTIPLE, OTHER`).option("--name <name>", "Goal name (required)").option("--code <code>", "Single external tracker alias/status for this goal").option("--codes <codes>", "Comma-separated external tracker aliases/statuses for this goal").option("--conversion-type <type>", "Conversion type (required)").option("--goal-order <n>", "Priority 1-10, maps to G1-G10 (required)").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC", "NO_VALUE").option("--fixed-value <amount>", "Fixed value per conversion (when value-type is FIXED)").option("--signature-required <bool>", "Require HMAC signature for DYNAMIC goals (true/false)", "false").option("--count-type <type>", "ONE per user or EVERY conversion", "EVERY").option("--window <hours>", "Attribution window in hours", "720").option("--description <text>", "Goal description").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
6961
7283
  let body;
6962
7284
  if (opts.fromJson) {
6963
7285
  const { readFileSync: readFileSync5 } = await import("fs");
@@ -6977,9 +7299,11 @@ Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
6977
7299
  }
6978
7300
  body = {
6979
7301
  name: opts.name,
7302
+ codes: parseCodes(opts.codes ?? opts.code),
6980
7303
  conversionType: opts.conversionType,
6981
7304
  goalOrder: Number(opts.goalOrder),
6982
7305
  valueType: opts.valueType,
7306
+ signatureRequired: parseBoolOption(opts.signatureRequired),
6983
7307
  countType: opts.countType,
6984
7308
  conversionWindowHours: Number(opts.window)
6985
7309
  };
@@ -6999,7 +7323,7 @@ Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
6999
7323
  console.log(` Goal ID: ${goal.goalId ?? goal.goal_id}`);
7000
7324
  console.log(` Postback URL: ${goal.postbackUrl ?? goal.postback_url ?? "(see get)"}`);
7001
7325
  });
7002
- cmd.command("update").description("Update a conversion goal.").argument("<id>", "Goal ID").option("--name <name>", "New name").option("--description <text>", "New description").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC").option("--fixed-value <amount>", "Fixed value").option("--count-type <type>", "ONE or EVERY").option("--window <hours>", "Attribution window").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
7326
+ cmd.command("update").description("Update a conversion goal.").argument("<id>", "Goal ID").option("--name <name>", "New name").option("--code <code>", "Replace external tracker aliases with one code").option("--codes <codes>", "Replace external tracker aliases with comma-separated codes").option("--description <text>", "New description").option("--value-type <type>", "NO_VALUE, FIXED, or DYNAMIC").option("--fixed-value <amount>", "Fixed value").option("--signature-required <bool>", "Require HMAC signature for DYNAMIC goals (true/false)").option("--count-type <type>", "ONE or EVERY").option("--window <hours>", "Attribution window").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
7003
7327
  let body;
7004
7328
  if (opts.fromJson) {
7005
7329
  const { readFileSync: readFileSync5 } = await import("fs");
@@ -7008,12 +7332,16 @@ Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
7008
7332
  body = {};
7009
7333
  if (opts.name)
7010
7334
  body.name = opts.name;
7335
+ if (opts.codes !== undefined || opts.code !== undefined)
7336
+ body.codes = parseCodes(opts.codes ?? opts.code);
7011
7337
  if (opts.description)
7012
7338
  body.description = opts.description;
7013
7339
  if (opts.valueType)
7014
7340
  body.valueType = opts.valueType;
7015
7341
  if (opts.fixedValue)
7016
7342
  body.fixedValue = Number(opts.fixedValue);
7343
+ if (opts.signatureRequired !== undefined)
7344
+ body.signatureRequired = parseBoolOption(opts.signatureRequired);
7017
7345
  if (opts.countType)
7018
7346
  body.countType = opts.countType;
7019
7347
  if (opts.window)
@@ -7063,6 +7391,40 @@ Conversion types: APP_INSTALL, LEAD_SOI, LEAD_DOI, PURCHASE_CC, PURCHASE_COD,
7063
7391
  });
7064
7392
  return cmd;
7065
7393
  }
7394
+ function parseBoolOption(value) {
7395
+ if (typeof value === "boolean")
7396
+ return value;
7397
+ const normalized = String(value ?? "").trim().toLowerCase();
7398
+ if (["true", "1", "yes", "y"].includes(normalized))
7399
+ return true;
7400
+ if (["false", "0", "no", "n", ""].includes(normalized))
7401
+ return false;
7402
+ console.error(`Error: expected boolean value, got ${value}`);
7403
+ process.exit(1);
7404
+ }
7405
+ function parseCodes(value) {
7406
+ if (value === undefined || value === null)
7407
+ return [];
7408
+ const values = Array.isArray(value) ? value : String(value).split(",");
7409
+ const normalized = values.map((item) => String(item).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "")).filter(Boolean);
7410
+ return Array.from(new Set(normalized));
7411
+ }
7412
+ function cleanBaseUrl(value) {
7413
+ return String(value).replace(/\/+$/, "");
7414
+ }
7415
+ function buildConversionUrl(baseUrl, goalId, clickId, status, value) {
7416
+ return `${baseUrl}/conversion?${conversionParams(goalId, clickId, status, value)}`;
7417
+ }
7418
+ function conversionParams(goalId, clickId, status, value) {
7419
+ const params = new URLSearchParams;
7420
+ params.set("goal", goalId);
7421
+ params.set("click_id", clickId);
7422
+ if (status)
7423
+ params.set("status", status);
7424
+ if (value)
7425
+ params.set("value", value);
7426
+ return params.toString().replace(/%7B/g, "{").replace(/%7D/g, "}");
7427
+ }
7066
7428
 
7067
7429
  // src/commands/algorithms.ts
7068
7430
  var COLUMNS10 = [
@@ -7073,6 +7435,18 @@ var COLUMNS10 = [
7073
7435
  { key: "status", header: "STATUS", width: 10 },
7074
7436
  { key: "campaignCount", header: "CAMPAIGNS", width: 10 }
7075
7437
  ];
7438
+ var RULE_COLUMNS = [
7439
+ { key: "id", header: "ID", width: 36 },
7440
+ { key: "name", header: "NAME", width: 28 },
7441
+ { key: "dimension", header: "DIMENSION", width: 14 },
7442
+ { key: "metric", header: "METRIC", width: 12 },
7443
+ { key: "threshold", header: "THRESHOLD", width: 10 },
7444
+ { key: "check", header: "CHECK", width: 24 },
7445
+ { key: "action", header: "ACTION", width: 12 },
7446
+ { key: "bidModifier", header: "MOD", width: 8 },
7447
+ { key: "isActive", header: "ACTIVE", width: 8 },
7448
+ { key: "priority", header: "PRI", width: 6 }
7449
+ ];
7076
7450
  function createAlgorithmsCommand() {
7077
7451
  const cmd = new Command("algorithms").description(`Bidder algorithm management (DSP)
7078
7452
 
@@ -7081,7 +7455,9 @@ Examples:
7081
7455
  $ a8techads algorithms list
7082
7456
  $ a8techads algorithms get <id>
7083
7457
  $ a8techads algorithms create --name "CPA Base" --optimization-goal CPA --target-value 5
7084
- $ a8techads algorithms update <id> --conversion-goal-id <goal-id>
7458
+ $ a8techads algorithms update <id> --conversion-goal-id <goal-id> --auto-adjust-bids true --auto-block-enabled false
7459
+ $ a8techads algorithms rules create <id> --name "Propeller source cut" --dimension source_site --metric clicks --threshold 50 --condition equals --check-metric conversions --check-value 0 --action reduce_bid --bid-modifier 0.6
7460
+ $ a8techads algorithms progress --campaign <campaign-id> --dimensions sub_id1,quarter_hour --target-cpa 5
7085
7461
  $ a8techads algorithms pause <id>`);
7086
7462
  addFormatOption(cmd.command("list").description("List bidder algorithms.").option("--status <status>", "Filter by status (ACTIVE, PAUSED, ARCHIVED)").option("--limit <n>", "Max results", "20")).action(async (opts) => {
7087
7463
  const params = new URLSearchParams;
@@ -7091,7 +7467,7 @@ Examples:
7091
7467
  const resp = await apiRequest({ path: `${dspPrefix()}/bidder-algorithms?${params}` });
7092
7468
  const json = await resp.json();
7093
7469
  if (!resp.ok) {
7094
- console.error(`Error: ${json.error ?? resp.statusText}`);
7470
+ console.error(`Error: ${formatApiError(json, resp.statusText)}`);
7095
7471
  process.exit(1);
7096
7472
  }
7097
7473
  const rows = (json.data ?? json).map((algo) => ({
@@ -7108,27 +7484,27 @@ Examples:
7108
7484
  const resp = await apiRequest({ path: `${dspPrefix()}/bidder-algorithms/${id}` });
7109
7485
  const json = await resp.json();
7110
7486
  if (!resp.ok) {
7111
- console.error(`Error: ${json.error ?? resp.statusText}`);
7487
+ console.error(`Error: ${formatApiError(json, resp.statusText)}`);
7112
7488
  process.exit(1);
7113
7489
  }
7114
7490
  printDetail(json.data ?? json, opts.format);
7115
7491
  });
7116
- cmd.command("create").description("Create a bidder algorithm.").option("--name <name>", "Algorithm name (required)").option("--description <text>", "Description").option("--optimization-goal <goal>", "CPA, ROAS, CTR, or CVR").option("--target-value <number>", "Target value").option("--conversion-goal-id <id>", "Conversion goal ID").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
7492
+ cmd.command("create").description("Create a bidder algorithm.").option("--name <name>", "Algorithm name (required)").option("--description <text>", "Description").option("--optimization-goal <goal>", "CPA, ROAS, CTR, or CVR").option("--target-value <number>", "Target value").option("--conversion-goal-id <id>", "Conversion goal ID").option("--data-period-days <n>", "Data lookback period in days").option("--max-test-budget <amount>", "Maximum test budget before reducing/blocking weak traffic").option("--min-impressions <n>", "Minimum impressions before evaluating").option("--min-clicks <n>", "Minimum clicks before evaluating").option("--auto-adjust-bids <bool>", "Enable automatic bid modifiers").option("--auto-block-enabled <bool>", "Enable automatic block actions").option("--min-bid-modifier <number>", "Minimum bid modifier, e.g. 0.6").option("--max-bid-modifier <number>", "Maximum bid modifier, e.g. 1.3").option("--block-after-impressions <n>", "Block threshold by impressions").option("--block-after-spend <amount>", "Block threshold by spend").option("--block-if-no-conversions <bool>", "Block when no conversions after thresholds").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
7117
7493
  const body = await buildAlgorithmBody(opts, true);
7118
7494
  const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/bidder-algorithms`, body });
7119
7495
  const json = await resp.json();
7120
7496
  if (!resp.ok) {
7121
- console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
7497
+ console.error(`Error: ${formatApiError(json, resp.statusText)}`);
7122
7498
  process.exit(1);
7123
7499
  }
7124
7500
  console.log(`Algorithm created: ${json.data?.id ?? json.id}`);
7125
7501
  });
7126
- cmd.command("update").description("Update a bidder algorithm.").argument("<id>", "Algorithm ID").option("--name <name>", "Algorithm name").option("--description <text>", "Description").option("--optimization-goal <goal>", "CPA, ROAS, CTR, or CVR").option("--target-value <number>", "Target value").option("--conversion-goal-id <id>", "Conversion goal ID").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
7502
+ cmd.command("update").description("Update a bidder algorithm.").argument("<id>", "Algorithm ID").option("--name <name>", "Algorithm name").option("--description <text>", "Description").option("--optimization-goal <goal>", "CPA, ROAS, CTR, or CVR").option("--target-value <number>", "Target value").option("--conversion-goal-id <id>", "Conversion goal ID").option("--data-period-days <n>", "Data lookback period in days").option("--max-test-budget <amount>", "Maximum test budget before reducing/blocking weak traffic").option("--min-impressions <n>", "Minimum impressions before evaluating").option("--min-clicks <n>", "Minimum clicks before evaluating").option("--auto-adjust-bids <bool>", "Enable automatic bid modifiers").option("--auto-block-enabled <bool>", "Enable automatic block actions").option("--min-bid-modifier <number>", "Minimum bid modifier, e.g. 0.6").option("--max-bid-modifier <number>", "Maximum bid modifier, e.g. 1.3").option("--block-after-impressions <n>", "Block threshold by impressions").option("--block-after-spend <amount>", "Block threshold by spend").option("--block-if-no-conversions <bool>", "Block when no conversions after thresholds").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
7127
7503
  const body = await buildAlgorithmBody(opts, false);
7128
7504
  const resp = await apiRequest({ method: "PATCH", path: `${dspPrefix()}/bidder-algorithms/${id}`, body });
7129
7505
  const json = await resp.json().catch(() => ({}));
7130
7506
  if (!resp.ok) {
7131
- console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
7507
+ console.error(`Error: ${formatApiError(json, resp.statusText)}`);
7132
7508
  process.exit(1);
7133
7509
  }
7134
7510
  console.log(`Algorithm ${id} updated.`);
@@ -7145,7 +7521,7 @@ Examples:
7145
7521
  });
7146
7522
  const json = await resp.json().catch(() => ({}));
7147
7523
  if (!resp.ok) {
7148
- console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
7524
+ console.error(`Error: ${formatApiError(json, resp.statusText)}`);
7149
7525
  process.exit(1);
7150
7526
  }
7151
7527
  console.log(`Algorithm ${id} archived.`);
@@ -7158,12 +7534,14 @@ Examples:
7158
7534
  });
7159
7535
  const json = await resp.json().catch(() => ({}));
7160
7536
  if (!resp.ok) {
7161
- console.error(`Error: ${json.error ?? json.errors ?? resp.statusText}`);
7537
+ console.error(`Error: ${formatApiError(json, resp.statusText)}`);
7162
7538
  process.exit(1);
7163
7539
  }
7164
7540
  console.log(`Algorithm ${id} ${action}d.`);
7165
7541
  });
7166
7542
  }
7543
+ addRulesCommand(cmd);
7544
+ addProgressCommand(cmd);
7167
7545
  return cmd;
7168
7546
  }
7169
7547
  async function buildAlgorithmBody(opts, creating) {
@@ -7177,11 +7555,22 @@ async function buildAlgorithmBody(opts, creating) {
7177
7555
  if (opts.description)
7178
7556
  body.description = opts.description;
7179
7557
  if (opts.optimizationGoal)
7180
- body.optimizationGoal = String(opts.optimizationGoal).toUpperCase();
7558
+ body.optimizationGoal = String(opts.optimizationGoal).toLowerCase();
7181
7559
  if (opts.targetValue !== undefined)
7182
7560
  body.targetValue = Number(opts.targetValue);
7183
7561
  if (opts.conversionGoalId)
7184
7562
  body.conversionGoalId = opts.conversionGoalId;
7563
+ putNumber(body, "dataPeriodDays", opts.dataPeriodDays);
7564
+ putNumber(body, "maxTestBudget", opts.maxTestBudget);
7565
+ putNumber(body, "minImpressions", opts.minImpressions);
7566
+ putNumber(body, "minClicks", opts.minClicks);
7567
+ putBoolean(body, "autoAdjustBids", opts.autoAdjustBids);
7568
+ putBoolean(body, "autoBlockEnabled", opts.autoBlockEnabled);
7569
+ putNumber(body, "minBidModifier", opts.minBidModifier);
7570
+ putNumber(body, "maxBidModifier", opts.maxBidModifier);
7571
+ putNumber(body, "blockAfterImpressions", opts.blockAfterImpressions);
7572
+ putNumber(body, "blockAfterSpend", opts.blockAfterSpend);
7573
+ putBoolean(body, "blockIfNoConversions", opts.blockIfNoConversions);
7185
7574
  if (creating) {
7186
7575
  if (!body.name) {
7187
7576
  console.error("Error: --name is required.");
@@ -7198,6 +7587,245 @@ async function buildAlgorithmBody(opts, creating) {
7198
7587
  }
7199
7588
  return body;
7200
7589
  }
7590
+ function addRulesCommand(cmd) {
7591
+ const rules = new Command("rules").description("Manage bidder optimization rules.").addHelpText("after", `
7592
+ Examples:
7593
+ $ a8techads algorithms rules list <algorithm-id>
7594
+ $ a8techads algorithms rules create <algorithm-id> --name "No conversion after 50 clicks" --dimension source_site --metric clicks --threshold 50 --condition equals --check-metric conversions --check-value 0 --action reduce_bid --bid-modifier 0.6
7595
+ $ a8techads algorithms rules update <rule-id> --bid-modifier 0.7
7596
+ $ a8techads algorithms rules delete <rule-id> --yes`);
7597
+ addFormatOption(rules.command("list").description("List rules for a bidder algorithm.").argument("<algorithm-id>", "Algorithm ID")).action(async (algorithmId, opts) => {
7598
+ const resp = await apiRequest({ path: `${dspPrefix()}/bidder-algorithms/${algorithmId}/blocking-rules` });
7599
+ const json = await resp.json();
7600
+ if (!resp.ok) {
7601
+ console.error(`Error: ${formatApiError(json, resp.statusText)}`);
7602
+ process.exit(1);
7603
+ }
7604
+ const rows = (json.data ?? json).map(formatRuleRow);
7605
+ printData(rows, RULE_COLUMNS, opts.format);
7606
+ });
7607
+ rules.command("create").description("Create an optimization rule for a bidder algorithm.").argument("<algorithm-id>", "Algorithm ID").option("--name <name>", "Rule name (required)").option("--dimension <dimension>", "zone, site, source_site, country, region, device, os, browser, language, mobile_carrier").option("--metric <metric>", "impressions, clicks, or spend").option("--threshold <number>", "Trigger threshold").option("--condition <type>", "greater_than, greater_than_or_equal, less_than, less_than_or_equal, or equals").option("--check-metric <metric>", "ctr, clicks, conversions, ecpa, roi, or profit").option("--check-value <number>", "Performance metric value to compare").option("--action <action>", "block, reduce_bid, or alert").option("--bid-modifier <number>", "Bid modifier for reduce_bid action").option("--priority <n>", "Rule priority, lower runs first").option("--active <bool>", "Whether the rule is active", "true").option("--from-json <file>", "Create from JSON file").action(async (algorithmId, opts) => {
7608
+ const body = await buildRuleBody(opts, true);
7609
+ const resp = await apiRequest({
7610
+ method: "POST",
7611
+ path: `${dspPrefix()}/bidder-algorithms/${algorithmId}/blocking-rules`,
7612
+ body
7613
+ });
7614
+ const json = await resp.json().catch(() => ({}));
7615
+ if (!resp.ok) {
7616
+ console.error(`Error: ${formatApiError(json, resp.statusText)}`);
7617
+ process.exit(1);
7618
+ }
7619
+ console.log(`Rule created: ${json.data?.id ?? json.id}`);
7620
+ });
7621
+ rules.command("update").description("Update an optimization rule.").argument("<rule-id>", "Rule ID").option("--name <name>", "Rule name").option("--dimension <dimension>", "zone, site, source_site, country, region, device, os, browser, language, mobile_carrier").option("--metric <metric>", "impressions, clicks, or spend").option("--threshold <number>", "Trigger threshold").option("--condition <type>", "greater_than, greater_than_or_equal, less_than, less_than_or_equal, or equals").option("--check-metric <metric>", "ctr, clicks, conversions, ecpa, roi, or profit").option("--check-value <number>", "Performance metric value to compare").option("--action <action>", "block, reduce_bid, or alert").option("--bid-modifier <number>", "Bid modifier for reduce_bid action").option("--priority <n>", "Rule priority, lower runs first").option("--active <bool>", "Whether the rule is active").option("--from-json <file>", "Update from JSON file").action(async (ruleId, opts) => {
7622
+ const body = await buildRuleBody(opts, false);
7623
+ const resp = await apiRequest({
7624
+ method: "PATCH",
7625
+ path: `${dspPrefix()}/blocking-rules/${ruleId}`,
7626
+ body
7627
+ });
7628
+ const json = await resp.json().catch(() => ({}));
7629
+ if (!resp.ok) {
7630
+ console.error(`Error: ${formatApiError(json, resp.statusText)}`);
7631
+ process.exit(1);
7632
+ }
7633
+ console.log(`Rule ${ruleId} updated.`);
7634
+ });
7635
+ rules.command("delete").description("Delete an optimization rule.").argument("<rule-id>", "Rule ID").option("--yes", "Skip confirmation").action(async (ruleId, opts) => {
7636
+ if (!opts.yes) {
7637
+ console.error("Add --yes to confirm deletion.");
7638
+ process.exit(1);
7639
+ }
7640
+ const resp = await apiRequest({ method: "DELETE", path: `${dspPrefix()}/blocking-rules/${ruleId}` });
7641
+ if (!resp.ok && resp.status !== 204) {
7642
+ const json = await resp.json().catch(() => ({}));
7643
+ console.error(`Error: ${formatApiError(json, resp.statusText)}`);
7644
+ process.exit(1);
7645
+ }
7646
+ console.log(`Rule ${ruleId} deleted.`);
7647
+ });
7648
+ cmd.addCommand(rules);
7649
+ }
7650
+ function addProgressCommand(cmd) {
7651
+ addFormatOption(cmd.command("progress").description("Monitor optimization progress for a campaign through DSP reports.").requiredOption("--campaign <id>", "Campaign ID to filter").option("--dimensions <list>", "Comma-separated dimensions, e.g. sub_id1 or sub_id1,quarter_hour", "sub_id1").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)").option("--target-cpa <amount>", "Target CPA used for suggestions").option("--limit <n>", "Max rows", "100").option("--semantic-mode <mode>", "event_time or attributed", "event_time").addHelpText("after", `
7652
+ Examples:
7653
+ $ a8techads algorithms progress --campaign <campaign-id> --dimensions sub_id1 --from 2026-06-01 --to 2026-06-02 --target-cpa 5
7654
+ $ a8techads algorithms progress --campaign <campaign-id> --dimensions sub_id1,quarter_hour --semantic-mode event_time --limit 200`)).action(async (opts) => {
7655
+ const dimensions = parseCsvList3(opts.dimensions);
7656
+ const targetCpa = opts.targetCpa === undefined ? undefined : Number(opts.targetCpa);
7657
+ const body = {
7658
+ dimensions,
7659
+ metrics: ["requests", "bids", "wins", "impressions", "clicks", "conversions", "spend", "ctr", "cvr", "cpa"],
7660
+ filters: { campaign: opts.campaign },
7661
+ limit: Number(opts.limit),
7662
+ sortBy: "clicks",
7663
+ sortOrder: "desc",
7664
+ semanticMode: opts.semanticMode
7665
+ };
7666
+ if (opts.from)
7667
+ body.dateRange = { ...body.dateRange ?? {}, start: opts.from };
7668
+ if (opts.to)
7669
+ body.dateRange = { ...body.dateRange ?? {}, end: opts.to };
7670
+ const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/reports/query`, body });
7671
+ const json = await resp.json().catch(() => ({}));
7672
+ if (!resp.ok) {
7673
+ console.error(`Error: ${formatApiError(json, resp.statusText)}`);
7674
+ process.exit(1);
7675
+ }
7676
+ const data = json.data ?? json;
7677
+ const rows = data.rows ?? data.results ?? json.rows ?? json.results ?? [];
7678
+ if (!Array.isArray(rows) || rows.length === 0) {
7679
+ console.log("No results.");
7680
+ return;
7681
+ }
7682
+ const formatted = rows.map((row) => ({
7683
+ ...row,
7684
+ suggestion: suggestOptimization(row, targetCpa)
7685
+ }));
7686
+ if (opts.format === "json") {
7687
+ console.log(JSON.stringify({ ...data, results: formatted }, null, 2));
7688
+ return;
7689
+ }
7690
+ const columns = [
7691
+ ...dimensions.map((dim) => ({ key: dim, header: dim.toUpperCase(), width: Math.max(dim.length, 14) })),
7692
+ { key: "requests", header: "REQ", width: 10 },
7693
+ { key: "bids", header: "BID", width: 10 },
7694
+ { key: "wins", header: "WIN", width: 10 },
7695
+ { key: "impressions", header: "IMP", width: 10 },
7696
+ { key: "clicks", header: "CLICK", width: 10 },
7697
+ { key: "conversions", header: "CONV", width: 8 },
7698
+ { key: "spend", header: "SPEND", width: 10, format: money },
7699
+ { key: "ctr", header: "CTR%", width: 8, format: pct },
7700
+ { key: "cvr", header: "CVR%", width: 8, format: pct },
7701
+ { key: "cpa", header: "CPA", width: 10, format: money },
7702
+ { key: "suggestion", header: "SUGGESTION", width: 22 }
7703
+ ];
7704
+ printData(formatted, columns, opts.format);
7705
+ const summary = data.summary ?? json.summary;
7706
+ if (summary && opts.format === "table") {
7707
+ console.log(`
7708
+ Summary:`);
7709
+ printDetail(summary, "table");
7710
+ }
7711
+ });
7712
+ }
7713
+ async function buildRuleBody(opts, creating) {
7714
+ if (opts.fromJson) {
7715
+ const { readFileSync: readFileSync5 } = await import("fs");
7716
+ return JSON.parse(readFileSync5(opts.fromJson, "utf-8"));
7717
+ }
7718
+ const body = {};
7719
+ if (opts.name)
7720
+ body.name = opts.name;
7721
+ if (opts.dimension)
7722
+ body.dimension = opts.dimension;
7723
+ if (opts.metric)
7724
+ body.metric = opts.metric;
7725
+ putNumber(body, "threshold", opts.threshold);
7726
+ if (opts.condition)
7727
+ body.conditionType = opts.condition;
7728
+ if (opts.checkMetric)
7729
+ body.checkMetric = opts.checkMetric;
7730
+ putNumber(body, "checkValue", opts.checkValue);
7731
+ if (opts.action)
7732
+ body.action = opts.action;
7733
+ putNumber(body, "bidModifier", opts.bidModifier);
7734
+ putNumber(body, "priority", opts.priority);
7735
+ putBoolean(body, "isActive", opts.active);
7736
+ if (creating) {
7737
+ for (const [key, option] of [
7738
+ ["name", "--name"],
7739
+ ["dimension", "--dimension"],
7740
+ ["metric", "--metric"],
7741
+ ["threshold", "--threshold"],
7742
+ ["conditionType", "--condition"],
7743
+ ["checkMetric", "--check-metric"],
7744
+ ["checkValue", "--check-value"],
7745
+ ["action", "--action"]
7746
+ ]) {
7747
+ if (body[key] === undefined || body[key] === "") {
7748
+ console.error(`Error: ${option} is required.`);
7749
+ process.exit(1);
7750
+ }
7751
+ }
7752
+ }
7753
+ if (body.action === "reduce_bid" && body.bidModifier === undefined) {
7754
+ console.error("Error: --bid-modifier is required when --action reduce_bid.");
7755
+ process.exit(1);
7756
+ }
7757
+ return body;
7758
+ }
7759
+ function formatRuleRow(rule) {
7760
+ return {
7761
+ id: rule.id,
7762
+ name: rule.name,
7763
+ dimension: rule.dimension,
7764
+ metric: rule.metric,
7765
+ threshold: rule.threshold,
7766
+ check: `${rule.checkMetric} ${rule.conditionType} ${rule.checkValue}`,
7767
+ action: rule.action,
7768
+ bidModifier: rule.bidModifier,
7769
+ isActive: rule.isActive,
7770
+ priority: rule.priority
7771
+ };
7772
+ }
7773
+ function suggestOptimization(row, targetCpa) {
7774
+ const clicks = asNumber(row.clicks);
7775
+ const conversions = asNumber(row.conversions);
7776
+ const spend = asNumber(row.spend);
7777
+ const cpa = conversions > 0 ? spend / conversions : asNumber(row.cpa);
7778
+ if (clicks < 30)
7779
+ return "hold: new traffic";
7780
+ if (clicks >= 50 && conversions === 0)
7781
+ return "reduce bid 0.6x";
7782
+ if (targetCpa && conversions > 0 && cpa > targetCpa)
7783
+ return "reduce bid 0.7x";
7784
+ if (targetCpa && conversions > 0 && cpa > 0 && cpa < targetCpa)
7785
+ return "raise bid 1.1-1.3x";
7786
+ return "hold";
7787
+ }
7788
+ function putNumber(body, key, value) {
7789
+ if (value !== undefined && value !== null && value !== "")
7790
+ body[key] = Number(value);
7791
+ }
7792
+ function putBoolean(body, key, value) {
7793
+ if (value === undefined || value === null || value === "")
7794
+ return;
7795
+ body[key] = parseBoolean(String(value), key);
7796
+ }
7797
+ function parseBoolean(value, optionName) {
7798
+ const normalized = value.trim().toLowerCase();
7799
+ if (["true", "1", "yes", "y", "on"].includes(normalized))
7800
+ return true;
7801
+ if (["false", "0", "no", "n", "off"].includes(normalized))
7802
+ return false;
7803
+ console.error(`Error: ${optionName} must be true or false.`);
7804
+ process.exit(1);
7805
+ }
7806
+ function parseCsvList3(value) {
7807
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
7808
+ }
7809
+ function asNumber(value) {
7810
+ const n = Number(value ?? 0);
7811
+ return Number.isFinite(n) ? n : 0;
7812
+ }
7813
+ function money(value) {
7814
+ const n = Number(value ?? 0);
7815
+ return Number.isFinite(n) ? `$${n.toFixed(4)}` : "-";
7816
+ }
7817
+ function pct(value) {
7818
+ const n = Number(value ?? 0);
7819
+ return Number.isFinite(n) ? n.toFixed(2) : "-";
7820
+ }
7821
+ function formatApiError(json, fallback) {
7822
+ const value = json?.error ?? json?.errors ?? json?.message ?? json;
7823
+ if (value === undefined || value === null || value === "")
7824
+ return fallback;
7825
+ if (typeof value === "string")
7826
+ return value;
7827
+ return JSON.stringify(value);
7828
+ }
7201
7829
 
7202
7830
  // src/commands/payouts.ts
7203
7831
  function createPayoutsCommand() {
@@ -8117,16 +8745,22 @@ Examples:
8117
8745
  addFormatOption(contracts.command("summary").description("Summarize supply contracts.").option("--limit <n>", "Max results to scan", "200").option("--region <region>", "Filter by region").option("--status <status>", "Filter by status").option("--supplier <name>", "Filter by supplier name contains").option("--type <type>", "Filter by contract type")).action(async (opts) => {
8118
8746
  await summarizeResource(`${consolePrefix()}/supply-contracts`, opts, "contracts");
8119
8747
  });
8120
- contracts.command("create").description("Create a supply contract.").requiredOption("--name <name>", "Contract name").requiredOption("--region <regions>", "Serving regions, comma-separated").requiredOption("--supplier-tenant-id <id>", "Supplier tenant ID").requiredOption("--supply-product-id <id>", "Traffic package ID").option("--description <text>", "Description").option("--contract-type <type>", "GUARANTEED, PREFERRED_DEAL, PRIVATE_MARKETPLACE, OPEN_AUCTION", "OPEN_AUCTION").option("--source-masking-mode <mode>", "OPAQUE, REGION_ONLY, PRODUCT_ONLY", "OPAQUE").option("--priority <n>", "Contract-level routing priority", "0").option("--match-rules <json>", 'Request match rules JSON, e.g. [{"fieldPath":"ext.alpineads.route_key","operator":"eq","value":"hilltopads-open"}]').option("--qps-cap <n>", "QPS cap").option("--target-fill-rate <n>", "Target fill rate, decimal string").option("--currency <code>", "Currency", "USD").option("--start-date <iso>", "ISO UTC datetime").option("--end-date <iso>", "ISO UTC datetime").option("--status <status>", "DRAFT, ACTIVE, PAUSED, ARCHIVED", "DRAFT").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
8748
+ contracts.command("create").description("Create a supply contract.").requiredOption("--name <name>", "Contract name").requiredOption("--region <regions>", "Serving regions, comma-separated").requiredOption("--supply-product-id <id>", "Traffic package ID").option("--description <text>", "Description").option("--source-type <type>", "INTERNAL or EXTERNAL", "EXTERNAL").option("--supplier-tenant-id <id>", "Supplier tenant ID for EXTERNAL contracts").option("--contract-type <type>", "GUARANTEED, PREFERRED_DEAL, PRIVATE_MARKETPLACE, OPEN_AUCTION", "OPEN_AUCTION").option("--source-masking-mode <mode>", "OPAQUE, REGION_ONLY, PRODUCT_ONLY", "OPAQUE").option("--priority <n>", "Contract-level routing priority", "0").option("--match-rules <json>", 'Request match rules JSON, e.g. [{"fieldPath":"ext.alpineads.route_key","operator":"eq","value":"hilltopads-open"}]').option("--qps-cap <n>", "QPS cap").option("--target-fill-rate <n>", "Target fill rate, decimal string").option("--currency <code>", "Currency", "USD").option("--start-date <iso>", "ISO UTC datetime").option("--end-date <iso>", "ISO UTC datetime").option("--status <status>", "DRAFT, ACTIVE, PAUSED, ARCHIVED", "DRAFT").option("--from-json <file>", "Create from JSON file").action(async (opts) => {
8121
8749
  let body;
8122
8750
  if (opts.fromJson) {
8123
8751
  const { readFileSync: readFileSync6 } = await import("fs");
8124
8752
  body = JSON.parse(readFileSync6(opts.fromJson, "utf-8"));
8125
8753
  } else {
8754
+ const sourceType = String(opts.sourceType || "EXTERNAL").toUpperCase();
8755
+ if (sourceType === "EXTERNAL" && !opts.supplierTenantId) {
8756
+ console.error("Error: --supplier-tenant-id is required for EXTERNAL contracts.");
8757
+ process.exit(1);
8758
+ }
8126
8759
  body = {
8127
8760
  name: opts.name,
8128
8761
  region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean),
8129
- supplierTenantId: opts.supplierTenantId,
8762
+ sourceType,
8763
+ supplierTenantId: sourceType === "EXTERNAL" ? opts.supplierTenantId : null,
8130
8764
  supplyProductId: opts.supplyProductId,
8131
8765
  description: opts.description,
8132
8766
  contractType: opts.contractType,
@@ -8149,7 +8783,7 @@ Examples:
8149
8783
  }
8150
8784
  console.log(`Supply contract created: ${json.data?.id ?? json.id}`);
8151
8785
  });
8152
- contracts.command("update").description("Update a supply contract.").argument("<id>", "Supply contract ID").option("--name <name>", "Contract name").option("--region <regions>", "Serving regions, comma-separated").option("--description <text>", "Description").option("--contract-type <type>", "GUARANTEED, PREFERRED_DEAL, PRIVATE_MARKETPLACE, OPEN_AUCTION").option("--source-masking-mode <mode>", "OPAQUE, REGION_ONLY, PRODUCT_ONLY").option("--priority <n>", "Contract-level routing priority").option("--match-rules <json>", "Request match rules JSON").option("--qps-cap <n>", "QPS cap").option("--target-fill-rate <n>", "Target fill rate, decimal string").option("--currency <code>", "Currency").option("--start-date <iso>", "ISO UTC datetime").option("--end-date <iso>", "ISO UTC datetime").option("--status <status>", "DRAFT, ACTIVE, PAUSED, ARCHIVED").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
8786
+ contracts.command("update").description("Update a supply contract.").argument("<id>", "Supply contract ID").option("--name <name>", "Contract name").option("--region <regions>", "Serving regions, comma-separated").option("--description <text>", "Description").option("--source-type <type>", "INTERNAL or EXTERNAL").option("--supplier-tenant-id <id>", "Supplier tenant ID for EXTERNAL contracts").option("--contract-type <type>", "GUARANTEED, PREFERRED_DEAL, PRIVATE_MARKETPLACE, OPEN_AUCTION").option("--source-masking-mode <mode>", "OPAQUE, REGION_ONLY, PRODUCT_ONLY").option("--priority <n>", "Contract-level routing priority").option("--match-rules <json>", "Request match rules JSON").option("--qps-cap <n>", "QPS cap").option("--target-fill-rate <n>", "Target fill rate, decimal string").option("--currency <code>", "Currency").option("--start-date <iso>", "ISO UTC datetime").option("--end-date <iso>", "ISO UTC datetime").option("--status <status>", "DRAFT, ACTIVE, PAUSED, ARCHIVED").option("--from-json <file>", "Update from JSON file").action(async (id, opts) => {
8153
8787
  let body;
8154
8788
  if (opts.fromJson) {
8155
8789
  const { readFileSync: readFileSync6 } = await import("fs");
@@ -8159,6 +8793,8 @@ Examples:
8159
8793
  ...opts.name ? { name: opts.name } : {},
8160
8794
  ...opts.region ? { region: String(opts.region).split(",").map((item) => item.trim()).filter(Boolean) } : {},
8161
8795
  ...opts.description ? { description: opts.description } : {},
8796
+ ...opts.sourceType ? { sourceType: String(opts.sourceType).toUpperCase() } : {},
8797
+ ...opts.sourceType && String(opts.sourceType).toUpperCase() === "INTERNAL" ? { supplierTenantId: null } : opts.supplierTenantId ? { supplierTenantId: opts.supplierTenantId } : {},
8162
8798
  ...opts.contractType ? { contractType: opts.contractType } : {},
8163
8799
  ...opts.sourceMaskingMode ? { sourceMaskingMode: opts.sourceMaskingMode } : {},
8164
8800
  ...opts.priority ? { priority: Number(opts.priority) } : {},
@@ -8474,13 +9110,243 @@ Examples:
8474
9110
  return cmd;
8475
9111
  }
8476
9112
 
9113
+ // src/commands/oauth-clients.ts
9114
+ var COLUMNS11 = [
9115
+ { key: "id", header: "ID", width: 36 },
9116
+ { key: "name", header: "NAME", width: 28 },
9117
+ { key: "clientId", header: "CLIENT ID", width: 36 },
9118
+ { key: "tenantType", header: "CAPABILITY", width: 12 },
9119
+ { key: "role", header: "ROLE", width: 18 },
9120
+ { key: "isActive", header: "ACTIVE", width: 8 },
9121
+ { key: "createdAt", header: "CREATED", width: 20 }
9122
+ ];
9123
+ function createOAuthClientsCommand() {
9124
+ const cmd = new Command("oauth-clients").description(`OAuth client_credentials management.
9125
+
9126
+ Requires: tenant admin or platform admin role.`).addHelpText("after", `
9127
+ Examples:
9128
+ $ a8techads oauth-clients list
9129
+ $ a8techads oauth-clients create --name "monedita ssp" --capability PUBLISHER
9130
+ $ a8techads oauth-clients create --name "ci bot" --capability ADVERTISER --save-profile ci-bot
9131
+ $ a8techads oauth-clients delete <id> --yes`);
9132
+ addFormatOption(cmd.command("list").description("List OAuth clients for the current tenant.")).action(async (opts) => {
9133
+ const resp = await apiRequest({ path: oauthClientPath() });
9134
+ const json = await resp.json();
9135
+ if (!resp.ok) {
9136
+ console.error(`Error: ${json.error ?? json.message ?? resp.statusText}`);
9137
+ process.exit(1);
9138
+ }
9139
+ const rows = asArray(json.data ?? json).map(normalizeClient);
9140
+ printData(rows, COLUMNS11, opts.format);
9141
+ });
9142
+ addFormatOption(cmd.command("create").description(`Create an OAuth client bound to a tenant user.
9143
+
9144
+ The secret is returned once. Use --save-profile to store it under ~/.alpineads/oauth-client.json.`).requiredOption("--name <name>", "Client display name").option("--user-id <id>", "Bind the client to a specific tenant user; defaults to the current actor").option("--capability <capability>", "ADVERTISER or PUBLISHER. Defaults to server-side tenant role resolution.").option("--save-profile <profile>", "Save returned client_id/client_secret as a local OAuth profile").option("--api-url <url>", "API URL saved with --save-profile", "https://api.a8.tech").option("--auth-url <url>", "Auth URL saved with --save-profile", "https://auth.a8.tech")).action(async (opts) => {
9145
+ const body = { name: opts.name };
9146
+ if (opts.userId)
9147
+ body.user_id = opts.userId;
9148
+ if (opts.capability)
9149
+ body.capability = normalizeCapability(opts.capability);
9150
+ const resp = await apiRequest({ method: "POST", path: oauthClientPath(), body });
9151
+ const json = await resp.json();
9152
+ if (!resp.ok) {
9153
+ console.error(`Error: ${json.error ?? json.message ?? json.errors ?? resp.statusText}`);
9154
+ process.exit(1);
9155
+ }
9156
+ const client = json.data ?? json;
9157
+ const normalized = normalizeClient(client);
9158
+ if (opts.format === "json") {
9159
+ console.log(JSON.stringify(client, null, 2));
9160
+ } else {
9161
+ printDetail(normalized, opts.format);
9162
+ if (client.clientSecret ?? client.client_secret) {
9163
+ console.log(`clientSecret ${client.clientSecret ?? client.client_secret}`);
9164
+ }
9165
+ }
9166
+ const clientId = client.clientId ?? client.client_id ?? client.hydraClientId ?? client.hydra_client_id;
9167
+ const clientSecret = client.clientSecret ?? client.client_secret;
9168
+ if (opts.saveProfile) {
9169
+ if (!clientId || !clientSecret) {
9170
+ console.error("Error: server response did not include clientId/clientSecret; profile was not saved.");
9171
+ process.exit(1);
9172
+ }
9173
+ const saved = saveOAuthClientConfig({
9174
+ profile: opts.saveProfile,
9175
+ clientId,
9176
+ clientSecret,
9177
+ apiUrl: opts.apiUrl,
9178
+ authUrl: opts.authUrl
9179
+ });
9180
+ console.log(`Saved OAuth profile: ${saved.profile}`);
9181
+ } else {
9182
+ console.log("Secret is shown once. Re-run with --save-profile <name> if this client should be used by CLI automation.");
9183
+ }
9184
+ });
9185
+ cmd.command("delete").alias("revoke").description("Revoke an OAuth client.").argument("<id>", "OAuth client ID").option("--yes", "Skip confirmation").action(async (id, opts) => {
9186
+ if (!opts.yes) {
9187
+ console.error("Add --yes to confirm revoking this OAuth client.");
9188
+ process.exit(1);
9189
+ }
9190
+ const resp = await apiRequest({ method: "DELETE", path: `${oauthClientPath()}/${id}` });
9191
+ const json = await resp.json().catch(() => ({}));
9192
+ if (!resp.ok) {
9193
+ console.error(`Error: ${json.error ?? json.message ?? resp.statusText}`);
9194
+ process.exit(1);
9195
+ }
9196
+ console.log(`OAuth client ${id} revoked.`);
9197
+ });
9198
+ return cmd;
9199
+ }
9200
+ function oauthClientPath() {
9201
+ return `${dspPrefix()}/settings/oauth-clients`;
9202
+ }
9203
+ function normalizeClient(client) {
9204
+ return {
9205
+ id: client.id,
9206
+ name: client.name,
9207
+ clientId: client.clientId ?? client.client_id ?? client.hydraClientId ?? client.hydra_client_id,
9208
+ tenantType: client.tenantType ?? client.tenant_type,
9209
+ role: client.role,
9210
+ isActive: client.isActive ?? client.is_active ?? true,
9211
+ createdAt: client.createdAt ?? client.created_at
9212
+ };
9213
+ }
9214
+ function normalizeCapability(value) {
9215
+ const normalized = value.trim().toUpperCase();
9216
+ if (!["ADVERTISER", "PUBLISHER"].includes(normalized)) {
9217
+ console.error("Error: --capability must be ADVERTISER or PUBLISHER.");
9218
+ process.exit(1);
9219
+ }
9220
+ return normalized;
9221
+ }
9222
+ function asArray(value) {
9223
+ if (Array.isArray(value))
9224
+ return value;
9225
+ if (value && typeof value === "object") {
9226
+ const maybe = value;
9227
+ if (Array.isArray(maybe.clients))
9228
+ return maybe.clients;
9229
+ }
9230
+ return [];
9231
+ }
9232
+
9233
+ // src/commands/workflows.ts
9234
+ var WORKFLOWS = [
9235
+ {
9236
+ name: "profile-login",
9237
+ area: "Auth",
9238
+ description: "Create, switch, and validate human or client_credentials profiles.",
9239
+ commands: [
9240
+ "a8techads auth login --profile owner",
9241
+ "a8techads profile use owner",
9242
+ "a8techads context show",
9243
+ 'a8techads oauth-clients create --name "monedita ssp" --capability PUBLISHER --save-profile monedita-ssp',
9244
+ "a8techads profile use monedita-ssp",
9245
+ "a8techads auth token"
9246
+ ]
9247
+ },
9248
+ {
9249
+ name: "dsp-campaign",
9250
+ area: "DSP",
9251
+ description: "Create the assets needed for a campaign and move it through review.",
9252
+ commands: [
9253
+ "a8techads context dsp",
9254
+ "a8techads media-assets upload --file ./creative.png",
9255
+ "a8techads landing-pages list",
9256
+ 'a8techads campaigns create --name "Rewarded Offer Demo" --budget 100',
9257
+ 'a8techads variations create --campaign <campaign-id> --name "Offer A" --from-json variation.json',
9258
+ "a8techads variations submit <variation-id>",
9259
+ "a8techads campaigns submit <campaign-id>"
9260
+ ]
9261
+ },
9262
+ {
9263
+ name: "conversion-goals",
9264
+ area: "DSP",
9265
+ description: "Create tracker-compatible goals and print postback instructions.",
9266
+ commands: [
9267
+ "a8techads context dsp",
9268
+ 'a8techads conversion-goals create --name "Install" --code install --conversion-type APP_INSTALL --goal-order 1',
9269
+ 'a8techads conversion-goals create --name "Registration" --code registration --conversion-type LEAD_SOI --goal-order 2',
9270
+ "a8techads conversion-goals integration <goal-id> --status install",
9271
+ "a8techads conversion-goals integration <goal-id> --status registration"
9272
+ ]
9273
+ },
9274
+ {
9275
+ name: "ssp-inventory",
9276
+ area: "SSP",
9277
+ description: "Create SSP inventory and generate JavaScript or iframe tags.",
9278
+ commands: [
9279
+ "a8techads context ssp",
9280
+ 'a8techads sites create --name "Monedita" --domain monedita.jogabox.net',
9281
+ 'a8techads zones create --site <site-id> --name "Offerwall" --format offerwall',
9282
+ 'a8techads zones create --site <site-id> --name "Rewarded" --format rewarded_video',
9283
+ 'a8techads zones tag <offerwall-zone-id> --local --format offerwall --theme dark --sub "{PLAYER_ID}"',
9284
+ 'a8techads zones tag <rewarded-zone-id> --local --format rewarded_video --theme dark --sub "{PLAYER_ID}"'
9285
+ ]
9286
+ },
9287
+ {
9288
+ name: "ssp-reconcile",
9289
+ area: "Reports",
9290
+ description: "Pull authoritative SSP metrics for revenue reconciliation.",
9291
+ commands: [
9292
+ "a8techads context ssp",
9293
+ "a8techads reports ssp-pull --from 2026-06-14 --to 2026-06-15 --group-by zone,day,country --metrics requests,fills,impressions,clicks,revenue",
9294
+ "a8techads reports ssp-pull --from 2026-06-14 --to 2026-06-15 --zone <zone-id> --format json"
9295
+ ]
9296
+ },
9297
+ {
9298
+ name: "supply-ops",
9299
+ area: "Console",
9300
+ description: "Inspect supply products, contracts, leases, and runtime allocation state.",
9301
+ commands: [
9302
+ "a8techads context set-app console",
9303
+ "a8techads supply-ops products list",
9304
+ "a8techads supply-ops contracts list",
9305
+ "a8techads supply-ops leases list",
9306
+ "a8techads admin system bidding-gate show --format json"
9307
+ ]
9308
+ }
9309
+ ];
9310
+ var COLUMNS12 = [
9311
+ { key: "name", header: "WORKFLOW", width: 24 },
9312
+ { key: "area", header: "AREA", width: 10 },
9313
+ { key: "description", header: "DESCRIPTION", width: 72 }
9314
+ ];
9315
+ function createWorkflowsCommand() {
9316
+ const cmd = new Command("workflows").description("Discover page-level operating workflows and the CLI commands behind them.").addHelpText("after", `
9317
+ Examples:
9318
+ $ a8techads workflows list
9319
+ $ a8techads workflows show ssp-inventory
9320
+ $ a8techads workflows show conversion-goals`);
9321
+ cmd.command("list").description("List available workflows.").action(() => {
9322
+ printData(WORKFLOWS.map(({ name, area, description }) => ({ name, area, description })), COLUMNS12);
9323
+ });
9324
+ cmd.command("show").description("Show commands for a workflow.").argument("<name>", "Workflow name").action((name) => {
9325
+ const workflow = WORKFLOWS.find((item) => item.name === name);
9326
+ if (!workflow) {
9327
+ console.error(`Unknown workflow: ${name}`);
9328
+ console.error(`Available workflows: ${WORKFLOWS.map((item) => item.name).join(", ")}`);
9329
+ process.exit(1);
9330
+ }
9331
+ console.log(`${workflow.name} (${workflow.area})`);
9332
+ console.log(workflow.description);
9333
+ console.log("");
9334
+ for (const command of workflow.commands) {
9335
+ console.log(command);
9336
+ }
9337
+ });
9338
+ return cmd;
9339
+ }
9340
+
8477
9341
  // src/index.ts
8478
9342
  function createProgram() {
8479
- const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.1").addHelpText("after", `
9343
+ const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.4").addHelpText("after", `
8480
9344
  Command Groups:
8481
9345
  auth Authentication (login, logout, token, status)
8482
9346
  profile Multi-profile management
8483
9347
  context Workspace context (tenant, capability, app)
9348
+ workflows Page-level operating workflows and command recipes
9349
+ oauth-clients OAuth client_credentials management
8484
9350
  audiences Audience management (DSP)
8485
9351
  campaigns Campaign management (DSP)
8486
9352
  variations Ad variation management (DSP)
@@ -8505,12 +9371,15 @@ Command Groups:
8505
9371
  Getting Started:
8506
9372
  $ a8techads auth login # Authenticate
8507
9373
  $ a8techads context show # Check current context
9374
+ $ a8techads workflows list # Discover end-to-end workflows
8508
9375
  $ a8techads campaigns list # List campaigns (DSP)
8509
9376
  $ a8techads sites list # List sites (SSP)
8510
9377
  $ a8techads reports query --metrics spend # Run analytics query`);
8511
9378
  program2.addCommand(createAuthCommand());
8512
9379
  program2.addCommand(createProfileCommand());
8513
9380
  program2.addCommand(createContextCommand());
9381
+ program2.addCommand(createWorkflowsCommand());
9382
+ program2.addCommand(createOAuthClientsCommand());
8514
9383
  program2.addCommand(createAudiencesCommand());
8515
9384
  program2.addCommand(createCampaignsCommand());
8516
9385
  program2.addCommand(createVariationsCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a8techads/cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "A8TechAds CLI — programmatic ad platform management",
5
5
  "type": "module",
6
6
  "bin": {