@getvitops/utils 3.0.0 → 4.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/CHANGELOG.md +139 -0
- package/README.md +1 -1
- package/dist/ads/index.d.mts +228 -0
- package/dist/ads/index.d.mts.map +1 -0
- package/dist/ads/index.mjs +380 -0
- package/dist/ads/index.mjs.map +1 -0
- package/dist/cloudflare-BW-Iyi-B.mjs +92 -0
- package/dist/cloudflare-BW-Iyi-B.mjs.map +1 -0
- package/dist/cloudflare-BiK61Oq4.d.mts +27 -0
- package/dist/cloudflare-BiK61Oq4.d.mts.map +1 -0
- package/dist/indexing/index.d.mts +4 -7
- package/dist/indexing/index.d.mts.map +1 -1
- package/dist/indexing/index.mjs +13 -37
- package/dist/indexing/index.mjs.map +1 -1
- package/dist/notify/index.d.mts +207 -0
- package/dist/notify/index.d.mts.map +1 -0
- package/dist/notify/index.mjs +320 -0
- package/dist/notify/index.mjs.map +1 -0
- package/dist/onboarding/index.d.mts +207 -0
- package/dist/onboarding/index.d.mts.map +1 -0
- package/dist/onboarding/index.mjs +359 -0
- package/dist/onboarding/index.mjs.map +1 -0
- package/dist/platform-0nlOIy8h.mjs +96 -0
- package/dist/platform-0nlOIy8h.mjs.map +1 -0
- package/dist/token--18wN9UR.d.mts +46 -0
- package/dist/token--18wN9UR.d.mts.map +1 -0
- package/dist/token-BV7tPHh2.mjs +96 -0
- package/dist/token-BV7tPHh2.mjs.map +1 -0
- package/dist/tracking/index.d.mts +94 -0
- package/dist/tracking/index.d.mts.map +1 -0
- package/dist/tracking/index.mjs +77 -0
- package/dist/tracking/index.mjs.map +1 -0
- package/dist/types-C3lHaSrU.d.mts +69 -0
- package/dist/types-C3lHaSrU.d.mts.map +1 -0
- package/package.json +17 -1
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { n as findZoneId, r as listApexTxt, t as createApexTxt } from "../cloudflare-BW-Iyi-B.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/ads/providers.ts
|
|
4
|
+
/** `gtag` is shared by every Google tag; the id shape (`AW-…`) is what differs. */
|
|
5
|
+
const gtagBootstrap = (id) => `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag('js',new Date());gtag('config','${id}');`;
|
|
6
|
+
/** The queue-stub shape shared by fbq/ttq/rdt/snaptr/pintrk. */
|
|
7
|
+
const queueStub = (global, methods) => `window.${global}=window.${global}||function(){(window.${global}.q=window.${global}.q||[]).push(arguments)};window.${global}.methods=${methods};`;
|
|
8
|
+
const AD_PLATFORMS = {
|
|
9
|
+
google: {
|
|
10
|
+
name: "Google Ads",
|
|
11
|
+
operatorCountry: "the United States",
|
|
12
|
+
clickIdParams: [
|
|
13
|
+
"gclid",
|
|
14
|
+
"gbraid",
|
|
15
|
+
"wbraid"
|
|
16
|
+
],
|
|
17
|
+
consoleUrl: "https://ads.google.com/",
|
|
18
|
+
verification: {
|
|
19
|
+
method: "none",
|
|
20
|
+
reason: "Google Ads has no domain verification — the conversion tag is the link. (Merchant Center claims a website; that is a different product.)"
|
|
21
|
+
},
|
|
22
|
+
tag: {
|
|
23
|
+
needs: "pixelId",
|
|
24
|
+
src: (id) => `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}`,
|
|
25
|
+
bootstrap: gtagBootstrap,
|
|
26
|
+
cookies: [
|
|
27
|
+
"_gcl_au",
|
|
28
|
+
"_gcl_aw",
|
|
29
|
+
"_gcl_dc",
|
|
30
|
+
"_gac_*"
|
|
31
|
+
],
|
|
32
|
+
privacyUrl: "https://policies.google.com/privacy",
|
|
33
|
+
optOut: "https://adssettings.google.com/"
|
|
34
|
+
},
|
|
35
|
+
manualSteps: (e) => [...e.conversionLabel ? [] : ["create a conversion action in Google Ads and record its label as `conversionLabel`"], "link the Google Ads account to Search Console / GA4 in the UI if you want imported conversions"]
|
|
36
|
+
},
|
|
37
|
+
meta: {
|
|
38
|
+
name: "Meta",
|
|
39
|
+
operatorCountry: "the United States",
|
|
40
|
+
clickIdParams: ["fbclid"],
|
|
41
|
+
consoleUrl: "https://business.facebook.com/settings/owned-domains",
|
|
42
|
+
verification: {
|
|
43
|
+
method: "dns-txt",
|
|
44
|
+
txtPrefix: "facebook-domain-verification=",
|
|
45
|
+
where: "Business settings → Brand safety → Domains → Add → DNS verification"
|
|
46
|
+
},
|
|
47
|
+
tag: {
|
|
48
|
+
needs: "pixelId",
|
|
49
|
+
src: () => "https://connect.facebook.net/en_US/fbevents.js",
|
|
50
|
+
bootstrap: (id) => `${queueStub("fbq", `['init','track','trackCustom','consent']`)}fbq('init','${id}');fbq('track','PageView');`,
|
|
51
|
+
cookies: ["_fbp", "_fbc"],
|
|
52
|
+
privacyUrl: "https://www.facebook.com/privacy/policy/",
|
|
53
|
+
optOut: "https://www.facebook.com/adpreferences/ad_settings"
|
|
54
|
+
},
|
|
55
|
+
manualSteps: () => ["click Verify on the domain in Business settings once the TXT record has propagated — the Business Management API needs a system-user token and is deliberately not called"]
|
|
56
|
+
},
|
|
57
|
+
linkedin: {
|
|
58
|
+
name: "LinkedIn",
|
|
59
|
+
operatorCountry: "the United States",
|
|
60
|
+
clickIdParams: ["li_fat_id"],
|
|
61
|
+
consoleUrl: "https://www.linkedin.com/campaignmanager/",
|
|
62
|
+
verification: {
|
|
63
|
+
method: "none",
|
|
64
|
+
reason: "LinkedIn Ads has no domain verification — the Insight Tag is the link. (Verifying a LinkedIn *Page* is a separate, unrelated flow.)"
|
|
65
|
+
},
|
|
66
|
+
tag: {
|
|
67
|
+
needs: "accountId",
|
|
68
|
+
src: () => "https://snap.licdn.com/li.lms-analytics/insight.min.js",
|
|
69
|
+
bootstrap: (id) => `window._linkedin_partner_id='${id}';window._linkedin_data_partner_ids=window._linkedin_data_partner_ids||[];window._linkedin_data_partner_ids.push('${id}');`,
|
|
70
|
+
cookies: [
|
|
71
|
+
"li_sugr",
|
|
72
|
+
"li_gc",
|
|
73
|
+
"bcookie",
|
|
74
|
+
"lidc",
|
|
75
|
+
"UserMatchHistory"
|
|
76
|
+
],
|
|
77
|
+
privacyUrl: "https://www.linkedin.com/legal/privacy-policy",
|
|
78
|
+
optOut: "https://www.linkedin.com/psettings/guest-controls/retargeting-opt-out"
|
|
79
|
+
},
|
|
80
|
+
manualSteps: () => ["add conversions in Campaign Manager — the Insight Tag alone records page visits, not conversions"]
|
|
81
|
+
},
|
|
82
|
+
reddit: {
|
|
83
|
+
name: "Reddit",
|
|
84
|
+
operatorCountry: "the United States",
|
|
85
|
+
clickIdParams: ["rdt_cid"],
|
|
86
|
+
consoleUrl: "https://ads.reddit.com/",
|
|
87
|
+
verification: {
|
|
88
|
+
method: "none",
|
|
89
|
+
reason: "Reddit Ads has no domain verification — the Reddit Pixel is the link."
|
|
90
|
+
},
|
|
91
|
+
tag: {
|
|
92
|
+
needs: "pixelId",
|
|
93
|
+
src: () => "https://www.redditstatic.com/ads/pixel.js",
|
|
94
|
+
bootstrap: (id) => `${queueStub("rdt", `['init','track']`)}rdt('init','${id}');rdt('track','PageVisit');`,
|
|
95
|
+
cookies: ["_rdt_uuid"],
|
|
96
|
+
privacyUrl: "https://www.reddit.com/policies/privacy-policy"
|
|
97
|
+
},
|
|
98
|
+
manualSteps: () => []
|
|
99
|
+
},
|
|
100
|
+
tiktok: {
|
|
101
|
+
name: "TikTok",
|
|
102
|
+
operatorCountry: "Singapore",
|
|
103
|
+
clickIdParams: ["ttclid"],
|
|
104
|
+
consoleUrl: "https://ads.tiktok.com/",
|
|
105
|
+
verification: {
|
|
106
|
+
method: "dns-txt",
|
|
107
|
+
txtPrefix: "tiktok-developers-site-verification=",
|
|
108
|
+
where: "Ads Manager → Assets → Events → Web events → Verify domain → DNS"
|
|
109
|
+
},
|
|
110
|
+
tag: {
|
|
111
|
+
needs: "pixelId",
|
|
112
|
+
src: (id) => `https://analytics.tiktok.com/i18n/pixel/events.js?sdkid=${encodeURIComponent(id)}&lib=ttq`,
|
|
113
|
+
bootstrap: (id) => `window.TiktokAnalyticsObject='ttq';${queueStub("ttq", `['page','track','identify','instances','debug','on','off','once','ready','alias','group','enableCookie','disableCookie']`)}ttq('init','${id}');ttq('page');`,
|
|
114
|
+
cookies: ["_ttp"],
|
|
115
|
+
privacyUrl: "https://www.tiktok.com/legal/privacy-policy"
|
|
116
|
+
},
|
|
117
|
+
manualSteps: () => []
|
|
118
|
+
},
|
|
119
|
+
microsoft: {
|
|
120
|
+
name: "Microsoft Ads",
|
|
121
|
+
operatorCountry: "the United States",
|
|
122
|
+
clickIdParams: ["msclkid"],
|
|
123
|
+
consoleUrl: "https://ads.microsoft.com/",
|
|
124
|
+
verification: {
|
|
125
|
+
method: "none",
|
|
126
|
+
reason: "Microsoft Ads has no domain verification — the UET tag is the link."
|
|
127
|
+
},
|
|
128
|
+
tag: {
|
|
129
|
+
needs: "pixelId",
|
|
130
|
+
bootstrap: (id) => `(function(w,d,t,r,u){var f,n,i;w[u]=w[u]||[],f=function(){var o={ti:'${id}',enableAutoSpaTracking:true};o.q=w[u],w[u]=new UET(o),w[u].push('pageLoad')},n=d.createElement(t),n.src=r,n.async=1,n.onload=n.onreadystatechange=function(){var s=this.readyState;s&&s!=='loaded'&&s!=='complete'||(f(),n.onload=n.onreadystatechange=null)},i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)})(window,document,'script','https://bat.bing.com/bat.js','uetq');`,
|
|
131
|
+
cookies: [
|
|
132
|
+
"_uetsid",
|
|
133
|
+
"_uetvid",
|
|
134
|
+
"MUID"
|
|
135
|
+
],
|
|
136
|
+
privacyUrl: "https://privacy.microsoft.com/privacystatement",
|
|
137
|
+
optOut: "https://account.microsoft.com/privacy/ad-settings"
|
|
138
|
+
},
|
|
139
|
+
manualSteps: () => []
|
|
140
|
+
},
|
|
141
|
+
pinterest: {
|
|
142
|
+
name: "Pinterest",
|
|
143
|
+
operatorCountry: "the United States",
|
|
144
|
+
clickIdParams: ["epik"],
|
|
145
|
+
consoleUrl: "https://ads.pinterest.com/",
|
|
146
|
+
verification: {
|
|
147
|
+
method: "dns-txt",
|
|
148
|
+
txtPrefix: "pinterest-site-verification=",
|
|
149
|
+
where: "Business hub → Claim → Websites → Claim → Add TXT record"
|
|
150
|
+
},
|
|
151
|
+
tag: {
|
|
152
|
+
needs: "pixelId",
|
|
153
|
+
bootstrap: (id) => `${queueStub("pintrk", `['load','page','track']`)}!function(){var t=document.createElement('script');t.async=!0,t.src='https://s.pinimg.com/ct/core.js';var e=document.getElementsByTagName('script')[0];e.parentNode.insertBefore(t,e)}();pintrk('load','${id}');pintrk('page');`,
|
|
154
|
+
cookies: [
|
|
155
|
+
"_pinterest_ct_ua",
|
|
156
|
+
"_pin_unauth",
|
|
157
|
+
"_epik"
|
|
158
|
+
],
|
|
159
|
+
privacyUrl: "https://policy.pinterest.com/privacy-policy"
|
|
160
|
+
},
|
|
161
|
+
manualSteps: () => ["confirm the claim in Business hub → Claim once the TXT record has propagated"]
|
|
162
|
+
},
|
|
163
|
+
snapchat: {
|
|
164
|
+
name: "Snapchat",
|
|
165
|
+
operatorCountry: "the United States",
|
|
166
|
+
clickIdParams: ["ScCid"],
|
|
167
|
+
consoleUrl: "https://ads.snapchat.com/",
|
|
168
|
+
verification: {
|
|
169
|
+
method: "dns-txt",
|
|
170
|
+
txtPrefix: "snap-domain-verification=",
|
|
171
|
+
where: "Business Manager → Public Profiles / Domains → Verify domain → DNS"
|
|
172
|
+
},
|
|
173
|
+
tag: {
|
|
174
|
+
needs: "pixelId",
|
|
175
|
+
src: () => "https://sc-static.net/scevent.min.js",
|
|
176
|
+
bootstrap: (id) => `${queueStub("snaptr", `['init','track']`)}snaptr('init','${id}');snaptr('track','PAGE_VIEW');`,
|
|
177
|
+
cookies: [
|
|
178
|
+
"_scid",
|
|
179
|
+
"_scid_r",
|
|
180
|
+
"sc_at"
|
|
181
|
+
],
|
|
182
|
+
privacyUrl: "https://snap.com/privacy/privacy-policy"
|
|
183
|
+
},
|
|
184
|
+
manualSteps: () => []
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
/** Every provider, in table order — the one place iteration order is decided. */
|
|
188
|
+
const AD_PROVIDER_KEYS = Object.keys(AD_PLATFORMS);
|
|
189
|
+
/** Whether a string names a known provider (for CLI arg validation). */
|
|
190
|
+
const isAdProvider = (value) => value in AD_PLATFORMS;
|
|
191
|
+
/**
|
|
192
|
+
* The DNS TXT record content for a token.
|
|
193
|
+
*
|
|
194
|
+
* A value already containing `=` is taken as the whole record and left alone. That
|
|
195
|
+
* is the same escape hatch an icon name containing `:` gets: a platform can change
|
|
196
|
+
* its prefix spelling without waiting on a toolchain release, and an operator who
|
|
197
|
+
* pasted the full record from the UI gets what they meant rather than a
|
|
198
|
+
* double-prefixed record that verifies against nothing.
|
|
199
|
+
*/
|
|
200
|
+
function txtRecord(provider, token) {
|
|
201
|
+
const trimmed = token.trim();
|
|
202
|
+
if (trimmed.includes("=")) return trimmed;
|
|
203
|
+
return `${AD_PLATFORMS[provider].verification.txtPrefix ?? ""}${trimmed}`;
|
|
204
|
+
}
|
|
205
|
+
/** The tag id for an entry, or undefined when the field it needs is unset. */
|
|
206
|
+
function tagId(entry) {
|
|
207
|
+
return entry[AD_PLATFORMS[entry.provider].tag.needs];
|
|
208
|
+
}
|
|
209
|
+
/** The consent category gating an entry's tag. `marketing` unless stated otherwise. */
|
|
210
|
+
const categoryOf = (entry) => entry.category ?? "marketing";
|
|
211
|
+
const escapeAttr = (value) => value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
212
|
+
/**
|
|
213
|
+
* One provider's tag, rendered as a **consent-gated, inert** `<script>`.
|
|
214
|
+
*
|
|
215
|
+
* `type="text/plain"` with the URL on `data-src` is what makes the gate real: the
|
|
216
|
+
* browser neither parses the body nor fetches the library, so an undecided
|
|
217
|
+
* visitor's page issues no third-party request. Never give one of these a live
|
|
218
|
+
* `src` — a gate that instead asks a loaded tracker not to track is a promise.
|
|
219
|
+
*
|
|
220
|
+
* Returns null when the entry has no tag id, so a caller can report the gap rather
|
|
221
|
+
* than emit a snippet that initialises `undefined`.
|
|
222
|
+
*/
|
|
223
|
+
function renderTag(entry, strategy = "idle") {
|
|
224
|
+
const platform = AD_PLATFORMS[entry.provider];
|
|
225
|
+
const id = tagId(entry);
|
|
226
|
+
if (!id) return null;
|
|
227
|
+
const attrs = [
|
|
228
|
+
"type=\"text/plain\"",
|
|
229
|
+
"data-vitops-tag",
|
|
230
|
+
`data-consent="${categoryOf(entry)}"`,
|
|
231
|
+
`data-strategy="${strategy}"`,
|
|
232
|
+
...platform.tag.src ? [`data-src="${escapeAttr(platform.tag.src(id))}"`] : [],
|
|
233
|
+
`data-consent-cookies="${escapeAttr(platform.tag.cookies.join(","))}"`
|
|
234
|
+
];
|
|
235
|
+
return `<!-- ${platform.name} — gated on ${categoryOf(entry)} consent -->\n<script ${attrs.join(" ")}>${platform.tag.bootstrap(id, entry)}<\/script>`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/ads/plan.ts
|
|
240
|
+
/**
|
|
241
|
+
* Every decision `vitops ads setup` makes, as one pure function.
|
|
242
|
+
*
|
|
243
|
+
* Pure in the load-bearing sense: no `fetch`, no `fs`, no clock, no stdin. The
|
|
244
|
+
* executor gathers each domain's live DNS state and applies what it is told; this
|
|
245
|
+
* module decides, from that state, what is left to do. The same split
|
|
246
|
+
* `onboarding/plan.ts` makes, for the same two payoffs: `--dry` is a *complete*
|
|
247
|
+
* account of a run, and `--check` is an honest drift report.
|
|
248
|
+
*
|
|
249
|
+
* Idempotency lives here — a step whose desired state already holds resolves to
|
|
250
|
+
* `skip`, so a re-run of a linked property is all skips by construction rather than
|
|
251
|
+
* by the executor checking twice.
|
|
252
|
+
*
|
|
253
|
+
* One thing this planner adds over the onboarding one: a step that cannot proceed
|
|
254
|
+
* for want of a **config value** resolves to `blocked` carrying the field name in
|
|
255
|
+
* `needs`. That is a machine-readable gap, which is what lets the CLI *ask* for the
|
|
256
|
+
* missing token and re-plan, while the planner itself stays pure and knows nothing
|
|
257
|
+
* about a terminal. A blocked step is drift, never a skip — the first run of a
|
|
258
|
+
* property whose token has not been fetched yet must not read as "already linked".
|
|
259
|
+
*/
|
|
260
|
+
/**
|
|
261
|
+
* Decide one property's steps.
|
|
262
|
+
*
|
|
263
|
+
* The `dns-txt` branch has three outcomes, and keeping them distinct is the point:
|
|
264
|
+
* our record is present (skip), a *different* record from the same platform is
|
|
265
|
+
* present (create, and say so — a token rotation leaves the old record in place,
|
|
266
|
+
* since this command never edits or deletes DNS), or no token is configured yet
|
|
267
|
+
* (blocked, naming the field).
|
|
268
|
+
*/
|
|
269
|
+
function planProperty(setup, state) {
|
|
270
|
+
const platform = AD_PLATFORMS[setup.provider];
|
|
271
|
+
const reminders = [...platform.manualSteps(setup)];
|
|
272
|
+
let txt;
|
|
273
|
+
let txtContent;
|
|
274
|
+
if (platform.verification.method === "none") txt = {
|
|
275
|
+
action: "skip",
|
|
276
|
+
detail: platform.verification.reason ?? "no domain verification"
|
|
277
|
+
};
|
|
278
|
+
else if (!setup.domainVerification) txt = {
|
|
279
|
+
action: "blocked",
|
|
280
|
+
detail: `needs the verification token from ${platform.verification.where ?? platform.consoleUrl}`,
|
|
281
|
+
needs: "domainVerification"
|
|
282
|
+
};
|
|
283
|
+
else {
|
|
284
|
+
const desired = txtRecord(setup.provider, setup.domainVerification);
|
|
285
|
+
const prefix = platform.verification.txtPrefix;
|
|
286
|
+
const stale = prefix != null && state.txtContents.some((c) => c.startsWith(prefix) && c !== desired);
|
|
287
|
+
txt = state.txtContents.includes(desired) ? {
|
|
288
|
+
action: "skip",
|
|
289
|
+
detail: "verification TXT already present"
|
|
290
|
+
} : {
|
|
291
|
+
action: "create",
|
|
292
|
+
detail: stale ? "create verification TXT (an older token from this platform is still present — remove it yourself; this command never deletes DNS)" : "create verification TXT"
|
|
293
|
+
};
|
|
294
|
+
if (txt.action === "create") txtContent = desired;
|
|
295
|
+
}
|
|
296
|
+
const id = tagId(setup);
|
|
297
|
+
const tag = id ? {
|
|
298
|
+
action: "skip",
|
|
299
|
+
detail: `tag ready (${platform.tag.needs} ${id})`
|
|
300
|
+
} : {
|
|
301
|
+
action: "blocked",
|
|
302
|
+
detail: `needs ${platform.tag.needs} from ${platform.consoleUrl} to emit the tag`,
|
|
303
|
+
needs: platform.tag.needs
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
provider: setup.provider,
|
|
307
|
+
name: platform.name,
|
|
308
|
+
...setup.domain ? { domain: setup.domain } : {},
|
|
309
|
+
txt,
|
|
310
|
+
tag,
|
|
311
|
+
...txtContent ? { txtContent } : {},
|
|
312
|
+
reminders
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/** Plan every configured property. */
|
|
316
|
+
function plan(config, states) {
|
|
317
|
+
const notes = [];
|
|
318
|
+
return {
|
|
319
|
+
properties: config.properties.map((setup) => {
|
|
320
|
+
const key = setup.domain ?? "";
|
|
321
|
+
const state = states.get(key);
|
|
322
|
+
if (!state && AD_PLATFORMS[setup.provider].verification.method === "dns-txt") notes.push(`${setup.provider}: no observed DNS state for ${key || "(no domain)"}`);
|
|
323
|
+
return planProperty(setup, state ?? { txtContents: [] });
|
|
324
|
+
}),
|
|
325
|
+
notes
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
/** Any step that isn't a skip — i.e. the live state or the config has drifted. */
|
|
329
|
+
function hasDrift(p) {
|
|
330
|
+
return p.properties.some((x) => x.txt.action !== "skip" || x.tag.action !== "skip");
|
|
331
|
+
}
|
|
332
|
+
/** Every field a run still needs, per provider — what the CLI asks for. */
|
|
333
|
+
function missingFields(p) {
|
|
334
|
+
const out = [];
|
|
335
|
+
for (const x of p.properties) for (const step of [x.txt, x.tag]) if (step.action === "blocked" && step.needs) out.push({
|
|
336
|
+
provider: x.provider,
|
|
337
|
+
needs: step.needs
|
|
338
|
+
});
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
/** Render a plan as the block `--dry` and `--check` print. */
|
|
342
|
+
function formatPlan(p) {
|
|
343
|
+
const glyph = (s) => s.action === "skip" ? "·" : s.action === "blocked" ? "?" : "+";
|
|
344
|
+
const lines = [];
|
|
345
|
+
for (const x of p.properties) {
|
|
346
|
+
lines.push(`${x.provider}${x.domain ? ` (${x.domain})` : ""}`);
|
|
347
|
+
lines.push(` ${glyph(x.txt)} verify ${x.txt.detail}`);
|
|
348
|
+
lines.push(` ${glyph(x.tag)} tag ${x.tag.detail}`);
|
|
349
|
+
for (const r of x.reminders) lines.push(` ! ${r}`);
|
|
350
|
+
}
|
|
351
|
+
for (const n of p.notes) lines.push(`! ${n}`);
|
|
352
|
+
return lines.join("\n");
|
|
353
|
+
}
|
|
354
|
+
const pad = (s, n) => s.padEnd(n);
|
|
355
|
+
/** Render the end-of-run summary: one row per property, plus any reminders. */
|
|
356
|
+
function formatSummary(results) {
|
|
357
|
+
const headers = [
|
|
358
|
+
"PROVIDER",
|
|
359
|
+
"DOMAIN",
|
|
360
|
+
"VERIFY",
|
|
361
|
+
"TAG"
|
|
362
|
+
];
|
|
363
|
+
const rows = results.map((r) => [
|
|
364
|
+
r.provider,
|
|
365
|
+
r.domain,
|
|
366
|
+
r.txt,
|
|
367
|
+
r.tag
|
|
368
|
+
]);
|
|
369
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)));
|
|
370
|
+
const line = (cells) => cells.map((c, i) => pad(c, widths[i] ?? 0)).join(" ").trimEnd();
|
|
371
|
+
const out = [line(headers), widths.map((w) => "─".repeat(w)).join(" ")];
|
|
372
|
+
for (const row of rows) out.push(line(row));
|
|
373
|
+
const reminders = results.flatMap((r) => r.reminders.map((m) => ` ! ${r.provider}: ${m}`));
|
|
374
|
+
if (reminders.length) out.push("", "Reminders (manual — no API):", ...reminders);
|
|
375
|
+
return out.join("\n");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
//#endregion
|
|
379
|
+
export { AD_PLATFORMS, AD_PROVIDER_KEYS, categoryOf, createApexTxt, findZoneId, formatPlan, formatSummary, hasDrift, isAdProvider, listApexTxt, missingFields, plan, planProperty, renderTag, tagId, txtRecord };
|
|
380
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/ads/providers.ts","../../src/ads/plan.ts"],"sourcesContent":["/**\n * What each ad platform actually lets a build tool do — the table every `vitops ads`\n * subcommand reads.\n *\n * Written the way the indexing module was written: start from what the platforms\n * accept, because it is much narrower than it looks, and record the *absence* of a\n * path as a fact rather than leaving it as a gap.\n *\n * - **Four platforms verify a domain by DNS TXT** (Meta, TikTok, Pinterest,\n * Snapchat). That record is the one thing here that can be created for you.\n * - **Four have no domain verification at all** (Google Ads, LinkedIn, Reddit,\n * Microsoft Ads). Linking is the tag and the account id, nothing else. Those\n * resolve to a skip *plus* a named reason — a silent omission would read as\n * \"already done\".\n * - **No platform Marketing API is called.** Meta's Business Management\n * `verify_domain` needs a system-user token; the Google Ads API needs an\n * approved developer token and puts the consumer's own account on the line.\n * That is the same reasoning that keeps the Google Indexing API unwired: a\n * toolchain other people install must not ship a documented path to a terms\n * violation. The final \"Verify\" click stays a reminder.\n *\n * The verification token is **not a secret** — it is published in DNS, and the\n * platform fetching it back is the ownership proof, exactly like the IndexNow key.\n * So it lives in the config while credentials stay in the environment.\n *\n * Each tag is the platform's documented bootstrap reduced to its queue stub, with\n * the remote library on `data-src` for the consent gate to attach after the\n * bootstrap has run (`@getvitops/core`'s `activateScript` emits the inline body\n * first for exactly this reason). Two platforms — Microsoft and Pinterest — ship a\n * snippet that creates its own `<script>`; those declare no `src`, and their body\n * stays inert until activation, so an undecided visitor still issues no request.\n */\nimport type { AdConsentCategory, AdProvider, AdPropertySetup } from './types.ts';\n\n/** How a platform proves you own the domain. */\nexport interface AdVerificationSpec {\n method: 'dns-txt' | 'none';\n /**\n * Prefix prepended to a bare token to form the record content. Owned here so a\n * config holds the token the platform shows, not a hand-assembled record — but\n * see `txtRecord`: a value already containing `=` passes through whole, which is\n * the escape hatch when a platform changes its spelling.\n */\n txtPrefix?: string;\n /** Where in the platform UI the token is found. Printed when asking for it. */\n where?: string;\n /** Why there is nothing to automate. Required when `method` is `none`. */\n reason?: string;\n}\n\n/** The tag, and which config field carries its id. */\nexport interface AdTagSpec {\n /** Which field the tag id comes from — and therefore what to ask for when absent. */\n needs: 'pixelId' | 'accountId';\n /** The remote library. Absent when the platform's own bootstrap loads itself. */\n src?: (id: string) => string;\n /** The inline bootstrap, which the gate runs before attaching `src`. */\n bootstrap: (id: string, entry: AdPropertySetup) => string;\n /**\n * Cookies the tag sets, for `data-consent-cookies` (what a revoke clears) and\n * for the generated cookie notice. A `*` suffix is a prefix match.\n */\n cookies: string[];\n /** The provider's own privacy policy, for the cookie notice. */\n privacyUrl: string;\n /** A visitor-facing opt-out, where the platform offers one. */\n optOut?: string;\n}\n\nexport interface AdPlatform {\n /** The platform as a reader would recognise it. */\n name: string;\n /**\n * The jurisdiction that can compel this platform to produce what it receives —\n * where the contracting entity is established. Feeds the generated privacy\n * policy's cross-border transfer disclosure, which is a claim about foreign\n * *access*, not foreign storage (see `Processor.operatorCountry` in the\n * generator's `legal/providers.ts`). A consumer whose contracting entity differs\n * declares their own processor entry instead.\n */\n operatorCountry: string;\n /** Click-ID parameters this platform stamps on an ad landing URL. */\n clickIdParams: string[];\n /** Where the operator manages the property. */\n consoleUrl: string;\n verification: AdVerificationSpec;\n tag: AdTagSpec;\n /** Follow-ups no API covers, given this entry. */\n manualSteps: (entry: AdPropertySetup) => string[];\n}\n\n/** `gtag` is shared by every Google tag; the id shape (`AW-…`) is what differs. */\nconst gtagBootstrap = (id: string): string =>\n `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}` +\n `gtag('js',new Date());gtag('config','${id}');`;\n\n/** The queue-stub shape shared by fbq/ttq/rdt/snaptr/pintrk. */\nconst queueStub = (global: string, methods: string): string =>\n `window.${global}=window.${global}||function(){(window.${global}.q=window.${global}.q||[]).push(arguments)};` +\n `window.${global}.methods=${methods};`;\n\nexport const AD_PLATFORMS: Record<AdProvider, AdPlatform> = {\n google: {\n name: 'Google Ads',\n operatorCountry: 'the United States',\n clickIdParams: ['gclid', 'gbraid', 'wbraid'],\n consoleUrl: 'https://ads.google.com/',\n verification: {\n method: 'none',\n reason:\n 'Google Ads has no domain verification — the conversion tag is the link. (Merchant Center claims a website; that is a different product.)',\n },\n tag: {\n // The customer id (`123-456-7890`) is not the tag id: the conversion tag is\n // `AW-…`, which is why this asks for `pixelId` rather than reusing accountId.\n needs: 'pixelId',\n src: (id) => `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}`,\n bootstrap: gtagBootstrap,\n cookies: ['_gcl_au', '_gcl_aw', '_gcl_dc', '_gac_*'],\n privacyUrl: 'https://policies.google.com/privacy',\n optOut: 'https://adssettings.google.com/',\n },\n manualSteps: (e) => [\n ...(e.conversionLabel\n ? []\n : ['create a conversion action in Google Ads and record its label as `conversionLabel`']),\n 'link the Google Ads account to Search Console / GA4 in the UI if you want imported conversions',\n ],\n },\n\n meta: {\n name: 'Meta',\n operatorCountry: 'the United States',\n clickIdParams: ['fbclid'],\n consoleUrl: 'https://business.facebook.com/settings/owned-domains',\n verification: {\n method: 'dns-txt',\n txtPrefix: 'facebook-domain-verification=',\n where: 'Business settings → Brand safety → Domains → Add → DNS verification',\n },\n tag: {\n needs: 'pixelId',\n src: () => 'https://connect.facebook.net/en_US/fbevents.js',\n bootstrap: (id) =>\n `${queueStub('fbq', `['init','track','trackCustom','consent']`)}` +\n `fbq('init','${id}');fbq('track','PageView');`,\n cookies: ['_fbp', '_fbc'],\n privacyUrl: 'https://www.facebook.com/privacy/policy/',\n optOut: 'https://www.facebook.com/adpreferences/ad_settings',\n },\n manualSteps: () => [\n 'click Verify on the domain in Business settings once the TXT record has propagated — the Business Management API needs a system-user token and is deliberately not called',\n ],\n },\n\n linkedin: {\n name: 'LinkedIn',\n operatorCountry: 'the United States',\n clickIdParams: ['li_fat_id'],\n consoleUrl: 'https://www.linkedin.com/campaignmanager/',\n verification: {\n method: 'none',\n reason:\n 'LinkedIn Ads has no domain verification — the Insight Tag is the link. (Verifying a LinkedIn *Page* is a separate, unrelated flow.)',\n },\n tag: {\n needs: 'accountId',\n src: () => 'https://snap.licdn.com/li.lms-analytics/insight.min.js',\n bootstrap: (id) =>\n `window._linkedin_partner_id='${id}';` +\n `window._linkedin_data_partner_ids=window._linkedin_data_partner_ids||[];` +\n `window._linkedin_data_partner_ids.push('${id}');`,\n cookies: ['li_sugr', 'li_gc', 'bcookie', 'lidc', 'UserMatchHistory'],\n privacyUrl: 'https://www.linkedin.com/legal/privacy-policy',\n optOut: 'https://www.linkedin.com/psettings/guest-controls/retargeting-opt-out',\n },\n manualSteps: () => [\n 'add conversions in Campaign Manager — the Insight Tag alone records page visits, not conversions',\n ],\n },\n\n reddit: {\n name: 'Reddit',\n operatorCountry: 'the United States',\n clickIdParams: ['rdt_cid'],\n consoleUrl: 'https://ads.reddit.com/',\n verification: {\n method: 'none',\n reason: 'Reddit Ads has no domain verification — the Reddit Pixel is the link.',\n },\n tag: {\n needs: 'pixelId',\n src: () => 'https://www.redditstatic.com/ads/pixel.js',\n bootstrap: (id) =>\n `${queueStub('rdt', `['init','track']`)}rdt('init','${id}');rdt('track','PageVisit');`,\n cookies: ['_rdt_uuid'],\n privacyUrl: 'https://www.reddit.com/policies/privacy-policy',\n },\n manualSteps: () => [],\n },\n\n tiktok: {\n name: 'TikTok',\n operatorCountry: 'Singapore',\n clickIdParams: ['ttclid'],\n consoleUrl: 'https://ads.tiktok.com/',\n verification: {\n method: 'dns-txt',\n txtPrefix: 'tiktok-developers-site-verification=',\n where: 'Ads Manager → Assets → Events → Web events → Verify domain → DNS',\n },\n tag: {\n needs: 'pixelId',\n src: (id) =>\n `https://analytics.tiktok.com/i18n/pixel/events.js?sdkid=${encodeURIComponent(id)}&lib=ttq`,\n bootstrap: (id) =>\n `window.TiktokAnalyticsObject='ttq';` +\n `${queueStub('ttq', `['page','track','identify','instances','debug','on','off','once','ready','alias','group','enableCookie','disableCookie']`)}` +\n `ttq('init','${id}');ttq('page');`,\n cookies: ['_ttp'],\n privacyUrl: 'https://www.tiktok.com/legal/privacy-policy',\n },\n manualSteps: () => [],\n },\n\n microsoft: {\n name: 'Microsoft Ads',\n operatorCountry: 'the United States',\n clickIdParams: ['msclkid'],\n consoleUrl: 'https://ads.microsoft.com/',\n verification: {\n method: 'none',\n reason: 'Microsoft Ads has no domain verification — the UET tag is the link.',\n },\n tag: {\n needs: 'pixelId',\n // No `src`: UET must be constructed *after* bat.js evaluates, so the\n // platform's own snippet attaches an onload handler to a script it creates.\n // Inert until activation all the same — the body is never parsed before then.\n bootstrap: (id) =>\n `(function(w,d,t,r,u){var f,n,i;w[u]=w[u]||[],f=function(){var o={ti:'${id}',enableAutoSpaTracking:true};` +\n `o.q=w[u],w[u]=new UET(o),w[u].push('pageLoad')},n=d.createElement(t),n.src=r,n.async=1,` +\n `n.onload=n.onreadystatechange=function(){var s=this.readyState;s&&s!=='loaded'&&s!=='complete'||(f(),n.onload=n.onreadystatechange=null)},` +\n `i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)})(window,document,'script','https://bat.bing.com/bat.js','uetq');`,\n cookies: ['_uetsid', '_uetvid', 'MUID'],\n privacyUrl: 'https://privacy.microsoft.com/privacystatement',\n optOut: 'https://account.microsoft.com/privacy/ad-settings',\n },\n manualSteps: () => [],\n },\n\n pinterest: {\n name: 'Pinterest',\n operatorCountry: 'the United States',\n clickIdParams: ['epik'],\n consoleUrl: 'https://ads.pinterest.com/',\n verification: {\n method: 'dns-txt',\n txtPrefix: 'pinterest-site-verification=',\n where: 'Business hub → Claim → Websites → Claim → Add TXT record',\n },\n tag: {\n needs: 'pixelId',\n // Pinterest's stub creates the script itself once `pintrk('load')` runs.\n bootstrap: (id) =>\n `${queueStub('pintrk', `['load','page','track']`)}` +\n `!function(){var t=document.createElement('script');t.async=!0,t.src='https://s.pinimg.com/ct/core.js';` +\n `var e=document.getElementsByTagName('script')[0];e.parentNode.insertBefore(t,e)}();` +\n `pintrk('load','${id}');pintrk('page');`,\n cookies: ['_pinterest_ct_ua', '_pin_unauth', '_epik'],\n privacyUrl: 'https://policy.pinterest.com/privacy-policy',\n },\n manualSteps: () => [\n 'confirm the claim in Business hub → Claim once the TXT record has propagated',\n ],\n },\n\n snapchat: {\n name: 'Snapchat',\n operatorCountry: 'the United States',\n clickIdParams: ['ScCid'],\n consoleUrl: 'https://ads.snapchat.com/',\n verification: {\n method: 'dns-txt',\n txtPrefix: 'snap-domain-verification=',\n where: 'Business Manager → Public Profiles / Domains → Verify domain → DNS',\n },\n tag: {\n needs: 'pixelId',\n src: () => 'https://sc-static.net/scevent.min.js',\n bootstrap: (id) =>\n `${queueStub('snaptr', `['init','track']`)}snaptr('init','${id}');snaptr('track','PAGE_VIEW');`,\n cookies: ['_scid', '_scid_r', 'sc_at'],\n privacyUrl: 'https://snap.com/privacy/privacy-policy',\n },\n manualSteps: () => [],\n },\n};\n\n/** Every provider, in table order — the one place iteration order is decided. */\nexport const AD_PROVIDER_KEYS = Object.keys(AD_PLATFORMS) as AdProvider[];\n\n/** Whether a string names a known provider (for CLI arg validation). */\nexport const isAdProvider = (value: string): value is AdProvider => value in AD_PLATFORMS;\n\n/**\n * The DNS TXT record content for a token.\n *\n * A value already containing `=` is taken as the whole record and left alone. That\n * is the same escape hatch an icon name containing `:` gets: a platform can change\n * its prefix spelling without waiting on a toolchain release, and an operator who\n * pasted the full record from the UI gets what they meant rather than a\n * double-prefixed record that verifies against nothing.\n */\nexport function txtRecord(provider: AdProvider, token: string): string {\n const trimmed = token.trim();\n if (trimmed.includes('=')) return trimmed;\n return `${AD_PLATFORMS[provider].verification.txtPrefix ?? ''}${trimmed}`;\n}\n\n/** The tag id for an entry, or undefined when the field it needs is unset. */\nexport function tagId(entry: AdPropertySetup): string | undefined {\n const field = AD_PLATFORMS[entry.provider].tag.needs;\n return entry[field];\n}\n\n/** The consent category gating an entry's tag. `marketing` unless stated otherwise. */\nexport const categoryOf = (entry: AdPropertySetup): AdConsentCategory =>\n entry.category ?? 'marketing';\n\nconst escapeAttr = (value: string): string =>\n value.replace(/&/g, '&').replace(/\"/g, '"').replace(/</g, '<');\n\n/**\n * One provider's tag, rendered as a **consent-gated, inert** `<script>`.\n *\n * `type=\"text/plain\"` with the URL on `data-src` is what makes the gate real: the\n * browser neither parses the body nor fetches the library, so an undecided\n * visitor's page issues no third-party request. Never give one of these a live\n * `src` — a gate that instead asks a loaded tracker not to track is a promise.\n *\n * Returns null when the entry has no tag id, so a caller can report the gap rather\n * than emit a snippet that initialises `undefined`.\n */\nexport function renderTag(\n entry: AdPropertySetup,\n strategy: 'idle' | 'async' | 'interaction' = 'idle',\n): string | null {\n const platform = AD_PLATFORMS[entry.provider];\n const id = tagId(entry);\n if (!id) return null;\n const attrs = [\n 'type=\"text/plain\"',\n 'data-vitops-tag',\n `data-consent=\"${categoryOf(entry)}\"`,\n `data-strategy=\"${strategy}\"`,\n ...(platform.tag.src ? [`data-src=\"${escapeAttr(platform.tag.src(id))}\"`] : []),\n `data-consent-cookies=\"${escapeAttr(platform.tag.cookies.join(','))}\"`,\n ];\n return `<!-- ${platform.name} — gated on ${categoryOf(entry)} consent -->\\n<script ${attrs.join(' ')}>${platform.tag.bootstrap(id, entry)}</script>`;\n}\n","/**\n * Every decision `vitops ads setup` makes, as one pure function.\n *\n * Pure in the load-bearing sense: no `fetch`, no `fs`, no clock, no stdin. The\n * executor gathers each domain's live DNS state and applies what it is told; this\n * module decides, from that state, what is left to do. The same split\n * `onboarding/plan.ts` makes, for the same two payoffs: `--dry` is a *complete*\n * account of a run, and `--check` is an honest drift report.\n *\n * Idempotency lives here — a step whose desired state already holds resolves to\n * `skip`, so a re-run of a linked property is all skips by construction rather than\n * by the executor checking twice.\n *\n * One thing this planner adds over the onboarding one: a step that cannot proceed\n * for want of a **config value** resolves to `blocked` carrying the field name in\n * `needs`. That is a machine-readable gap, which is what lets the CLI *ask* for the\n * missing token and re-plan, while the planner itself stays pure and knows nothing\n * about a terminal. A blocked step is drift, never a skip — the first run of a\n * property whose token has not been fetched yet must not read as \"already linked\".\n */\nimport { AD_PLATFORMS, tagId, txtRecord } from './providers.ts';\nimport type { AdDomainState, AdPropertySetup, AdsConfig } from './types.ts';\n\n/** A step's decision. `blocked` means the config is missing something. */\nexport type StepAction = 'create' | 'skip' | 'blocked';\n\n/** A config field the operator can supply to unblock a step. */\nexport type MissingField = 'domainVerification' | 'accountId' | 'pixelId';\n\nexport interface StepPlan {\n action: StepAction;\n /** Why it's a skip, or what the mutation will do, or what is missing. */\n detail: string;\n /** Set only on `blocked`: which config field would unblock it. */\n needs?: MissingField | undefined;\n}\n\n/** The steps for one ad property, decided against its live state. */\nexport interface AdPropertyPlan {\n provider: AdPropertySetup['provider'];\n /** The platform name, so a formatter needn't reach back into the table. */\n name: string;\n /** The domain being verified, when the platform verifies one. */\n domain?: string | undefined;\n /** Ensure the apex verification TXT. */\n txt: StepPlan;\n /** Whether the tag can be emitted (`ads tags`). */\n tag: StepPlan;\n /** The exact TXT content to create, when `txt.action` is `create`. */\n txtContent?: string | undefined;\n /** Manual follow-ups — including *why* a platform has no verification step. */\n reminders: string[];\n}\n\nexport interface AdsPlan {\n properties: AdPropertyPlan[];\n /** Operator-facing warnings that span the whole run. */\n notes: string[];\n}\n\n/**\n * Decide one property's steps.\n *\n * The `dns-txt` branch has three outcomes, and keeping them distinct is the point:\n * our record is present (skip), a *different* record from the same platform is\n * present (create, and say so — a token rotation leaves the old record in place,\n * since this command never edits or deletes DNS), or no token is configured yet\n * (blocked, naming the field).\n */\nexport function planProperty(setup: AdPropertySetup, state: AdDomainState): AdPropertyPlan {\n const platform = AD_PLATFORMS[setup.provider];\n const reminders = [...platform.manualSteps(setup)];\n\n let txt: StepPlan;\n let txtContent: string | undefined;\n\n if (platform.verification.method === 'none') {\n txt = { action: 'skip', detail: platform.verification.reason ?? 'no domain verification' };\n } else if (!setup.domainVerification) {\n txt = {\n action: 'blocked',\n detail: `needs the verification token from ${platform.verification.where ?? platform.consoleUrl}`,\n needs: 'domainVerification',\n };\n } else {\n const desired = txtRecord(setup.provider, setup.domainVerification);\n const prefix = platform.verification.txtPrefix;\n const stale =\n prefix != null && state.txtContents.some((c) => c.startsWith(prefix) && c !== desired);\n txt = state.txtContents.includes(desired)\n ? { action: 'skip', detail: 'verification TXT already present' }\n : {\n action: 'create',\n detail: stale\n ? 'create verification TXT (an older token from this platform is still present — remove it yourself; this command never deletes DNS)'\n : 'create verification TXT',\n };\n if (txt.action === 'create') txtContent = desired;\n }\n\n const id = tagId(setup);\n const tag: StepPlan = id\n ? { action: 'skip', detail: `tag ready (${platform.tag.needs} ${id})` }\n : {\n action: 'blocked',\n detail: `needs ${platform.tag.needs} from ${platform.consoleUrl} to emit the tag`,\n needs: platform.tag.needs,\n };\n\n return {\n provider: setup.provider,\n name: platform.name,\n ...(setup.domain ? { domain: setup.domain } : {}),\n txt,\n tag,\n ...(txtContent ? { txtContent } : {}),\n reminders,\n };\n}\n\n/** Plan every configured property. */\nexport function plan(config: AdsConfig, states: Map<string, AdDomainState>): AdsPlan {\n const notes: string[] = [];\n const properties = config.properties.map((setup) => {\n const key = setup.domain ?? '';\n const state = states.get(key);\n if (!state && AD_PLATFORMS[setup.provider].verification.method === 'dns-txt')\n // Should not happen — the CLI observes every verifiable domain before\n // planning — but a missing observation must not read as \"nothing to do\".\n notes.push(`${setup.provider}: no observed DNS state for ${key || '(no domain)'}`);\n return planProperty(setup, state ?? { txtContents: [] });\n });\n return { properties, notes };\n}\n\n/** Any step that isn't a skip — i.e. the live state or the config has drifted. */\nexport function hasDrift(p: AdsPlan): boolean {\n return p.properties.some((x) => x.txt.action !== 'skip' || x.tag.action !== 'skip');\n}\n\n/** Every field a run still needs, per provider — what the CLI asks for. */\nexport function missingFields(p: AdsPlan): { provider: string; needs: MissingField }[] {\n const out: { provider: string; needs: MissingField }[] = [];\n for (const x of p.properties) {\n for (const step of [x.txt, x.tag]) {\n if (step.action === 'blocked' && step.needs)\n out.push({ provider: x.provider, needs: step.needs });\n }\n }\n return out;\n}\n\n/** Render a plan as the block `--dry` and `--check` print. */\nexport function formatPlan(p: AdsPlan): string {\n const glyph = (s: StepPlan) => (s.action === 'skip' ? '·' : s.action === 'blocked' ? '?' : '+');\n const lines: string[] = [];\n for (const x of p.properties) {\n lines.push(`${x.provider}${x.domain ? ` (${x.domain})` : ''}`);\n lines.push(` ${glyph(x.txt)} verify ${x.txt.detail}`);\n lines.push(` ${glyph(x.tag)} tag ${x.tag.detail}`);\n for (const r of x.reminders) lines.push(` ! ${r}`);\n }\n for (const n of p.notes) lines.push(`! ${n}`);\n return lines.join('\\n');\n}\n\n/** The outcome of one property's run, for the summary table. */\nexport interface AdPropertyResult {\n provider: string;\n domain: string;\n txt: 'created' | 'present' | 'blocked' | 'failed' | 'n/a';\n tag: 'ready' | 'blocked';\n reminders: string[];\n}\n\nconst pad = (s: string, n: number) => s.padEnd(n);\n\n/** Render the end-of-run summary: one row per property, plus any reminders. */\nexport function formatSummary(results: AdPropertyResult[]): string {\n const headers = ['PROVIDER', 'DOMAIN', 'VERIFY', 'TAG'];\n const rows = results.map((r) => [r.provider, r.domain, r.txt, r.tag]);\n const widths = headers.map((h, i) =>\n Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)),\n );\n const line = (cells: string[]) =>\n cells\n .map((c, i) => pad(c, widths[i] ?? 0))\n .join(' ')\n .trimEnd();\n\n const out = [line(headers), widths.map((w) => '─'.repeat(w)).join(' ')];\n for (const row of rows) out.push(line(row));\n\n const reminders = results.flatMap((r) => r.reminders.map((m) => ` ! ${r.provider}: ${m}`));\n if (reminders.length) out.push('', 'Reminders (manual — no API):', ...reminders);\n return out.join('\\n');\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//#region src/onboarding/cloudflare.ts
|
|
2
|
+
/**
|
|
3
|
+
* Cloudflare DNS — the one thing this command writes.
|
|
4
|
+
*
|
|
5
|
+
* Scoped to exactly what onboarding needs and nothing more: find a zone by name,
|
|
6
|
+
* read the apex TXT records, and *create* the verification TXT when it is missing.
|
|
7
|
+
* There is deliberately no update and no delete — the command's contract is that it
|
|
8
|
+
* never removes a DNS record, so the destructive verbs are simply absent rather
|
|
9
|
+
* than guarded.
|
|
10
|
+
*
|
|
11
|
+
* The token is a `Zone:DNS:Edit` credential from the environment
|
|
12
|
+
* (`CLOUDFLARE_API_TOKEN`); listing a zone by name also needs `Zone:Read`, which
|
|
13
|
+
* Cloudflare's standard "Edit zone DNS" token template already bundles. As with the
|
|
14
|
+
* indexing executors, `fetchImpl` is injected so the request shape is testable
|
|
15
|
+
* without the network, and every call returns a structured result rather than
|
|
16
|
+
* throwing — the caller decides what a failure means per domain.
|
|
17
|
+
*/
|
|
18
|
+
const API = "https://api.cloudflare.com/client/v4";
|
|
19
|
+
const cfError = (status, body) => {
|
|
20
|
+
const first = body.errors?.[0];
|
|
21
|
+
return first ? `${first.code} ${first.message}` : `HTTP ${status}`;
|
|
22
|
+
};
|
|
23
|
+
/** Find the zone id for a domain (`GET /zones?name=`). */
|
|
24
|
+
async function findZoneId(token, domain, fetchImpl = fetch) {
|
|
25
|
+
const res = await fetchImpl(`${API}/zones?name=${encodeURIComponent(domain)}`, { headers: { authorization: `Bearer ${token}` } });
|
|
26
|
+
const body = await res.json();
|
|
27
|
+
if (!res.ok || !body.success) return {
|
|
28
|
+
ok: false,
|
|
29
|
+
status: res.status,
|
|
30
|
+
message: cfError(res.status, body)
|
|
31
|
+
};
|
|
32
|
+
const zone = body.result?.[0];
|
|
33
|
+
if (!zone) return {
|
|
34
|
+
ok: false,
|
|
35
|
+
status: res.status,
|
|
36
|
+
message: `no Cloudflare zone named "${domain}" (is it in this account, and does the token cover it?)`
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
status: res.status,
|
|
41
|
+
zoneId: zone.id
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** List the apex TXT record contents for a zone. */
|
|
45
|
+
async function listApexTxt(token, zoneId, domain, fetchImpl = fetch) {
|
|
46
|
+
const res = await fetchImpl(`${API}/zones/${zoneId}/dns_records?type=TXT&name=${encodeURIComponent(domain)}`, { headers: { authorization: `Bearer ${token}` } });
|
|
47
|
+
const body = await res.json();
|
|
48
|
+
if (!res.ok || !body.success) return {
|
|
49
|
+
ok: false,
|
|
50
|
+
status: res.status,
|
|
51
|
+
contents: [],
|
|
52
|
+
message: cfError(res.status, body)
|
|
53
|
+
};
|
|
54
|
+
const contents = (body.result ?? []).map((r) => r.content.replace(/^"|"$/g, ""));
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
status: res.status,
|
|
58
|
+
contents
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Create an apex TXT record. **Create only** — never called when one already
|
|
63
|
+
* exists (the planner decides that), and there is no sibling that edits or removes.
|
|
64
|
+
*/
|
|
65
|
+
async function createApexTxt(token, zoneId, domain, content, fetchImpl = fetch) {
|
|
66
|
+
const res = await fetchImpl(`${API}/zones/${zoneId}/dns_records`, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: {
|
|
69
|
+
authorization: `Bearer ${token}`,
|
|
70
|
+
"content-type": "application/json"
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
type: "TXT",
|
|
74
|
+
name: domain,
|
|
75
|
+
content
|
|
76
|
+
})
|
|
77
|
+
});
|
|
78
|
+
const body = await res.json();
|
|
79
|
+
if (!res.ok || !body.success) return {
|
|
80
|
+
ok: false,
|
|
81
|
+
status: res.status,
|
|
82
|
+
message: cfError(res.status, body)
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
status: res.status
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
export { findZoneId as n, listApexTxt as r, createApexTxt as t };
|
|
92
|
+
//# sourceMappingURL=cloudflare-BW-Iyi-B.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudflare-BW-Iyi-B.mjs","names":[],"sources":["../src/onboarding/cloudflare.ts"],"sourcesContent":["/**\n * Cloudflare DNS — the one thing this command writes.\n *\n * Scoped to exactly what onboarding needs and nothing more: find a zone by name,\n * read the apex TXT records, and *create* the verification TXT when it is missing.\n * There is deliberately no update and no delete — the command's contract is that it\n * never removes a DNS record, so the destructive verbs are simply absent rather\n * than guarded.\n *\n * The token is a `Zone:DNS:Edit` credential from the environment\n * (`CLOUDFLARE_API_TOKEN`); listing a zone by name also needs `Zone:Read`, which\n * Cloudflare's standard \"Edit zone DNS\" token template already bundles. As with the\n * indexing executors, `fetchImpl` is injected so the request shape is testable\n * without the network, and every call returns a structured result rather than\n * throwing — the caller decides what a failure means per domain.\n */\nconst API = 'https://api.cloudflare.com/client/v4';\n\ninterface CfEnvelope<T> {\n success: boolean;\n errors?: { code: number; message: string }[];\n result?: T;\n}\n\nconst cfError = (status: number, body: CfEnvelope<unknown>): string => {\n const first = body.errors?.[0];\n return first ? `${first.code} ${first.message}` : `HTTP ${status}`;\n};\n\n/** Find the zone id for a domain (`GET /zones?name=`). */\nexport async function findZoneId(\n token: string,\n domain: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; zoneId?: string; message?: string }> {\n const res = await fetchImpl(`${API}/zones?name=${encodeURIComponent(domain)}`, {\n headers: { authorization: `Bearer ${token}` },\n });\n const body = (await res.json()) as CfEnvelope<{ id: string; name: string }[]>;\n if (!res.ok || !body.success)\n return { ok: false, status: res.status, message: cfError(res.status, body) };\n const zone = body.result?.[0];\n if (!zone)\n return {\n ok: false,\n status: res.status,\n message: `no Cloudflare zone named \"${domain}\" (is it in this account, and does the token cover it?)`,\n };\n return { ok: true, status: res.status, zoneId: zone.id };\n}\n\n/** List the apex TXT record contents for a zone. */\nexport async function listApexTxt(\n token: string,\n zoneId: string,\n domain: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; contents: string[]; message?: string }> {\n const res = await fetchImpl(\n `${API}/zones/${zoneId}/dns_records?type=TXT&name=${encodeURIComponent(domain)}`,\n { headers: { authorization: `Bearer ${token}` } },\n );\n const body = (await res.json()) as CfEnvelope<{ content: string }[]>;\n if (!res.ok || !body.success)\n return { ok: false, status: res.status, contents: [], message: cfError(res.status, body) };\n // Cloudflare returns TXT content quoted; normalise so an exact compare works.\n const contents = (body.result ?? []).map((r) => r.content.replace(/^\"|\"$/g, ''));\n return { ok: true, status: res.status, contents };\n}\n\n/**\n * Create an apex TXT record. **Create only** — never called when one already\n * exists (the planner decides that), and there is no sibling that edits or removes.\n */\nexport async function createApexTxt(\n token: string,\n zoneId: string,\n domain: string,\n content: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; message?: string }> {\n const res = await fetchImpl(`${API}/zones/${zoneId}/dns_records`, {\n method: 'POST',\n headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },\n body: JSON.stringify({ type: 'TXT', name: domain, content }),\n });\n const body = (await res.json()) as CfEnvelope<{ id: string }>;\n if (!res.ok || !body.success)\n return { ok: false, status: res.status, message: cfError(res.status, body) };\n return { ok: true, status: res.status };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,MAAM,MAAM;AAQZ,MAAM,WAAW,QAAgB,SAAsC;CACrE,MAAM,QAAQ,KAAK,SAAS;CAC5B,OAAO,QAAQ,GAAG,MAAM,KAAK,GAAG,MAAM,YAAY,QAAQ;AAC5D;;AAGA,eAAsB,WACpB,OACA,QACA,YAA0B,OACmD;CAC7E,MAAM,MAAM,MAAM,UAAU,GAAG,IAAI,cAAc,mBAAmB,MAAM,KAAK,EAC7E,SAAS,EAAE,eAAe,UAAU,QAAQ,EAC9C,CAAC;CACD,MAAM,OAAQ,MAAM,IAAI,KAAK;CAC7B,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,SACnB,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ,SAAS,QAAQ,IAAI,QAAQ,IAAI;CAAE;CAC7E,MAAM,OAAO,KAAK,SAAS;CAC3B,IAAI,CAAC,MACH,OAAO;EACL,IAAI;EACJ,QAAQ,IAAI;EACZ,SAAS,6BAA6B,OAAO;CAC/C;CACF,OAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;EAAQ,QAAQ,KAAK;CAAG;AACzD;;AAGA,eAAsB,YACpB,OACA,QACA,QACA,YAA0B,OACsD;CAChF,MAAM,MAAM,MAAM,UAChB,GAAG,IAAI,SAAS,OAAO,6BAA6B,mBAAmB,MAAM,KAC7E,EAAE,SAAS,EAAE,eAAe,UAAU,QAAQ,EAAE,CAClD;CACA,MAAM,OAAQ,MAAM,IAAI,KAAK;CAC7B,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,SACnB,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ,UAAU,CAAC;EAAG,SAAS,QAAQ,IAAI,QAAQ,IAAI;CAAE;CAE3F,MAAM,YAAY,KAAK,UAAU,CAAC,EAAC,CAAE,KAAK,MAAM,EAAE,QAAQ,QAAQ,UAAU,EAAE,CAAC;CAC/E,OAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;EAAQ;CAAS;AAClD;;;;;AAMA,eAAsB,cACpB,OACA,QACA,QACA,SACA,YAA0B,OACkC;CAC5D,MAAM,MAAM,MAAM,UAAU,GAAG,IAAI,SAAS,OAAO,eAAe;EAChE,QAAQ;EACR,SAAS;GAAE,eAAe,UAAU;GAAS,gBAAgB;EAAmB;EAChF,MAAM,KAAK,UAAU;GAAE,MAAM;GAAO,MAAM;GAAQ;EAAQ,CAAC;CAC7D,CAAC;CACD,MAAM,OAAQ,MAAM,IAAI,KAAK;CAC7B,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,SACnB,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ,SAAS,QAAQ,IAAI,QAAQ,IAAI;CAAE;CAC7E,OAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;CAAO;AACxC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/onboarding/cloudflare.d.ts
|
|
2
|
+
/** Find the zone id for a domain (`GET /zones?name=`). */
|
|
3
|
+
declare function findZoneId(token: string, domain: string, fetchImpl?: typeof fetch): Promise<{
|
|
4
|
+
ok: boolean;
|
|
5
|
+
status: number;
|
|
6
|
+
zoneId?: string;
|
|
7
|
+
message?: string;
|
|
8
|
+
}>;
|
|
9
|
+
/** List the apex TXT record contents for a zone. */
|
|
10
|
+
declare function listApexTxt(token: string, zoneId: string, domain: string, fetchImpl?: typeof fetch): Promise<{
|
|
11
|
+
ok: boolean;
|
|
12
|
+
status: number;
|
|
13
|
+
contents: string[];
|
|
14
|
+
message?: string;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Create an apex TXT record. **Create only** — never called when one already
|
|
18
|
+
* exists (the planner decides that), and there is no sibling that edits or removes.
|
|
19
|
+
*/
|
|
20
|
+
declare function createApexTxt(token: string, zoneId: string, domain: string, content: string, fetchImpl?: typeof fetch): Promise<{
|
|
21
|
+
ok: boolean;
|
|
22
|
+
status: number;
|
|
23
|
+
message?: string;
|
|
24
|
+
}>;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { findZoneId as n, listApexTxt as r, createApexTxt as t };
|
|
27
|
+
//# sourceMappingURL=cloudflare-BiK61Oq4.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudflare-BiK61Oq4.d.mts","names":[],"sources":["../src/onboarding/cloudflare.ts"],"mappings":";;iBA8BsB,UAAA,CACpB,KAAA,UACA,MAAA,UACA,SAAA,UAAkB,KAAA,GACjB,OAAO;EAAG,EAAA;EAAa,MAAA;EAAgB,MAAA;EAAiB,OAAA;AAAA;;iBAkBrC,WAAA,CACpB,KAAA,UACA,MAAA,UACA,MAAA,UACA,SAAA,UAAkB,KAAA,GACjB,OAAO;EAAG,EAAA;EAAa,MAAA;EAAgB,QAAA;EAAoB,OAAA;AAAA;;AAvBI;AAkBlE;;iBAsBsB,aAAA,CACpB,KAAA,UACA,MAAA,UACA,MAAA,UACA,OAAA,UACA,SAAA,UAAkB,KAAA,GACjB,OAAO;EAAG,EAAA;EAAa,MAAA;EAAgB,OAAA;AAAA"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { a as googleAccessToken, i as ServiceAccount, n as GoogleOAuth, r as SCOPES, t as GoogleCredential } from "../token--18wN9UR.mjs";
|
|
2
|
+
|
|
1
3
|
//#region src/indexing/types.d.ts
|
|
2
4
|
/**
|
|
3
5
|
* The indexing module's own option surface.
|
|
@@ -222,11 +224,6 @@ declare function verifyKeyFile(keyLocation: string, key: string, fetchImpl?: typ
|
|
|
222
224
|
declare function submitBatch(plan: IndexNowPlan, urls: string[], fetchImpl?: typeof fetch): Promise<SubmitResult>;
|
|
223
225
|
//#endregion
|
|
224
226
|
//#region src/indexing/gsc.d.ts
|
|
225
|
-
/** The fields this module needs from a service-account JSON key. */
|
|
226
|
-
interface ServiceAccount {
|
|
227
|
-
client_email: string;
|
|
228
|
-
private_key: string;
|
|
229
|
-
}
|
|
230
227
|
/**
|
|
231
228
|
* Parse a service-account credential from its two supported sources.
|
|
232
229
|
*
|
|
@@ -241,7 +238,7 @@ declare function parseServiceAccount(raw: string): ServiceAccount;
|
|
|
241
238
|
*
|
|
242
239
|
* `now` is a parameter so this is testable without mocking the clock.
|
|
243
240
|
*/
|
|
244
|
-
declare function
|
|
241
|
+
declare function serviceAccountToken(account: ServiceAccount, fetchImpl?: typeof fetch, now?: number): Promise<string>;
|
|
245
242
|
/**
|
|
246
243
|
* Re-submit a sitemap — the automated form of the manual resubmit in the UI.
|
|
247
244
|
*
|
|
@@ -273,5 +270,5 @@ interface InspectionResult {
|
|
|
273
270
|
*/
|
|
274
271
|
declare function inspectUrl(token: string, siteUrl: string, inspectionUrl: string, fetchImpl?: typeof fetch): Promise<InspectionResult>;
|
|
275
272
|
//#endregion
|
|
276
|
-
export { type ChannelPlan, INDEXNOW_BATCH, INDEXNOW_ENDPOINT, type IndexNowConfig, type IndexNowPlan, type IndexingConfig, type IndexingPlan, type InspectionResult, type ParsedSitemap, type PlanInput, SNAPSHOT_PATH, type SearchConsoleConfig, type SearchConsolePlan, type ServiceAccount, type SitemapEntry, type SitemapReader, type Snapshot, type SubmitResult, type UrlReason, collectEntries, formatPlan,
|
|
273
|
+
export { type ChannelPlan, type GoogleCredential, type GoogleOAuth, INDEXNOW_BATCH, INDEXNOW_ENDPOINT, type IndexNowConfig, type IndexNowPlan, type IndexingConfig, type IndexingPlan, type InspectionResult, type ParsedSitemap, type PlanInput, SCOPES, SNAPSHOT_PATH, type SearchConsoleConfig, type SearchConsolePlan, type ServiceAccount, type SitemapEntry, type SitemapReader, type Snapshot, type SubmitResult, type UrlReason, collectEntries, formatPlan, googleAccessToken, inspectUrl, keyFileContents, newKey, parseServiceAccount, parseSitemap, plan, readSnapshot, resolveKeyLocation, resolveSitemapUrl, serviceAccountToken, submitBatch, submitSitemap, toSnapshot, verifyKeyFile, writeSnapshot };
|
|
277
274
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/indexing/types.ts","../../src/indexing/plan.ts","../../src/indexing/sitemap.ts","../../src/indexing/snapshot.ts","../../src/indexing/indexnow.ts","../../src/indexing/gsc.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/indexing/types.ts","../../src/indexing/plan.ts","../../src/indexing/sitemap.ts","../../src/indexing/snapshot.ts","../../src/indexing/indexnow.ts","../../src/indexing/gsc.ts"],"mappings":"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"}
|