@synterai/sdk-js 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +66 -4
- package/dist/index.d.mts +187 -11
- package/dist/index.d.ts +187 -11
- package/dist/index.js +222 -19
- package/dist/index.mjs +217 -19
- package/package.json +9 -8
package/dist/index.mjs
CHANGED
|
@@ -98,7 +98,7 @@ function flattenArgsToFlags(args) {
|
|
|
98
98
|
|
|
99
99
|
// src/resources/campaigns.ts
|
|
100
100
|
var LIST_CAMPAIGNS_SCRIPT_BY_PLATFORM = {
|
|
101
|
-
google: "
|
|
101
|
+
google: "google_ads_get_campaigns",
|
|
102
102
|
meta: "meta_ads_list_campaigns",
|
|
103
103
|
linkedin: "linkedin_ads_list_campaigns",
|
|
104
104
|
microsoft: "microsoft_ads_list_campaigns",
|
|
@@ -149,7 +149,7 @@ var PAUSE_CAMPAIGN_ARG_MAPPING = [
|
|
|
149
149
|
];
|
|
150
150
|
var UPDATE_BUDGET_ARG_MAPPING = [
|
|
151
151
|
{ field: "campaign_id", flag: "--campaign-id", type: "string", required: true },
|
|
152
|
-
{ field: "daily_budget", flag: "--budget", type: "number", required: true }
|
|
152
|
+
{ field: "daily_budget", flag: "--daily-budget", type: "number", required: true }
|
|
153
153
|
];
|
|
154
154
|
var CampaignsResource = class {
|
|
155
155
|
constructor(client) {
|
|
@@ -222,13 +222,79 @@ var CampaignsResource = class {
|
|
|
222
222
|
const args = buildArgsFromMapping(UPDATE_BUDGET_ARG_MAPPING, input);
|
|
223
223
|
return this.client.runTool("update_campaign_budget", args, "google");
|
|
224
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Update campaign budget with deterministic readback verification.
|
|
227
|
+
*
|
|
228
|
+
* Executes the budget update, waits for propagation, and reads back
|
|
229
|
+
* campaign status from the platform to verify the update succeeded on the wire.
|
|
230
|
+
*/
|
|
231
|
+
async updateBudgetAndVerify(input) {
|
|
232
|
+
const mutation = await this.updateBudget(input);
|
|
233
|
+
const maxWaitMs = input.maxWaitMs ?? 5e3;
|
|
234
|
+
const pollIntervalMs = input.pollIntervalMs ?? 1e3;
|
|
235
|
+
const startTime = Date.now();
|
|
236
|
+
let lastReadback;
|
|
237
|
+
while (Date.now() - startTime <= maxWaitMs) {
|
|
238
|
+
try {
|
|
239
|
+
const readback = await this.list({
|
|
240
|
+
platform: input.platform
|
|
241
|
+
});
|
|
242
|
+
lastReadback = readback;
|
|
243
|
+
const campaigns = readback.campaigns || readback.data || [];
|
|
244
|
+
const match = campaigns.find(
|
|
245
|
+
(c) => String(c.id || c.campaign_id) === String(input.campaign_id)
|
|
246
|
+
);
|
|
247
|
+
if (match) {
|
|
248
|
+
const budget = Number(match.daily_budget_usd || match.daily_budget || match.budget);
|
|
249
|
+
if (Math.abs(budget - input.daily_budget) < 0.01) {
|
|
250
|
+
return { mutation, verified: true, readback: match };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
256
|
+
}
|
|
257
|
+
return { mutation, verified: false, readback: lastReadback };
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Pause a campaign with deterministic readback verification.
|
|
261
|
+
*
|
|
262
|
+
* Executes the pause command, waits for propagation, and reads back
|
|
263
|
+
* campaign status from the platform to verify status === 'PAUSED' on the wire.
|
|
264
|
+
*/
|
|
265
|
+
async pauseAndVerify(input) {
|
|
266
|
+
const mutation = await this.pause(input);
|
|
267
|
+
const maxWaitMs = input.maxWaitMs ?? 5e3;
|
|
268
|
+
const pollIntervalMs = input.pollIntervalMs ?? 1e3;
|
|
269
|
+
const startTime = Date.now();
|
|
270
|
+
let lastReadback;
|
|
271
|
+
while (Date.now() - startTime <= maxWaitMs) {
|
|
272
|
+
try {
|
|
273
|
+
const readback = await this.list({
|
|
274
|
+
platform: input.platform,
|
|
275
|
+
status: "PAUSED"
|
|
276
|
+
});
|
|
277
|
+
lastReadback = readback;
|
|
278
|
+
const campaigns = readback.campaigns || readback.data || [];
|
|
279
|
+
const match = campaigns.find(
|
|
280
|
+
(c) => String(c.id || c.campaign_id) === String(input.campaign_id)
|
|
281
|
+
);
|
|
282
|
+
if (match) {
|
|
283
|
+
return { mutation, verified: true, readback: match };
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
288
|
+
}
|
|
289
|
+
return { mutation, verified: false, readback: lastReadback };
|
|
290
|
+
}
|
|
225
291
|
};
|
|
226
292
|
|
|
227
293
|
// src/resources/analytics.ts
|
|
228
294
|
var GET_PERFORMANCE_SCRIPT_BY_PLATFORM = {
|
|
229
|
-
google: "
|
|
295
|
+
google: "pull_google_ads",
|
|
230
296
|
meta: "pull_meta_ads",
|
|
231
|
-
linkedin: "
|
|
297
|
+
linkedin: "pull_linkedin_ads",
|
|
232
298
|
microsoft: "pull_microsoft_ads",
|
|
233
299
|
reddit: "pull_reddit_ads",
|
|
234
300
|
tiktok: "pull_tiktok_ads",
|
|
@@ -260,17 +326,22 @@ var AnalyticsResource = class {
|
|
|
260
326
|
"google",
|
|
261
327
|
input.platform
|
|
262
328
|
);
|
|
263
|
-
const args = buildArgsFromMapping(
|
|
329
|
+
const args = buildArgsFromMapping(
|
|
330
|
+
GET_PERFORMANCE_ARG_MAPPING,
|
|
331
|
+
{ ...input, date_range: input.date_range ?? "LAST_7_DAYS" },
|
|
332
|
+
platform
|
|
333
|
+
);
|
|
264
334
|
return this.client.runTool(scriptName, args, platform);
|
|
265
335
|
}
|
|
266
336
|
/** Get daily spend breakdown across all connected ad accounts. */
|
|
267
337
|
async getDailySpend(input = {}) {
|
|
268
|
-
const platform
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
338
|
+
const { platform, scriptName } = resolvePlatformScriptName(
|
|
339
|
+
GET_PERFORMANCE_SCRIPT_BY_PLATFORM,
|
|
340
|
+
"google",
|
|
341
|
+
input.platform
|
|
342
|
+
);
|
|
343
|
+
const days = input.days ?? 7;
|
|
344
|
+
return this.client.runTool(scriptName, ["--days", String(days)], platform);
|
|
274
345
|
}
|
|
275
346
|
};
|
|
276
347
|
|
|
@@ -310,6 +381,21 @@ var CREATE_CONVERSION_ARG_MAPPING = [
|
|
|
310
381
|
var DIAGNOSE_TRACKING_ARG_MAPPING = [
|
|
311
382
|
{ field: "url", flag: "--url", type: "string", required: true }
|
|
312
383
|
];
|
|
384
|
+
var GET_PIXEL_HEALTH_ARG_MAPPING = [
|
|
385
|
+
{ field: "site_key", flag: "--site-key", type: "string" }
|
|
386
|
+
];
|
|
387
|
+
var LIST_EVENT_DELIVERIES_ARG_MAPPING = [
|
|
388
|
+
{ field: "site_key", flag: "--site-key", type: "string" },
|
|
389
|
+
{ field: "status", flag: "--status", type: "string" },
|
|
390
|
+
{ field: "platform", flag: "--platform", type: "string" },
|
|
391
|
+
{ field: "limit", flag: "--limit", type: "number" }
|
|
392
|
+
];
|
|
393
|
+
var GET_CLICK_ATTRIBUTION_ARG_MAPPING = [
|
|
394
|
+
{ field: "site_key", flag: "--site-key", type: "string" },
|
|
395
|
+
{ field: "user_id", flag: "--user-id", type: "number" },
|
|
396
|
+
{ field: "email", flag: "--email", type: "string" },
|
|
397
|
+
{ field: "limit", flag: "--limit", type: "number" }
|
|
398
|
+
];
|
|
313
399
|
var ConversionsResource = class {
|
|
314
400
|
constructor(client) {
|
|
315
401
|
this.client = client;
|
|
@@ -335,7 +421,19 @@ var ConversionsResource = class {
|
|
|
335
421
|
*/
|
|
336
422
|
async diagnoseTracking(input) {
|
|
337
423
|
const args = buildArgsFromMapping(DIAGNOSE_TRACKING_ARG_MAPPING, input);
|
|
338
|
-
return this.client.runTool("
|
|
424
|
+
return this.client.runTool("diagnose_tracking", args);
|
|
425
|
+
}
|
|
426
|
+
async getPixelHealth(input = {}) {
|
|
427
|
+
const args = buildArgsFromMapping(GET_PIXEL_HEALTH_ARG_MAPPING, input);
|
|
428
|
+
return this.client.runTool("get_pixel_health", args);
|
|
429
|
+
}
|
|
430
|
+
async listEventDeliveries(input = {}) {
|
|
431
|
+
const args = buildArgsFromMapping(LIST_EVENT_DELIVERIES_ARG_MAPPING, input);
|
|
432
|
+
return this.client.runTool("list_event_deliveries", args);
|
|
433
|
+
}
|
|
434
|
+
async getClickAttribution(input = {}) {
|
|
435
|
+
const args = buildArgsFromMapping(GET_CLICK_ATTRIBUTION_ARG_MAPPING, input);
|
|
436
|
+
return this.client.runTool("get_click_attribution", args);
|
|
339
437
|
}
|
|
340
438
|
};
|
|
341
439
|
|
|
@@ -428,7 +526,6 @@ var RedditResource = class {
|
|
|
428
526
|
|
|
429
527
|
// src/resources/audiences.ts
|
|
430
528
|
var CONSENT_WARNING = "[@synterai/sdk-js] i_have_consent was not provided. Confirm lawful basis under GDPR/CCPA and the destination platform's terms for every identifier before staging or syncing PII, then pass `i_have_consent: true`. (Soft-launch notice \u2014 mirrors the backend's own behavior; this call is not blocked.)";
|
|
431
|
-
var ARTIFACT_ENDPOINT = "https://api.syntermedia.ai/artifacts/audience-sync-input";
|
|
432
529
|
var SYNC_AUDIENCE_SCRIPT_BY_PLATFORM = {
|
|
433
530
|
google: "google_ads_create_customer_match",
|
|
434
531
|
meta: "meta_ads_create_audience",
|
|
@@ -460,7 +557,7 @@ var AudiencesResource = class {
|
|
|
460
557
|
* identifiers inline when over ~500 entries.
|
|
461
558
|
*
|
|
462
559
|
* Bypasses the primary `tools/run` dispatcher: different host
|
|
463
|
-
* (`https://api.
|
|
560
|
+
* (`https://api.synterai.com`) and a different auth header
|
|
464
561
|
* (`X-Synter-Key`, not `Authorization: Bearer`) — see SPEC.md/catalog.json
|
|
465
562
|
* `transport.special`.
|
|
466
563
|
*/
|
|
@@ -475,7 +572,7 @@ var AudiencesResource = class {
|
|
|
475
572
|
if (input.script_name !== void 0) payload.script_name = input.script_name;
|
|
476
573
|
if (input.ttl_seconds !== void 0) payload.ttl_seconds = input.ttl_seconds;
|
|
477
574
|
if (input.i_have_consent !== void 0) payload.i_have_consent = input.i_have_consent;
|
|
478
|
-
return this.client.sendAbsolute(
|
|
575
|
+
return this.client.sendAbsolute(`${this.client.getArtifactBaseUrl()}/artifacts/audience-sync-input`, {
|
|
479
576
|
method: "POST",
|
|
480
577
|
headers: {
|
|
481
578
|
"X-Synter-Key": this.client.getApiKey(),
|
|
@@ -550,6 +647,91 @@ var AudiencesResource = class {
|
|
|
550
647
|
}
|
|
551
648
|
};
|
|
552
649
|
|
|
650
|
+
// src/resources/shield.ts
|
|
651
|
+
var ShieldResource = class {
|
|
652
|
+
constructor(client) {
|
|
653
|
+
this.client = client;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Inspect Agent Shield velocity limits, burst thresholds, circuit breakers,
|
|
657
|
+
* and kill-switch status before executing ad-spend mutations.
|
|
658
|
+
*/
|
|
659
|
+
async inspect(input = {}) {
|
|
660
|
+
const args = [];
|
|
661
|
+
if (input.platform) {
|
|
662
|
+
args.push("--platform", input.platform);
|
|
663
|
+
}
|
|
664
|
+
const result = await this.client.runTool("inspect_agent_shield", args, input.platform);
|
|
665
|
+
return result;
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// src/resources/angstrom.ts
|
|
670
|
+
var AngstromResource = class {
|
|
671
|
+
constructor(client) {
|
|
672
|
+
this.client = client;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Verify attribution claims against Synter Ångström's deterministic confidence rubric.
|
|
676
|
+
*/
|
|
677
|
+
async verify(input = {}) {
|
|
678
|
+
const args = [];
|
|
679
|
+
if (input.platform) args.push("--platform", input.platform);
|
|
680
|
+
if (input.claimed_conversions !== void 0) args.push("--conversions", String(input.claimed_conversions));
|
|
681
|
+
if (input.claimed_revenue_usd !== void 0) args.push("--revenue", String(input.claimed_revenue_usd));
|
|
682
|
+
if (input.touchpoint_ids && input.touchpoint_ids.length > 0) {
|
|
683
|
+
args.push("--touchpoints", input.touchpoint_ids.join(","));
|
|
684
|
+
}
|
|
685
|
+
const result = await this.client.runTool("verify_angstrom_claim", args, input.platform);
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// src/resources/skills.ts
|
|
691
|
+
var SkillsResource = class {
|
|
692
|
+
constructor(client) {
|
|
693
|
+
this.client = client;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Load an autonomous growth runbook by ID.
|
|
697
|
+
*/
|
|
698
|
+
async get(skillId) {
|
|
699
|
+
const result = await this.client.runTool("get_skill", ["--skill-id", skillId]);
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* List available autonomous growth runbook IDs.
|
|
704
|
+
*/
|
|
705
|
+
list() {
|
|
706
|
+
return [
|
|
707
|
+
"pmax-brand-cannibalization",
|
|
708
|
+
"search-hygiene-shield",
|
|
709
|
+
"marginal-cpa-budget-rebalancer",
|
|
710
|
+
"meta-asc-dark-pool-auditor",
|
|
711
|
+
"single-truth-attribution"
|
|
712
|
+
];
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// src/resources/visibility.ts
|
|
717
|
+
var VisibilityResource = class {
|
|
718
|
+
constructor(client) {
|
|
719
|
+
this.client = client;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Read AI search brand visibility and AEO share of voice across ChatGPT, Claude, Perplexity, and Gemini.
|
|
723
|
+
*/
|
|
724
|
+
async get(input = {}) {
|
|
725
|
+
const args = [];
|
|
726
|
+
if (input.query) args.push("--query", input.query);
|
|
727
|
+
if (input.competitor_domains && input.competitor_domains.length > 0) {
|
|
728
|
+
args.push("--competitors", input.competitor_domains.join(","));
|
|
729
|
+
}
|
|
730
|
+
const result = await this.client.runTool("get_llm_visibility", args);
|
|
731
|
+
return result;
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
|
|
553
735
|
// src/toolbox/amp-tools.ts
|
|
554
736
|
var AmpToolbox = class {
|
|
555
737
|
constructor(client, config = {}) {
|
|
@@ -783,7 +965,8 @@ var AmpToolbox = class {
|
|
|
783
965
|
};
|
|
784
966
|
|
|
785
967
|
// src/client.ts
|
|
786
|
-
var DEFAULT_BASE_URL = "https://
|
|
968
|
+
var DEFAULT_BASE_URL = "https://synterai.com";
|
|
969
|
+
var DEFAULT_ARTIFACT_BASE_URL = "https://api.synterai.com";
|
|
787
970
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
788
971
|
var DEFAULT_MAX_RETRIES = 3;
|
|
789
972
|
var RETRY_BASE_MS = 1e3;
|
|
@@ -801,7 +984,9 @@ var Synter = class {
|
|
|
801
984
|
throw new SynterValidationError("`apiKey` is required to construct a Synter client.", "apiKey");
|
|
802
985
|
}
|
|
803
986
|
this.apiKey = fullConfig.apiKey;
|
|
804
|
-
|
|
987
|
+
const env = typeof process !== "undefined" ? process.env : void 0;
|
|
988
|
+
this.baseUrl = fullConfig.baseUrl ?? (env?.SYNTER_API_URL || DEFAULT_BASE_URL);
|
|
989
|
+
this.artifactBaseUrl = fullConfig.artifactBaseUrl ?? (env?.SYNTER_ARTIFACT_API_URL || DEFAULT_ARTIFACT_BASE_URL);
|
|
805
990
|
this.timeout = fullConfig.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
806
991
|
this.maxRetries = fullConfig.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
807
992
|
this.fetchImpl = fullConfig.fetch ?? globalThis.fetch.bind(globalThis);
|
|
@@ -814,6 +999,10 @@ var Synter = class {
|
|
|
814
999
|
this.linkedin = new LinkedInResource(this);
|
|
815
1000
|
this.reddit = new RedditResource(this);
|
|
816
1001
|
this.audiences = new AudiencesResource(this);
|
|
1002
|
+
this.shield = new ShieldResource(this);
|
|
1003
|
+
this.angstrom = new AngstromResource(this);
|
|
1004
|
+
this.skills = new SkillsResource(this);
|
|
1005
|
+
this.visibility = new VisibilityResource(this);
|
|
817
1006
|
this.amp = new AmpToolbox(this, ampConfig);
|
|
818
1007
|
}
|
|
819
1008
|
// ---------------------------------------------------------------------
|
|
@@ -853,7 +1042,7 @@ var Synter = class {
|
|
|
853
1042
|
// ---------------------------------------------------------------------
|
|
854
1043
|
/** List all connected ad accounts across platforms. */
|
|
855
1044
|
async listAdAccounts() {
|
|
856
|
-
return this.runTool("
|
|
1045
|
+
return this.runTool("google_ads_get_customers", [], "google");
|
|
857
1046
|
}
|
|
858
1047
|
/**
|
|
859
1048
|
* Upload an image as an asset for use in ads.
|
|
@@ -892,7 +1081,11 @@ var Synter = class {
|
|
|
892
1081
|
"Content-Type": "application/json"
|
|
893
1082
|
};
|
|
894
1083
|
}
|
|
895
|
-
/**
|
|
1084
|
+
/** Artifact host used by audience staging. */
|
|
1085
|
+
getArtifactBaseUrl() {
|
|
1086
|
+
return this.artifactBaseUrl;
|
|
1087
|
+
}
|
|
1088
|
+
/** The raw API key, for tools that need a non-Bearer header. */
|
|
896
1089
|
getApiKey() {
|
|
897
1090
|
return this.apiKey;
|
|
898
1091
|
}
|
|
@@ -1380,18 +1573,23 @@ export {
|
|
|
1380
1573
|
AmpToolbox,
|
|
1381
1574
|
AnalyticsIntegrationManager,
|
|
1382
1575
|
AnalyticsResource,
|
|
1576
|
+
AngstromResource,
|
|
1383
1577
|
AudiencesResource,
|
|
1384
1578
|
CampaignsResource,
|
|
1385
1579
|
ConversionsResource,
|
|
1386
1580
|
CreativeResource,
|
|
1581
|
+
DEFAULT_ARTIFACT_BASE_URL,
|
|
1387
1582
|
DEFAULT_BASE_URL,
|
|
1388
1583
|
KeywordsResource,
|
|
1389
1584
|
LinkedInResource,
|
|
1390
1585
|
MetaResource,
|
|
1391
1586
|
RedditResource,
|
|
1587
|
+
ShieldResource,
|
|
1588
|
+
SkillsResource,
|
|
1392
1589
|
Synter,
|
|
1393
1590
|
SynterError,
|
|
1394
1591
|
SynterValidationError,
|
|
1592
|
+
VisibilityResource,
|
|
1395
1593
|
buildConversionPayload,
|
|
1396
1594
|
buildTrackingURL,
|
|
1397
1595
|
buildUTMParameters,
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synterai/sdk-js",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "TypeScript SDK for the Synter advertising API — manage campaigns across Google Ads, Meta, LinkedIn, Reddit, Microsoft, TikTok, and X",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Official Synter TypeScript SDK for the Synter advertising API — manage campaigns across Google Ads, Meta, LinkedIn, Reddit, Microsoft, TikTok, and X. @iflow-mcp/* copies are unofficial.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"files": [
|
|
9
|
-
"dist"
|
|
9
|
+
"dist",
|
|
10
|
+
"CHANGELOG.md"
|
|
10
11
|
],
|
|
11
12
|
"scripts": {
|
|
12
13
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
@@ -42,15 +43,15 @@
|
|
|
42
43
|
"analytics",
|
|
43
44
|
"ppc"
|
|
44
45
|
],
|
|
45
|
-
"author": "
|
|
46
|
+
"author": "Synter <hello@synterai.com>",
|
|
46
47
|
"license": "MIT",
|
|
47
48
|
"repository": {
|
|
48
49
|
"type": "git",
|
|
49
|
-
"url": "https://github.com/
|
|
50
|
-
"directory": "
|
|
50
|
+
"url": "git+https://github.com/Synter-Media-AI/synter-docs.git",
|
|
51
|
+
"directory": "sdk/typescript"
|
|
51
52
|
},
|
|
52
|
-
"homepage": "https://
|
|
53
|
-
"bugs": "https://github.com/
|
|
53
|
+
"homepage": "https://synterai.com",
|
|
54
|
+
"bugs": "https://github.com/Synter-Media-AI/synter-docs/issues",
|
|
54
55
|
"dependencies": {},
|
|
55
56
|
"devDependencies": {
|
|
56
57
|
"@types/node": "^20.10.0",
|