cli-meta-ads 0.1.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/AGENTS.md +188 -0
- package/AI_CONTEXT.md +144 -0
- package/CLAUDE.md +183 -0
- package/README.md +590 -0
- package/REQUIREMENTS.md +148 -0
- package/dist/auth/constants.d.ts +1 -0
- package/dist/auth/constants.js +1 -0
- package/dist/auth/guards.d.ts +5 -0
- package/dist/auth/guards.js +16 -0
- package/dist/auth/login.d.ts +28 -0
- package/dist/auth/login.js +222 -0
- package/dist/cli/action.d.ts +11 -0
- package/dist/cli/action.js +77 -0
- package/dist/cli/build-cli.d.ts +2 -0
- package/dist/cli/build-cli.js +110 -0
- package/dist/cli/context.d.ts +24 -0
- package/dist/cli/context.js +19 -0
- package/dist/client/meta-api-client.d.ts +50 -0
- package/dist/client/meta-api-client.js +258 -0
- package/dist/client/meta-discovery.d.ts +13 -0
- package/dist/client/meta-discovery.js +88 -0
- package/dist/commands/accounts.d.ts +4 -0
- package/dist/commands/accounts.js +42 -0
- package/dist/commands/ads.d.ts +4 -0
- package/dist/commands/ads.js +148 -0
- package/dist/commands/adsets.d.ts +4 -0
- package/dist/commands/adsets.js +49 -0
- package/dist/commands/anomalies.d.ts +4 -0
- package/dist/commands/anomalies.js +44 -0
- package/dist/commands/assets.d.ts +4 -0
- package/dist/commands/assets.js +116 -0
- package/dist/commands/audiences.d.ts +4 -0
- package/dist/commands/audiences.js +40 -0
- package/dist/commands/auth.d.ts +4 -0
- package/dist/commands/auth.js +139 -0
- package/dist/commands/campaigns.d.ts +4 -0
- package/dist/commands/campaigns.js +273 -0
- package/dist/commands/capi.d.ts +4 -0
- package/dist/commands/capi.js +64 -0
- package/dist/commands/creatives.d.ts +4 -0
- package/dist/commands/creatives.js +49 -0
- package/dist/commands/diagnostics.d.ts +4 -0
- package/dist/commands/diagnostics.js +88 -0
- package/dist/commands/helpers.d.ts +13 -0
- package/dist/commands/helpers.js +50 -0
- package/dist/commands/launch.d.ts +4 -0
- package/dist/commands/launch.js +109 -0
- package/dist/commands/performance.d.ts +4 -0
- package/dist/commands/performance.js +55 -0
- package/dist/commands/pixel.d.ts +4 -0
- package/dist/commands/pixel.js +68 -0
- package/dist/commands/report.d.ts +4 -0
- package/dist/commands/report.js +30 -0
- package/dist/config/file-config.d.ts +6 -0
- package/dist/config/file-config.js +174 -0
- package/dist/config/types.d.ts +32 -0
- package/dist/config/types.js +1 -0
- package/dist/domain/account-scope.d.ts +7 -0
- package/dist/domain/account-scope.js +28 -0
- package/dist/domain/analytics.d.ts +52 -0
- package/dist/domain/analytics.js +125 -0
- package/dist/domain/approval-service.d.ts +10 -0
- package/dist/domain/approval-service.js +48 -0
- package/dist/domain/asset-feed-compiler.d.ts +43 -0
- package/dist/domain/asset-feed-compiler.js +104 -0
- package/dist/domain/launch-service.d.ts +200 -0
- package/dist/domain/launch-service.js +558 -0
- package/dist/domain/meta-ads-service.d.ts +620 -0
- package/dist/domain/meta-ads-service.js +841 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -0
- package/dist/output/render.d.ts +3 -0
- package/dist/output/render.js +103 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +1 -0
- package/dist/utils/currency.d.ts +4 -0
- package/dist/utils/currency.js +40 -0
- package/dist/utils/date-range.d.ts +20 -0
- package/dist/utils/date-range.js +115 -0
- package/dist/utils/errors.d.ts +35 -0
- package/dist/utils/errors.js +68 -0
- package/dist/utils/ids.d.ts +4 -0
- package/dist/utils/ids.js +23 -0
- package/dist/utils/meta-placement-assets.d.ts +44 -0
- package/dist/utils/meta-placement-assets.js +315 -0
- package/dist/utils/security.d.ts +5 -0
- package/dist/utils/security.js +104 -0
- package/dist/validators/common.d.ts +10 -0
- package/dist/validators/common.js +56 -0
- package/dist/validators/create-spec.d.ts +373 -0
- package/dist/validators/create-spec.js +394 -0
- package/dist/validators/launch-spec.d.ts +229 -0
- package/dist/validators/launch-spec.js +371 -0
- package/docs/TECHNICAL.md +480 -0
- package/examples/README.md +29 -0
- package/examples/launch/assets/feed4x5.png +0 -0
- package/examples/launch/assets/story9x16.png +0 -0
- package/examples/launch/multi-format-launch.json +90 -0
- package/examples/single-object/ad.json +6 -0
- package/examples/single-object/adset.json +30 -0
- package/examples/single-object/campaign.json +6 -0
- package/examples/single-object/creative.json +19 -0
- package/package.json +62 -0
- package/skills/meta-cli-operator/SKILL.md +105 -0
- package/skills/meta-cli-operator/agents/openai.yaml +4 -0
- package/skills/meta-cli-operator/references/update-matrix.md +117 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
export const launchCreativeFormatKeys = ["feed4x5", "square1x1", "story9x16"];
|
|
2
|
+
const publisherPlatforms = [
|
|
3
|
+
"facebook",
|
|
4
|
+
"instagram",
|
|
5
|
+
"messenger",
|
|
6
|
+
"audience_network",
|
|
7
|
+
"threads"
|
|
8
|
+
];
|
|
9
|
+
const facebookPositions = [
|
|
10
|
+
"feed",
|
|
11
|
+
"right_hand_column",
|
|
12
|
+
"marketplace",
|
|
13
|
+
"video_feeds",
|
|
14
|
+
"search",
|
|
15
|
+
"story",
|
|
16
|
+
"notification",
|
|
17
|
+
"facebook_reels"
|
|
18
|
+
];
|
|
19
|
+
const instagramPositions = [
|
|
20
|
+
"stream",
|
|
21
|
+
"story",
|
|
22
|
+
"explore",
|
|
23
|
+
"explore_home",
|
|
24
|
+
"profile_feed",
|
|
25
|
+
"ig_search",
|
|
26
|
+
"reels",
|
|
27
|
+
"profile_reels"
|
|
28
|
+
];
|
|
29
|
+
const messengerPositions = [
|
|
30
|
+
"sponsored_messages",
|
|
31
|
+
"story"
|
|
32
|
+
];
|
|
33
|
+
const audienceNetworkPositions = [
|
|
34
|
+
"classic",
|
|
35
|
+
"instream_video",
|
|
36
|
+
"rewarded_video"
|
|
37
|
+
];
|
|
38
|
+
const threadsPositions = ["threads_stream"];
|
|
39
|
+
const defaultAutomaticPublisherPlatforms = [
|
|
40
|
+
"facebook",
|
|
41
|
+
"instagram",
|
|
42
|
+
"messenger",
|
|
43
|
+
"audience_network"
|
|
44
|
+
];
|
|
45
|
+
const defaultAutomaticFacebookPositions = [...facebookPositions];
|
|
46
|
+
const defaultAutomaticInstagramPositions = [...instagramPositions];
|
|
47
|
+
const defaultAutomaticMessengerPositions = [...messengerPositions];
|
|
48
|
+
const defaultAutomaticAudienceNetworkPositions = [...audienceNetworkPositions];
|
|
49
|
+
const supportedFeedPlacementIds = new Set([
|
|
50
|
+
"facebook.feed",
|
|
51
|
+
"facebook.marketplace",
|
|
52
|
+
"facebook.video_feeds",
|
|
53
|
+
"instagram.stream",
|
|
54
|
+
"instagram.profile_feed",
|
|
55
|
+
"threads.threads_stream"
|
|
56
|
+
]);
|
|
57
|
+
const supportedSquarePlacementIds = new Set([
|
|
58
|
+
"facebook.right_hand_column",
|
|
59
|
+
"facebook.search",
|
|
60
|
+
"facebook.notification",
|
|
61
|
+
"instagram.explore",
|
|
62
|
+
"instagram.explore_home",
|
|
63
|
+
"instagram.ig_search"
|
|
64
|
+
]);
|
|
65
|
+
const supportedVerticalPlacementIds = new Set([
|
|
66
|
+
"facebook.story",
|
|
67
|
+
"facebook.facebook_reels",
|
|
68
|
+
"instagram.story",
|
|
69
|
+
"instagram.reels",
|
|
70
|
+
"instagram.profile_reels",
|
|
71
|
+
"messenger.story"
|
|
72
|
+
]);
|
|
73
|
+
const supportedFallbackPlacementIds = new Set([
|
|
74
|
+
...supportedFeedPlacementIds,
|
|
75
|
+
...supportedSquarePlacementIds
|
|
76
|
+
]);
|
|
77
|
+
const explicitlyUnsupportedPlacementIds = new Set([
|
|
78
|
+
"messenger.sponsored_messages",
|
|
79
|
+
"audience_network.classic",
|
|
80
|
+
"audience_network.instream_video",
|
|
81
|
+
"audience_network.rewarded_video"
|
|
82
|
+
]);
|
|
83
|
+
export const defaultCreativeFormatPlacementTargets = {
|
|
84
|
+
feed4x5: [...supportedFeedPlacementIds].filter((placementId) => placementId !== "threads.threads_stream"),
|
|
85
|
+
square1x1: [...supportedSquarePlacementIds],
|
|
86
|
+
story9x16: [...supportedVerticalPlacementIds]
|
|
87
|
+
};
|
|
88
|
+
function isRecord(value) {
|
|
89
|
+
return typeof value === "object" && value !== null;
|
|
90
|
+
}
|
|
91
|
+
function asStringArray(value) {
|
|
92
|
+
if (!Array.isArray(value)) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
return value
|
|
96
|
+
.filter((entry) => typeof entry === "string")
|
|
97
|
+
.map((entry) => entry.trim())
|
|
98
|
+
.filter((entry) => entry.length > 0);
|
|
99
|
+
}
|
|
100
|
+
function unique(values) {
|
|
101
|
+
return [...new Set(values)];
|
|
102
|
+
}
|
|
103
|
+
function sortByReference(values, reference) {
|
|
104
|
+
const index = new Map(reference.map((entry, idx) => [entry, idx]));
|
|
105
|
+
return [...values].sort((left, right) => {
|
|
106
|
+
const leftIndex = index.get(left) ?? Number.MAX_SAFE_INTEGER;
|
|
107
|
+
const rightIndex = index.get(right) ?? Number.MAX_SAFE_INTEGER;
|
|
108
|
+
if (leftIndex !== rightIndex) {
|
|
109
|
+
return leftIndex - rightIndex;
|
|
110
|
+
}
|
|
111
|
+
return left.localeCompare(right);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function normalizePublisherPlatforms(values) {
|
|
115
|
+
return unique(values.filter((entry) => publisherPlatforms.includes(entry)));
|
|
116
|
+
}
|
|
117
|
+
function toPlacementIds(platform, positions) {
|
|
118
|
+
return positions.map((position) => `${platform}.${position}`);
|
|
119
|
+
}
|
|
120
|
+
function selectPublisherPlatforms(targeting) {
|
|
121
|
+
const explicitPublisherPlatforms = normalizePublisherPlatforms(asStringArray(targeting.publisher_platforms));
|
|
122
|
+
if (explicitPublisherPlatforms.length > 0) {
|
|
123
|
+
return {
|
|
124
|
+
publisherPlatforms: explicitPublisherPlatforms,
|
|
125
|
+
publisherPlatformsWereExplicit: true
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const inferredPublisherPlatforms = normalizePublisherPlatforms([
|
|
129
|
+
...(asStringArray(targeting.facebook_positions).length > 0 ? ["facebook"] : []),
|
|
130
|
+
...(asStringArray(targeting.instagram_positions).length > 0 ? ["instagram"] : []),
|
|
131
|
+
...(asStringArray(targeting.messenger_positions).length > 0 ? ["messenger"] : []),
|
|
132
|
+
...(asStringArray(targeting.audience_network_positions).length > 0 ? ["audience_network"] : []),
|
|
133
|
+
...(asStringArray(targeting.threads_positions).length > 0 ? ["threads"] : [])
|
|
134
|
+
]);
|
|
135
|
+
if (inferredPublisherPlatforms.length > 0) {
|
|
136
|
+
return {
|
|
137
|
+
publisherPlatforms: inferredPublisherPlatforms,
|
|
138
|
+
publisherPlatformsWereExplicit: false
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
publisherPlatforms: [...defaultAutomaticPublisherPlatforms],
|
|
143
|
+
publisherPlatformsWereExplicit: false
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function expandPositions(publisherPlatformsValue, targeting, platform, fieldName, automaticDefaults) {
|
|
147
|
+
if (!publisherPlatformsValue.includes(platform)) {
|
|
148
|
+
return {
|
|
149
|
+
automatic: false,
|
|
150
|
+
positions: []
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const positions = asStringArray(targeting[fieldName]);
|
|
154
|
+
if (positions.length > 0) {
|
|
155
|
+
return {
|
|
156
|
+
automatic: false,
|
|
157
|
+
positions: unique(positions)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
automatic: automaticDefaults.length > 0,
|
|
162
|
+
positions: [...automaticDefaults]
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function pickPlacementIds(placementIds, matcher) {
|
|
166
|
+
return placementIds.filter((placementId) => matcher.has(placementId));
|
|
167
|
+
}
|
|
168
|
+
function toPlacementId(value) {
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
export function expandPlacementTargeting(targeting) {
|
|
172
|
+
const rawTargeting = isRecord(targeting) ? targeting : {};
|
|
173
|
+
const { publisherPlatforms: selectedPublisherPlatforms, publisherPlatformsWereExplicit } = selectPublisherPlatforms(rawTargeting);
|
|
174
|
+
const facebook = expandPositions(selectedPublisherPlatforms, rawTargeting, "facebook", "facebook_positions", defaultAutomaticFacebookPositions);
|
|
175
|
+
const instagram = expandPositions(selectedPublisherPlatforms, rawTargeting, "instagram", "instagram_positions", defaultAutomaticInstagramPositions);
|
|
176
|
+
const messenger = expandPositions(selectedPublisherPlatforms, rawTargeting, "messenger", "messenger_positions", defaultAutomaticMessengerPositions);
|
|
177
|
+
const audienceNetwork = expandPositions(selectedPublisherPlatforms, rawTargeting, "audience_network", "audience_network_positions", defaultAutomaticAudienceNetworkPositions);
|
|
178
|
+
const threads = expandPositions(selectedPublisherPlatforms, rawTargeting, "threads", "threads_positions", []);
|
|
179
|
+
const automaticPublisherPlatforms = [
|
|
180
|
+
...(facebook.automatic ? ["facebook"] : []),
|
|
181
|
+
...(instagram.automatic ? ["instagram"] : []),
|
|
182
|
+
...(messenger.automatic ? ["messenger"] : []),
|
|
183
|
+
...(audienceNetwork.automatic ? ["audience_network"] : []),
|
|
184
|
+
...(threads.automatic ? ["threads"] : [])
|
|
185
|
+
];
|
|
186
|
+
const placementIds = [
|
|
187
|
+
...toPlacementIds("facebook", facebook.positions),
|
|
188
|
+
...toPlacementIds("instagram", instagram.positions),
|
|
189
|
+
...toPlacementIds("messenger", messenger.positions),
|
|
190
|
+
...toPlacementIds("audience_network", audienceNetwork.positions),
|
|
191
|
+
...toPlacementIds("threads", threads.positions)
|
|
192
|
+
];
|
|
193
|
+
const explicitPlacementIds = [
|
|
194
|
+
...toPlacementIds("facebook", asStringArray(rawTargeting.facebook_positions)),
|
|
195
|
+
...toPlacementIds("instagram", asStringArray(rawTargeting.instagram_positions)),
|
|
196
|
+
...toPlacementIds("messenger", asStringArray(rawTargeting.messenger_positions)),
|
|
197
|
+
...toPlacementIds("audience_network", asStringArray(rawTargeting.audience_network_positions)),
|
|
198
|
+
...toPlacementIds("threads", asStringArray(rawTargeting.threads_positions))
|
|
199
|
+
];
|
|
200
|
+
const knownPlacementIds = pickPlacementIds(placementIds, new Set([
|
|
201
|
+
...supportedFeedPlacementIds,
|
|
202
|
+
...supportedSquarePlacementIds,
|
|
203
|
+
...supportedVerticalPlacementIds,
|
|
204
|
+
...explicitlyUnsupportedPlacementIds
|
|
205
|
+
]));
|
|
206
|
+
const unknownPlacementIds = placementIds.filter((placementId) => !knownPlacementIds.includes(placementId));
|
|
207
|
+
const unsupportedPlacementIds = unique([
|
|
208
|
+
...pickPlacementIds(placementIds, explicitlyUnsupportedPlacementIds),
|
|
209
|
+
...unknownPlacementIds
|
|
210
|
+
]);
|
|
211
|
+
return {
|
|
212
|
+
automatic: automaticPublisherPlatforms.length > 0,
|
|
213
|
+
automaticPublisherPlatforms,
|
|
214
|
+
audienceNetworkPositions: sortByReference(audienceNetwork.positions, audienceNetworkPositions),
|
|
215
|
+
explicitPlacementIds: unique(explicitPlacementIds),
|
|
216
|
+
facebookPositions: sortByReference(facebook.positions, facebookPositions),
|
|
217
|
+
feedPlacementIds: unique([
|
|
218
|
+
...pickPlacementIds(placementIds, supportedFeedPlacementIds),
|
|
219
|
+
...pickPlacementIds(placementIds, supportedSquarePlacementIds)
|
|
220
|
+
]),
|
|
221
|
+
instagramPositions: sortByReference(instagram.positions, instagramPositions),
|
|
222
|
+
knownPlacementIds: unique(knownPlacementIds),
|
|
223
|
+
messengerPositions: sortByReference(messenger.positions, messengerPositions),
|
|
224
|
+
placementIds: unique(placementIds),
|
|
225
|
+
publisherPlatforms: selectedPublisherPlatforms,
|
|
226
|
+
publisherPlatformsWereExplicit,
|
|
227
|
+
squarePlacementIds: pickPlacementIds(placementIds, supportedSquarePlacementIds),
|
|
228
|
+
threadsPositions: sortByReference(threads.positions, threadsPositions),
|
|
229
|
+
unknownPlacementIds,
|
|
230
|
+
unsupportedPlacementIds,
|
|
231
|
+
verticalPlacementIds: pickPlacementIds(placementIds, supportedVerticalPlacementIds)
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
export function hasPlacementFormats(formats) {
|
|
235
|
+
return Boolean(formats && launchCreativeFormatKeys.some((key) => formats[key]));
|
|
236
|
+
}
|
|
237
|
+
export function hasOnlyInstagramVerticalPlacements(targeting) {
|
|
238
|
+
return targeting.verticalPlacementIds.length > 0
|
|
239
|
+
&& targeting.verticalPlacementIds.every((placementId) => placementId.startsWith("instagram."));
|
|
240
|
+
}
|
|
241
|
+
export function formatPlacementIds(placementIds) {
|
|
242
|
+
return [...placementIds].sort().join(", ");
|
|
243
|
+
}
|
|
244
|
+
export function pickVerticalPlacementIds(placementIds) {
|
|
245
|
+
return unique(placementIds.map(toPlacementId).filter((placementId) => supportedVerticalPlacementIds.has(placementId)));
|
|
246
|
+
}
|
|
247
|
+
export function pickFeedPlacementIds(placementIds) {
|
|
248
|
+
return unique(placementIds.map(toPlacementId).filter((placementId) => supportedFeedPlacementIds.has(placementId) || supportedSquarePlacementIds.has(placementId)));
|
|
249
|
+
}
|
|
250
|
+
export function pickUnsupportedPlacementIds(placementIds) {
|
|
251
|
+
return unique(placementIds.map(toPlacementId).filter((placementId) => explicitlyUnsupportedPlacementIds.has(placementId)
|
|
252
|
+
|| (!supportedFeedPlacementIds.has(placementId)
|
|
253
|
+
&& !supportedSquarePlacementIds.has(placementId)
|
|
254
|
+
&& !supportedVerticalPlacementIds.has(placementId))));
|
|
255
|
+
}
|
|
256
|
+
export function buildPlacementCustomizationSpec(placementIds) {
|
|
257
|
+
const facebook = new Set();
|
|
258
|
+
const instagram = new Set();
|
|
259
|
+
const messenger = new Set();
|
|
260
|
+
const audienceNetwork = new Set();
|
|
261
|
+
const threads = new Set();
|
|
262
|
+
placementIds.forEach((placementId) => {
|
|
263
|
+
const [platform, position] = placementId.split(".", 2);
|
|
264
|
+
if (!position) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
switch (platform) {
|
|
268
|
+
case "facebook":
|
|
269
|
+
facebook.add(position);
|
|
270
|
+
break;
|
|
271
|
+
case "instagram":
|
|
272
|
+
instagram.add(position);
|
|
273
|
+
break;
|
|
274
|
+
case "messenger":
|
|
275
|
+
messenger.add(position);
|
|
276
|
+
break;
|
|
277
|
+
case "audience_network":
|
|
278
|
+
audienceNetwork.add(position);
|
|
279
|
+
break;
|
|
280
|
+
case "threads":
|
|
281
|
+
threads.add(position);
|
|
282
|
+
break;
|
|
283
|
+
default:
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
const publisherPlatformsValue = unique([
|
|
288
|
+
...(facebook.size > 0 ? ["facebook"] : []),
|
|
289
|
+
...(instagram.size > 0 ? ["instagram"] : []),
|
|
290
|
+
...(messenger.size > 0 ? ["messenger"] : []),
|
|
291
|
+
...(audienceNetwork.size > 0 ? ["audience_network"] : []),
|
|
292
|
+
...(threads.size > 0 ? ["threads"] : [])
|
|
293
|
+
]);
|
|
294
|
+
return {
|
|
295
|
+
...(publisherPlatformsValue.length > 0 ? { publisher_platforms: publisherPlatformsValue } : {}),
|
|
296
|
+
...(facebook.size > 0
|
|
297
|
+
? { facebook_positions: sortByReference([...facebook], facebookPositions) }
|
|
298
|
+
: {}),
|
|
299
|
+
...(instagram.size > 0
|
|
300
|
+
? { instagram_positions: sortByReference([...instagram], instagramPositions) }
|
|
301
|
+
: {}),
|
|
302
|
+
...(messenger.size > 0
|
|
303
|
+
? { messenger_positions: sortByReference([...messenger], messengerPositions) }
|
|
304
|
+
: {}),
|
|
305
|
+
...(audienceNetwork.size > 0
|
|
306
|
+
? { audience_network_positions: sortByReference([...audienceNetwork], audienceNetworkPositions) }
|
|
307
|
+
: {}),
|
|
308
|
+
...(threads.size > 0
|
|
309
|
+
? { threads_positions: sortByReference([...threads], threadsPositions) }
|
|
310
|
+
: {})
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
export function coversFallbackPlacements(placementId) {
|
|
314
|
+
return supportedFallbackPlacementIds.has(placementId);
|
|
315
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function normalizeHttpsUrl(value: string, label: string): string;
|
|
2
|
+
export declare function normalizeAuthRedirectUri(value: string, label: string): string;
|
|
3
|
+
export declare function sanitizeUrl(value: string): string;
|
|
4
|
+
export declare function sanitizeText(value: string): string;
|
|
5
|
+
export declare function sanitizeArgvForLogs(argv: string[]): string[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { AppError, ExitCode } from "./errors.js";
|
|
2
|
+
const REDACTED = "<redacted>";
|
|
3
|
+
const REDACTED_QUERY_PARAMS = new Set(["access_token", "appsecret_proof", "code"]);
|
|
4
|
+
const REDACTED_ARG_VALUE_OPTIONS = new Set(["--config"]);
|
|
5
|
+
export function normalizeHttpsUrl(value, label) {
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
let url;
|
|
8
|
+
try {
|
|
9
|
+
url = new URL(trimmed);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new AppError(`${label} must be a valid https URL.`, ExitCode.Config);
|
|
13
|
+
}
|
|
14
|
+
if (url.protocol !== "https:") {
|
|
15
|
+
throw new AppError(`${label} must use https.`, ExitCode.Config);
|
|
16
|
+
}
|
|
17
|
+
return url.toString();
|
|
18
|
+
}
|
|
19
|
+
function isLoopbackHost(hostname) {
|
|
20
|
+
if (hostname === "localhost" || hostname === "::1" || hostname === "[::1]") {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (/^127(?:\.\d{1,3}){3}$/.test(hostname)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
export function normalizeAuthRedirectUri(value, label) {
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
let url;
|
|
31
|
+
try {
|
|
32
|
+
url = new URL(trimmed);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new AppError(`${label} must be a valid http or https URL.`, ExitCode.Config);
|
|
36
|
+
}
|
|
37
|
+
if (url.protocol === "https:") {
|
|
38
|
+
return url.toString();
|
|
39
|
+
}
|
|
40
|
+
if (url.protocol === "http:" && isLoopbackHost(url.hostname)) {
|
|
41
|
+
return url.toString();
|
|
42
|
+
}
|
|
43
|
+
throw new AppError(`${label} must use https unless it targets localhost or a loopback address.`, ExitCode.Config);
|
|
44
|
+
}
|
|
45
|
+
export function sanitizeUrl(value) {
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(value);
|
|
48
|
+
for (const parameter of REDACTED_QUERY_PARAMS) {
|
|
49
|
+
if (url.searchParams.has(parameter)) {
|
|
50
|
+
url.searchParams.set(parameter, REDACTED);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (url.hash.startsWith("#")) {
|
|
54
|
+
const fragmentParams = new URLSearchParams(url.hash.slice(1));
|
|
55
|
+
let mutated = false;
|
|
56
|
+
for (const parameter of REDACTED_QUERY_PARAMS) {
|
|
57
|
+
if (fragmentParams.has(parameter)) {
|
|
58
|
+
fragmentParams.set(parameter, REDACTED);
|
|
59
|
+
mutated = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (mutated) {
|
|
63
|
+
const nextHash = fragmentParams.toString();
|
|
64
|
+
url.hash = nextHash ? `#${nextHash}` : "";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return url.toString();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return value.replace(/([?&](?:access_token|appsecret_proof|code)=)[^&]*/gi, `$1${REDACTED}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function sanitizeText(value) {
|
|
74
|
+
const sanitizedUrls = value.replace(/https?:\/\/[^\s"'<>]+/gi, (candidate) => sanitizeUrl(candidate));
|
|
75
|
+
return sanitizedUrls
|
|
76
|
+
.replace(/((?:access_token|appsecret_proof|code)=)[^&\s"'<>]+/gi, `$1${REDACTED}`)
|
|
77
|
+
.replace(/((?:access_token|appsecret_proof|code)%3[Dd])[^&\s"'<>]+/g, `$1${REDACTED}`)
|
|
78
|
+
.replace(/("(?:access_token|appsecret_proof|code)"\s*:\s*")[^"]*"/gi, `$1${REDACTED}"`)
|
|
79
|
+
.replace(/('(?:access_token|appsecret_proof|code)'\s*:\s*')[^']*'/gi, `$1${REDACTED}'`);
|
|
80
|
+
}
|
|
81
|
+
export function sanitizeArgvForLogs(argv) {
|
|
82
|
+
const sanitized = [];
|
|
83
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
84
|
+
const entry = argv[index];
|
|
85
|
+
if (!entry) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (REDACTED_ARG_VALUE_OPTIONS.has(entry)) {
|
|
89
|
+
sanitized.push(entry);
|
|
90
|
+
if (argv[index + 1]) {
|
|
91
|
+
sanitized.push(REDACTED);
|
|
92
|
+
index += 1;
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const [flag, value] = entry.split("=", 2);
|
|
97
|
+
if (flag && value !== undefined && REDACTED_ARG_VALUE_OPTIONS.has(flag)) {
|
|
98
|
+
sanitized.push(`${flag}=${REDACTED}`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
sanitized.push(sanitizeText(entry));
|
|
102
|
+
}
|
|
103
|
+
return sanitized;
|
|
104
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const accountSelectionSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
3
|
+
export declare function parseAccountSelection(value: string | undefined): string;
|
|
4
|
+
export declare function parseCampaignId(value: string): string;
|
|
5
|
+
export declare function parseAdId(value: string): string;
|
|
6
|
+
export declare function parseVideoId(value: string): string;
|
|
7
|
+
export declare function parseAudienceId(value: string): string;
|
|
8
|
+
export declare function parsePositiveNumber(value: string, label: string): number;
|
|
9
|
+
export declare function parseThreshold(value: string | number | undefined): number;
|
|
10
|
+
export declare function parseBreakdowns(value: string | undefined): string[];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AppError, ExitCode } from "../utils/errors.js";
|
|
3
|
+
import { isAllAccountsSelection, normalizeAdAccountId, normalizeObjectId } from "../utils/ids.js";
|
|
4
|
+
export const accountSelectionSchema = z
|
|
5
|
+
.string()
|
|
6
|
+
.trim()
|
|
7
|
+
.min(1)
|
|
8
|
+
.transform((value) => (isAllAccountsSelection(value) ? "all" : normalizeAdAccountId(value)));
|
|
9
|
+
export function parseAccountSelection(value) {
|
|
10
|
+
if (!value) {
|
|
11
|
+
throw new AppError("An account is required.", ExitCode.Usage);
|
|
12
|
+
}
|
|
13
|
+
return accountSelectionSchema.parse(value);
|
|
14
|
+
}
|
|
15
|
+
export function parseCampaignId(value) {
|
|
16
|
+
return normalizeObjectId(value, "campaign id");
|
|
17
|
+
}
|
|
18
|
+
export function parseAdId(value) {
|
|
19
|
+
return normalizeObjectId(value, "ad id");
|
|
20
|
+
}
|
|
21
|
+
export function parseVideoId(value) {
|
|
22
|
+
return normalizeObjectId(value, "video id");
|
|
23
|
+
}
|
|
24
|
+
export function parseAudienceId(value) {
|
|
25
|
+
return normalizeObjectId(value, "audience id");
|
|
26
|
+
}
|
|
27
|
+
export function parsePositiveNumber(value, label) {
|
|
28
|
+
const numeric = Number(value);
|
|
29
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
30
|
+
throw new AppError(`${label} must be a positive number.`, ExitCode.Usage);
|
|
31
|
+
}
|
|
32
|
+
return numeric;
|
|
33
|
+
}
|
|
34
|
+
export function parseThreshold(value) {
|
|
35
|
+
const numeric = Number(value ?? 20);
|
|
36
|
+
if (!Number.isFinite(numeric) || numeric < 1 || numeric > 1000) {
|
|
37
|
+
throw new AppError("Threshold must be a number between 1 and 1000.", ExitCode.Usage);
|
|
38
|
+
}
|
|
39
|
+
return numeric;
|
|
40
|
+
}
|
|
41
|
+
export function parseBreakdowns(value) {
|
|
42
|
+
if (!value) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const allowed = new Set(["age", "gender", "publisher_platform", "platform_position", "device_platform"]);
|
|
46
|
+
const breakdowns = value
|
|
47
|
+
.split(",")
|
|
48
|
+
.map((entry) => entry.trim())
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
for (const breakdown of breakdowns) {
|
|
51
|
+
if (!allowed.has(breakdown)) {
|
|
52
|
+
throw new AppError(`Unsupported breakdown: ${breakdown}`, ExitCode.Usage);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return breakdowns;
|
|
56
|
+
}
|