adapty 0.3.0 → 0.4.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.
Files changed (84) hide show
  1. package/README.md +134 -5
  2. package/dist/commands/asa/ad-groups/create.d.ts +22 -0
  3. package/dist/commands/asa/ad-groups/create.js +54 -0
  4. package/dist/commands/asa/ad-groups/get.d.ts +11 -0
  5. package/dist/commands/asa/ad-groups/get.js +21 -0
  6. package/dist/commands/asa/ad-groups/list.d.ts +18 -0
  7. package/dist/commands/asa/ad-groups/list.js +24 -0
  8. package/dist/commands/asa/ad-groups/update.d.ts +23 -0
  9. package/dist/commands/asa/ad-groups/update.js +62 -0
  10. package/dist/commands/asa/ads/create.d.ts +16 -0
  11. package/dist/commands/asa/ads/create.js +41 -0
  12. package/dist/commands/asa/ads/get.d.ts +11 -0
  13. package/dist/commands/asa/ads/get.js +21 -0
  14. package/dist/commands/asa/ads/list.d.ts +18 -0
  15. package/dist/commands/asa/ads/list.js +24 -0
  16. package/dist/commands/asa/ads/update.d.ts +17 -0
  17. package/dist/commands/asa/ads/update.js +44 -0
  18. package/dist/commands/asa/apps/list.d.ts +13 -0
  19. package/dist/commands/asa/apps/list.js +18 -0
  20. package/dist/commands/asa/automations/create.d.ts +16 -0
  21. package/dist/commands/asa/automations/create.js +60 -0
  22. package/dist/commands/asa/automations/get.d.ts +11 -0
  23. package/dist/commands/asa/automations/get.js +21 -0
  24. package/dist/commands/asa/automations/list.d.ts +13 -0
  25. package/dist/commands/asa/automations/list.js +18 -0
  26. package/dist/commands/asa/automations/run.d.ts +16 -0
  27. package/dist/commands/asa/automations/run.js +41 -0
  28. package/dist/commands/asa/automations/runs.d.ts +16 -0
  29. package/dist/commands/asa/automations/runs.js +24 -0
  30. package/dist/commands/asa/automations/update.d.ts +21 -0
  31. package/dist/commands/asa/automations/update.js +73 -0
  32. package/dist/commands/asa/campaigns/create.d.ts +25 -0
  33. package/dist/commands/asa/campaigns/create.js +67 -0
  34. package/dist/commands/asa/campaigns/get.d.ts +11 -0
  35. package/dist/commands/asa/campaigns/get.js +21 -0
  36. package/dist/commands/asa/campaigns/list.d.ts +17 -0
  37. package/dist/commands/asa/campaigns/list.js +24 -0
  38. package/dist/commands/asa/campaigns/update.d.ts +23 -0
  39. package/dist/commands/asa/campaigns/update.js +66 -0
  40. package/dist/commands/asa/competitors/summary.d.ts +11 -0
  41. package/dist/commands/asa/competitors/summary.js +56 -0
  42. package/dist/commands/asa/connect.d.ts +12 -0
  43. package/dist/commands/asa/connect.js +39 -0
  44. package/dist/commands/asa/creatives/list.d.ts +15 -0
  45. package/dist/commands/asa/creatives/list.js +24 -0
  46. package/dist/commands/asa/keywords/add.d.ts +20 -0
  47. package/dist/commands/asa/keywords/add.js +73 -0
  48. package/dist/commands/asa/keywords/list.d.ts +19 -0
  49. package/dist/commands/asa/keywords/list.js +24 -0
  50. package/dist/commands/asa/keywords/update.d.ts +21 -0
  51. package/dist/commands/asa/keywords/update.js +64 -0
  52. package/dist/commands/asa/metrics/index.d.ts +21 -0
  53. package/dist/commands/asa/metrics/index.js +68 -0
  54. package/dist/commands/asa/metrics/overview.d.ts +15 -0
  55. package/dist/commands/asa/metrics/overview.js +51 -0
  56. package/dist/commands/asa/negative-keywords/add.d.ts +18 -0
  57. package/dist/commands/asa/negative-keywords/add.js +70 -0
  58. package/dist/commands/asa/negative-keywords/list.d.ts +19 -0
  59. package/dist/commands/asa/negative-keywords/list.js +29 -0
  60. package/dist/commands/asa/orgs/list.d.ts +13 -0
  61. package/dist/commands/asa/orgs/list.js +18 -0
  62. package/dist/commands/asa/product-pages/list.d.ts +15 -0
  63. package/dist/commands/asa/product-pages/list.js +24 -0
  64. package/dist/commands/asa/product-pages/sync.d.ts +13 -0
  65. package/dist/commands/asa/product-pages/sync.js +28 -0
  66. package/dist/commands/asa/search-terms/list.d.ts +20 -0
  67. package/dist/commands/asa/search-terms/list.js +25 -0
  68. package/dist/commands/asa/whoami.d.ts +8 -0
  69. package/dist/commands/asa/whoami.js +22 -0
  70. package/dist/lib/api-client.d.ts +15 -3
  71. package/dist/lib/api-client.js +49 -17
  72. package/dist/lib/asa-client.d.ts +16 -0
  73. package/dist/lib/asa-client.js +46 -0
  74. package/dist/lib/asa-confirm.d.ts +18 -0
  75. package/dist/lib/asa-confirm.js +41 -0
  76. package/dist/lib/asa-flags.d.ts +88 -0
  77. package/dist/lib/asa-flags.js +137 -0
  78. package/dist/lib/asa-schemas.d.ts +287 -0
  79. package/dist/lib/asa-schemas.js +1 -0
  80. package/dist/lib/errors.d.ts +25 -2
  81. package/dist/lib/errors.js +48 -7
  82. package/dist/lib/output.js +45 -15
  83. package/oclif.manifest.json +2802 -157
  84. package/package.json +40 -1
@@ -0,0 +1,19 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaNegativeKeywordDTO } from '../../../lib/asa-schemas.js';
3
+ import { type PaginatedResponse } from '../../../lib/flags.js';
4
+ export default class AsaNegativeKeywordsList extends Command {
5
+ static description: string;
6
+ static enableJsonFlag: boolean;
7
+ static examples: string[];
8
+ static flags: {
9
+ 'campaign-level-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ 'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
16
+ 'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
17
+ };
18
+ run(): Promise<PaginatedResponse<AsaNegativeKeywordDTO>>;
19
+ }
@@ -0,0 +1,29 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { createAsaClient } from '../../../lib/asa-client.js';
3
+ import { adGroupScopeFlags, asaPaginationFlags, scopeParams } from '../../../lib/asa-flags.js';
4
+ import { paginationParams } from '../../../lib/flags.js';
5
+ import { printList } from '../../../lib/output.js';
6
+ export default class AsaNegativeKeywordsList extends Command {
7
+ static description = 'List negative keywords; ad_group_id is empty for campaign-level ones';
8
+ static enableJsonFlag = true;
9
+ static examples = [
10
+ '<%= config.bin %> asa negative-keywords list',
11
+ '<%= config.bin %> asa negative-keywords list --campaign CAMPAIGN_UUID',
12
+ ];
13
+ static flags = {
14
+ ...asaPaginationFlags,
15
+ ...adGroupScopeFlags,
16
+ 'campaign-level-only': Flags.boolean({ description: 'Keep only campaign-level rows (ad_group_id is null)' }),
17
+ };
18
+ async run() {
19
+ const { flags } = await this.parse(AsaNegativeKeywordsList);
20
+ const client = await createAsaClient(this.config);
21
+ const result = await client.get('/negative-keywords', {
22
+ ...paginationParams(flags),
23
+ ...scopeParams(flags),
24
+ ...(flags['campaign-level-only'] ? { campaign_level_only: 'true' } : {}),
25
+ });
26
+ printList(result.data, this.log.bind(this), result.meta.pagination);
27
+ return result;
28
+ }
29
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaCampaignGroupDTO } from '../../../lib/asa-schemas.js';
3
+ import { type PaginatedResponse } from '../../../lib/flags.js';
4
+ export default class AsaOrgsList extends Command {
5
+ static description: string;
6
+ static enableJsonFlag: boolean;
7
+ static examples: string[];
8
+ static flags: {
9
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<PaginatedResponse<AsaCampaignGroupDTO>>;
13
+ }
@@ -0,0 +1,18 @@
1
+ import { Command } from '@oclif/core';
2
+ import { createAsaClient } from '../../../lib/asa-client.js';
3
+ import { asaPaginationFlags } from '../../../lib/asa-flags.js';
4
+ import { paginationParams } from '../../../lib/flags.js';
5
+ import { printList } from '../../../lib/output.js';
6
+ export default class AsaOrgsList extends Command {
7
+ static description = 'List the Apple Search Ads organizations this company can spend from';
8
+ static enableJsonFlag = true;
9
+ static examples = ['<%= config.bin %> asa orgs list'];
10
+ static flags = { ...asaPaginationFlags };
11
+ async run() {
12
+ const { flags } = await this.parse(AsaOrgsList);
13
+ const client = await createAsaClient(this.config);
14
+ const result = await client.get('/campaign-groups', paginationParams(flags));
15
+ printList(result.data, this.log.bind(this), result.meta.pagination);
16
+ return result;
17
+ }
18
+ }
@@ -0,0 +1,15 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaProductPageDTO } from '../../../lib/asa-schemas.js';
3
+ import { type PaginatedResponse } from '../../../lib/flags.js';
4
+ export default class AsaProductPagesList extends Command {
5
+ static description: string;
6
+ static enableJsonFlag: boolean;
7
+ static examples: string[];
8
+ static flags: {
9
+ app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
12
+ 'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
14
+ run(): Promise<PaginatedResponse<AsaProductPageDTO>>;
15
+ }
@@ -0,0 +1,24 @@
1
+ import { Command } from '@oclif/core';
2
+ import { createAsaClient } from '../../../lib/asa-client.js';
3
+ import { asaPaginationFlags, assetScopeFlags, scopeParams } from '../../../lib/asa-flags.js';
4
+ import { paginationParams } from '../../../lib/flags.js';
5
+ import { printList } from '../../../lib/output.js';
6
+ export default class AsaProductPagesList extends Command {
7
+ static description = 'List custom product pages (read-only; authoring stays in App Store Connect)';
8
+ static enableJsonFlag = true;
9
+ static examples = [
10
+ '<%= config.bin %> asa product-pages list',
11
+ '<%= config.bin %> asa product-pages list --app APP_UUID',
12
+ ];
13
+ static flags = { ...asaPaginationFlags, ...assetScopeFlags };
14
+ async run() {
15
+ const { flags } = await this.parse(AsaProductPagesList);
16
+ const client = await createAsaClient(this.config);
17
+ const result = await client.get('/product-pages', {
18
+ ...paginationParams(flags),
19
+ ...scopeParams(flags),
20
+ });
21
+ printList(result.data, this.log.bind(this), result.meta.pagination);
22
+ return result;
23
+ }
24
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaProductPageSyncDTO } from '../../../lib/asa-schemas.js';
3
+ export default class AsaProductPagesSync extends Command {
4
+ static description: string;
5
+ static enableJsonFlag: boolean;
6
+ static examples: string[];
7
+ static flags: {
8
+ 'adam-id': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<AsaProductPageSyncDTO>;
13
+ }
@@ -0,0 +1,28 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { asaWrite, createAsaClient } from '../../../lib/asa-client.js';
3
+ import { confirmFlags, confirmMutation } from '../../../lib/asa-confirm.js';
4
+ import { idempotencyFlags } from '../../../lib/asa-flags.js';
5
+ import { printResponse } from '../../../lib/output.js';
6
+ export default class AsaProductPagesSync extends Command {
7
+ static description = 'Refresh custom product pages from Apple';
8
+ static enableJsonFlag = true;
9
+ static examples = ['<%= config.bin %> asa product-pages sync', '<%= config.bin %> asa product-pages sync --adam-id 123456'];
10
+ static flags = {
11
+ ...confirmFlags,
12
+ ...idempotencyFlags,
13
+ 'adam-id': Flags.integer({ description: 'Limit the refresh to one app; omit to cover every app' }),
14
+ };
15
+ async run() {
16
+ const { flags } = await this.parse(AsaProductPagesSync);
17
+ const body = { ...(flags['adam-id'] === undefined ? {} : { adam_id: flags['adam-id'] }) };
18
+ await confirmMutation(this, { body, method: 'POST', path: '/product-pages/sync/', summary: 'Queue a product page refresh from Apple' }, flags.yes);
19
+ const client = await createAsaClient(this.config);
20
+ const { result } = await asaWrite(client, 'post', '/product-pages/sync', {
21
+ body,
22
+ idempotencyKey: flags['idempotency-key'],
23
+ });
24
+ this.log(result.replayed ? 'Already running; nothing new was queued.' : 'Sync queued.');
25
+ printResponse(result, this.log.bind(this));
26
+ return result;
27
+ }
28
+ }
@@ -0,0 +1,20 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaSearchTermDTO } from '../../../lib/asa-schemas.js';
3
+ import { type PaginatedResponse } from '../../../lib/flags.js';
4
+ export default class AsaSearchTermsList extends Command {
5
+ static description: string;
6
+ static enableJsonFlag: boolean;
7
+ static examples: string[];
8
+ static flags: {
9
+ 'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'date-from': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ 'date-to': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
17
+ 'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
18
+ };
19
+ run(): Promise<PaginatedResponse<AsaSearchTermDTO>>;
20
+ }
@@ -0,0 +1,25 @@
1
+ import { Command } from '@oclif/core';
2
+ import { createAsaClient } from '../../../lib/asa-client.js';
3
+ import { adGroupScopeFlags, asaPaginationFlags, periodFlags, periodParams, scopeParams } from '../../../lib/asa-flags.js';
4
+ import { paginationParams } from '../../../lib/flags.js';
5
+ import { printList } from '../../../lib/output.js';
6
+ export default class AsaSearchTermsList extends Command {
7
+ static description = 'List the search terms your ads matched, with their metrics';
8
+ static enableJsonFlag = true;
9
+ static examples = [
10
+ '<%= config.bin %> asa search-terms list --date-from 2026-07-01 --date-to 2026-07-31',
11
+ '<%= config.bin %> asa search-terms list --ad-group AD_GROUP_UUID',
12
+ ];
13
+ static flags = { ...asaPaginationFlags, ...periodFlags, ...adGroupScopeFlags };
14
+ async run() {
15
+ const { flags } = await this.parse(AsaSearchTermsList);
16
+ const client = await createAsaClient(this.config);
17
+ const result = await client.get('/search-terms', {
18
+ ...paginationParams(flags),
19
+ ...periodParams(flags),
20
+ ...scopeParams(flags),
21
+ });
22
+ printList(result.data, this.log.bind(this), result.meta.pagination);
23
+ return result;
24
+ }
25
+ }
@@ -0,0 +1,8 @@
1
+ import { Command } from '@oclif/core';
2
+ import type { AsaMeDTO } from '../../lib/asa-schemas.js';
3
+ export default class AsaWhoami extends Command {
4
+ static description: string;
5
+ static enableJsonFlag: boolean;
6
+ static examples: string[];
7
+ run(): Promise<AsaMeDTO>;
8
+ }
@@ -0,0 +1,22 @@
1
+ import { Command } from '@oclif/core';
2
+ import { createAsaClient } from '../../lib/asa-client.js';
3
+ import { printResponse } from '../../lib/output.js';
4
+ export default class AsaWhoami extends Command {
5
+ static description = 'Show which company the token unlocks and whether Apple Ads is connected';
6
+ static enableJsonFlag = true;
7
+ static examples = ['<%= config.bin %> asa whoami'];
8
+ async run() {
9
+ await this.parse(AsaWhoami);
10
+ const client = await createAsaClient(this.config);
11
+ const result = await client.get('/me');
12
+ printResponse(result, this.log.bind(this));
13
+ if (result.apple_credentials_status !== 'active') {
14
+ this.log('\nApple Ads is not connected. Run `adapty asa connect` to link an account.');
15
+ }
16
+ if (result.access_source === 'none') {
17
+ this.log('\nNo active Ads Manager subscription for this company: connecting an account works, but every');
18
+ this.log('data command answers 402 until the subscription is in place.');
19
+ }
20
+ return result;
21
+ }
22
+ }
@@ -1,15 +1,27 @@
1
+ import { type ApiErrorFormat } from './errors.js';
2
+ export type QueryParams = Record<string, string | string[] | undefined>;
3
+ export interface RequestOptions {
4
+ headers?: Record<string, string>;
5
+ onResponse?: (headers: Headers) => void;
6
+ }
1
7
  export interface ApiClientOptions {
2
8
  baseUrl?: string;
9
+ defaultBaseUrl?: string;
10
+ errorFormat?: ApiErrorFormat;
3
11
  token?: null | string;
12
+ urlEnvVar?: string;
4
13
  userAgent?: string;
5
14
  }
6
15
  export declare class ApiClient {
7
16
  private baseUrl;
17
+ private errorFormat;
8
18
  private token;
9
19
  private userAgent;
10
20
  constructor(opts?: ApiClientOptions);
11
- get<T = unknown>(path: string, params?: Record<string, string>): Promise<T>;
12
- post<T = unknown>(path: string, body?: unknown): Promise<T>;
13
- put<T = unknown>(path: string, body?: unknown): Promise<T>;
21
+ get<T = unknown>(path: string, params?: QueryParams): Promise<T>;
22
+ post<T = unknown>(path: string, body?: unknown, params?: QueryParams, opts?: RequestOptions): Promise<T>;
23
+ put<T = unknown>(path: string, body?: unknown, params?: QueryParams, opts?: RequestOptions): Promise<T>;
24
+ private buildUrl;
25
+ private isRetryableRateLimit;
14
26
  private request;
15
27
  }
@@ -1,42 +1,62 @@
1
1
  import { ApiError, NetworkError, parseApiError } from './errors.js';
2
2
  const DEFAULT_API_URL = 'https://api-admin.adapty.io/api/v1/developer';
3
+ const MAX_RETRY_AFTER_SECONDS = 60;
3
4
  function ensureTrailingSlash(path) {
4
5
  return path.endsWith('/') ? path : `${path}/`;
5
6
  }
6
7
  export class ApiClient {
7
8
  baseUrl;
9
+ errorFormat;
8
10
  token;
9
11
  userAgent;
10
12
  constructor(opts = {}) {
11
- this.baseUrl = (opts.baseUrl ?? process.env.ADAPTY_API_URL ?? DEFAULT_API_URL).replace(/\/$/, '');
12
- if (this.baseUrl !== DEFAULT_API_URL) {
13
+ const defaultBaseUrl = opts.defaultBaseUrl ?? DEFAULT_API_URL;
14
+ const envBaseUrl = process.env[opts.urlEnvVar ?? 'ADAPTY_API_URL'];
15
+ this.baseUrl = (opts.baseUrl ?? envBaseUrl ?? defaultBaseUrl).replace(/\/$/, '');
16
+ if (this.baseUrl !== defaultBaseUrl) {
13
17
  process.stderr.write(`Warning: using non-default API URL: ${this.baseUrl}\n`);
14
18
  }
19
+ this.errorFormat = opts.errorFormat ?? 'developer';
15
20
  this.token = opts.token ?? null;
16
21
  this.userAgent = opts.userAgent ?? 'adapty-cli';
17
22
  }
18
23
  async get(path, params) {
19
- let url = `${this.baseUrl}${ensureTrailingSlash(path)}`;
20
- if (params) {
21
- const qs = new URLSearchParams(params);
22
- url += `?${qs.toString()}`;
23
- }
24
- return this.request(url, { method: 'GET' });
24
+ return this.request(this.buildUrl(path, params), { method: 'GET' });
25
25
  }
26
- async post(path, body) {
27
- return this.request(`${this.baseUrl}${ensureTrailingSlash(path)}`, {
26
+ async post(path, body, params, opts) {
27
+ return this.request(this.buildUrl(path, params), {
28
28
  body: body ? JSON.stringify(body) : undefined,
29
29
  method: 'POST',
30
- });
30
+ }, opts);
31
31
  }
32
- async put(path, body) {
33
- return this.request(`${this.baseUrl}${ensureTrailingSlash(path)}`, {
32
+ async put(path, body, params, opts) {
33
+ return this.request(this.buildUrl(path, params), {
34
34
  body: body ? JSON.stringify(body) : undefined,
35
35
  method: 'PUT',
36
- });
36
+ }, opts);
37
+ }
38
+ buildUrl(path, params) {
39
+ const url = `${this.baseUrl}${ensureTrailingSlash(path)}`;
40
+ if (!params)
41
+ return url;
42
+ const search = new URLSearchParams();
43
+ for (const [key, value] of Object.entries(params)) {
44
+ if (value === undefined)
45
+ continue;
46
+ for (const item of Array.isArray(value) ? value : [value])
47
+ search.append(key, item);
48
+ }
49
+ return search.size === 0 ? url : `${url}?${search.toString()}`;
50
+ }
51
+ isRetryableRateLimit(error) {
52
+ return (this.errorFormat === 'asa' &&
53
+ error.statusCode === 429 &&
54
+ error.errorCode !== 'cli_cooldown_active' &&
55
+ error.retryAfterSeconds !== undefined &&
56
+ error.retryAfterSeconds <= MAX_RETRY_AFTER_SECONDS);
37
57
  }
38
58
  // eslint-disable-next-line no-undef
39
- async request(url, init) {
59
+ async request(url, init, opts = {}, retried = false) {
40
60
  const headers = {
41
61
  'User-Agent': this.userAgent,
42
62
  };
@@ -46,6 +66,7 @@ export class ApiClient {
46
66
  if (this.token) {
47
67
  headers.Authorization = `Bearer ${this.token}`;
48
68
  }
69
+ Object.assign(headers, opts.headers);
49
70
  let response;
50
71
  try {
51
72
  response = await fetch(url, { ...init, headers });
@@ -53,24 +74,35 @@ export class ApiClient {
53
74
  catch (error) {
54
75
  throw new NetworkError(error instanceof Error ? error.message : 'Connection failed');
55
76
  }
77
+ opts.onResponse?.(response.headers);
56
78
  if (response.status === 204) {
57
79
  return undefined;
58
80
  }
81
+ const retryAfter = Number.parseInt(response.headers.get('Retry-After') ?? '', 10);
82
+ const errorOptions = this.errorFormat === 'asa' && !Number.isNaN(retryAfter) ? { retryAfterSeconds: retryAfter } : {};
59
83
  let body;
60
84
  try {
61
85
  body = await response.json();
62
86
  }
63
87
  catch {
64
88
  if (!response.ok) {
65
- throw new ApiError(response.status, `http_${response.status}`, {});
89
+ throw new ApiError(response.status, `http_${response.status}`, {}, errorOptions);
66
90
  }
67
91
  return undefined;
68
92
  }
69
93
  if (!response.ok) {
70
- const error = parseApiError(response.status, body);
94
+ const error = parseApiError(response.status, body, errorOptions, this.errorFormat);
71
95
  if (response.status === 401) {
72
96
  error.message = 'Token expired or invalid. Run `adapty auth login`.';
73
97
  }
98
+ if (!retried && this.isRetryableRateLimit(error)) {
99
+ const seconds = error.retryAfterSeconds ?? 0;
100
+ process.stderr.write(`Rate limited (${error.errorCode}); waiting ${seconds}s per Retry-After, then retrying once.\n`);
101
+ await new Promise((resolve) => {
102
+ setTimeout(resolve, seconds * 1000);
103
+ });
104
+ return this.request(url, init, opts, true);
105
+ }
74
106
  throw error;
75
107
  }
76
108
  return body;
@@ -0,0 +1,16 @@
1
+ import type { Config } from '@oclif/core';
2
+ import { ApiClient, type QueryParams } from './api-client.js';
3
+ export declare const ASA_API_URL = "https://api-asa-admin.adapty.io/api/v1/cli";
4
+ export declare const ASA_API_URL_ENV_VAR = "ADAPTY_ASA_API_URL";
5
+ export declare function createAsaClient(config: Config): Promise<ApiClient>;
6
+ export interface AsaWriteOptions {
7
+ body?: unknown;
8
+ idempotencyKey?: string;
9
+ params?: QueryParams;
10
+ }
11
+ export interface AsaWriteOutcome<T> {
12
+ replayed: boolean;
13
+ result: T;
14
+ }
15
+ export declare function asaWrite<T>(client: ApiClient, method: 'post' | 'put', path: string, opts?: AsaWriteOptions): Promise<AsaWriteOutcome<T>>;
16
+ export declare function noteReplay(replayed: boolean, log: (msg: string) => void): void;
@@ -0,0 +1,46 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { ApiClient } from './api-client.js';
3
+ import { resolveToken } from './auth.js';
4
+ import { buildUserAgent } from './client-from-config.js';
5
+ import { AuthRequiredError, NetworkError } from './errors.js';
6
+ export const ASA_API_URL = 'https://api-asa-admin.adapty.io/api/v1/cli';
7
+ export const ASA_API_URL_ENV_VAR = 'ADAPTY_ASA_API_URL';
8
+ export async function createAsaClient(config) {
9
+ const token = await resolveToken(config.configDir);
10
+ if (!token)
11
+ throw new AuthRequiredError();
12
+ return new ApiClient({
13
+ defaultBaseUrl: ASA_API_URL,
14
+ errorFormat: 'asa',
15
+ token,
16
+ urlEnvVar: ASA_API_URL_ENV_VAR,
17
+ userAgent: buildUserAgent(config),
18
+ });
19
+ }
20
+ export async function asaWrite(client, method, path, opts = {}) {
21
+ const key = opts.idempotencyKey ?? randomUUID();
22
+ let replayed = false;
23
+ const requestOpts = {
24
+ headers: { 'Idempotency-Key': key },
25
+ onResponse(headers) {
26
+ replayed = headers.get('Idempotency-Replayed') === 'true';
27
+ },
28
+ };
29
+ const send = () => method === 'post'
30
+ ? client.post(path, opts.body, opts.params, requestOpts)
31
+ : client.put(path, opts.body, opts.params, requestOpts);
32
+ try {
33
+ const result = await send();
34
+ return { replayed, result };
35
+ }
36
+ catch (error) {
37
+ if (!(error instanceof NetworkError))
38
+ throw error;
39
+ const result = await send();
40
+ return { replayed, result };
41
+ }
42
+ }
43
+ export function noteReplay(replayed, log) {
44
+ if (replayed)
45
+ log('Already applied earlier — showing the stored result.');
46
+ }
@@ -0,0 +1,18 @@
1
+ import { Command } from '@oclif/core';
2
+ export declare const confirmFlags: {
3
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
4
+ };
5
+ export type ConfirmDecision = 'ask' | 'proceed' | 'refuse';
6
+ export declare function decideConfirmation(opts: {
7
+ isTty: boolean;
8
+ json: boolean;
9
+ yes: boolean;
10
+ }): ConfirmDecision;
11
+ export interface MutationPreview {
12
+ body?: unknown;
13
+ method: string;
14
+ path: string;
15
+ summary: string;
16
+ }
17
+ export declare function renderPreview(preview: MutationPreview): string;
18
+ export declare function confirmMutation(command: Command, preview: MutationPreview, yes: boolean): Promise<void>;
@@ -0,0 +1,41 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { createInterface } from 'node:readline/promises';
3
+ export const confirmFlags = {
4
+ yes: Flags.boolean({
5
+ char: 'y',
6
+ description: 'Apply without asking; required when the output is piped or --json is used',
7
+ }),
8
+ };
9
+ export function decideConfirmation(opts) {
10
+ if (opts.yes)
11
+ return 'proceed';
12
+ if (opts.json || !opts.isTty)
13
+ return 'refuse';
14
+ return 'ask';
15
+ }
16
+ export function renderPreview(preview) {
17
+ const lines = [preview.summary, `${preview.method} ${preview.path}`];
18
+ if (preview.body !== undefined)
19
+ lines.push(JSON.stringify(preview.body, null, 2));
20
+ return lines.join('\n');
21
+ }
22
+ export async function confirmMutation(command, preview, yes) {
23
+ const decision = decideConfirmation({ isTty: process.stdin.isTTY === true, json: command.jsonEnabled(), yes });
24
+ if (decision === 'proceed')
25
+ return;
26
+ if (decision === 'refuse') {
27
+ const reason = `${preview.summary} changes your account. Re-run with --yes to apply it without a prompt.`;
28
+ process.stderr.write(`${reason}\n${renderPreview(preview)}\n`);
29
+ command.error(reason, { exit: 2 });
30
+ }
31
+ process.stderr.write(`${renderPreview(preview)}\n`);
32
+ const reader = createInterface({ input: process.stdin, output: process.stderr });
33
+ try {
34
+ const answer = await reader.question('Apply? [y/N] ');
35
+ if (!/^y(es)?$/i.test(answer.trim()))
36
+ command.error('Cancelled, nothing was sent.', { exit: 1 });
37
+ }
38
+ finally {
39
+ reader.close();
40
+ }
41
+ }
@@ -0,0 +1,88 @@
1
+ import type { QueryParams } from './api-client.js';
2
+ import type { AsaMoney, AsaMutationError } from './asa-schemas.js';
3
+ export declare const MAX_BULK_ITEMS = 100;
4
+ export declare const MAX_BY_DAYS = 16;
5
+ export declare const ASA_METRIC_ENTITIES: string[];
6
+ export declare const ASA_GROUP_BY_DIMENSIONS: string[];
7
+ export declare const asaPaginationFlags: {
8
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ export declare const byDaysFlag: {
12
+ 'by-days': import("@oclif/core/interfaces").OptionFlag<number[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
14
+ export declare const periodFlags: {
15
+ 'date-from': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ 'date-to': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ };
18
+ export declare const assetScopeFlags: {
19
+ app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ 'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ };
22
+ export declare const orgScopeFlags: {
23
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
+ app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
+ 'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
26
+ };
27
+ export declare const campaignScopeFlags: {
28
+ campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
29
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
30
+ app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
31
+ 'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
32
+ };
33
+ export declare const adGroupScopeFlags: {
34
+ 'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
35
+ campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
36
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
37
+ app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
38
+ 'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
39
+ };
40
+ export declare const adScopeFlags: {
41
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
42
+ 'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
43
+ campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
44
+ 'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
45
+ };
46
+ export declare const statusFilter: (options: string[]) => {
47
+ status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
48
+ };
49
+ interface ScopeFlags {
50
+ 'ad-group'?: string[];
51
+ app?: string[];
52
+ campaign?: string[];
53
+ 'campaign-group'?: string[];
54
+ search?: string;
55
+ status?: string;
56
+ }
57
+ export declare function scopeParams(flags: ScopeFlags): QueryParams;
58
+ export declare const scheduleFlags: {
59
+ 'end-time': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
60
+ 'start-time': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
61
+ };
62
+ export declare const pricingModelFlag: {
63
+ 'pricing-model': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
64
+ };
65
+ export declare function startOfDayUtc(date: string | undefined): string | undefined;
66
+ export declare function todayUtc(): string;
67
+ export declare function periodParams(flags: {
68
+ 'date-from'?: string;
69
+ 'date-to'?: string;
70
+ }): Record<string, string>;
71
+ export declare function moneyFlag(description: string, opts?: {
72
+ required?: boolean;
73
+ }): import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
74
+ export declare const currencyFlag: {
75
+ currency: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
76
+ };
77
+ export declare const idempotencyFlags: {
78
+ 'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
79
+ };
80
+ export declare function money(amount: string | undefined, currency: string): AsaMoney | undefined;
81
+ interface BulkOutcome {
82
+ applied: unknown[];
83
+ errors: AsaMutationError[];
84
+ isValidationFailure: boolean;
85
+ kind: string;
86
+ }
87
+ export declare function reportBulkOutcome({ applied, errors, isValidationFailure, kind }: BulkOutcome, log: (msg: string) => void): void;
88
+ export {};