adapty 0.8.4-beta.1 → 0.8.5

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 (107) hide show
  1. package/README.md +133 -3
  2. package/bin/dev.js +2 -2
  3. package/bin/run.js +2 -2
  4. package/dist/commands/access-levels/create.js +1 -1
  5. package/dist/commands/access-levels/list.d.ts +1 -1
  6. package/dist/commands/apps/create.js +9 -6
  7. package/dist/commands/apps/list.d.ts +1 -1
  8. package/dist/commands/apps/update.js +7 -4
  9. package/dist/commands/asa/ad-groups/create.js +9 -7
  10. package/dist/commands/asa/ad-groups/get.js +3 -2
  11. package/dist/commands/asa/ad-groups/list.d.ts +1 -1
  12. package/dist/commands/asa/ad-groups/list.js +1 -1
  13. package/dist/commands/asa/ad-groups/update.js +22 -12
  14. package/dist/commands/asa/ads/create.js +7 -5
  15. package/dist/commands/asa/ads/get.js +3 -2
  16. package/dist/commands/asa/ads/list.d.ts +1 -1
  17. package/dist/commands/asa/ads/list.js +1 -1
  18. package/dist/commands/asa/ads/update.js +9 -5
  19. package/dist/commands/asa/apps/list.d.ts +1 -1
  20. package/dist/commands/asa/apps/list.js +1 -1
  21. package/dist/commands/asa/automations/create.d.ts +9 -0
  22. package/dist/commands/asa/automations/create.js +26 -7
  23. package/dist/commands/asa/automations/get.js +3 -2
  24. package/dist/commands/asa/automations/list.d.ts +1 -1
  25. package/dist/commands/asa/automations/list.js +1 -1
  26. package/dist/commands/asa/automations/run.js +5 -3
  27. package/dist/commands/asa/automations/runs.d.ts +1 -1
  28. package/dist/commands/asa/automations/runs.js +4 -4
  29. package/dist/commands/asa/automations/update.d.ts +9 -0
  30. package/dist/commands/asa/automations/update.js +36 -12
  31. package/dist/commands/asa/campaigns/bulk-create.js +31 -18
  32. package/dist/commands/asa/campaigns/bulk-list.js +7 -5
  33. package/dist/commands/asa/campaigns/bulk-status.js +3 -2
  34. package/dist/commands/asa/campaigns/create.js +10 -8
  35. package/dist/commands/asa/campaigns/get.js +3 -2
  36. package/dist/commands/asa/campaigns/list.d.ts +1 -1
  37. package/dist/commands/asa/campaigns/list.js +1 -1
  38. package/dist/commands/asa/campaigns/update.js +25 -15
  39. package/dist/commands/asa/competitors/summary.js +5 -5
  40. package/dist/commands/asa/connect.js +5 -3
  41. package/dist/commands/asa/creatives/list.d.ts +1 -1
  42. package/dist/commands/asa/creatives/list.js +1 -1
  43. package/dist/commands/asa/keywords/add.js +13 -10
  44. package/dist/commands/asa/keywords/list.d.ts +1 -1
  45. package/dist/commands/asa/keywords/list.js +1 -1
  46. package/dist/commands/asa/keywords/update.js +15 -10
  47. package/dist/commands/asa/metrics/index.d.ts +6 -3
  48. package/dist/commands/asa/metrics/index.js +38 -14
  49. package/dist/commands/asa/metrics/overview.js +3 -3
  50. package/dist/commands/asa/negative-keywords/add.js +10 -7
  51. package/dist/commands/asa/negative-keywords/list.d.ts +1 -1
  52. package/dist/commands/asa/negative-keywords/list.js +1 -1
  53. package/dist/commands/asa/orgs/list.d.ts +1 -1
  54. package/dist/commands/asa/orgs/list.js +1 -1
  55. package/dist/commands/asa/product-pages/list.d.ts +1 -1
  56. package/dist/commands/asa/product-pages/list.js +1 -1
  57. package/dist/commands/asa/product-pages/sync.js +1 -1
  58. package/dist/commands/asa/search-terms/list.d.ts +1 -1
  59. package/dist/commands/asa/search-terms/list.js +1 -1
  60. package/dist/commands/asa/whoami.js +7 -2
  61. package/dist/commands/auth/login.js +3 -1
  62. package/dist/commands/flows/config/get.js +2 -2
  63. package/dist/commands/flows/config/preview.d.ts +2 -2
  64. package/dist/commands/flows/config/preview.js +6 -4
  65. package/dist/commands/flows/config/update.js +5 -3
  66. package/dist/commands/flows/config/validate.js +5 -3
  67. package/dist/commands/flows/list.d.ts +1 -1
  68. package/dist/commands/flows/media/upload.js +1 -1
  69. package/dist/commands/flows/publish.js +5 -5
  70. package/dist/commands/paywalls/create.js +1 -1
  71. package/dist/commands/paywalls/list.d.ts +1 -1
  72. package/dist/commands/paywalls/placements.d.ts +2 -2
  73. package/dist/commands/paywalls/update.js +1 -1
  74. package/dist/commands/placements/create.js +17 -12
  75. package/dist/commands/placements/list.d.ts +1 -1
  76. package/dist/commands/placements/update.js +20 -15
  77. package/dist/commands/products/create.js +20 -13
  78. package/dist/commands/products/list.d.ts +1 -1
  79. package/dist/commands/products/update.js +1 -1
  80. package/dist/commands/segments/list.d.ts +1 -1
  81. package/dist/lib/api-client.d.ts +10 -6
  82. package/dist/lib/api-client.js +56 -35
  83. package/dist/lib/api-schemas.d.ts +56 -56
  84. package/dist/lib/app-url.js +2 -1
  85. package/dist/lib/asa-client.d.ts +13 -8
  86. package/dist/lib/asa-client.js +9 -4
  87. package/dist/lib/asa-flags.d.ts +37 -13
  88. package/dist/lib/asa-flags.js +87 -20
  89. package/dist/lib/asa-keyword-action.d.ts +18 -0
  90. package/dist/lib/asa-keyword-action.js +175 -0
  91. package/dist/lib/asa-schemas.d.ts +100 -80
  92. package/dist/lib/auth.js +2 -1
  93. package/dist/lib/client-from-config.d.ts +1 -1
  94. package/dist/lib/client-from-config.js +2 -1
  95. package/dist/lib/config.d.ts +2 -2
  96. package/dist/lib/confirm.d.ts +4 -4
  97. package/dist/lib/confirm.js +12 -7
  98. package/dist/lib/errors.d.ts +18 -18
  99. package/dist/lib/errors.js +14 -10
  100. package/dist/lib/flags.d.ts +3 -3
  101. package/dist/lib/flags.js +2 -1
  102. package/dist/lib/flow-help.js +11 -6
  103. package/dist/lib/output.js +23 -12
  104. package/dist/lib/preview.d.ts +5 -5
  105. package/dist/lib/preview.js +2 -1
  106. package/oclif.manifest.json +372 -200
  107. package/package.json +10 -8
@@ -1,6 +1,6 @@
1
- import { Args, Command } from '@oclif/core';
2
1
  import { readFile } from 'node:fs/promises';
3
2
  import { basename, extname } from 'node:path';
3
+ import { Args, Command } from '@oclif/core';
4
4
  import { createAuthenticatedClient } from '../../../lib/client-from-config.js';
5
5
  import { appFlag } from '../../../lib/flags.js';
6
6
  import { printResponse } from '../../../lib/output.js';
@@ -9,8 +9,8 @@ export default class FlowsPublish extends Command {
9
9
  static args = {
10
10
  flow_id: Args.string({ description: 'Flow ID (UUID)', required: true }),
11
11
  };
12
- static description = 'Publish a flow (async — publication completes in the background). ' +
13
- 'Publishing is async: returns status `publishing`. Poll `adapty flows get <flow_id>` for `published`/`publication_failed`; on failure, `adapty flows config get <flow_id>` shows why.';
12
+ static description = 'Publish a flow (async — publication completes in the background). '
13
+ + 'Publishing is async: returns status `publishing`. Poll `adapty flows get <flow_id>` for `published`/`publication_failed`; on failure, `adapty flows config get <flow_id>` shows why.';
14
14
  static enableJsonFlag = true;
15
15
  static examples = ['<%= config.bin %> flows publish --app UUID 550e8400-e29b-41d4-a716-446655440000'];
16
16
  static flags = {
@@ -35,9 +35,9 @@ export default class FlowsPublish extends Command {
35
35
  }
36
36
  catch (error) {
37
37
  if (error instanceof ApiError && (error.statusCode === 400 || error.errorCode === 'validation_error')) {
38
- this.error(`Flow publish failed: ${error.message}\n` +
39
- 'Publishing needs a flow with a valid config. Fix it, then run `adapty flows publish` again:\n' +
40
- flowFixLinks(args.flow_id), { exit: 1 });
38
+ this.error(`Flow publish failed: ${error.message}\n`
39
+ + 'Publishing needs a flow with a valid config. Fix it, then run `adapty flows publish` again:\n'
40
+ + flowFixLinks(args.flow_id), { exit: 1 });
41
41
  }
42
42
  throw error;
43
43
  }
@@ -15,7 +15,7 @@ export default class PaywallsCreate extends Command {
15
15
  multiple: true,
16
16
  required: true,
17
17
  }),
18
- title: Flags.string({ description: 'Paywall title', required: true }),
18
+ 'title': Flags.string({ description: 'Paywall title', required: true }),
19
19
  };
20
20
  async run() {
21
21
  const { flags } = await this.parse(PaywallsCreate);
@@ -1,6 +1,6 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import type { PaywallDTO } from '../../lib/api-schemas.js';
3
- import { type PaginatedResponse } from '../../lib/flags.js';
3
+ import type { PaginatedResponse } from '../../lib/flags.js';
4
4
  export default class PaywallsList extends Command {
5
5
  static description: string;
6
6
  static enableJsonFlag: boolean;
@@ -1,8 +1,8 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import type { PlacementSummaryDTO } from '../../lib/api-schemas.js';
3
- interface PlacementsByPaywallResponse {
3
+ type PlacementsByPaywallResponse = {
4
4
  data: PlacementSummaryDTO[];
5
- }
5
+ };
6
6
  export default class PaywallsPlacements extends Command {
7
7
  static args: {
8
8
  paywall_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
@@ -18,7 +18,7 @@ export default class PaywallsUpdate extends Command {
18
18
  multiple: true,
19
19
  required: true,
20
20
  }),
21
- title: Flags.string({ description: 'Paywall title', required: true }),
21
+ 'title': Flags.string({ description: 'Paywall title', required: true }),
22
22
  };
23
23
  async run() {
24
24
  const { args, flags } = await this.parse(PaywallsUpdate);
@@ -14,11 +14,11 @@ export default class PlacementsCreate extends Command {
14
14
  ];
15
15
  static flags = {
16
16
  ...appFlag,
17
- audiences: Flags.string({
18
- description: 'JSON array of audience entries. Every entry needs an explicit content_type. ' +
19
- 'Paywall: {content_type:"paywall", segment_ids, paywall_id, priority}. ' +
20
- 'Flow: {content_type:"flow", segment_ids, flow_id, priority}. ' +
21
- 'A flow must be published first (flows publish) — attaching a draft flow returns 400.',
17
+ 'audiences': Flags.string({
18
+ description: 'JSON array of audience entries. Every entry needs an explicit content_type. '
19
+ + 'Paywall: {content_type:"paywall", segment_ids, paywall_id, priority}. '
20
+ + 'Flow: {content_type:"flow", segment_ids, flow_id, priority}. '
21
+ + 'A flow must be published first (flows publish) — attaching a draft flow returns 400.',
22
22
  exactlyOne: ['paywall-id', 'audiences'],
23
23
  }),
24
24
  'developer-id': Flags.string({ description: 'Developer ID for the placement', required: true }),
@@ -26,7 +26,7 @@ export default class PlacementsCreate extends Command {
26
26
  description: 'Paywall ID (UUID). DEPRECATED: use --audiences.',
27
27
  exactlyOne: ['paywall-id', 'audiences'],
28
28
  }),
29
- title: Flags.string({ description: 'Placement title', required: true }),
29
+ 'title': Flags.string({ description: 'Placement title', required: true }),
30
30
  };
31
31
  async run() {
32
32
  const { flags } = await this.parse(PlacementsCreate);
@@ -37,13 +37,15 @@ export default class PlacementsCreate extends Command {
37
37
  title: flags.title,
38
38
  };
39
39
  if (flags['paywall-id']) {
40
- process.stderr.write('⚠️ --paywall-id is deprecated. Use --audiences instead.\n' +
41
- ' `paywall_id` will be removed from the API in a future release.\n');
40
+ process.stderr.write('⚠️ --paywall-id is deprecated. Use --audiences instead.\n'
41
+ + ' `paywall_id` will be removed from the API in a future release.\n');
42
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- FIXME if you see this
42
43
  body.paywall_id = flags['paywall-id'];
43
44
  }
44
45
  else {
45
46
  let parsed;
46
47
  try {
48
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- FIXME if you see this
47
49
  parsed = JSON.parse(flags.audiences);
48
50
  }
49
51
  catch (error) {
@@ -52,12 +54,14 @@ export default class PlacementsCreate extends Command {
52
54
  if (!Array.isArray(parsed)) {
53
55
  this.error('--audiences must be a JSON array of audience entries.', { exit: 2 });
54
56
  }
55
- for (const [index, entry] of parsed.entries()) {
57
+ const entries = parsed;
58
+ for (const [index, entry] of entries.entries()) {
56
59
  const problem = audienceEntryProblem(entry);
57
- if (problem)
60
+ if (problem) {
58
61
  this.error(`--audiences[${index}]: ${problem}`, { exit: 2 });
62
+ }
59
63
  }
60
- body.audiences = parsed;
64
+ body.audiences = entries;
61
65
  }
62
66
  const client = await createAuthenticatedClient(this.config);
63
67
  let result;
@@ -66,8 +70,9 @@ export default class PlacementsCreate extends Command {
66
70
  }
67
71
  catch (error) {
68
72
  const message = draftFlowError(error, flags.app, body.audiences);
69
- if (message)
73
+ if (message) {
70
74
  this.error(message, { exit: 2 });
75
+ }
71
76
  throw error;
72
77
  }
73
78
  this.log('Placement created!');
@@ -1,6 +1,6 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import type { PlacementSummaryDTO } from '../../lib/api-schemas.js';
3
- import { type PaginatedResponse } from '../../lib/flags.js';
3
+ import type { PaginatedResponse } from '../../lib/flags.js';
4
4
  export default class PlacementsList extends Command {
5
5
  static description: string;
6
6
  static enableJsonFlag: boolean;
@@ -17,11 +17,11 @@ export default class PlacementsUpdate extends Command {
17
17
  ];
18
18
  static flags = {
19
19
  ...appFlag,
20
- audiences: Flags.string({
21
- description: 'JSON array of audience entries. Every entry needs an explicit content_type. ' +
22
- 'Paywall: {content_type:"paywall", segment_ids, paywall_id, priority}. ' +
23
- 'Flow: {content_type:"flow", segment_ids, flow_id, priority}. ' +
24
- 'A flow must be published first (flows publish) — attaching a draft flow returns 400.',
20
+ 'audiences': Flags.string({
21
+ description: 'JSON array of audience entries. Every entry needs an explicit content_type. '
22
+ + 'Paywall: {content_type:"paywall", segment_ids, paywall_id, priority}. '
23
+ + 'Flow: {content_type:"flow", segment_ids, flow_id, priority}. '
24
+ + 'A flow must be published first (flows publish) — attaching a draft flow returns 400.',
25
25
  exactlyOne: ['paywall-id', 'audiences'],
26
26
  }),
27
27
  'developer-id': Flags.string({ description: 'Developer ID for the placement', required: true }),
@@ -29,7 +29,7 @@ export default class PlacementsUpdate extends Command {
29
29
  description: 'Paywall ID (UUID). DEPRECATED: use --audiences.',
30
30
  exactlyOne: ['paywall-id', 'audiences'],
31
31
  }),
32
- title: Flags.string({ description: 'Placement title', required: true }),
32
+ 'title': Flags.string({ description: 'Placement title', required: true }),
33
33
  };
34
34
  async run() {
35
35
  const { args, flags } = await this.parse(PlacementsUpdate);
@@ -43,16 +43,18 @@ export default class PlacementsUpdate extends Command {
43
43
  title: flags.title,
44
44
  };
45
45
  if (flags['paywall-id']) {
46
- process.stderr.write('⚠️ --paywall-id is deprecated. Use --audiences instead.\n' +
47
- ' `paywall_id` will be removed from the API in a future release.\n');
48
- process.stderr.write('⚠️ --paywall-id will rewrite all audiences on this placement.\n' +
49
- ' If the placement has segment-specific paywalls, they will be replaced\n' +
50
- ' by a single default audience. Use --audiences to preserve them.\n');
46
+ process.stderr.write('⚠️ --paywall-id is deprecated. Use --audiences instead.\n'
47
+ + ' `paywall_id` will be removed from the API in a future release.\n');
48
+ process.stderr.write('⚠️ --paywall-id will rewrite all audiences on this placement.\n'
49
+ + ' If the placement has segment-specific paywalls, they will be replaced\n'
50
+ + ' by a single default audience. Use --audiences to preserve them.\n');
51
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- FIXME if you see this
51
52
  body.paywall_id = flags['paywall-id'];
52
53
  }
53
54
  else {
54
55
  let parsed;
55
56
  try {
57
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- FIXME if you see this
56
58
  parsed = JSON.parse(flags.audiences);
57
59
  }
58
60
  catch (error) {
@@ -61,12 +63,14 @@ export default class PlacementsUpdate extends Command {
61
63
  if (!Array.isArray(parsed)) {
62
64
  this.error('--audiences must be a JSON array of audience entries.', { exit: 2 });
63
65
  }
64
- for (const [index, entry] of parsed.entries()) {
66
+ const entries = parsed;
67
+ for (const [index, entry] of entries.entries()) {
65
68
  const problem = audienceEntryProblem(entry);
66
- if (problem)
69
+ if (problem) {
67
70
  this.error(`--audiences[${index}]: ${problem}`, { exit: 2 });
71
+ }
68
72
  }
69
- body.audiences = parsed;
73
+ body.audiences = entries;
70
74
  }
71
75
  const client = await createAuthenticatedClient(this.config);
72
76
  let result;
@@ -75,8 +79,9 @@ export default class PlacementsUpdate extends Command {
75
79
  }
76
80
  catch (error) {
77
81
  const message = draftFlowError(error, flags.app, body.audiences);
78
- if (message)
82
+ if (message) {
79
83
  this.error(message, { exit: 2 });
84
+ }
80
85
  throw error;
81
86
  }
82
87
  this.log('Placement updated!');
@@ -26,13 +26,13 @@ export default class ProductsCreate extends Command {
26
26
  'ios-product-id': Flags.string({ description: 'iOS product ID', helpGroup: 'STORE BINDINGS' }),
27
27
  'paddle-price-id': Flags.string({ description: 'Paddle price ID (requires --paddle-product-id)', helpGroup: 'STORE BINDINGS' }),
28
28
  'paddle-product-id': Flags.string({ description: 'Paddle product ID (requires --paddle-price-id)', helpGroup: 'STORE BINDINGS' }),
29
- period: Flags.string({
29
+ 'period': Flags.string({
30
30
  description: 'Subscription period (weekly, monthly, two_months, trimonthly, semiannual, annual, lifetime)',
31
31
  required: true,
32
32
  }),
33
33
  'stripe-price-id': Flags.string({ description: 'Stripe price ID (requires --stripe-product-id)', helpGroup: 'STORE BINDINGS' }),
34
34
  'stripe-product-id': Flags.string({ description: 'Stripe product ID (requires --stripe-price-id)', helpGroup: 'STORE BINDINGS' }),
35
- title: Flags.string({ description: 'Product title', required: true }),
35
+ 'title': Flags.string({ description: 'Product title', required: true }),
36
36
  };
37
37
  async run() {
38
38
  const { flags } = await this.parse(ProductsCreate);
@@ -45,10 +45,10 @@ export default class ProductsCreate extends Command {
45
45
  if (xor(flags['paddle-product-id'], flags['paddle-price-id'])) {
46
46
  this.error('--paddle-product-id and --paddle-price-id are required together', { exit: 2 });
47
47
  }
48
- if (!flags['ios-product-id'] &&
49
- !flags['android-product-id'] &&
50
- !flags['stripe-product-id'] &&
51
- !flags['paddle-product-id']) {
48
+ if (!flags['ios-product-id']
49
+ && !flags['android-product-id']
50
+ && !flags['stripe-product-id']
51
+ && !flags['paddle-product-id']) {
52
52
  this.error('At least one store binding is required (ios/android/stripe/paddle)', { exit: 2 });
53
53
  }
54
54
  if (flags['android-product-id'] && !flags['android-base-plan-id'] && flags.period !== 'lifetime') {
@@ -60,20 +60,27 @@ export default class ProductsCreate extends Command {
60
60
  period: flags.period,
61
61
  title: flags.title,
62
62
  };
63
- if (flags['ios-product-id'])
63
+ if (flags['ios-product-id']) {
64
64
  body.ios_product_id = flags['ios-product-id'];
65
- if (flags['android-product-id'])
65
+ }
66
+ if (flags['android-product-id']) {
66
67
  body.android_product_id = flags['android-product-id'];
67
- if (flags['android-base-plan-id'])
68
+ }
69
+ if (flags['android-base-plan-id']) {
68
70
  body.android_base_plan_id = flags['android-base-plan-id'];
69
- if (flags['stripe-product-id'])
71
+ }
72
+ if (flags['stripe-product-id']) {
70
73
  body.stripe_product_id = flags['stripe-product-id'];
71
- if (flags['stripe-price-id'])
74
+ }
75
+ if (flags['stripe-price-id']) {
72
76
  body.stripe_price_id = flags['stripe-price-id'];
73
- if (flags['paddle-product-id'])
77
+ }
78
+ if (flags['paddle-product-id']) {
74
79
  body.paddle_product_id = flags['paddle-product-id'];
75
- if (flags['paddle-price-id'])
80
+ }
81
+ if (flags['paddle-price-id']) {
76
82
  body.paddle_price_id = flags['paddle-price-id'];
83
+ }
77
84
  const result = await client.post(`/apps/${flags.app}/products`, body);
78
85
  this.log('Product created!');
79
86
  printResponse(result, this.log.bind(this));
@@ -1,6 +1,6 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import type { ProductDTO } from '../../lib/api-schemas.js';
3
- import { type PaginatedResponse } from '../../lib/flags.js';
3
+ import type { PaginatedResponse } from '../../lib/flags.js';
4
4
  export default class ProductsList extends Command {
5
5
  static description: string;
6
6
  static enableJsonFlag: boolean;
@@ -14,7 +14,7 @@ export default class ProductsUpdate extends Command {
14
14
  static flags = {
15
15
  ...appFlag,
16
16
  'access-level-id': Flags.string({ description: 'Access level ID (UUID)', required: true }),
17
- title: Flags.string({ description: 'Product title', required: true }),
17
+ 'title': Flags.string({ description: 'Product title', required: true }),
18
18
  };
19
19
  async run() {
20
20
  const { args, flags } = await this.parse(ProductsUpdate);
@@ -1,6 +1,6 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import type { SegmentDTO } from '../../lib/api-schemas.js';
3
- import { type PaginatedResponse } from '../../lib/flags.js';
3
+ import type { PaginatedResponse } from '../../lib/flags.js';
4
4
  export default class SegmentsList extends Command {
5
5
  static description: string;
6
6
  static enableJsonFlag: boolean;
@@ -1,20 +1,22 @@
1
- import { type ApiErrorFormat } from './errors.js';
1
+ import type { ApiErrorFormat } from './errors.js';
2
2
  export type QueryParams = Record<string, string | string[] | undefined>;
3
- export interface RequestOptions {
3
+ export type RequestOptions = {
4
4
  headers?: Record<string, string>;
5
5
  onResponse?: (headers: Headers) => void;
6
- }
7
- export interface ApiClientOptions {
6
+ };
7
+ export type ApiClientOptions = {
8
8
  baseUrl?: string;
9
9
  defaultBaseUrl?: string;
10
10
  errorFormat?: ApiErrorFormat;
11
+ quiet?: boolean;
11
12
  token?: null | string;
12
13
  urlEnvVar?: string;
13
14
  userAgent?: string;
14
- }
15
+ };
15
16
  export declare class ApiClient {
16
17
  private baseUrl;
17
18
  private errorFormat;
19
+ private quiet;
18
20
  private token;
19
21
  private userAgent;
20
22
  constructor(opts?: ApiClientOptions);
@@ -22,7 +24,9 @@ export declare class ApiClient {
22
24
  post<T = unknown>(path: string, body?: unknown, params?: QueryParams, opts?: RequestOptions): Promise<T>;
23
25
  postForm<T = unknown>(path: string, form: FormData, params?: QueryParams, opts?: RequestOptions): Promise<T>;
24
26
  put<T = unknown>(path: string, body?: unknown, params?: QueryParams, opts?: RequestOptions): Promise<T>;
27
+ private buildHeaders;
25
28
  private buildUrl;
26
- private isRetryableRateLimit;
29
+ private isRetryableRefusal;
30
+ private readBody;
27
31
  private request;
28
32
  }
@@ -1,12 +1,16 @@
1
1
  import { ApiError, NetworkError, parseApiError } from './errors.js';
2
2
  const DEFAULT_API_URL = 'https://api-admin.adapty.io/api/v1/developer';
3
3
  const MAX_RETRY_AFTER_SECONDS = 60;
4
+ // 429 is the caller going too fast; 503 is a dependency being down. Both name a wait and clear up on their own.
5
+ const RETRYABLE_STATUSES = new Set([429, 503]);
4
6
  function ensureTrailingSlash(path) {
5
7
  return path.endsWith('/') ? path : `${path}/`;
6
8
  }
9
+ const NO_RETRIES = { network: false, refused: false };
7
10
  export class ApiClient {
8
11
  baseUrl;
9
12
  errorFormat;
13
+ quiet;
10
14
  token;
11
15
  userAgent;
12
16
  constructor(opts = {}) {
@@ -17,6 +21,7 @@ export class ApiClient {
17
21
  process.stderr.write(`Warning: using non-default API URL: ${this.baseUrl}\n`);
18
22
  }
19
23
  this.errorFormat = opts.errorFormat ?? 'developer';
24
+ this.quiet = opts.quiet ?? false;
20
25
  this.token = opts.token ?? null;
21
26
  this.userAgent = opts.userAgent ?? 'adapty-cli';
22
27
  }
@@ -25,7 +30,7 @@ export class ApiClient {
25
30
  }
26
31
  async post(path, body, params, opts) {
27
32
  return this.request(this.buildUrl(path, params), {
28
- body: body ? JSON.stringify(body) : undefined,
33
+ body: body ? JSON.stringify(body) : null,
29
34
  method: 'POST',
30
35
  }, opts);
31
36
  }
@@ -34,48 +39,70 @@ export class ApiClient {
34
39
  }
35
40
  async put(path, body, params, opts) {
36
41
  return this.request(this.buildUrl(path, params), {
37
- body: body ? JSON.stringify(body) : undefined,
42
+ body: body ? JSON.stringify(body) : null,
38
43
  method: 'PUT',
39
44
  }, opts);
40
45
  }
46
+ buildHeaders(init, opts) {
47
+ const headers = { 'User-Agent': this.userAgent };
48
+ if (init.body && !(init.body instanceof FormData)) {
49
+ headers['Content-Type'] = 'application/json';
50
+ }
51
+ if (this.token) {
52
+ headers.Authorization = `Bearer ${this.token}`;
53
+ }
54
+ return { ...headers, ...opts.headers };
55
+ }
41
56
  buildUrl(path, params) {
42
57
  const url = `${this.baseUrl}${ensureTrailingSlash(path)}`;
43
- if (!params)
58
+ if (!params) {
44
59
  return url;
60
+ }
45
61
  const search = new URLSearchParams();
46
62
  for (const [key, value] of Object.entries(params)) {
47
- if (value === undefined)
63
+ if (value === undefined) {
48
64
  continue;
49
- for (const item of Array.isArray(value) ? value : [value])
65
+ }
66
+ for (const item of Array.isArray(value) ? value : [value]) {
50
67
  search.append(key, item);
68
+ }
51
69
  }
52
70
  return search.size === 0 ? url : `${url}?${search.toString()}`;
53
71
  }
54
- isRetryableRateLimit(error) {
55
- return (this.errorFormat === 'asa' &&
56
- error.statusCode === 429 &&
57
- error.errorCode !== 'cli_cooldown_active' &&
58
- error.retryAfterSeconds !== undefined &&
59
- error.retryAfterSeconds <= MAX_RETRY_AFTER_SECONDS);
72
+ isRetryableRefusal(error) {
73
+ return (this.errorFormat === 'asa'
74
+ && RETRYABLE_STATUSES.has(error.statusCode)
75
+ && error.errorCode !== 'cli_cooldown_active'
76
+ && error.retryAfterSeconds !== undefined
77
+ && error.retryAfterSeconds <= MAX_RETRY_AFTER_SECONDS);
60
78
  }
61
- // eslint-disable-next-line no-undef
62
- async request(url, init, opts = {}, retried = false) {
63
- const headers = {
64
- 'User-Agent': this.userAgent,
65
- };
66
- if (init.body && !(init.body instanceof FormData)) {
67
- headers['Content-Type'] = 'application/json';
79
+ async readBody(response, errorOptions) {
80
+ try {
81
+ return await response.json();
68
82
  }
69
- if (this.token) {
70
- headers.Authorization = `Bearer ${this.token}`;
83
+ catch {
84
+ if (!response.ok) {
85
+ throw new ApiError(response.status, `http_${response.status}`, {}, errorOptions);
86
+ }
87
+ throw new ApiError(response.status, 'malformed_response', {}, {
88
+ ...errorOptions,
89
+ detail: `The server answered ${response.status} with a body that is not JSON, so the response was cut short `
90
+ + 'rather than refused. Nothing was read; retry the request.',
91
+ });
71
92
  }
72
- Object.assign(headers, opts.headers);
93
+ }
94
+ async request(url, init, opts = {}, retried = NO_RETRIES) {
95
+ const headers = this.buildHeaders(init, opts);
73
96
  let response;
74
97
  try {
75
98
  response = await fetch(url, { ...init, headers });
76
99
  }
77
100
  catch (error) {
78
- throw new NetworkError(error instanceof Error ? error.message : 'Connection failed');
101
+ const failure = new NetworkError(error instanceof Error ? error.message : 'Connection failed');
102
+ if (retried.network || init.method !== 'GET') {
103
+ throw failure;
104
+ }
105
+ return this.request(url, init, opts, { ...retried, network: true });
79
106
  }
80
107
  opts.onResponse?.(response.headers);
81
108
  if (response.status === 204) {
@@ -83,28 +110,22 @@ export class ApiClient {
83
110
  }
84
111
  const retryAfter = Number.parseInt(response.headers.get('Retry-After') ?? '', 10);
85
112
  const errorOptions = this.errorFormat === 'asa' && !Number.isNaN(retryAfter) ? { retryAfterSeconds: retryAfter } : {};
86
- let body;
87
- try {
88
- body = await response.json();
89
- }
90
- catch {
91
- if (!response.ok) {
92
- throw new ApiError(response.status, `http_${response.status}`, {}, errorOptions);
93
- }
94
- return undefined;
95
- }
113
+ const body = await this.readBody(response, errorOptions);
96
114
  if (!response.ok) {
97
115
  const error = parseApiError(response.status, body, errorOptions, this.errorFormat);
98
116
  if (response.status === 401) {
99
117
  error.message = 'Token expired or invalid. Run `adapty auth login`.';
100
118
  }
101
- if (!retried && this.isRetryableRateLimit(error)) {
119
+ if (!retried.refused && this.isRetryableRefusal(error)) {
102
120
  const seconds = error.retryAfterSeconds ?? 0;
103
- process.stderr.write(`Rate limited (${error.errorCode}); waiting ${seconds}s per Retry-After, then retrying once.\n`);
121
+ if (!this.quiet) {
122
+ const reason = error.statusCode === 429 ? 'Rate limited' : 'Temporarily unavailable';
123
+ process.stderr.write(`${reason} (${error.errorCode}); waiting ${seconds}s per Retry-After, then retrying once.\n`);
124
+ }
104
125
  await new Promise((resolve) => {
105
126
  setTimeout(resolve, seconds * 1000);
106
127
  });
107
- return this.request(url, init, opts, true);
128
+ return this.request(url, init, opts, { ...retried, refused: true });
108
129
  }
109
130
  throw error;
110
131
  }