@getvitops/utils 4.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 CHANGED
@@ -1,5 +1,74 @@
1
1
  # @getvitops/utils
2
2
 
3
+ ## 4.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2c890c0: Link a site to its ad properties: `site.ads`, `vitops ads`, and `<Ads />`.
8
+
9
+ `vitops search` covers the whole Search Console relationship. Nothing covered the equivalent one
10
+ with ad platforms, and a site's ad accounts had nowhere to live — so a Meta pixel pasted into a
11
+ template was invisible to the rest of the toolchain. It set `_fbp` on a site whose generated cookie
12
+ notice never mentioned it, whose consent gate never cleared it on revoke, and — for LinkedIn and
13
+ Pinterest — whose attribution never captured the click ID at all, so every conversion from those
14
+ platforms arrived indistinguishable from organic traffic.
15
+
16
+ **New `site.ads` block**, keyed by platform (`google`, `meta`, `linkedin`, `reddit`, `tiktok`,
17
+ `microsoft`, `pinterest`, `snapchat`):
18
+
19
+ ```jsonc
20
+ {
21
+ "site": {
22
+ "ads": {
23
+ "meta": { "pixelId": "123456789", "domainVerification": "abc123" },
24
+ "google": {
25
+ "accountId": "123-456-7890",
26
+ "pixelId": "AW-987654321",
27
+ "conversionLabel": "xyz",
28
+ },
29
+ },
30
+ },
31
+ }
32
+ ```
33
+
34
+ **`vitops ads setup`** ensures each platform's domain-verification DNS record. Only four platforms
35
+ verify a domain at all — Meta, TikTok, Pinterest and Snapchat, by apex DNS TXT — and that record is
36
+ the one thing created for you (in Cloudflare, via `CLOUDFLARE_API_TOKEN`; created only, never edited
37
+ or deleted). Google Ads, LinkedIn, Reddit and Microsoft Ads have no domain verification: linking
38
+ there is the tag and the account id, and the run says so rather than skipping in silence. No
39
+ platform Marketing API is called — Meta's needs a system-user token and Google's an approved
40
+ developer token with your own account on the line — so the final "Verify" click is surfaced as a
41
+ reminder. `--dry` prints the plan, `--check` reports drift and exits non-zero.
42
+
43
+ **It asks for what the config is missing.** A verification token does not exist until someone
44
+ fetches it from the platform UI, so a first run prompts for it, naming the exact UI path, folds the
45
+ answer into the plan and writes it to your config. The token is not a secret — it is published in
46
+ DNS, and the platform fetching it back is the ownership proof, exactly like the IndexNow key.
47
+ Prompting requires a TTY: with `--dry`, `--check`, `--no-prompt`, or in CI, you get a named error
48
+ instead and the run never hangs. `--no-write` keeps the answer out of the config.
49
+
50
+ **`vitops ads tags`** prints each pixel as an inert, consent-gated `<script>` — `type="text/plain"`
51
+ with the library URL on `data-src`, so an undecided visitor's page issues no third-party request.
52
+ For Bricks, WordPress, Eleventy: any stack without the Astro integration.
53
+
54
+ **`vitops ads lint`** reports the gaps that are invisible at runtime: a click ID the platform stamps
55
+ that attribution does not capture, a pixel while `site.tracking` is off, a property with no tag id.
56
+
57
+ **`<Ads />` in `@getvitops/astro`**, a sibling of `<Analytics />` rather than part of it — ad
58
+ properties come from the site config, state their own consent category (`marketing` by default,
59
+ rather than being derived), and switch per environment on their own
60
+ (`environments.<env>.ads`, defaulting to `analytics`, then true, so a preview deployment can send
61
+ pageviews without firing conversion pixels). Both components now render through one `<GatedTags />`,
62
+ so the inert markup has a single implementation.
63
+
64
+ **Your cookie notice now discloses every configured pixel** — name, cookies and opt-out — from the
65
+ same table that writes `data-consent-cookies`, so the notice and the revoke cannot disagree. If you
66
+ add `site.ads` to a config whose privacy policy or cookie notice you have already published,
67
+ re-generate and re-read them.
68
+
69
+ `li_fat_id` (LinkedIn) and `epik` (Pinterest) join the click-ID capture vocabulary; a site running
70
+ either platform starts attributing conversions it previously recorded as organic.
71
+
3
72
  ## 4.0.0
4
73
 
5
74
  ### Major Changes
@@ -0,0 +1,228 @@
1
+ import { n as findZoneId, r as listApexTxt, t as createApexTxt } from "../cloudflare-BiK61Oq4.mjs";
2
+
3
+ //#region src/ads/types.d.ts
4
+ /**
5
+ * The ads module's own option surface.
6
+ *
7
+ * Deliberately structural rather than `Config['site']['ads']` imported from
8
+ * `@getvitops/generator`: the generator already depends on this package, so
9
+ * importing back would be a cycle. The vocabulary mirrors that block field for
10
+ * field, so the CLI's adapter is a flat map — the same arrangement, and for the
11
+ * same reason, as `IndexingConfig` and `DomainSetup` beside it.
12
+ *
13
+ * Optional fields are written `?: T | undefined` throughout: `exactOptionalPropertyTypes`
14
+ * is on, and these describe a *parsed JSON config* where an absent key and an explicit
15
+ * `undefined` are the same thing.
16
+ */
17
+ /**
18
+ * The ad platforms this toolchain knows how to link a site to.
19
+ *
20
+ * A closed set, mirroring `AD_PROVIDERS` in the generator's `config.ts` — the
21
+ * capability table in `providers.ts` has one entry per member, and a test in the
22
+ * generator asserts the two lists agree. An open string would mean a typo'd
23
+ * provider silently getting no verification step and no tag.
24
+ */
25
+ type AdProvider = 'google' | 'meta' | 'linkedin' | 'reddit' | 'tiktok' | 'microsoft' | 'pinterest' | 'snapchat';
26
+ /** Which consent category gates a provider's tag. */
27
+ type AdConsentCategory = 'marketing' | 'analytics';
28
+ /** One ad property to link, as the config declares it. */
29
+ interface AdPropertySetup {
30
+ provider: AdProvider;
31
+ /**
32
+ * The account as the platform shows it — Google Ads customer id, Meta ad
33
+ * account, LinkedIn partner id, Reddit advertiser id.
34
+ */
35
+ accountId?: string | undefined;
36
+ /** The tag/pixel id, when the platform issues one distinct from the account. */
37
+ pixelId?: string | undefined;
38
+ /** Google Ads only: the conversion label paired with the conversion id. */
39
+ conversionLabel?: string | undefined;
40
+ /** The host to verify, bare (`acme.ca`). The CLI defaults it from the canonical domain. */
41
+ domain?: string | undefined;
42
+ /**
43
+ * The verification token from the platform UI.
44
+ *
45
+ * A bare token gets the provider's TXT prefix applied; a value already
46
+ * containing `=` is used as the whole record content, which is the escape hatch
47
+ * for a platform changing its prefix without waiting on a toolchain release.
48
+ */
49
+ domainVerification?: string | undefined;
50
+ /** Category the tag waits on. Defaults to `marketing`. */
51
+ category?: AdConsentCategory | undefined;
52
+ enabled?: boolean | undefined;
53
+ }
54
+ /** Everything `plan()` needs, once the CLI has read the config. */
55
+ interface AdsConfig {
56
+ /** The properties to link. Already filtered to `--provider` if that was passed. */
57
+ properties: AdPropertySetup[];
58
+ }
59
+ /**
60
+ * One property's observed live state, gathered by the executor before planning.
61
+ *
62
+ * Same shape of arrangement as `DomainState`: the decision is pure, the
63
+ * observation is not. `txtContents` is every apex TXT record on the domain, so
64
+ * the planner can decide both "our record is present" and "a *different* record
65
+ * from this provider is present" — the second is a token rotation, which needs
66
+ * saying rather than silently adding a second record.
67
+ */
68
+ interface AdDomainState {
69
+ /** The Cloudflare zone id for the domain, if one was found. */
70
+ zoneId?: string | undefined;
71
+ /** Every apex TXT record content, unquoted. */
72
+ txtContents: string[];
73
+ }
74
+ //#endregion
75
+ //#region src/ads/plan.d.ts
76
+ /** A step's decision. `blocked` means the config is missing something. */
77
+ type StepAction = 'create' | 'skip' | 'blocked';
78
+ /** A config field the operator can supply to unblock a step. */
79
+ type MissingField = 'domainVerification' | 'accountId' | 'pixelId';
80
+ interface StepPlan {
81
+ action: StepAction;
82
+ /** Why it's a skip, or what the mutation will do, or what is missing. */
83
+ detail: string;
84
+ /** Set only on `blocked`: which config field would unblock it. */
85
+ needs?: MissingField | undefined;
86
+ }
87
+ /** The steps for one ad property, decided against its live state. */
88
+ interface AdPropertyPlan {
89
+ provider: AdPropertySetup['provider'];
90
+ /** The platform name, so a formatter needn't reach back into the table. */
91
+ name: string;
92
+ /** The domain being verified, when the platform verifies one. */
93
+ domain?: string | undefined;
94
+ /** Ensure the apex verification TXT. */
95
+ txt: StepPlan;
96
+ /** Whether the tag can be emitted (`ads tags`). */
97
+ tag: StepPlan;
98
+ /** The exact TXT content to create, when `txt.action` is `create`. */
99
+ txtContent?: string | undefined;
100
+ /** Manual follow-ups — including *why* a platform has no verification step. */
101
+ reminders: string[];
102
+ }
103
+ interface AdsPlan {
104
+ properties: AdPropertyPlan[];
105
+ /** Operator-facing warnings that span the whole run. */
106
+ notes: string[];
107
+ }
108
+ /**
109
+ * Decide one property's steps.
110
+ *
111
+ * The `dns-txt` branch has three outcomes, and keeping them distinct is the point:
112
+ * our record is present (skip), a *different* record from the same platform is
113
+ * present (create, and say so — a token rotation leaves the old record in place,
114
+ * since this command never edits or deletes DNS), or no token is configured yet
115
+ * (blocked, naming the field).
116
+ */
117
+ declare function planProperty(setup: AdPropertySetup, state: AdDomainState): AdPropertyPlan;
118
+ /** Plan every configured property. */
119
+ declare function plan(config: AdsConfig, states: Map<string, AdDomainState>): AdsPlan;
120
+ /** Any step that isn't a skip — i.e. the live state or the config has drifted. */
121
+ declare function hasDrift(p: AdsPlan): boolean;
122
+ /** Every field a run still needs, per provider — what the CLI asks for. */
123
+ declare function missingFields(p: AdsPlan): {
124
+ provider: string;
125
+ needs: MissingField;
126
+ }[];
127
+ /** Render a plan as the block `--dry` and `--check` print. */
128
+ declare function formatPlan(p: AdsPlan): string;
129
+ /** The outcome of one property's run, for the summary table. */
130
+ interface AdPropertyResult {
131
+ provider: string;
132
+ domain: string;
133
+ txt: 'created' | 'present' | 'blocked' | 'failed' | 'n/a';
134
+ tag: 'ready' | 'blocked';
135
+ reminders: string[];
136
+ }
137
+ /** Render the end-of-run summary: one row per property, plus any reminders. */
138
+ declare function formatSummary(results: AdPropertyResult[]): string;
139
+ //#endregion
140
+ //#region src/ads/providers.d.ts
141
+ /** How a platform proves you own the domain. */
142
+ interface AdVerificationSpec {
143
+ method: 'dns-txt' | 'none';
144
+ /**
145
+ * Prefix prepended to a bare token to form the record content. Owned here so a
146
+ * config holds the token the platform shows, not a hand-assembled record — but
147
+ * see `txtRecord`: a value already containing `=` passes through whole, which is
148
+ * the escape hatch when a platform changes its spelling.
149
+ */
150
+ txtPrefix?: string;
151
+ /** Where in the platform UI the token is found. Printed when asking for it. */
152
+ where?: string;
153
+ /** Why there is nothing to automate. Required when `method` is `none`. */
154
+ reason?: string;
155
+ }
156
+ /** The tag, and which config field carries its id. */
157
+ interface AdTagSpec {
158
+ /** Which field the tag id comes from — and therefore what to ask for when absent. */
159
+ needs: 'pixelId' | 'accountId';
160
+ /** The remote library. Absent when the platform's own bootstrap loads itself. */
161
+ src?: (id: string) => string;
162
+ /** The inline bootstrap, which the gate runs before attaching `src`. */
163
+ bootstrap: (id: string, entry: AdPropertySetup) => string;
164
+ /**
165
+ * Cookies the tag sets, for `data-consent-cookies` (what a revoke clears) and
166
+ * for the generated cookie notice. A `*` suffix is a prefix match.
167
+ */
168
+ cookies: string[];
169
+ /** The provider's own privacy policy, for the cookie notice. */
170
+ privacyUrl: string;
171
+ /** A visitor-facing opt-out, where the platform offers one. */
172
+ optOut?: string;
173
+ }
174
+ interface AdPlatform {
175
+ /** The platform as a reader would recognise it. */
176
+ name: string;
177
+ /**
178
+ * The jurisdiction that can compel this platform to produce what it receives —
179
+ * where the contracting entity is established. Feeds the generated privacy
180
+ * policy's cross-border transfer disclosure, which is a claim about foreign
181
+ * *access*, not foreign storage (see `Processor.operatorCountry` in the
182
+ * generator's `legal/providers.ts`). A consumer whose contracting entity differs
183
+ * declares their own processor entry instead.
184
+ */
185
+ operatorCountry: string;
186
+ /** Click-ID parameters this platform stamps on an ad landing URL. */
187
+ clickIdParams: string[];
188
+ /** Where the operator manages the property. */
189
+ consoleUrl: string;
190
+ verification: AdVerificationSpec;
191
+ tag: AdTagSpec;
192
+ /** Follow-ups no API covers, given this entry. */
193
+ manualSteps: (entry: AdPropertySetup) => string[];
194
+ }
195
+ declare const AD_PLATFORMS: Record<AdProvider, AdPlatform>;
196
+ /** Every provider, in table order — the one place iteration order is decided. */
197
+ declare const AD_PROVIDER_KEYS: AdProvider[];
198
+ /** Whether a string names a known provider (for CLI arg validation). */
199
+ declare const isAdProvider: (value: string) => value is AdProvider;
200
+ /**
201
+ * The DNS TXT record content for a token.
202
+ *
203
+ * A value already containing `=` is taken as the whole record and left alone. That
204
+ * is the same escape hatch an icon name containing `:` gets: a platform can change
205
+ * its prefix spelling without waiting on a toolchain release, and an operator who
206
+ * pasted the full record from the UI gets what they meant rather than a
207
+ * double-prefixed record that verifies against nothing.
208
+ */
209
+ declare function txtRecord(provider: AdProvider, token: string): string;
210
+ /** The tag id for an entry, or undefined when the field it needs is unset. */
211
+ declare function tagId(entry: AdPropertySetup): string | undefined;
212
+ /** The consent category gating an entry's tag. `marketing` unless stated otherwise. */
213
+ declare const categoryOf: (entry: AdPropertySetup) => AdConsentCategory;
214
+ /**
215
+ * One provider's tag, rendered as a **consent-gated, inert** `<script>`.
216
+ *
217
+ * `type="text/plain"` with the URL on `data-src` is what makes the gate real: the
218
+ * browser neither parses the body nor fetches the library, so an undecided
219
+ * visitor's page issues no third-party request. Never give one of these a live
220
+ * `src` — a gate that instead asks a loaded tracker not to track is a promise.
221
+ *
222
+ * Returns null when the entry has no tag id, so a caller can report the gap rather
223
+ * than emit a snippet that initialises `undefined`.
224
+ */
225
+ declare function renderTag(entry: AdPropertySetup, strategy?: 'idle' | 'async' | 'interaction'): string | null;
226
+ //#endregion
227
+ export { AD_PLATFORMS, AD_PROVIDER_KEYS, type AdConsentCategory, type AdDomainState, type AdPlatform, type AdPropertyPlan, type AdPropertyResult, type AdPropertySetup, type AdProvider, type AdTagSpec, type AdVerificationSpec, type AdsConfig, type AdsPlan, type MissingField, type StepAction, type StepPlan, categoryOf, createApexTxt, findZoneId, formatPlan, formatSummary, hasDrift, isAdProvider, listApexTxt, missingFields, plan, planProperty, renderTag, tagId, txtRecord };
228
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/ads/types.ts","../../src/ads/plan.ts","../../src/ads/providers.ts"],"mappings":";;;;;;AAsBA;;;;AAAsB;AAWtB;;;;AAA6B;AAG7B;;;;;;;;AAAA,KAdY,UAAA;;KAWA,iBAAA;;UAGK,eAAA;EACf,QAAA,EAAU,UAAA;EAsBH;AAAA;AAIT;;EArBE,SAAA;EAuBA;EArBA,OAAA;EAiCe;EA/Bf,eAAA;;EAEA,MAAA;EAiCW;;;;ACzDb;;;EDgCE,kBAAA;EChCoB;EDkCpB,QAAA,GAAW,iBAAiB;EAC5B,OAAA;AAAA;;UAIe,SAAA;EClCA;EDoCf,UAAA,EAAY,eAAe;AAAA;;;;;;;;AC/BP;AAItB;UDuCiB,aAAA;;EAEf,MAAA;EClCK;EDoCL,WAAW;AAAA;;;;KCzDD,UAAA;ADFZ;AAAA,KCKY,YAAA;AAAA,UAEK,QAAA;EACf,MAAA,EAAQ,UAAA;EDRY;ECUpB,MAAA;EDC2B;ECC3B,KAAA,GAAQ,YAAY;AAAA;ADDO;AAAA,UCKZ,cAAA;EACf,QAAA,EAAU,eAAA;;EAEV,IAAA;EDJA;ECMA,MAAA;EDDA;ECGA,GAAA,EAAK,QAAA;EDCL;ECCA,GAAA,EAAK,QAAA;EDSL;ECPA,UAAA;EDSW;ECPX,SAAA;AAAA;AAAA,UAGe,OAAA;EACf,UAAA,EAAY,cAAc;EDQF;ECNxB,KAAA;AAAA;ADQ2B;AAY7B;;;;AAIa;;;;AAhBgB,iBCIb,YAAA,CAAa,KAAA,EAAO,eAAA,EAAiB,KAAA,EAAO,aAAA,GAAgB,cAAA;;iBAoD5D,IAAA,CAAK,MAAA,EAAQ,SAAA,EAAW,MAAA,EAAQ,GAAA,SAAY,aAAA,IAAiB,OAAA;;iBAe7D,QAAA,CAAS,CAAU,EAAP,OAAO;AA7GnC;AAAA,iBAkHgB,aAAA,CAAc,CAAA,EAAG,OAAA;EAAY,QAAA;EAAkB,KAAA,EAAO,YAAY;AAAA;AAhHlF;AAAA,iBA4HgB,UAAA,CAAW,CAAU,EAAP,OAAO;;UAcpB,gBAAA;EACf,QAAA;EACA,MAAA;EACA,GAAA;EACA,GAAA;EACA,SAAA;AAAA;AA1IoB;AAAA,iBAgJN,aAAA,CAAc,OAA2B,EAAlB,gBAAgB;;;ADjH1B;AAAA,UE9BZ,kBAAA;EACf,MAAA;;;AF6CW;;;;EEtCX,SAAA;EDnBoB;ECqBpB,KAAA;EDrBoB;ECuBpB,MAAA;AAAA;;UAIe,SAAA;EDxBO;EC0BtB,KAAA;EDxBe;EC0Bf,GAAA,IAAO,EAAA;;EAEP,SAAA,GAAY,EAAA,UAAY,KAAA,EAAO,eAAe;ED3B9C;;;;ECgCA,OAAA;ED5BoB;EC8BpB,UAAA;ED1Be;EC4Bf,MAAA;AAAA;AAAA,UAGe,UAAA;EDxBV;EC0BL,IAAA;EDxBa;;;;;;;;ECiCb,eAAA;EDjCK;ECmCL,aAAA;ED/BA;ECiCA,UAAA;EACA,YAAA,EAAc,kBAAA;EACd,GAAA,EAAK,SAAA;EDhCiB;ECkCtB,WAAA,GAAc,KAAA,EAAO,eAAA;AAAA;AAAA,cAaV,YAAA,EAAc,MAAA,CAAO,UAAA,EAAY,UAAA;;cAuMjC,gBAAA,EAAgD,UAAU;;cAG1D,YAAA,GAAgB,KAAA,aAAgB,KAAA,IAAS,UAAmC;AD1OzF;;;;;;;;;AAAA,iBCqPgB,SAAA,CAAU,QAAA,EAAU,UAAU,EAAE,KAAA;;iBAOhC,KAAA,CAAM,KAAsB,EAAf,eAAe;;cAM/B,UAAA,GAAc,KAAA,EAAO,eAAA,KAAkB,iBACrB;ADnQ2D;AAoD1F;;;;;;;;;;AApD0F,iBCmR1E,SAAA,CACd,KAAA,EAAO,eAAe,EACtB,QAAA"}