@getvitops/utils 8.2.0-alpha.0 → 8.3.0-alpha.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 +399 -23
- package/dist/{adc-D6aw2qgq.mjs → adc-BiW9BzqA.mjs} +2 -3
- package/dist/{adc-D6aw2qgq.mjs.map → adc-BiW9BzqA.mjs.map} +1 -1
- package/dist/{adc-BoEoogyo.d.mts → adc-DvX3-P7a.d.mts} +2 -4
- package/dist/adc-DvX3-P7a.d.mts.map +1 -0
- package/dist/ads/index.d.mts +37 -57
- package/dist/ads/index.d.mts.map +1 -1
- package/dist/ads/index.mjs +21 -10
- package/dist/ads/index.mjs.map +1 -1
- package/dist/audit/index.d.mts +180 -0
- package/dist/audit/index.d.mts.map +1 -0
- package/dist/audit/index.mjs +509 -0
- package/dist/audit/index.mjs.map +1 -0
- package/dist/color/index.d.mts +7 -5
- package/dist/color/index.d.mts.map +1 -1
- package/dist/color/index.mjs +10 -5
- package/dist/color/index.mjs.map +1 -1
- package/dist/content.d.mts +78 -0
- package/dist/content.d.mts.map +1 -0
- package/dist/content.mjs +45 -0
- package/dist/content.mjs.map +1 -0
- package/dist/crm/index.d.mts +5 -87
- package/dist/crm/index.d.mts.map +1 -1
- package/dist/crm/index.mjs +10 -19
- package/dist/crm/index.mjs.map +1 -1
- package/dist/email/index.d.mts +1 -11
- package/dist/email/index.d.mts.map +1 -1
- package/dist/email/index.mjs +6 -10
- package/dist/email/index.mjs.map +1 -1
- package/dist/favicon.d.mts +0 -2
- package/dist/favicon.d.mts.map +1 -1
- package/dist/favicon.mjs +1 -2
- package/dist/git/index.d.mts +0 -2
- package/dist/git/index.d.mts.map +1 -1
- package/dist/git/index.mjs +1 -2
- package/dist/hosting/index.d.mts +0 -8
- package/dist/hosting/index.d.mts.map +1 -1
- package/dist/hosting/index.mjs +4 -7
- package/dist/hosting/index.mjs.map +1 -1
- package/dist/{identity-BoRddTx3.mjs → identity-C4yInICW.mjs} +2 -3
- package/dist/{identity-BoRddTx3.mjs.map → identity-C4yInICW.mjs.map} +1 -1
- package/dist/index.d.mts +2 -88
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +36 -52
- package/dist/index.mjs.map +1 -1
- package/dist/indexing/index.d.mts +3 -42
- package/dist/indexing/index.d.mts.map +1 -1
- package/dist/indexing/index.mjs +8 -13
- package/dist/indexing/index.mjs.map +1 -1
- package/dist/{json-store-30CFCbZZ.mjs → json-store-VJKEaoVX.mjs} +2 -3
- package/dist/{json-store-30CFCbZZ.mjs.map → json-store-VJKEaoVX.mjs.map} +1 -1
- package/dist/media/index.d.mts +0 -12
- package/dist/media/index.d.mts.map +1 -1
- package/dist/media/index.mjs +7 -13
- package/dist/media/index.mjs.map +1 -1
- package/dist/notify/index.d.mts +64 -44
- package/dist/notify/index.d.mts.map +1 -1
- package/dist/notify/index.mjs +138 -11
- package/dist/notify/index.mjs.map +1 -1
- package/dist/onboarding/index.d.mts +26 -6
- package/dist/onboarding/index.d.mts.map +1 -1
- package/dist/onboarding/index.mjs +92 -5
- package/dist/onboarding/index.mjs.map +1 -1
- package/dist/{plan-BLKikoOG.mjs → plan-B9USuc17.mjs} +3 -4
- package/dist/{plan-BLKikoOG.mjs.map → plan-B9USuc17.mjs.map} +1 -1
- package/dist/{plan-Hq8Pb0Uw.d.mts → plan-CH3Wt5ZV.d.mts} +8 -30
- package/dist/plan-CH3Wt5ZV.d.mts.map +1 -0
- package/dist/plan-CddqsmJJ.d.mts +82 -0
- package/dist/plan-CddqsmJJ.d.mts.map +1 -0
- package/dist/plan-DpHv6NO8.mjs +93 -0
- package/dist/plan-DpHv6NO8.mjs.map +1 -0
- package/dist/{plan-format-A18SvJ7C.mjs → plan-format-CIz6Feg2.mjs} +2 -3
- package/dist/{plan-format-A18SvJ7C.mjs.map → plan-format-CIz6Feg2.mjs.map} +1 -1
- package/dist/{platform-0nlOIy8h.mjs → platform-Cb_RtsF1.mjs} +3 -5
- package/dist/{platform-0nlOIy8h.mjs.map → platform-Cb_RtsF1.mjs.map} +1 -1
- package/dist/reviews/index.d.mts +4 -120
- package/dist/reviews/index.d.mts.map +1 -1
- package/dist/reviews/index.mjs +6 -99
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/reviews/select.d.mts +2 -0
- package/dist/reviews/select.mjs +3 -0
- package/dist/schema/index.d.mts +0 -12
- package/dist/schema/index.d.mts.map +1 -1
- package/dist/schema/index.mjs +6 -12
- package/dist/schema/index.mjs.map +1 -1
- package/dist/security/index.d.mts +0 -2
- package/dist/security/index.d.mts.map +1 -1
- package/dist/security/index.mjs +1 -2
- package/dist/{token-DmmoQ7um.d.mts → token-CHR1O4w5.d.mts} +3 -3
- package/dist/token-CHR1O4w5.d.mts.map +1 -0
- package/dist/{token-DkKC5m23.mjs → token-CJSl9eIS.mjs} +33 -14
- package/dist/token-CJSl9eIS.mjs.map +1 -0
- package/dist/tracking/index.d.mts +1 -9
- package/dist/tracking/index.d.mts.map +1 -1
- package/dist/tracking/index.mjs +3 -4
- package/dist/turnstile/index.d.mts +105 -0
- package/dist/turnstile/index.d.mts.map +1 -0
- package/dist/turnstile/index.mjs +281 -0
- package/dist/turnstile/index.mjs.map +1 -0
- package/dist/{types-7ELci8RQ.d.mts → types-oac2P5or.d.mts} +3 -23
- package/dist/types-oac2P5or.d.mts.map +1 -0
- package/dist/url/index.d.mts +18 -0
- package/dist/url/index.d.mts.map +1 -0
- package/dist/url/index.mjs +34 -0
- package/dist/url/index.mjs.map +1 -0
- package/package.json +43 -2
- package/dist/adc-BoEoogyo.d.mts.map +0 -1
- package/dist/cloudflare-BiK61Oq4.d.mts +0 -27
- package/dist/cloudflare-BiK61Oq4.d.mts.map +0 -1
- package/dist/cloudflare-CD8vwrgj.mjs +0 -92
- package/dist/cloudflare-CD8vwrgj.mjs.map +0 -1
- package/dist/plan-Hq8Pb0Uw.d.mts.map +0 -1
- package/dist/token-DkKC5m23.mjs.map +0 -1
- package/dist/token-DmmoQ7um.d.mts.map +0 -1
- package/dist/types-7ELci8RQ.d.mts.map +0 -1
package/dist/ads/index.d.mts
CHANGED
|
@@ -1,27 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The ads module's own option surface.
|
|
5
|
-
*
|
|
6
|
-
* Deliberately structural rather than `Config['site']['ads']` imported from
|
|
7
|
-
* `@getvitops/generator`: the generator already depends on this package, so
|
|
8
|
-
* importing back would be a cycle. The vocabulary mirrors that block field for
|
|
9
|
-
* field, so the CLI's adapter is a flat map — the same arrangement, and for the
|
|
10
|
-
* same reason, as `IndexingConfig` and `DomainSetup` beside it.
|
|
11
|
-
*
|
|
12
|
-
* Optional fields are written `?: T | undefined` throughout: `exactOptionalPropertyTypes`
|
|
13
|
-
* is on, and these describe a *parsed JSON config* where an absent key and an explicit
|
|
14
|
-
* `undefined` are the same thing.
|
|
15
|
-
*/
|
|
1
|
+
import { AdPropertyConfig } from "@getvitops/config";
|
|
2
|
+
import { AdProvider as AdProvider$1 } from "@getvitops/config/providers";
|
|
16
3
|
/**
|
|
17
4
|
* The ad platforms this toolchain knows how to link a site to.
|
|
18
5
|
*
|
|
19
|
-
* A closed set,
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
6
|
+
* A closed set, and `AD_PROVIDERS` in `@getvitops/config` is its single source: the capability
|
|
7
|
+
* table in `providers.ts` is a `Record<AdProvider, …>`, so a provider added there without an entry
|
|
8
|
+
* here is a compile error. (This was a hand-copied union held to the schema's enum by a test in the
|
|
9
|
+
* generator; the schema now lives below this package, so the copy and the test are gone.) An open
|
|
10
|
+
* string would mean a typo'd provider silently getting no verification step and no tag.
|
|
23
11
|
*/
|
|
24
|
-
type AdProvider =
|
|
12
|
+
type AdProvider = AdProvider$1;
|
|
25
13
|
/** Which consent category gates a provider's tag. */
|
|
26
14
|
type AdConsentCategory = 'marketing' | 'analytics';
|
|
27
15
|
/**
|
|
@@ -52,38 +40,35 @@ interface ConversionLabelMap {
|
|
|
52
40
|
* action that existed before this was a map. Absent input normalises to `{}`.
|
|
53
41
|
*/
|
|
54
42
|
export declare function normalizeConversionLabels(label: string | ConversionLabelMap | undefined): ConversionLabelMap;
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
*
|
|
78
|
-
* A bare token gets the provider's TXT prefix applied; a value already
|
|
79
|
-
* containing `=` is used as the whole record content, which is the escape hatch
|
|
80
|
-
* for a platform changing its prefix without waiting on a toolchain release.
|
|
81
|
-
*/
|
|
82
|
-
domainVerification?: string | undefined;
|
|
83
|
-
/** Category the tag waits on. Defaults to `marketing`. */
|
|
84
|
-
category?: AdConsentCategory | undefined;
|
|
85
|
-
enabled?: boolean | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* One Google Ads conversion action **resource id** per action this site
|
|
45
|
+
* uploads server-side — a numeric id (`"123456789"`) or a full resource name
|
|
46
|
+
* (`"customers/123/conversionActions/456"`), never the `conversionLabel`
|
|
47
|
+
* string. Written out field by field for the same `exactOptionalPropertyTypes`
|
|
48
|
+
* reason as `ConversionLabelMap` above.
|
|
49
|
+
*
|
|
50
|
+
* **This is a genuinely different identifier from `conversionLabel`, not an
|
|
51
|
+
* alternate spelling of it.** `conversionLabel` is the opaque label the web
|
|
52
|
+
* tag embeds in `send_to: "AW-…/label"` — it identifies the conversion action
|
|
53
|
+
* to `gtag.js`, not to the Ads API. The API's click-conversion upload
|
|
54
|
+
* (`ConversionUploadService.UploadClickConversions`) addresses the action by
|
|
55
|
+
* `conversion_action` **resource name**, a value the label cannot be turned
|
|
56
|
+
* into locally — Google returns both when a conversion action is created, but
|
|
57
|
+
* only the label ships to the browser today. Until something in this
|
|
58
|
+
* toolchain calls `ConversionActionService` to create/list actions and print
|
|
59
|
+
* both back, this field has to be copied in by hand from the Ads UI/API.
|
|
60
|
+
*/
|
|
61
|
+
interface ConversionActionIdMap {
|
|
62
|
+
call?: string | undefined;
|
|
63
|
+
email?: string | undefined;
|
|
64
|
+
form?: string | undefined;
|
|
86
65
|
}
|
|
66
|
+
/** Same back-compat shorthand as `normalizeConversionLabels`, for ids. */
|
|
67
|
+
export declare function normalizeConversionActionIds(id: string | ConversionActionIdMap | undefined): ConversionActionIdMap;
|
|
68
|
+
/** One ad property to link: the config block, plus the platform it is keyed by. */
|
|
69
|
+
type AdPropertySetup = AdPropertyConfig & {
|
|
70
|
+
provider: AdProvider;
|
|
71
|
+
};
|
|
87
72
|
/** Everything `plan()` needs, once the CLI has read the config. */
|
|
88
73
|
interface AdsConfig {
|
|
89
74
|
/** The properties to link. Already filtered to `--provider` if that was passed. */
|
|
@@ -103,8 +88,6 @@ interface AdDomainState {
|
|
|
103
88
|
/** Every apex TXT record content, unquoted. */
|
|
104
89
|
txtContents: string[];
|
|
105
90
|
}
|
|
106
|
-
//#endregion
|
|
107
|
-
//#region src/ads/plan.d.ts
|
|
108
91
|
/** A step's decision. `blocked` means the config is missing something. */
|
|
109
92
|
type StepAction = 'create' | 'skip' | 'blocked';
|
|
110
93
|
/** A config field the operator can supply to unblock a step. */
|
|
@@ -168,8 +151,6 @@ interface AdPropertyResult {
|
|
|
168
151
|
}
|
|
169
152
|
/** Render the end-of-run summary: one row per property, plus any reminders. */
|
|
170
153
|
export declare function formatSummary(results: AdPropertyResult[]): string;
|
|
171
|
-
//#endregion
|
|
172
|
-
//#region src/ads/providers.d.ts
|
|
173
154
|
/** How a platform proves you own the domain. */
|
|
174
155
|
interface AdVerificationSpec {
|
|
175
156
|
method: 'dns-txt' | 'none';
|
|
@@ -255,6 +236,5 @@ export declare const categoryOf: (entry: AdPropertySetup) => AdConsentCategory;
|
|
|
255
236
|
* than emit a snippet that initialises `undefined`.
|
|
256
237
|
*/
|
|
257
238
|
export declare function renderTag(entry: AdPropertySetup, strategy?: 'idle' | 'async' | 'interaction'): string | null;
|
|
258
|
-
|
|
259
|
-
export { type AdConsentCategory, type AdDomainState, type AdPlatform, type AdPropertyPlan, type AdPropertyResult, type AdPropertySetup, type AdProvider, type AdTagSpec, type AdVerificationSpec, type AdsConfig, type AdsPlan, type ConversionAction, type ConversionLabelMap, type MissingField, type StepAction, type StepPlan, createApexTxt, findZoneId, listApexTxt };
|
|
239
|
+
export type { AdConsentCategory, AdDomainState, AdPlatform, AdPropertyPlan, AdPropertyResult, AdPropertySetup, AdProvider, AdTagSpec, AdVerificationSpec, AdsConfig, AdsPlan, ConversionAction, ConversionActionIdMap, ConversionLabelMap, MissingField, StepAction, StepPlan };
|
|
260
240
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/ads/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/ads/types.ts","../../src/ads/plan.ts","../../src/ads/providers.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":["AdProviderConfig"],"sources":["../../src/ads/types.ts","../../src/ads/plan.ts","../../src/ads/providers.ts"],"mappings":";;;;;;;;;;;KAmBY,aAAaA;;KAGb;;;;;;;KAQA;;;;;;;;;;;;UAaK;EACf;EACA;EACA;;;;;;wBAOc,0BACd,gBAAgB,iCACf;;;;;;;;;;;;;;;;;;;UAuBc;EACf;EACA;EACA;;;wBAIc,6BACd,aAAa,oCACZ;;KAMS,kBAAkB;EAC5B,UAAU;;;UAIK;;EAEf,YAAY;;;;;;;;;;;UAYG;EACf;;EAEA;;;KC1FU;;KAGA;UAEK;EACf,QAAQ;;EAER;;EAEA,QAAQ;;;UAIO;EACf,UAAU;;EAEV;;EAEA;;EAEA,KAAK;;EAEL,KAAK;;EAEL;;EAEA;;UAGe;EACf,YAAY;;EAEZ;;;;;;;;;;;wBAYc,aAAa,OAAO,iBAAiB,OAAO,gBAAgB;;wBAoD5D,KAAK,QAAQ,WAAW,QAAQ,YAAY,iBAAiB;;wBAe7D,SAAS,GAAG;;wBAKZ,cAAc,GAAG;EAAY;EAAkB,OAAO;;;wBAYtD,WAAW,GAAG;;UAcb;EACf;EACA;EACA;EACA;EACA;;;wBAIc,cAAc,SAAS;;UC7ItB;EACf;;;;;;;EAOA;;EAEA;;EAEA;;;UAIe;;EAEf;;EAEA,OAAO;;EAEP,YAAY,YAAY,OAAO;;;;;EAK/B;;EAEA;;EAEA;;UAGe;;EAEf;;;;;;;;;EASA;;EAEA;;EAEA;EACA,cAAc;EACd,KAAK;;EAEL,cAAc,OAAO;;qBAwDV,cAAc,OAAO,YAAY;;qBAuMjC,kBAAgD;;qBAGhD,eAAgB,kBAAgB,SAAS;;;;;;;;;;wBAWtC,UAAU,UAAU,YAAY;;wBAOhC,MAAM,OAAO;;qBAMhB,aAAc,OAAO,oBAAkB;;;;;;;;;;;;wBAiBpC,UACd,OAAO,iBACP"}
|
package/dist/ads/index.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { t as table } from "../plan-format-
|
|
2
|
-
import { n as findZoneId, r as listApexTxt, t as createApexTxt } from "../cloudflare-CD8vwrgj.mjs";
|
|
1
|
+
import { t as table } from "../plan-format-CIz6Feg2.mjs";
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
// src/ads/types.ts
|
|
5
4
|
/**
|
|
6
5
|
* A bare string is back-compat shorthand for `{ call: label }` — the only
|
|
7
6
|
* action that existed before this was a map. Absent input normalises to `{}`.
|
|
@@ -10,9 +9,13 @@ function normalizeConversionLabels(label) {
|
|
|
10
9
|
if (label == null) return {};
|
|
11
10
|
return typeof label === "string" ? { call: label } : label;
|
|
12
11
|
}
|
|
12
|
+
/** Same back-compat shorthand as `normalizeConversionLabels`, for ids. */
|
|
13
|
+
function normalizeConversionActionIds(id) {
|
|
14
|
+
if (id == null) return {};
|
|
15
|
+
return typeof id === "string" ? { call: id } : id;
|
|
16
|
+
}
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
//#region src/ads/providers.ts
|
|
18
|
+
// src/ads/providers.ts
|
|
16
19
|
/**
|
|
17
20
|
* What each ad platform actually lets a build tool do — the table every `vitops ads`
|
|
18
21
|
* subcommand reads.
|
|
@@ -63,10 +66,20 @@ function normalizeConversionLabels(label) {
|
|
|
63
66
|
* `entry` is trusted here — `resolveAds()` validates every configured label
|
|
64
67
|
* against `SAFE_ID` before it reaches this function, the same guard `id`
|
|
65
68
|
* already gets.
|
|
69
|
+
*
|
|
70
|
+
* **`entry.reporting === 'server'` suppresses an action here, never
|
|
71
|
+
* silently.** An action fires client-side unless reporting is `'server'`
|
|
72
|
+
* *and* that same action has a `conversionActionId` configured — so turning
|
|
73
|
+
* reporting to `'server'` before the id is filled in leaves the action firing
|
|
74
|
+
* client-side rather than dropping the conversion. Once both are set, the
|
|
75
|
+
* server-side upload (`@getvitops/utils/notify`'s `uploadClickConversion`,
|
|
76
|
+
* wired in by `createConversionRoute`'s `googleAdsOffline` option) is the one
|
|
77
|
+
* place that action reports from — this is what avoids double-counting.
|
|
66
78
|
*/
|
|
67
79
|
const gtagBootstrap = (id, entry) => {
|
|
68
80
|
const labels = normalizeConversionLabels(entry.conversionLabel);
|
|
69
|
-
const
|
|
81
|
+
const actionIds = normalizeConversionActionIds(entry.conversionActionId);
|
|
82
|
+
const actions = Object.entries(labels).filter((pair) => !!pair[1] && !(entry.reporting === "server" && !!actionIds[pair[0]]));
|
|
70
83
|
return `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag('js',new Date());gtag('config','${id}');` + (actions.length ? `window.vitopsAdsConversions=window.vitopsAdsConversions||{};window.vitopsAdsConversions.google=${JSON.stringify(Object.fromEntries(actions.map(([action, label]) => [action, `${id}/${label}`])))};` : "");
|
|
71
84
|
};
|
|
72
85
|
/** The queue-stub shape shared by fbq/ttq/rdt/snaptr/pintrk. */
|
|
@@ -301,8 +314,7 @@ function renderTag(entry, strategy = "idle") {
|
|
|
301
314
|
return `<!-- ${platform.name} — gated on ${categoryOf(entry)} consent -->\n<script ${attrs.join(" ")}>${platform.tag.bootstrap(id, entry)}<\/script>`;
|
|
302
315
|
}
|
|
303
316
|
|
|
304
|
-
|
|
305
|
-
//#region src/ads/plan.ts
|
|
317
|
+
// src/ads/plan.ts
|
|
306
318
|
/**
|
|
307
319
|
* Every decision `vitops ads setup` makes, as one pure function.
|
|
308
320
|
*
|
|
@@ -435,6 +447,5 @@ function formatSummary(results) {
|
|
|
435
447
|
return table(headers, rows, reminders);
|
|
436
448
|
}
|
|
437
449
|
|
|
438
|
-
|
|
439
|
-
export { AD_PLATFORMS, AD_PROVIDER_KEYS, categoryOf, createApexTxt, findZoneId, formatPlan, formatSummary, hasDrift, isAdProvider, listApexTxt, missingFields, normalizeConversionLabels, plan, planProperty, renderTag, tagId, txtRecord };
|
|
450
|
+
export { AD_PLATFORMS, AD_PROVIDER_KEYS, categoryOf, formatPlan, formatSummary, hasDrift, isAdProvider, missingFields, normalizeConversionActionIds, normalizeConversionLabels, plan, planProperty, renderTag, tagId, txtRecord };
|
|
440
451
|
//# sourceMappingURL=index.mjs.map
|
package/dist/ads/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/ads/types.ts","../../src/ads/providers.ts","../../src/ads/plan.ts"],"sourcesContent":["/**\n * The ads module's own option surface.\n *\n * Deliberately structural rather than `Config['site']['ads']` imported from\n * `@getvitops/generator`: the generator already depends on this package, so\n * importing back would be a cycle. The vocabulary mirrors that block field for\n * field, so the CLI's adapter is a flat map — the same arrangement, and for the\n * same reason, as `IndexingConfig` and `DomainSetup` beside it.\n *\n * Optional fields are written `?: T | undefined` throughout: `exactOptionalPropertyTypes`\n * is on, and these describe a *parsed JSON config* where an absent key and an explicit\n * `undefined` are the same thing.\n */\n\n/**\n * The ad platforms this toolchain knows how to link a site to.\n *\n * A closed set, mirroring `AD_PROVIDERS` in the generator's `config.ts` — the\n * capability table in `providers.ts` has one entry per member, and a test in the\n * generator asserts the two lists agree. An open string would mean a typo'd\n * provider silently getting no verification step and no tag.\n */\nexport type AdProvider =\n | 'google'\n | 'meta'\n | 'linkedin'\n | 'reddit'\n | 'tiktok'\n | 'microsoft'\n | 'pinterest'\n | 'snapchat';\n\n/** Which consent category gates a provider's tag. */\nexport type AdConsentCategory = 'marketing' | 'analytics';\n\n/**\n * The three interactions Google Ads can report a distinct conversion action\n * for. `form` is fired client-side via `vitopsFormConversion()`\n * (`@getvitops/astro`'s tracking script), the other two from the tel:/mailto:\n * click handler.\n */\nexport type ConversionAction = 'call' | 'email' | 'form';\n\n/**\n * One label per action this site wants Google Ads to tell apart.\n *\n * Written out field by field, `?: string | undefined` — not\n * `Partial<Record<ConversionAction, string>>` — matching this file's own\n * convention above: `exactOptionalPropertyTypes` treats `{ email?: string }`\n * (present-or-absent) and `{ email?: string | undefined }` (present,\n * absent, or explicitly `undefined`) as different types, and a parsed JSON\n * config's optional field is the latter. `Partial<Record<...>>` produces the\n * former, which rejected the zod-inferred shape config.ts actually produces.\n */\nexport interface ConversionLabelMap {\n call?: string | undefined;\n email?: string | undefined;\n form?: string | undefined;\n}\n\n/**\n * A bare string is back-compat shorthand for `{ call: label }` — the only\n * action that existed before this was a map. Absent input normalises to `{}`.\n */\nexport function normalizeConversionLabels(\n label: string | ConversionLabelMap | undefined,\n): ConversionLabelMap {\n if (label == null) return {};\n return typeof label === 'string' ? { call: label } : label;\n}\n\n/** One ad property to link, as the config declares it. */\nexport interface AdPropertySetup {\n provider: AdProvider;\n /**\n * The account as the platform shows it — Google Ads customer id, Meta ad\n * account, LinkedIn partner id, Reddit advertiser id.\n */\n accountId?: string | undefined;\n /** The tag/pixel id, when the platform issues one distinct from the account. */\n pixelId?: string | undefined;\n /**\n * Google Ads only: the conversion label(s) paired with the conversion id —\n * either one bare label (back-compat, treated as the `call` action) or a map\n * of action → label, so a call, an email click and a form submission each\n * fire their own Google Ads conversion action instead of sharing one. See\n * `gtagBootstrap`/`normalizeConversionLabels` below.\n */\n conversionLabel?: string | ConversionLabelMap | undefined;\n /** The host to verify, bare (`acme.ca`). The CLI defaults it from the canonical domain. */\n domain?: string | undefined;\n /**\n * The verification token from the platform UI.\n *\n * A bare token gets the provider's TXT prefix applied; a value already\n * containing `=` is used as the whole record content, which is the escape hatch\n * for a platform changing its prefix without waiting on a toolchain release.\n */\n domainVerification?: string | undefined;\n /** Category the tag waits on. Defaults to `marketing`. */\n category?: AdConsentCategory | undefined;\n enabled?: boolean | undefined;\n}\n\n/** Everything `plan()` needs, once the CLI has read the config. */\nexport interface AdsConfig {\n /** The properties to link. Already filtered to `--provider` if that was passed. */\n properties: AdPropertySetup[];\n}\n\n/**\n * One property's observed live state, gathered by the executor before planning.\n *\n * Same shape of arrangement as `DomainState`: the decision is pure, the\n * observation is not. `txtContents` is every apex TXT record on the domain, so\n * the planner can decide both \"our record is present\" and \"a *different* record\n * from this provider is present\" — the second is a token rotation, which needs\n * saying rather than silently adding a second record.\n */\nexport interface AdDomainState {\n zoneId?: string | undefined;\n /** Every apex TXT record content, unquoted. */\n txtContents: string[];\n}\n","/**\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 { normalizeConversionLabels } from './types.ts';\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/**\n * `gtag` is shared by every Google tag; the id shape (`AW-…`) is what differs.\n *\n * `gtag('config', id)` only loads the tag — it never reports a conversion. When\n * `entry.conversionLabel` names one or more actions, this also exposes a\n * `{action: 'id/label'}` map on `window.vitopsAdsConversions.google` — one entry\n * per configured action, so a call, an email click and a form submission each\n * carry their own `send_to` target instead of all three sharing one (that\n * sharing was a real bug: a call and an email click used to report as the same\n * Google Ads conversion action, indistinguishable in the account). Bootstrap\n * time only loads the tag; `scripts/tracking.ts`'s click handler and\n * `vitopsFormConversion()` each read their own action's key and fire\n * `gtag('event', 'conversion', …)` at the moment of the actual interaction —\n * bootstrap-time firing would count every pageview as a conversion.\n *\n * `entry` is trusted here — `resolveAds()` validates every configured label\n * against `SAFE_ID` before it reaches this function, the same guard `id`\n * already gets.\n */\nconst gtagBootstrap = (id: string, entry: AdPropertySetup): string => {\n const labels = normalizeConversionLabels(entry.conversionLabel);\n const actions = Object.entries(labels).filter((pair): pair is [string, string] => !!pair[1]);\n return (\n `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}` +\n `gtag('js',new Date());gtag('config','${id}');` +\n (actions.length\n ? `window.vitopsAdsConversions=window.vitopsAdsConversions||{};` +\n `window.vitopsAdsConversions.google=${JSON.stringify(\n Object.fromEntries(actions.map(([action, label]) => [action, `${id}/${label}`])),\n )};`\n : '')\n );\n};\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 ...(Object.keys(normalizeConversionLabels(e.conversionLabel)).length\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';\nimport { table } from '../plan-format.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\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 reminders = results.flatMap((r) => r.reminders.map((m) => ` ! ${r.provider}: ${m}`));\n return table(headers, rows, reminders);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/ads/types.ts","../../src/ads/providers.ts","../../src/ads/plan.ts"],"sourcesContent":["/**\n * The ads module's own option surface.\n *\n * `AdPropertySetup` is the `site.ads.<platform>` block from `@getvitops/config` plus the platform it\n * is keyed by — inferred from the schema `validateConfig` runs, not re-declared here.\n */\n\nimport type { AdPropertyConfig } from '@getvitops/config';\nimport type { AdProvider as AdProviderConfig } from '@getvitops/config/providers';\n\n/**\n * The ad platforms this toolchain knows how to link a site to.\n *\n * A closed set, and `AD_PROVIDERS` in `@getvitops/config` is its single source: the capability\n * table in `providers.ts` is a `Record<AdProvider, …>`, so a provider added there without an entry\n * here is a compile error. (This was a hand-copied union held to the schema's enum by a test in the\n * generator; the schema now lives below this package, so the copy and the test are gone.) An open\n * string would mean a typo'd provider silently getting no verification step and no tag.\n */\nexport type AdProvider = AdProviderConfig;\n\n/** Which consent category gates a provider's tag. */\nexport type AdConsentCategory = 'marketing' | 'analytics';\n\n/**\n * The three interactions Google Ads can report a distinct conversion action\n * for. `form` is fired client-side via `vitopsFormConversion()`\n * (`@getvitops/astro`'s tracking script), the other two from the tel:/mailto:\n * click handler.\n */\nexport type ConversionAction = 'call' | 'email' | 'form';\n\n/**\n * One label per action this site wants Google Ads to tell apart.\n *\n * Written out field by field, `?: string | undefined` — not\n * `Partial<Record<ConversionAction, string>>` — matching this file's own\n * convention above: `exactOptionalPropertyTypes` treats `{ email?: string }`\n * (present-or-absent) and `{ email?: string | undefined }` (present,\n * absent, or explicitly `undefined`) as different types, and a parsed JSON\n * config's optional field is the latter. `Partial<Record<...>>` produces the\n * former, which rejected the zod-inferred shape config.ts actually produces.\n */\nexport interface ConversionLabelMap {\n call?: string | undefined;\n email?: string | undefined;\n form?: string | undefined;\n}\n\n/**\n * A bare string is back-compat shorthand for `{ call: label }` — the only\n * action that existed before this was a map. Absent input normalises to `{}`.\n */\nexport function normalizeConversionLabels(\n label: string | ConversionLabelMap | undefined,\n): ConversionLabelMap {\n if (label == null) return {};\n return typeof label === 'string' ? { call: label } : label;\n}\n\n/**\n * One Google Ads conversion action **resource id** per action this site\n * uploads server-side — a numeric id (`\"123456789\"`) or a full resource name\n * (`\"customers/123/conversionActions/456\"`), never the `conversionLabel`\n * string. Written out field by field for the same `exactOptionalPropertyTypes`\n * reason as `ConversionLabelMap` above.\n *\n * **This is a genuinely different identifier from `conversionLabel`, not an\n * alternate spelling of it.** `conversionLabel` is the opaque label the web\n * tag embeds in `send_to: \"AW-…/label\"` — it identifies the conversion action\n * to `gtag.js`, not to the Ads API. The API's click-conversion upload\n * (`ConversionUploadService.UploadClickConversions`) addresses the action by\n * `conversion_action` **resource name**, a value the label cannot be turned\n * into locally — Google returns both when a conversion action is created, but\n * only the label ships to the browser today. Until something in this\n * toolchain calls `ConversionActionService` to create/list actions and print\n * both back, this field has to be copied in by hand from the Ads UI/API.\n */\nexport interface ConversionActionIdMap {\n call?: string | undefined;\n email?: string | undefined;\n form?: string | undefined;\n}\n\n/** Same back-compat shorthand as `normalizeConversionLabels`, for ids. */\nexport function normalizeConversionActionIds(\n id: string | ConversionActionIdMap | undefined,\n): ConversionActionIdMap {\n if (id == null) return {};\n return typeof id === 'string' ? { call: id } : id;\n}\n\n/** One ad property to link: the config block, plus the platform it is keyed by. */\nexport type AdPropertySetup = AdPropertyConfig & {\n provider: AdProvider;\n};\n\n/** Everything `plan()` needs, once the CLI has read the config. */\nexport interface AdsConfig {\n /** The properties to link. Already filtered to `--provider` if that was passed. */\n properties: AdPropertySetup[];\n}\n\n/**\n * One property's observed live state, gathered by the executor before planning.\n *\n * Same shape of arrangement as `DomainState`: the decision is pure, the\n * observation is not. `txtContents` is every apex TXT record on the domain, so\n * the planner can decide both \"our record is present\" and \"a *different* record\n * from this provider is present\" — the second is a token rotation, which needs\n * saying rather than silently adding a second record.\n */\nexport interface AdDomainState {\n zoneId?: string | undefined;\n /** Every apex TXT record content, unquoted. */\n txtContents: string[];\n}\n","/**\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 { normalizeConversionActionIds, normalizeConversionLabels } from './types.ts';\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/**\n * `gtag` is shared by every Google tag; the id shape (`AW-…`) is what differs.\n *\n * `gtag('config', id)` only loads the tag — it never reports a conversion. When\n * `entry.conversionLabel` names one or more actions, this also exposes a\n * `{action: 'id/label'}` map on `window.vitopsAdsConversions.google` — one entry\n * per configured action, so a call, an email click and a form submission each\n * carry their own `send_to` target instead of all three sharing one (that\n * sharing was a real bug: a call and an email click used to report as the same\n * Google Ads conversion action, indistinguishable in the account). Bootstrap\n * time only loads the tag; `scripts/tracking.ts`'s click handler and\n * `vitopsFormConversion()` each read their own action's key and fire\n * `gtag('event', 'conversion', …)` at the moment of the actual interaction —\n * bootstrap-time firing would count every pageview as a conversion.\n *\n * `entry` is trusted here — `resolveAds()` validates every configured label\n * against `SAFE_ID` before it reaches this function, the same guard `id`\n * already gets.\n *\n * **`entry.reporting === 'server'` suppresses an action here, never\n * silently.** An action fires client-side unless reporting is `'server'`\n * *and* that same action has a `conversionActionId` configured — so turning\n * reporting to `'server'` before the id is filled in leaves the action firing\n * client-side rather than dropping the conversion. Once both are set, the\n * server-side upload (`@getvitops/utils/notify`'s `uploadClickConversion`,\n * wired in by `createConversionRoute`'s `googleAdsOffline` option) is the one\n * place that action reports from — this is what avoids double-counting.\n */\nconst gtagBootstrap = (id: string, entry: AdPropertySetup): string => {\n const labels = normalizeConversionLabels(entry.conversionLabel);\n const actionIds = normalizeConversionActionIds(entry.conversionActionId);\n const actions = Object.entries(labels).filter(\n (pair): pair is [string, string] =>\n !!pair[1] &&\n !(entry.reporting === 'server' && !!actionIds[pair[0] as keyof typeof actionIds]),\n );\n return (\n `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}` +\n `gtag('js',new Date());gtag('config','${id}');` +\n (actions.length\n ? `window.vitopsAdsConversions=window.vitopsAdsConversions||{};` +\n `window.vitopsAdsConversions.google=${JSON.stringify(\n Object.fromEntries(actions.map(([action, label]) => [action, `${id}/${label}`])),\n )};`\n : '')\n );\n};\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 ...(Object.keys(normalizeConversionLabels(e.conversionLabel)).length\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';\nimport { table } from '../plan-format.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\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 reminders = results.flatMap((r) => r.reminders.map((m) => ` ! ${r.provider}: ${m}`));\n return table(headers, rows, reminders);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/** Shapes shared by the audit planner, the executor and the report renderers. */
|
|
2
|
+
type AxeImpact = 'minor' | 'moderate' | 'serious' | 'critical';
|
|
3
|
+
interface AuditBudgets {
|
|
4
|
+
/** Largest Contentful Paint ceiling, ms. */
|
|
5
|
+
lcpMs: number;
|
|
6
|
+
/** Cumulative Layout Shift ceiling (unitless). */
|
|
7
|
+
cls: number;
|
|
8
|
+
/** Total Blocking Time ceiling, ms — the lab proxy for INP. */
|
|
9
|
+
tbtMs: number;
|
|
10
|
+
/** axe impacts that fail the page. Default: serious + critical. */
|
|
11
|
+
blockingImpacts: AxeImpact[];
|
|
12
|
+
}
|
|
13
|
+
interface AxeViolation {
|
|
14
|
+
id: string;
|
|
15
|
+
/** axe reports `null` impact for some incomplete rules; normalised to `moderate`. */
|
|
16
|
+
impact: AxeImpact;
|
|
17
|
+
help: string;
|
|
18
|
+
helpUrl: string;
|
|
19
|
+
/** Total nodes the rule failed on. */
|
|
20
|
+
nodeCount: number;
|
|
21
|
+
/** First few CSS selectors, for the report. */
|
|
22
|
+
targets: string[];
|
|
23
|
+
}
|
|
24
|
+
interface PageMetrics {
|
|
25
|
+
/** `undefined` when Lighthouse could not compute it — which fails the page, not passes it. */
|
|
26
|
+
lcpMs?: number | undefined;
|
|
27
|
+
cls?: number | undefined;
|
|
28
|
+
tbtMs?: number | undefined;
|
|
29
|
+
/** Lighthouse category scores, 0-100. */
|
|
30
|
+
scores: {
|
|
31
|
+
performance?: number | undefined;
|
|
32
|
+
seo?: number | undefined;
|
|
33
|
+
bestPractices?: number | undefined;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
interface BudgetFailure {
|
|
37
|
+
kind: 'lcp' | 'cls' | 'tbt' | 'axe' | 'metric-missing' | 'page-error' | 'required-page-missing';
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
type PageStatus = 'audited' | 'skipped' | 'error';
|
|
41
|
+
interface PageResult {
|
|
42
|
+
path: string;
|
|
43
|
+
url: string;
|
|
44
|
+
status: PageStatus;
|
|
45
|
+
httpStatus?: number;
|
|
46
|
+
/** Why a page was skipped or errored. */
|
|
47
|
+
reason?: string;
|
|
48
|
+
metrics?: PageMetrics;
|
|
49
|
+
violations?: AxeViolation[];
|
|
50
|
+
failures: BudgetFailure[];
|
|
51
|
+
}
|
|
52
|
+
type FormFactor = 'mobile' | 'desktop';
|
|
53
|
+
interface AuditReport {
|
|
54
|
+
generatedAt: string;
|
|
55
|
+
baseUrl: string;
|
|
56
|
+
formFactor: FormFactor;
|
|
57
|
+
budgets: AuditBudgets;
|
|
58
|
+
axeTags: string[];
|
|
59
|
+
pages: PageResult[];
|
|
60
|
+
/** True when no page has a failure. */
|
|
61
|
+
passed: boolean;
|
|
62
|
+
/** Always present: what a pass does and does not mean. */
|
|
63
|
+
disclaimer: string;
|
|
64
|
+
manualChecks: string[];
|
|
65
|
+
}
|
|
66
|
+
export declare const DEFAULT_BUDGETS: AuditBudgets;
|
|
67
|
+
/**
|
|
68
|
+
* `wcag2a`/`wcag2aa` are what AODA (WCAG 2.0 AA) asks for; `wcag21a`/`wcag21aa` add the
|
|
69
|
+
* 2.1 criteria we target. `wcag21aa` alone covers only the *new AA* criteria, not the
|
|
70
|
+
* new level-A ones, so both 2.1 tags are listed.
|
|
71
|
+
*/
|
|
72
|
+
export declare const AXE_TAGS: string[];
|
|
73
|
+
/** Pages tried on every run. `/` is required; the rest are skipped when they 404. */
|
|
74
|
+
export declare const DEFAULT_PAGES: string[];
|
|
75
|
+
export declare const DISCLAIMER: string;
|
|
76
|
+
export declare const MANUAL_CHECKS: string[];
|
|
77
|
+
/**
|
|
78
|
+
* A menu-ish thing: EmDash seed (`items`), `getMenu()` (`items`, items with `children`),
|
|
79
|
+
* or either shape nested. Only `name`, `url` and children are read.
|
|
80
|
+
*/
|
|
81
|
+
interface MenuLike {
|
|
82
|
+
name?: string;
|
|
83
|
+
items?: MenuItemLike[];
|
|
84
|
+
}
|
|
85
|
+
interface MenuItemLike {
|
|
86
|
+
url?: string | null;
|
|
87
|
+
children?: MenuItemLike[];
|
|
88
|
+
items?: MenuItemLike[];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Normalise to a site-relative path, or `undefined` for anything that is not an
|
|
92
|
+
* auditable page of this site: external URLs, hash-only links, `mailto:`/`tel:`,
|
|
93
|
+
* protocol-relative URLs, file downloads (anything with an extension) and the CMS
|
|
94
|
+
* admin. Query and hash are dropped; a trailing slash is too (except the root).
|
|
95
|
+
*/
|
|
96
|
+
export declare function normalizePath(url: string | null | undefined): string | undefined;
|
|
97
|
+
/** Is this one of the menus whose links are pages worth auditing? */
|
|
98
|
+
export declare function isAuditMenu(name: string | undefined): boolean;
|
|
99
|
+
/** Paths from the `primary*` / `footer*` menus (`footer-legal`, `primary-mobile`, …). */
|
|
100
|
+
export declare function pathsFromMenus(menus: MenuLike[]): string[];
|
|
101
|
+
/** Same, from an EmDash seed file's parsed JSON (`{ menus: [...] }`). */
|
|
102
|
+
export declare function pathsFromSeed(seed: unknown): string[];
|
|
103
|
+
interface PagePlanInput {
|
|
104
|
+
/** Paths found in menus. */
|
|
105
|
+
menuPaths?: string[];
|
|
106
|
+
/** Paths from `--pages`. */
|
|
107
|
+
extra?: string[];
|
|
108
|
+
/** Default true. `--no-defaults` for a site that wants an exact list. */
|
|
109
|
+
includeDefaults?: boolean;
|
|
110
|
+
}
|
|
111
|
+
/** The pages to try, `/` always first, unique, in a stable order. */
|
|
112
|
+
export declare function planPages(input?: PagePlanInput): string[];
|
|
113
|
+
/**
|
|
114
|
+
* What an HTTP status means for a candidate page. 404/410 → skipped (a single-page
|
|
115
|
+
* site has no `/contact`, and that is fine) — except `/`, which must exist. Any other
|
|
116
|
+
* non-2xx is an error: a 500 on `/privacy` is a defect, not an absent page.
|
|
117
|
+
*/
|
|
118
|
+
export declare function classifyStatus(path: string, httpStatus: number | undefined): {
|
|
119
|
+
status: 'audited' | 'skipped' | 'error';
|
|
120
|
+
reason?: string;
|
|
121
|
+
};
|
|
122
|
+
/** axe can report a null impact; treat it as `moderate`, never silently drop it. */
|
|
123
|
+
export declare function normalizeImpact(impact: string | null | undefined): AxeImpact;
|
|
124
|
+
/** `serious,critical` → impacts, rejecting typos so a budget cannot silently be empty. */
|
|
125
|
+
export declare function parseImpacts(list: string): AxeImpact[];
|
|
126
|
+
/** Everything that makes one audited page fail. Empty = the page passes. */
|
|
127
|
+
export declare function evaluatePage(metrics: PageMetrics | undefined, violations: AxeViolation[] | undefined, budgets: AuditBudgets): BudgetFailure[];
|
|
128
|
+
/** Build a `PageResult` for a page that was not audited (skipped or error). */
|
|
129
|
+
export declare function unauditedPage(path: string, url: string, httpStatus: number | undefined, verdict: {
|
|
130
|
+
status: 'skipped' | 'error';
|
|
131
|
+
reason?: string;
|
|
132
|
+
}): PageResult;
|
|
133
|
+
export declare function auditedPage(path: string, url: string, httpStatus: number, metrics: PageMetrics, violations: AxeViolation[], budgets: AuditBudgets): PageResult;
|
|
134
|
+
export declare function buildReport(input: {
|
|
135
|
+
generatedAt: string;
|
|
136
|
+
baseUrl: string;
|
|
137
|
+
formFactor: FormFactor;
|
|
138
|
+
budgets: AuditBudgets;
|
|
139
|
+
pages: PageResult[];
|
|
140
|
+
}): AuditReport;
|
|
141
|
+
/** `/` → `home`, `/a/b` → `a-b`: a safe per-page filename. */
|
|
142
|
+
export declare function pageSlug(path: string): string;
|
|
143
|
+
export declare function renderPageMarkdown(page: PageResult, budgets: AuditBudgets): string;
|
|
144
|
+
export declare function renderMarkdown(report: AuditReport): string;
|
|
145
|
+
/** Files to write, relative to the output dir: JSON + markdown overall and per page. */
|
|
146
|
+
export declare function reportFiles(report: AuditReport): Record<string, string>;
|
|
147
|
+
/** One-line-per-failure console summary. */
|
|
148
|
+
export declare function formatSummary(report: AuditReport): string;
|
|
149
|
+
/** A free TCP port, or `true` for "this specific one is free". */
|
|
150
|
+
export declare function isPortFree(port: number): Promise<boolean>;
|
|
151
|
+
/** Run a command to completion, inheriting stdio. Rejects on a non-zero exit. */
|
|
152
|
+
export declare function runCommand(cmd: string, args: string[], cwd: string): Promise<void>;
|
|
153
|
+
interface PreviewServer {
|
|
154
|
+
baseUrl: string;
|
|
155
|
+
stop(): void;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* `astro preview` on a dedicated port.
|
|
159
|
+
*
|
|
160
|
+
* Astro's preview server silently moves to the next port when its own is taken, and
|
|
161
|
+
* a readiness probe would then happily attach to whatever owns the original one — the
|
|
162
|
+
* failure `site/playwright.config.ts` documents. So the port is checked free first and
|
|
163
|
+
* the run aborts if it is not.
|
|
164
|
+
*/
|
|
165
|
+
export declare function startPreview(opts: {
|
|
166
|
+
cwd: string;
|
|
167
|
+
port: number;
|
|
168
|
+
timeoutMs?: number;
|
|
169
|
+
}): Promise<PreviewServer>;
|
|
170
|
+
interface RunOptions {
|
|
171
|
+
baseUrl: string;
|
|
172
|
+
paths: string[];
|
|
173
|
+
budgets: AuditBudgets;
|
|
174
|
+
formFactor: FormFactor;
|
|
175
|
+
onProgress?: (message: string) => void;
|
|
176
|
+
}
|
|
177
|
+
/** Audit each path. Never throws for a bad PAGE — only for missing tooling/browser. */
|
|
178
|
+
export declare function runAudit(opts: RunOptions): Promise<PageResult[]>;
|
|
179
|
+
export type { AuditBudgets, AuditReport, AxeImpact, AxeViolation, BudgetFailure, FormFactor, MenuItemLike, MenuLike, PageMetrics, PagePlanInput, PageResult, PageStatus, PreviewServer, RunOptions };
|
|
180
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/audit/types.ts","../../src/audit/plan.ts","../../src/audit/run.ts"],"mappings":";KAEY;UAEK;;EAEf;;EAEA;;EAEA;;EAEA,iBAAiB;;UAGF;EACf;;EAEA,QAAQ;EACR;EACA;;EAEA;;EAEA;;UAGe;;EAEf;EACA;EACA;;EAEA;IACE;IACA;IACA;;;UAIa;EACf;EACA;;KAGU;UAEK;EACf;EACA;EACA,QAAQ;EACR;;EAEA;EACA,UAAU;EACV,aAAa;EACb,UAAU;;KAGA;UAEK;EACf;EACA;EACA,YAAY;EACZ,SAAS;EACT;EACA,OAAO;;EAEP;;EAEA;EACA;;qBCrDW,iBAAiB;;;;;;qBAYjB;;qBAGA;qBAEA;qBAKA;;;;;UAgBI;EACf;EACA,QAAQ;;UAEO;EACf;EACA,WAAW;EACX,QAAQ;;;;;;;;wBASM,cAAc;;wBAsBd,YAAY;;wBAKZ,eAAe,OAAO;;wBAOtB,cAAc;UAKb;;EAEf;;EAEA;;EAEA;;;wBAIc,UAAU,QAAO;;;;;;wBAqBjB,eACd,cACA;EACG;EAAyC;;;wBAgB9B,gBAAgB,oCAAoC;;wBAKpD,aAAa,eAAe;;wBAe5B,aACd,SAAS,yBACT,YAAY,4BACZ,SAAS,eACR;;wBAyCa,cACd,cACA,aACA,gCACA;EAAW;EAA6B;IACvC;wBAmBa,YACd,cACA,aACA,oBACA,SAAS,aACT,YAAY,gBACZ,SAAS,eACR;wBAYa,YAAY;EAC1B;EACA;EACA,YAAY;EACZ,SAAS;EACT,OAAO;IACL;;wBAiBY,SAAS;wBAOT,mBAAmB,MAAM,YAAY,SAAS;wBAyC9C,eAAe,QAAQ;;wBAiCvB,YAAY,QAAQ,cAAc;;wBAgBlC,cAAc,QAAQ;;wBC3VtB,WAAW,eAAe;;wBAoB1B,WAAW,aAAa,gBAAgB,cAAc;UAUrD;EACf;EACA;;;;;;;;;;wBAWoB,aAAa;EACjC;EACA;EACA;IACE,QAAQ;UA+FK;EACf;EACA;EACA,SAAS;EACT,YAAY;EACZ,cAAc;;;wBAIM,SAAS,MAAM,aAAa,QAAQ"}
|