@getvitops/generator 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,153 @@
1
1
  # @getvitops/generator
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
+
72
+ - e764090: Separate where a processor stores information from whose law can reach it.
73
+
74
+ A processor carried one optional `country`, and the privacy policy flat-deduped it into a single
75
+ sentence. That conflated two facts a disclosure has to keep apart — **where the data rests** and
76
+ **which jurisdiction can compel access** — and privacy law turns on the second: the OPC's concern
77
+ is foreign _access_, not merely foreign storage.
78
+
79
+ Ordinary arrangements were inexpressible. Azure is data-resident in Canada and US-controlled;
80
+ Zoho can serve mail from a Canadian datacentre and telemetry from the US while being headquartered
81
+ in India; a Microsoft 365 tenant can be Canadian with its identity data in the US. For all of
82
+ them every available option was wrong: `country: "Canada"` rendered the incoherent _"outside of
83
+ Canada, including Canada"_, naming the operator's country asserted storage that wasn't happening,
84
+ and omitting it dropped the processor from the disclosure **silently**.
85
+
86
+ **New on a processor:**
87
+
88
+ ```jsonc
89
+ {
90
+ "name": "Zoho",
91
+ "purpose": "receiving and storing email sent to us",
92
+ "storage": [
93
+ { "country": "Canada", "scope": "mail and productivity data" },
94
+ { "country": "the United States", "scope": "analytics and telemetry" },
95
+ ],
96
+ "operatorCountry": "India",
97
+ }
98
+ ```
99
+
100
+ - `storage[]` — where information rests, each entry optionally `scope`d to a category. The scope
101
+ is what makes "Canadian tenant, identity data in the US" sayable.
102
+ - `operatorCountry` — the jurisdiction that can compel the provider to produce it. Gets its own
103
+ sentence: _"…established in, or controlled from, jurisdictions outside of Canada, including
104
+ India. Personal information those providers handle on our behalf may be subject to the laws of
105
+ those jurisdictions even when it is stored in Canada."_
106
+ - `country` still works, as shorthand asserting **both** facts — which is what it always
107
+ asserted, since the sentence it fed claimed storage and legal reach in one breath.
108
+
109
+ **Combining `country` with either explicit field is rejected** by `vitops validate`. Whether the
110
+ shorthand narrows the explicit fact or adds to it are two readings that make contradictory legal
111
+ claims, so neither is guessed.
112
+
113
+ A country in the policy's own jurisdiction is no longer treated as a transfer, which is the
114
+ `country: "Canada"` fix. Comparison ignores case and a leading "the".
115
+
116
+ ### This changes text you have already published
117
+
118
+ **Cloudflare, Cloudflare Turnstile, Vercel and Netlify now assert operator jurisdiction and no
119
+ storage.** They previously claimed `the United States` into a sentence saying "stored or processed
120
+ in" — a claim about the wrong fact. Cloudflare is anycast: a request from Toronto is answered from
121
+ a Toronto PoP, and Workers/R2 have residency controls the config cannot see.
122
+
123
+ So a site whose only foreign element is its host or Turnstile **stops claiming foreign storage**
124
+ and instead discloses foreign legal reach. That is a retraction in the direction of accuracy, and
125
+ it deserves a re-read. **If you pin a US region, say so** — declare a processor with
126
+ `storage: [{ country: "the United States" }]`. This generalises the rule the provider table
127
+ already applied to Matomo: "we don't know" is a fact.
128
+
129
+ **Everything else renders identically.** Google Analytics, Tag Manager, Clarity, Plausible,
130
+ Matomo Cloud and any processor you declared with `country` produce the same words as before,
131
+ because the shorthand expands to both facts and the operator sentence is suppressed when it would
132
+ only restate a country already disclosed as storage. The one cosmetic change: the closing
133
+ "subject to access requests…" sentence is now its own paragraph, since it is shared by both
134
+ clauses.
135
+
136
+ **A processor with no location at all is now reported** on stderr by `vitops legal`, naming the
137
+ document it will be missing from. It cannot appear in a transfer disclosure — there is nothing
138
+ true to say — but it used to vanish in silence, which is the failure that looks tidy. It is not a
139
+ validation error: a bare `{ name, purpose }` is still valid.
140
+
141
+ New exports from `@getvitops/generator`: `processorsMissingLocation`, `JURISDICTION_COUNTRIES`,
142
+ and the `ProcessorStorage` type. `PolicyVars` gains `jurisdictionCountry`, `storageCountries`,
143
+ `scopedStorage` and `operatorCountries`; `countries` is **deprecated but retained**, and still
144
+ names every country it ever named, so a custom template keeps rendering what it rendered.
145
+
146
+ ### Patch Changes
147
+
148
+ - Updated dependencies [2c890c0]
149
+ - @getvitops/utils@4.1.0
150
+
3
151
  ## 4.0.0
4
152
 
5
153
  ### Major Changes
@@ -1382,6 +1382,10 @@
1382
1382
  "description": "Whether analytics fire in this environment.",
1383
1383
  "type": "boolean"
1384
1384
  },
1385
+ "ads": {
1386
+ "description": "Whether `site.ads` pixels fire in this environment. Defaults to `analytics`, then true — a preview deployment sending pageviews is survivable, one firing conversion pixels is not, so this can be turned off on its own.",
1387
+ "type": "boolean"
1388
+ },
1385
1389
  "robots": {
1386
1390
  "description": "Robots policy (e.g. \"noindex,nofollow\" for dev).",
1387
1391
  "type": "string"
@@ -2452,6 +2456,61 @@
2452
2456
  },
2453
2457
  "additionalProperties": false
2454
2458
  },
2459
+ "ads": {
2460
+ "description": "Ad properties this site is linked to, keyed by platform. Read by `vitops ads setup` (ensure the DNS verification record), `vitops ads tags` (emit the consent-gated pixel), and the generated cookie notice (disclose what each pixel stores). Credentials never live here — the DNS write uses CLOUDFLARE_API_TOKEN from the environment.",
2461
+ "type": "object",
2462
+ "propertyNames": {
2463
+ "type": "string",
2464
+ "enum": [
2465
+ "google",
2466
+ "meta",
2467
+ "linkedin",
2468
+ "reddit",
2469
+ "tiktok",
2470
+ "microsoft",
2471
+ "pinterest",
2472
+ "snapchat"
2473
+ ]
2474
+ },
2475
+ "additionalProperties": {
2476
+ "type": "object",
2477
+ "properties": {
2478
+ "accountId": {
2479
+ "description": "The advertising account as the platform shows it — Google Ads customer ID, Meta ad account, LinkedIn partner ID, Reddit advertiser ID. For LinkedIn this is also the Insight Tag id; everywhere else the tag id is `pixelId`.",
2480
+ "type": "string"
2481
+ },
2482
+ "pixelId": {
2483
+ "description": "The tag/pixel ID the browser snippet initialises — Meta pixel, TikTok pixel, Reddit pixel, Microsoft UET tag ID, Pinterest tag ID, Snap pixel, or the Google Ads conversion ID (`AW-…`, which is NOT the customer ID).",
2484
+ "type": "string"
2485
+ },
2486
+ "conversionLabel": {
2487
+ "description": "Google Ads only: the conversion action label paired with the `AW-…` conversion ID.",
2488
+ "type": "string"
2489
+ },
2490
+ "domain": {
2491
+ "description": "Bare hostname to verify with this platform. Defaults to the host of `domains.canonical` — set it only when the ad account is verified against a different domain.",
2492
+ "type": "string"
2493
+ },
2494
+ "domainVerification": {
2495
+ "description": "The verification token from the platform UI, for the platforms that verify by DNS TXT (Meta, TikTok, Pinterest, Snapchat). `vitops ads setup` prompts for it and writes it here on first run. NOT a secret — it is published in DNS, and the platform fetching it back is the ownership proof, exactly like the IndexNow key. A value containing `=` is used as the whole record, which is the escape hatch if a platform changes its prefix.",
2496
+ "type": "string"
2497
+ },
2498
+ "category": {
2499
+ "description": "Consent category the tag waits on (default `marketing`). An ad pixel is advertising by default; `analytics` is for a site using one purely for its own measurement.",
2500
+ "type": "string",
2501
+ "enum": [
2502
+ "marketing",
2503
+ "analytics"
2504
+ ]
2505
+ },
2506
+ "enabled": {
2507
+ "description": "Set false to keep the property on record without emitting its tag.",
2508
+ "type": "boolean"
2509
+ }
2510
+ },
2511
+ "additionalProperties": false
2512
+ }
2513
+ },
2455
2514
  "security": {
2456
2515
  "description": "Security integrations (bot protection).",
2457
2516
  "type": "object",
@@ -2568,7 +2627,32 @@
2568
2627
  "description": "Why they receive it, as a noun phrase that reads after \"for\" (e.g. \"payment processing\")."
2569
2628
  },
2570
2629
  "country": {
2571
- "description": "Where they process it, as it should read in a sentence (e.g. \"the United States\"). Feeds the cross-border-transfer disclosure.",
2630
+ "description": "Shorthand for the common case where one country is both where they store it and whose laws reach it: asserts BOTH `storage: [{ country }]` AND `operatorCountry`. Reads inside a sentence (e.g. \"the United States\"). When the two differ — a Canadian region operated by a US company — state `storage` and `operatorCountry` instead; setting this alongside either is rejected.",
2631
+ "type": "string"
2632
+ },
2633
+ "storage": {
2634
+ "description": "Where the information actually rests. Feeds the \"stored or processed outside of <jurisdiction>\" disclosure and nothing else. Several entries are allowed, each optionally scoped to a category of information — which is what makes a Canadian-region tenant holding identity data in the US expressible.",
2635
+ "type": "array",
2636
+ "items": {
2637
+ "type": "object",
2638
+ "properties": {
2639
+ "country": {
2640
+ "type": "string",
2641
+ "description": "A country or bloc where this provider stores or processes the information, as it should read in a sentence (e.g. \"Canada\", \"the United States\", \"the European Union\"). Naming the jurisdiction the policy is written for is meaningful: it says this information does not leave it."
2642
+ },
2643
+ "scope": {
2644
+ "description": "Which information is held there, as a noun phrase that reads after \"in the case of\" (e.g. \"account and sign-in information\", \"mailbox contents\"). Omit when this location holds everything the provider receives — a scoped entry claims LESS than an unscoped one, so omitting it is the safe default.",
2645
+ "type": "string"
2646
+ }
2647
+ },
2648
+ "required": [
2649
+ "country"
2650
+ ],
2651
+ "additionalProperties": false
2652
+ }
2653
+ },
2654
+ "operatorCountry": {
2655
+ "description": "The jurisdiction that can compel this provider to hand the information over — where it is established, or from which it is controlled. Reads after \"the laws of\" (e.g. \"the United States\"); a bloc is acceptable. A SEPARATE fact from `storage`, because privacy law cares about foreign *access*, not only foreign storage: a Canadian-region service run by a US company is `storage: [{ country: \"Canada\" }]` with `operatorCountry: \"the United States\"`.",
2572
2656
  "type": "string"
2573
2657
  },
2574
2658
  "privacyUrl": {
package/dist/index.d.mts CHANGED
@@ -389,6 +389,41 @@ declare const IndexingSchema: z.ZodMiniObject<{
389
389
  }, z.z.core.$strip>>;
390
390
  priorityUrls: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniURL>>;
391
391
  }, z.z.core.$strip>;
392
+ /**
393
+ * The ad platforms `vitops ads` can link a site to.
394
+ *
395
+ * A closed enum, mirroring `AD_PROVIDER_KEYS` in `@getvitops/utils/ads` — that
396
+ * module owns what each platform *can do* (whether it verifies a domain, what its
397
+ * tag is, which click ID it stamps), this owns what a config may *say*. Mirrored
398
+ * rather than derived because a `z.enum` needs literals to reach the published JSON
399
+ * Schema; `config.test.ts` asserts the two lists are the same set, so a provider
400
+ * added to one and not the other fails the build rather than silently getting no
401
+ * verification step and no tag.
402
+ */
403
+ declare const AD_PROVIDERS: readonly ["google", "meta", "linkedin", "reddit", "tiktok", "microsoft", "pinterest", "snapchat"];
404
+ type AdProvider = (typeof AD_PROVIDERS)[number];
405
+ /**
406
+ * One ad property this site is linked to.
407
+ *
408
+ * Facts, not prose — the same rule the legal block follows. This records *which*
409
+ * account and *which* tag id, and the platform capability table decides what can be
410
+ * done about them. It is also what finally lets the rest of the toolchain see an ad
411
+ * pixel: the generated cookie notice discloses its cookies, `vitops ads lint`
412
+ * checks its click ID is captured, and the consent gate is handed the cookie names
413
+ * to clear on revoke. A pixel pasted into a template is invisible to all three.
414
+ */
415
+ declare const AdPropertySchema: z.ZodMiniObject<{
416
+ accountId: z.ZodMiniOptional<z.ZodMiniString<string>>;
417
+ pixelId: z.ZodMiniOptional<z.ZodMiniString<string>>;
418
+ conversionLabel: z.ZodMiniOptional<z.ZodMiniString<string>>;
419
+ domain: z.ZodMiniOptional<z.ZodMiniString<string>>;
420
+ domainVerification: z.ZodMiniOptional<z.ZodMiniString<string>>;
421
+ category: z.ZodMiniOptional<z.ZodMiniEnum<{
422
+ analytics: "analytics";
423
+ marketing: "marketing";
424
+ }>>;
425
+ enabled: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
426
+ }, z.z.core.$strip>;
392
427
  /**
393
428
  * Which template set the generated legal documents are written against. This is
394
429
  * a closed enum rather than a free string because an unrecognised value would
@@ -398,6 +433,22 @@ declare const IndexingSchema: z.ZodMiniObject<{
398
433
  */
399
434
  declare const JURISDICTIONS: readonly ["ca"];
400
435
  type Jurisdiction = (typeof JURISDICTIONS)[number];
436
+ /**
437
+ * The country each template set frames cross-border transfers as *leaving*.
438
+ *
439
+ * A jurisdiction's own country is not a transfer, so this is what filters the
440
+ * disclosure lists — without it, `country: "Canada"` renders the incoherent
441
+ * "outside of Canada, including Canada".
442
+ *
443
+ * It belongs to the **jurisdiction**, not to `organization.address`: a `ca` policy
444
+ * says "outside of Canada" even for a business incorporated elsewhere. (The
445
+ * address drives a different sentence — the terms' governing-law clause, via
446
+ * `governingCountry`.) `satisfies` makes a new jurisdiction unable to forget it,
447
+ * the same guard style as `TEMPLATES`.
448
+ */
449
+ declare const JURISDICTION_COUNTRIES: {
450
+ ca: string;
451
+ };
401
452
  declare const SiteSectionSchema: z.ZodMiniObject<{
402
453
  defaultLocale: z.ZodMiniString<string>;
403
454
  locales: z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniObject<{
@@ -445,6 +496,7 @@ declare const SiteSectionSchema: z.ZodMiniObject<{
445
496
  url: z.ZodMiniURL;
446
497
  api: z.ZodMiniOptional<z.ZodMiniURL>;
447
498
  analytics: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
499
+ ads: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
448
500
  robots: z.ZodMiniOptional<z.ZodMiniString<string>>;
449
501
  variant: z.ZodMiniOptional<z.ZodMiniString<string>>;
450
502
  }, z.z.core.$strip>>;
@@ -667,6 +719,27 @@ declare const SiteSectionSchema: z.ZodMiniObject<{
667
719
  }>>;
668
720
  platforms: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniString<string>>>;
669
721
  }, z.z.core.$strip>>;
722
+ ads: z.ZodMiniOptional<z.ZodMiniRecord<z.ZodMiniEnum<{
723
+ google: "google";
724
+ meta: "meta";
725
+ linkedin: "linkedin";
726
+ reddit: "reddit";
727
+ tiktok: "tiktok";
728
+ microsoft: "microsoft";
729
+ pinterest: "pinterest";
730
+ snapchat: "snapchat";
731
+ }> & z.z.core.$partial, z.ZodMiniObject<{
732
+ accountId: z.ZodMiniOptional<z.ZodMiniString<string>>;
733
+ pixelId: z.ZodMiniOptional<z.ZodMiniString<string>>;
734
+ conversionLabel: z.ZodMiniOptional<z.ZodMiniString<string>>;
735
+ domain: z.ZodMiniOptional<z.ZodMiniString<string>>;
736
+ domainVerification: z.ZodMiniOptional<z.ZodMiniString<string>>;
737
+ category: z.ZodMiniOptional<z.ZodMiniEnum<{
738
+ analytics: "analytics";
739
+ marketing: "marketing";
740
+ }>>;
741
+ enabled: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
742
+ }, z.z.core.$strip>>>;
670
743
  security: z.ZodMiniOptional<z.ZodMiniObject<{
671
744
  turnstile: z.ZodMiniOptional<z.ZodMiniObject<{
672
745
  siteKey: z.ZodMiniOptional<z.ZodMiniString<string>>;
@@ -697,6 +770,11 @@ declare const SiteSectionSchema: z.ZodMiniObject<{
697
770
  name: z.ZodMiniString<string>;
698
771
  purpose: z.ZodMiniString<string>;
699
772
  country: z.ZodMiniOptional<z.ZodMiniString<string>>;
773
+ storage: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
774
+ country: z.ZodMiniString<string>;
775
+ scope: z.ZodMiniOptional<z.ZodMiniString<string>>;
776
+ }, z.z.core.$strip>>>;
777
+ operatorCountry: z.ZodMiniOptional<z.ZodMiniString<string>>;
700
778
  privacyUrl: z.ZodMiniOptional<z.ZodMiniURL>;
701
779
  }, z.z.core.$strip>>>;
702
780
  }, z.z.core.$strip>>;
@@ -1121,6 +1199,7 @@ declare const ConfigSchema: z.ZodMiniObject<{
1121
1199
  url: z.ZodMiniURL;
1122
1200
  api: z.ZodMiniOptional<z.ZodMiniURL>;
1123
1201
  analytics: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
1202
+ ads: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
1124
1203
  robots: z.ZodMiniOptional<z.ZodMiniString<string>>;
1125
1204
  variant: z.ZodMiniOptional<z.ZodMiniString<string>>;
1126
1205
  }, z.z.core.$strip>>;
@@ -1343,6 +1422,27 @@ declare const ConfigSchema: z.ZodMiniObject<{
1343
1422
  }>>;
1344
1423
  platforms: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniString<string>>>;
1345
1424
  }, z.z.core.$strip>>;
1425
+ ads: z.ZodMiniOptional<z.ZodMiniRecord<z.ZodMiniEnum<{
1426
+ google: "google";
1427
+ meta: "meta";
1428
+ linkedin: "linkedin";
1429
+ reddit: "reddit";
1430
+ tiktok: "tiktok";
1431
+ microsoft: "microsoft";
1432
+ pinterest: "pinterest";
1433
+ snapchat: "snapchat";
1434
+ }> & z.z.core.$partial, z.ZodMiniObject<{
1435
+ accountId: z.ZodMiniOptional<z.ZodMiniString<string>>;
1436
+ pixelId: z.ZodMiniOptional<z.ZodMiniString<string>>;
1437
+ conversionLabel: z.ZodMiniOptional<z.ZodMiniString<string>>;
1438
+ domain: z.ZodMiniOptional<z.ZodMiniString<string>>;
1439
+ domainVerification: z.ZodMiniOptional<z.ZodMiniString<string>>;
1440
+ category: z.ZodMiniOptional<z.ZodMiniEnum<{
1441
+ analytics: "analytics";
1442
+ marketing: "marketing";
1443
+ }>>;
1444
+ enabled: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
1445
+ }, z.z.core.$strip>>>;
1346
1446
  security: z.ZodMiniOptional<z.ZodMiniObject<{
1347
1447
  turnstile: z.ZodMiniOptional<z.ZodMiniObject<{
1348
1448
  siteKey: z.ZodMiniOptional<z.ZodMiniString<string>>;
@@ -1373,6 +1473,11 @@ declare const ConfigSchema: z.ZodMiniObject<{
1373
1473
  name: z.ZodMiniString<string>;
1374
1474
  purpose: z.ZodMiniString<string>;
1375
1475
  country: z.ZodMiniOptional<z.ZodMiniString<string>>;
1476
+ storage: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
1477
+ country: z.ZodMiniString<string>;
1478
+ scope: z.ZodMiniOptional<z.ZodMiniString<string>>;
1479
+ }, z.z.core.$strip>>>;
1480
+ operatorCountry: z.ZodMiniOptional<z.ZodMiniString<string>>;
1376
1481
  privacyUrl: z.ZodMiniOptional<z.ZodMiniURL>;
1377
1482
  }, z.z.core.$strip>>>;
1378
1483
  }, z.z.core.$strip>>;
@@ -1441,6 +1546,8 @@ type SiteIndexing = z.infer<typeof IndexingSchema>;
1441
1546
  type SiteIndexNow = z.infer<typeof IndexNowSchema>;
1442
1547
  /** Google Search Console property settings. The credential is never in the config. */
1443
1548
  type SiteSearchConsole = z.infer<typeof SearchConsoleSchema>;
1549
+ /** One `site.ads` entry — an ad property to link via `vitops ads setup`. */
1550
+ type SiteAdProperty = z.infer<typeof AdPropertySchema>;
1444
1551
  declare const CONFIG_SCHEMA_URL = "https://unpkg.com/@getvitops/generator/config.schema.json";
1445
1552
  /** The published JSON Schema (draft-2020-12), derived from the zod schema. */
1446
1553
  declare const configJsonSchema: {
@@ -1704,6 +1811,7 @@ declare const configJsonSchema: {
1704
1811
  url: z.ZodMiniURL;
1705
1812
  api: z.ZodMiniOptional<z.ZodMiniURL>;
1706
1813
  analytics: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
1814
+ ads: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
1707
1815
  robots: z.ZodMiniOptional<z.ZodMiniString<string>>;
1708
1816
  variant: z.ZodMiniOptional<z.ZodMiniString<string>>;
1709
1817
  }, z.z.core.$strip>>;
@@ -1926,6 +2034,27 @@ declare const configJsonSchema: {
1926
2034
  }>>;
1927
2035
  platforms: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniString<string>>>;
1928
2036
  }, z.z.core.$strip>>;
2037
+ ads: z.ZodMiniOptional<z.ZodMiniRecord<z.ZodMiniEnum<{
2038
+ google: "google";
2039
+ meta: "meta";
2040
+ linkedin: "linkedin";
2041
+ reddit: "reddit";
2042
+ tiktok: "tiktok";
2043
+ microsoft: "microsoft";
2044
+ pinterest: "pinterest";
2045
+ snapchat: "snapchat";
2046
+ }> & z.z.core.$partial, z.ZodMiniObject<{
2047
+ accountId: z.ZodMiniOptional<z.ZodMiniString<string>>;
2048
+ pixelId: z.ZodMiniOptional<z.ZodMiniString<string>>;
2049
+ conversionLabel: z.ZodMiniOptional<z.ZodMiniString<string>>;
2050
+ domain: z.ZodMiniOptional<z.ZodMiniString<string>>;
2051
+ domainVerification: z.ZodMiniOptional<z.ZodMiniString<string>>;
2052
+ category: z.ZodMiniOptional<z.ZodMiniEnum<{
2053
+ analytics: "analytics";
2054
+ marketing: "marketing";
2055
+ }>>;
2056
+ enabled: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
2057
+ }, z.z.core.$strip>>>;
1929
2058
  security: z.ZodMiniOptional<z.ZodMiniObject<{
1930
2059
  turnstile: z.ZodMiniOptional<z.ZodMiniObject<{
1931
2060
  siteKey: z.ZodMiniOptional<z.ZodMiniString<string>>;
@@ -1956,6 +2085,11 @@ declare const configJsonSchema: {
1956
2085
  name: z.ZodMiniString<string>;
1957
2086
  purpose: z.ZodMiniString<string>;
1958
2087
  country: z.ZodMiniOptional<z.ZodMiniString<string>>;
2088
+ storage: z.ZodMiniOptional<z.ZodMiniArray<z.ZodMiniObject<{
2089
+ country: z.ZodMiniString<string>;
2090
+ scope: z.ZodMiniOptional<z.ZodMiniString<string>>;
2091
+ }, z.z.core.$strip>>>;
2092
+ operatorCountry: z.ZodMiniOptional<z.ZodMiniString<string>>;
1959
2093
  privacyUrl: z.ZodMiniOptional<z.ZodMiniURL>;
1960
2094
  }, z.z.core.$strip>>>;
1961
2095
  }, z.z.core.$strip>>;
@@ -2851,13 +2985,48 @@ declare const BASE_HOOK: Record<string, string>;
2851
2985
  declare const TW_CLASH: ReadonlySet<string>;
2852
2986
  //#endregion
2853
2987
  //#region src/legal/providers.d.ts
2988
+ /** One place a provider holds information, and which information if not all of it. */
2989
+ interface ProcessorStorage {
2990
+ /** Reads inside a sentence: "…including the United States". */
2991
+ country: string;
2992
+ /**
2993
+ * Reads after "in the case of". Present means this location holds only SOME of
2994
+ * what the provider receives, which is a narrower claim than an unscoped entry —
2995
+ * so the template gives it its own sentence rather than folding it into the list,
2996
+ * where a reader would take it to cover everything.
2997
+ */
2998
+ scope?: string | undefined;
2999
+ }
2854
3000
  interface Processor {
2855
3001
  /** The provider as a reader would recognise it. */
2856
3002
  name: string;
2857
3003
  /** Reads after "for": "…to Stripe for payment processing". */
2858
3004
  purpose: string;
2859
- /** Reads inside a sentence: "…including the United States". */
3005
+ /**
3006
+ * Shorthand asserting BOTH `storage` and `operatorCountry` are this one country.
3007
+ *
3008
+ * Kept for the common case (and because it is public API), but note it is the
3009
+ * field whose *meaning* was never defined — its old doc comment described how it
3010
+ * READ, not what it asserted, which is how one string came to stand for two
3011
+ * different facts. Prefer the explicit pair when they differ.
3012
+ */
2860
3013
  country?: string | undefined;
3014
+ /**
3015
+ * Where the information actually rests. Feeds the "stored or processed outside
3016
+ * of <jurisdiction>" disclosure and nothing else.
3017
+ */
3018
+ storage?: ProcessorStorage[] | undefined;
3019
+ /**
3020
+ * The jurisdiction that can compel this provider to produce the information —
3021
+ * where it is established, or from which it is controlled. Reads after "the laws
3022
+ * of".
3023
+ *
3024
+ * A separate fact from `storage`, and the one privacy law actually turns on: the
3025
+ * OPC's concern is foreign *access*, not foreign storage. Azure in a Canadian
3026
+ * region is `storage: [{ country: 'Canada' }]`, `operatorCountry: 'the United
3027
+ * States'` — the bytes never move and US law still reaches them.
3028
+ */
3029
+ operatorCountry?: string | undefined;
2861
3030
  privacyUrl?: string | undefined;
2862
3031
  /**
2863
3032
  * Cookies this provider sets. An empty array is meaningful and not the same as
@@ -2918,28 +3087,28 @@ declare const KNOWN_PROCESSORS: {
2918
3087
  turnstile: {
2919
3088
  name: string;
2920
3089
  purpose: string;
2921
- country: string;
3090
+ operatorCountry: string;
2922
3091
  privacyUrl: string;
2923
3092
  cookies: string[];
2924
3093
  };
2925
3094
  cloudflare: {
2926
3095
  name: string;
2927
3096
  purpose: string;
2928
- country: string;
3097
+ operatorCountry: string;
2929
3098
  privacyUrl: string;
2930
3099
  cookies: never[];
2931
3100
  };
2932
3101
  vercel: {
2933
3102
  name: string;
2934
3103
  purpose: string;
2935
- country: string;
3104
+ operatorCountry: string;
2936
3105
  privacyUrl: string;
2937
3106
  cookies: never[];
2938
3107
  };
2939
3108
  netlify: {
2940
3109
  name: string;
2941
3110
  purpose: string;
2942
- country: string;
3111
+ operatorCountry: string;
2943
3112
  privacyUrl: string;
2944
3113
  cookies: never[];
2945
3114
  };
@@ -2961,6 +3130,19 @@ declare function detectProcessorKeys(cfg: Config): KnownProcessorKey[];
2961
3130
  * an explicit declaration is a statement of fact we have no grounds to drop.
2962
3131
  */
2963
3132
  declare function resolveProcessors(cfg: Config): Processor[];
3133
+ /**
3134
+ * Processors the config discloses but places nowhere.
3135
+ *
3136
+ * Such a processor cannot appear in the transfer disclosure — there is nothing true
3137
+ * to say about it — and today it vanishes from that section in silence, which is the
3138
+ * tidy-looking failure. Names only, and deliberately NOT on `PolicyVars`: that type
3139
+ * is the facts a template renders from, and a document must not editorialise about
3140
+ * its own gaps. The CLI reports these on stderr instead.
3141
+ *
3142
+ * Not a `validateConfig` error either: a bare `{ name, purpose }` validates today,
3143
+ * so rejecting it would make this a breaking change.
3144
+ */
3145
+ declare function processorsMissingLocation(cfg: Config): string[];
2964
3146
  //#endregion
2965
3147
  //#region src/legal/derive.d.ts
2966
3148
  /**
@@ -3018,7 +3200,42 @@ interface PolicyVars {
3018
3200
  purpose: string;
3019
3201
  retention: string;
3020
3202
  }[];
3021
- /** Reads after "including": "…the United States and the European Union". */
3203
+ /**
3204
+ * The country this document frames transfers as *leaving*, from the jurisdiction
3205
+ * rather than the org's address — a `ca` policy says "outside of Canada" even for
3206
+ * a business incorporated elsewhere. (`governingCountry` is the address, and
3207
+ * drives a different sentence: the terms' governing-law clause.)
3208
+ */
3209
+ jurisdictionCountry: string;
3210
+ /**
3211
+ * Reads after "including": foreign countries where information rests, with no
3212
+ * category attached — the broad claim.
3213
+ */
3214
+ storageCountries: string | undefined;
3215
+ /**
3216
+ * Foreign storage locations holding only SOME categories. Structured rather than
3217
+ * pre-joined, because the connective ("in the case of") is the template's word,
3218
+ * not a fact. Disjoint from `storageCountries`: a country already claimed
3219
+ * unscoped is never repeated here, since the unscoped claim is the broader one.
3220
+ */
3221
+ scopedStorage: {
3222
+ country: string;
3223
+ scope: string;
3224
+ }[];
3225
+ /**
3226
+ * Reads after "the laws of": foreign jurisdictions that can compel a provider to
3227
+ * produce information, EXCLUDING any already named in `storageCountries` —
3228
+ * saying US law reaches information already disclosed as stored in the US adds a
3229
+ * sentence and no fact.
3230
+ */
3231
+ operatorCountries: string | undefined;
3232
+ /**
3233
+ * Reads after "including": "…the United States and the European Union".
3234
+ *
3235
+ * @deprecated Split into `storageCountries` and `operatorCountries`, which keep
3236
+ * residency and legal reach apart. Retained, and still every country the field
3237
+ * ever named, so a consumer's own template renders what it always rendered.
3238
+ */
3022
3239
  countries: string | undefined;
3023
3240
  consentModel: 'opt-in' | 'opt-out' | 'none';
3024
3241
  cookieCategories: string[];
@@ -3126,5 +3343,5 @@ declare function generateLegal(cfg: Config, opts?: GenerateLegalOptions): Record
3126
3343
  */
3127
3344
  declare function defaultConfig(): DesignSystem;
3128
3345
  //#endregion
3129
- export { BASE_HOOK, type Block, CONFIG_SCHEMA_URL, CONTRAST, type ColorUtility, type Config, ConfigSchema, type ConfigValidationResult, type ContactObject, DARK_SEL, DOC_ORDER, DOC_SLUGS, type DesignSystem, DesignSystemSchema, type DocSet, type ExpandedHue, type Format, type FunctionalRole, type GenerateLegalOptions, type GenerateOptions, type GenerateResult, type IconSpriteOptions, type IconSpriteResult, JURISDICTIONS, type JsonSchemaNode, type Jurisdiction, KNOWN_PROCESSORS, type KnownProcessorKey, type LegalDoc, type LegalOutput, NUMERIC_STEPS, type OrganizationConfig, type PolicyVars, type PostalAddress, type Processor, REQUIRED_ROLES, ROLE_TOKEN_KEYS, type ResolvedInput, type RoleKind, type RoleSpec, SCHEMA_URL, type SchemaTreeNode, type SchemaTreeOptions, type SiteFont, SiteFontSchema, type SiteIndexNow, type SiteIndexing, type SiteSearchConsole, type SiteSection, type Span, type StylesheetFormat, TEMPLATES, TIERS, TIER_NAMES, TW_CLASH, type Tier, type TierAstro, type TierCss, type TierEntry, type TierProjection, type TierWc, type ValidationResult, buildIconSprite, configJsonSchema, deepMerge, defaultConfig, derivePolicyVars, detectProcessorKeys, enabledDocs, expandPalette, functionalRole, generate, generateDocs, generateLegal, isConfig, jsonSchema, loadIconSvg, parseMarkdown, renderInlineMarkdown, renderMarkdown, renderNodes, resolveConfig, resolveInput, resolvePrivacyContact, resolveProcessors, resolveTheme, roleColorUtilities, roleHue, roleKind, schemaTreeNodes, spriteId, stripNulls, tierPatterns, tierTags, toContentNodes, toHtmlFragment, toPortableText, tokenClass, tokenVar, validate, validateConfig };
3346
+ export { AD_PROVIDERS, type AdProvider, BASE_HOOK, type Block, CONFIG_SCHEMA_URL, CONTRAST, type ColorUtility, type Config, ConfigSchema, type ConfigValidationResult, type ContactObject, DARK_SEL, DOC_ORDER, DOC_SLUGS, type DesignSystem, DesignSystemSchema, type DocSet, type ExpandedHue, type Format, type FunctionalRole, type GenerateLegalOptions, type GenerateOptions, type GenerateResult, type IconSpriteOptions, type IconSpriteResult, JURISDICTIONS, JURISDICTION_COUNTRIES, type JsonSchemaNode, type Jurisdiction, KNOWN_PROCESSORS, type KnownProcessorKey, type LegalDoc, type LegalOutput, NUMERIC_STEPS, type OrganizationConfig, type PolicyVars, type PostalAddress, type Processor, type ProcessorStorage, REQUIRED_ROLES, ROLE_TOKEN_KEYS, type ResolvedInput, type RoleKind, type RoleSpec, SCHEMA_URL, type SchemaTreeNode, type SchemaTreeOptions, type SiteAdProperty, type SiteFont, SiteFontSchema, type SiteIndexNow, type SiteIndexing, type SiteSearchConsole, type SiteSection, type Span, type StylesheetFormat, TEMPLATES, TIERS, TIER_NAMES, TW_CLASH, type Tier, type TierAstro, type TierCss, type TierEntry, type TierProjection, type TierWc, type ValidationResult, buildIconSprite, configJsonSchema, deepMerge, defaultConfig, derivePolicyVars, detectProcessorKeys, enabledDocs, expandPalette, functionalRole, generate, generateDocs, generateLegal, isConfig, jsonSchema, loadIconSvg, parseMarkdown, processorsMissingLocation, renderInlineMarkdown, renderMarkdown, renderNodes, resolveConfig, resolveInput, resolvePrivacyContact, resolveProcessors, resolveTheme, roleColorUtilities, roleHue, roleKind, schemaTreeNodes, spriteId, stripNulls, tierPatterns, tierTags, toContentNodes, toHtmlFragment, toPortableText, tokenClass, tokenVar, validate, validateConfig };
3130
3347
  //# sourceMappingURL=index.d.mts.map