@getvitops/utils 7.0.0 → 8.0.1
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 +133 -13
- package/dist/adc-BFrzpMHq.d.mts +25 -0
- package/dist/adc-BFrzpMHq.d.mts.map +1 -0
- package/dist/adc-D6aw2qgq.mjs +45 -0
- package/dist/adc-D6aw2qgq.mjs.map +1 -0
- package/dist/ads/index.d.mts +0 -1
- package/dist/ads/index.d.mts.map +1 -1
- package/dist/ads/index.mjs +4 -13
- package/dist/ads/index.mjs.map +1 -1
- package/dist/{cloudflare-CD8vwrgj.mjs → cloudflare-CgAsXiqe.mjs} +24 -2
- package/dist/cloudflare-CgAsXiqe.mjs.map +1 -0
- package/dist/color/index.d.mts +0 -3
- package/dist/color/index.d.mts.map +1 -1
- package/dist/color/index.mjs +0 -3
- package/dist/color/index.mjs.map +1 -1
- package/dist/index.d.mts +5 -190
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +5 -240
- package/dist/index.mjs.map +1 -1
- package/dist/indexing/index.d.mts +2 -24
- package/dist/indexing/index.d.mts.map +1 -1
- package/dist/indexing/index.mjs +7 -69
- package/dist/indexing/index.mjs.map +1 -1
- package/dist/json-store-30CFCbZZ.mjs +38 -0
- package/dist/json-store-30CFCbZZ.mjs.map +1 -0
- package/dist/media/index.d.mts +0 -2
- package/dist/media/index.d.mts.map +1 -1
- package/dist/media/index.mjs +4 -20
- package/dist/media/index.mjs.map +1 -1
- package/dist/notify/index.d.mts +1 -2
- package/dist/notify/index.d.mts.map +1 -1
- package/dist/onboarding/index.d.mts +1 -1
- package/dist/onboarding/index.d.mts.map +1 -1
- package/dist/onboarding/index.mjs +7 -17
- package/dist/onboarding/index.mjs.map +1 -1
- package/dist/reviews/index.d.mts +154 -0
- package/dist/reviews/index.d.mts.map +1 -0
- package/dist/reviews/index.mjs +218 -0
- package/dist/reviews/index.mjs.map +1 -0
- package/dist/schema/index.d.mts +191 -0
- package/dist/schema/index.d.mts.map +1 -0
- package/dist/schema/index.mjs +241 -0
- package/dist/schema/index.mjs.map +1 -0
- package/dist/security/index.d.mts +48 -0
- package/dist/security/index.d.mts.map +1 -0
- package/dist/security/index.mjs +80 -0
- package/dist/security/index.mjs.map +1 -0
- package/dist/{token-BlShEM6J.d.mts → token-Bz_-igw-.d.mts} +3 -2
- package/dist/{token-BlShEM6J.d.mts.map → token-Bz_-igw-.d.mts.map} +1 -1
- package/dist/{token-CuyjDo6I.mjs → token-DkKC5m23.mjs} +16 -3
- package/dist/{token-CuyjDo6I.mjs.map → token-DkKC5m23.mjs.map} +1 -1
- package/dist/tracking/index.d.mts +1 -1
- package/dist/{types-C3lHaSrU.d.mts → types-CHHjzRrv.d.mts} +1 -2
- package/dist/types-CHHjzRrv.d.mts.map +1 -0
- package/package.json +25 -2
- package/dist/cloudflare-CD8vwrgj.mjs.map +0 -1
- package/dist/types-C3lHaSrU.d.mts.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,133 @@
|
|
|
1
1
|
# @getvitops/utils
|
|
2
2
|
|
|
3
|
+
## 8.0.1
|
|
4
|
+
|
|
5
|
+
## 8.0.0
|
|
6
|
+
|
|
7
|
+
### Major Changes
|
|
8
|
+
|
|
9
|
+
- Rebuild the lightbox on one dialog + the existing carousel, fixing the flicker on
|
|
10
|
+
prev/next and the stutter on open.
|
|
11
|
+
|
|
12
|
+
**Breaking (`css`/`bricks` and `tailwind` formats — `.lightbox-dialog`'s markup
|
|
13
|
+
contract changed):**
|
|
14
|
+
|
|
15
|
+
- `<Gallery />` now emits ONE `<dialog class="lightbox-dialog">` per gallery,
|
|
16
|
+
wrapping a `<Carousel />`, instead of one dialog per image. A `.gallery__trigger`
|
|
17
|
+
is now a plain `<a href="#slide-id">`, not a `command="show-modal"` button.
|
|
18
|
+
- `.lightbox-dialog__content` / `__image` / `__nav` / `__nav-button` /
|
|
19
|
+
`__caption` are gone. The dialog's child is `.lightbox-dialog__track` (a
|
|
20
|
+
`.carousel`) — prev/next, dots, arrow keys and swipe are the carousel's,
|
|
21
|
+
reused rather than reimplemented. `.lightbox-dialog__close` is unchanged.
|
|
22
|
+
- `<wc-gallery>` no longer builds nav buttons or steps between images — the
|
|
23
|
+
nested `<wc-carousel>` does. It fires `gallery-open` (unchanged shape) and
|
|
24
|
+
re-emits the carousel's own stepping as `gallery-change`.
|
|
25
|
+
- `packages/core/src/web-components/utils/gallery.ts` dropped `keyToAction` and
|
|
26
|
+
`swipeAction` (the carousel's own key/swipe handling covers both now).
|
|
27
|
+
`stepIndex`/`preloadTargets` are unchanged.
|
|
28
|
+
|
|
29
|
+
**Why:** stepping used to close one `<dialog>` and open another, which made
|
|
30
|
+
`animation.css`'s top-layer driver play a full exit-then-entry transition on
|
|
31
|
+
every step — visible as a flicker on prev/next, by construction, not a bug to
|
|
32
|
+
tune away. Opening also awaited the full image's `decode()` before starting the
|
|
33
|
+
view-transition morph, so the dialog visibly stalled on a slow network. One
|
|
34
|
+
dialog containing one carousel makes the first failure mode unrepresentable
|
|
35
|
+
(stepping is a scroll, not a close/open pair) and removes the wait for the
|
|
36
|
+
second (the morph starts immediately, painting the thumbnail as a CSS
|
|
37
|
+
background placeholder until the full image arrives).
|
|
38
|
+
|
|
39
|
+
**Also improved, not just fixed:** the no-JS fallback is no longer a _stripped_
|
|
40
|
+
lightbox — it's a genuine one. Each trigger is ordinary fragment navigation
|
|
41
|
+
(`:has(:target)` in `lightbox.css` reveals the dialog), which needs no Invoker
|
|
42
|
+
Commands polyfill in any engine, and it now has working prev/next with no
|
|
43
|
+
JavaScript at all — the previous per-dialog fallback had no way to express that.
|
|
44
|
+
|
|
45
|
+
**Migration:** a consumer using `<Gallery />` needs no changes — the component's
|
|
46
|
+
props are unchanged. Anyone who hand-authored `.lightbox-dialog` markup (per
|
|
47
|
+
`docs/concepts/patterns.md` / `docs/css/classes.md`) must move to the new single-
|
|
48
|
+
dialog-wrapping-a-carousel shape; see `patterns.astro`'s "Gallery, hand-authored"
|
|
49
|
+
demo for the reference markup. `<wc-gallery>` consumers listening for
|
|
50
|
+
`gallery-change` keep the same event shape (`{ index, total }`).
|
|
51
|
+
|
|
52
|
+
- a84839b: **Breaking — schema.org JSON-LD graph builders moved to `@getvitops/utils/schema`.**
|
|
53
|
+
|
|
54
|
+
`articleGraph`, `breadcrumbGraph`, `faqGraph`, `itemListGraph`, `localBusinessGraph` and
|
|
55
|
+
`organizationGraph` (plus their option/type exports) are no longer exported from the bare
|
|
56
|
+
`@getvitops/utils` entry point. Import them from `@getvitops/utils/schema` instead:
|
|
57
|
+
|
|
58
|
+
```diff
|
|
59
|
+
-import { articleGraph } from '@getvitops/utils';
|
|
60
|
+
+import { articleGraph } from '@getvitops/utils/schema';
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The bare entry point pulls in `sharp`/`png-to-ico` for favicon generation — exactly what a
|
|
64
|
+
server-rendered page must not carry into its Worker bundle when it renders a `<Article />`/
|
|
65
|
+
`<Organization />`/etc. schema component under `output: 'server'`. This is the same split
|
|
66
|
+
`notify`/`tracking`/`security` already made, for the same reason.
|
|
67
|
+
|
|
68
|
+
`@getvitops/astro`'s own `<Article />`/`<Organization />`/`<Breadcrumb />`/`<FAQ />`/
|
|
69
|
+
`<LocalBusiness />`/`<Carousel />` schema components already import from the new subpath as of
|
|
70
|
+
this release, so a consumer using only those needs no change — only a consumer importing these
|
|
71
|
+
builders directly from `@getvitops/utils`.
|
|
72
|
+
|
|
73
|
+
### Minor Changes
|
|
74
|
+
|
|
75
|
+
- Add Google Business Profile reviews: fetch, cache, render.
|
|
76
|
+
|
|
77
|
+
- **`vitops reviews`** (new CLI command) fetches a client's GBP reviews (legacy v4 API — the only
|
|
78
|
+
one that has them) and writes them to `.vitops/reviews.json`, meant to be committed so a build
|
|
79
|
+
never needs a live Google credential. `--dry` prints the plan; `--check` diffs against the cache
|
|
80
|
+
without writing. Requires the same `VITOPS_GOOGLE_CLIENT_ID`/`_CLIENT_SECRET`/`_REFRESH_TOKEN` (or
|
|
81
|
+
ADC login) `search setup` uses, scoped to `business.manage` — **and the calling GCP project must
|
|
82
|
+
be allowlisted by Google first**, a separate one-time approval from every other Google API here.
|
|
83
|
+
- **`site.reviews`** (config): which `organization.locations` to fetch, and per-location
|
|
84
|
+
`featured`/`minRating`/`sort`/`limit` selection. `schema` (JSON-LD) and `avatars` both default to
|
|
85
|
+
**off** — see the migration notes below.
|
|
86
|
+
- **`@getvitops/utils/reviews`** (new subpath): the pure plan/select/merge functions plus the GBP
|
|
87
|
+
fetcher and cache reader/writer, mirroring `./indexing`'s pure/IO split.
|
|
88
|
+
- **`<ReviewCard>`** (new Astro component, `@getvitops/astro`): one review card from a cache entry.
|
|
89
|
+
No web component — nothing here needs progressive enhancement. Composes the existing
|
|
90
|
+
`schemas/Review.astro` for JSON-LD only when both `schema` and `itemReviewed` are passed.
|
|
91
|
+
- **`<Marquee>`** (new Astro component): the tier-3 wrapper `wc-marquee`/`marquee.css` was missing.
|
|
92
|
+
- **`.review` pattern** (`css`/`bricks` and `tailwind` formats): a new config-generated
|
|
93
|
+
`patterns.items.review` entry plus `packages/core/css/patterns/review.css` for the structural
|
|
94
|
+
parts (rating, identity, body, reply, source link). Two new semantic icon names, `star` and
|
|
95
|
+
`star-half`, across every icon set.
|
|
96
|
+
|
|
97
|
+
**Migration:** no breaking change. A site adding `site.reviews` should read the "Google reviews"
|
|
98
|
+
section in AGENTS.md before enabling `schema` — Google's structured-data policy treats a business
|
|
99
|
+
marking up reviews of itself as ineligible for review rich results and risks a manual action, which
|
|
100
|
+
is why it defaults off.
|
|
101
|
+
|
|
102
|
+
- Repo-wide simplification pass: de-boilerplate the CLI, dedupe shared cache/plan
|
|
103
|
+
primitives in `@getvitops/utils`, drop dead code, and tighten dependency hygiene.
|
|
104
|
+
|
|
105
|
+
No breaking changes. (`vitops icons`'s `--site` flag was briefly going to be renamed to
|
|
106
|
+
`--input` for consistency with the other `Config`-anchored commands — deferred rather than
|
|
107
|
+
spending a major bump on a rename with no other breaking change riding along; it stays
|
|
108
|
+
`--site` for now.)
|
|
109
|
+
|
|
110
|
+
**Fixed:**
|
|
111
|
+
|
|
112
|
+
- `@getvitops/core`'s 12 runtime deps (`lit`, both `@oddbird` polyfills, both
|
|
113
|
+
`@webcomponents` packages, `colorjs.io`, etc.) were listed as `dependencies` despite
|
|
114
|
+
being bundled into `dist/elements.js`/`dist/polyfills.js`/`dist/deferred.js` — a
|
|
115
|
+
consumer installed them twice for nothing. Moved to `devDependencies`; no change to
|
|
116
|
+
what ships.
|
|
117
|
+
- Deleted `old-css-lib/` (59 files), superseded by `packages/core/css/` since the
|
|
118
|
+
in-memory bundling change — no consumer-facing effect, it was not part of any
|
|
119
|
+
published output.
|
|
120
|
+
- Deleted a dead Astro module (`packages/astro/src/scripts/deferred.ts`) that had
|
|
121
|
+
silently diverged from `packages/core/src/js/deferred.ts` and had zero importers.
|
|
122
|
+
|
|
123
|
+
**No consumer-facing change in `@getvitops/vite`** beyond the fixed-group version bump —
|
|
124
|
+
task-graph and caching fixes internal to this repo's own build.
|
|
125
|
+
|
|
126
|
+
Everything else (the `cli.ts` file split into `commands/{search,ads}.ts`, shared
|
|
127
|
+
`json-store.ts`/`plan-format.ts` helpers in `@getvitops/utils`, a `.github/actions/setup`
|
|
128
|
+
composite action, restatement-comment cleanup) is internal refactoring with no change to
|
|
129
|
+
any published API, CLI flag, or generated output.
|
|
130
|
+
|
|
3
131
|
## 7.0.0
|
|
4
132
|
|
|
5
133
|
### Minor Changes
|
|
@@ -393,28 +521,20 @@ here changed, and no code that imports `@getvitops/utils` needs editing.
|
|
|
393
521
|
{
|
|
394
522
|
"designSystem": {
|
|
395
523
|
"themes": {
|
|
396
|
-
"default": {
|
|
397
|
-
/* … */
|
|
398
|
-
},
|
|
524
|
+
"default": {/* … */},
|
|
399
525
|
},
|
|
400
526
|
},
|
|
401
527
|
"organization": {
|
|
402
528
|
"name": "Acme",
|
|
403
529
|
"contact": "hq",
|
|
404
530
|
"locations": {
|
|
405
|
-
"hq": {
|
|
406
|
-
/* … */
|
|
407
|
-
},
|
|
531
|
+
"hq": {/* … */},
|
|
408
532
|
},
|
|
409
533
|
},
|
|
410
534
|
"site": {
|
|
411
535
|
"defaultLocale": "en",
|
|
412
|
-
"domains": {
|
|
413
|
-
|
|
414
|
-
},
|
|
415
|
-
"analytics": {
|
|
416
|
-
/* … */
|
|
417
|
-
},
|
|
536
|
+
"domains": {/* … */},
|
|
537
|
+
"analytics": {/* … */},
|
|
418
538
|
},
|
|
419
539
|
}
|
|
420
540
|
```
|
|
@@ -483,7 +603,7 @@ here changed, and no code that imports `@getvitops/utils` needs editing.
|
|
|
483
603
|
Also new, and worth wiring at the same time — **`gitLastmod()` for real sitemap dates**:
|
|
484
604
|
|
|
485
605
|
```js
|
|
486
|
-
import vitops, { gitLastmod } from
|
|
606
|
+
import vitops, { gitLastmod } from "@getvitops/astro";
|
|
487
607
|
vitops({ sitemap: { serialize: await gitLastmod() } });
|
|
488
608
|
```
|
|
489
609
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { i as GoogleOAuth } from "./token-Bz_-igw-.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/google/adc.d.ts
|
|
4
|
+
/** A parsed ADC user credential, or `undefined` if `raw` is not one. */
|
|
5
|
+
declare function parseAdcUser(raw: string): GoogleOAuth | undefined;
|
|
6
|
+
/** The quota project gcloud recorded with an ADC credential, if any. */
|
|
7
|
+
declare function adcQuotaProject(raw: string): string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Where gcloud keeps ADC when nothing points at it explicitly.
|
|
10
|
+
*
|
|
11
|
+
* Reading the well-known path is what makes "I'm logged in" mean something, instead
|
|
12
|
+
* of asking the user to export a variable at a file gcloud already wrote.
|
|
13
|
+
* `CLOUDSDK_CONFIG` wins if set, as it does for gcloud itself.
|
|
14
|
+
*
|
|
15
|
+
* Every input is a parameter so the platform branches are testable on one machine.
|
|
16
|
+
*/
|
|
17
|
+
declare function adcCredentialsPath(opts: {
|
|
18
|
+
env: Record<string, string | undefined>;
|
|
19
|
+
platform: string;
|
|
20
|
+
home: string | undefined;
|
|
21
|
+
join: (...parts: string[]) => string;
|
|
22
|
+
}): string | undefined;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { adcQuotaProject as n, parseAdcUser as r, adcCredentialsPath as t };
|
|
25
|
+
//# sourceMappingURL=adc-BFrzpMHq.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adc-BFrzpMHq.d.mts","names":[],"sources":["../src/google/adc.ts"],"mappings":";;;;iBAkBgB,YAAA,CAAa,GAAA,WAAc,WAAW;;iBAwBtC,eAAA,CAAgB,GAAW;;;;;;AAsB1C;;;;iBALe,kBAAA,CAAmB,IAAA;EACjC,GAAA,EAAK,MAAM;EACX,QAAA;EACA,IAAA;EACA,IAAA,MAAU,KAAA;AAAA"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/google/adc.ts
|
|
2
|
+
/** A parsed ADC user credential, or `undefined` if `raw` is not one. */
|
|
3
|
+
function parseAdcUser(raw) {
|
|
4
|
+
let parsed;
|
|
5
|
+
try {
|
|
6
|
+
parsed = JSON.parse(raw);
|
|
7
|
+
} catch {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (parsed.type !== "authorized_user") return void 0;
|
|
11
|
+
if (!parsed.client_id || !parsed.client_secret || !parsed.refresh_token) return void 0;
|
|
12
|
+
return {
|
|
13
|
+
clientId: parsed.client_id,
|
|
14
|
+
clientSecret: parsed.client_secret,
|
|
15
|
+
refreshToken: parsed.refresh_token
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** The quota project gcloud recorded with an ADC credential, if any. */
|
|
19
|
+
function adcQuotaProject(raw) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(raw).quota_project_id || void 0;
|
|
22
|
+
} catch {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Where gcloud keeps ADC when nothing points at it explicitly.
|
|
28
|
+
*
|
|
29
|
+
* Reading the well-known path is what makes "I'm logged in" mean something, instead
|
|
30
|
+
* of asking the user to export a variable at a file gcloud already wrote.
|
|
31
|
+
* `CLOUDSDK_CONFIG` wins if set, as it does for gcloud itself.
|
|
32
|
+
*
|
|
33
|
+
* Every input is a parameter so the platform branches are testable on one machine.
|
|
34
|
+
*/
|
|
35
|
+
function adcCredentialsPath(opts) {
|
|
36
|
+
const { env, platform, home, join } = opts;
|
|
37
|
+
const sdk = env.CLOUDSDK_CONFIG;
|
|
38
|
+
if (sdk) return join(sdk, "application_default_credentials.json");
|
|
39
|
+
if (platform === "win32") return env.APPDATA ? join(env.APPDATA, "gcloud", "application_default_credentials.json") : void 0;
|
|
40
|
+
return home ? join(home, ".config", "gcloud", "application_default_credentials.json") : void 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { adcQuotaProject as n, parseAdcUser as r, adcCredentialsPath as t };
|
|
45
|
+
//# sourceMappingURL=adc-D6aw2qgq.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adc-D6aw2qgq.mjs","names":[],"sources":["../src/google/adc.ts"],"sourcesContent":["/**\n * Application Default Credentials — recognising a credential gcloud already wrote.\n *\n * `gcloud auth application-default login` writes\n * `{ client_id, client_secret, refresh_token, type: \"authorized_user\" }`, which maps\n * field-for-field onto the refresh-token grant this package already performs. So the\n * only thing that ever stood between \"I'm logged in to gcloud\" and `vitops search`\n * was recognising the shape.\n *\n * Pure, and here rather than in the CLI, for the reason the consent store and the\n * indexing planner are: the decidable part (is this a user credential? where would\n * gcloud have put it?) is worth asserting without a filesystem, and the CLI keeps\n * only the `readFileSync`. `type` is the discriminator Google's own libraries switch\n * on — never the presence of fields, since a service-account JSON also has a `type`.\n */\nimport type { GoogleOAuth } from './token.ts';\n\n/** A parsed ADC user credential, or `undefined` if `raw` is not one. */\nexport function parseAdcUser(raw: string): GoogleOAuth | undefined {\n let parsed: {\n type?: string;\n client_id?: string;\n client_secret?: string;\n refresh_token?: string;\n };\n try {\n parsed = JSON.parse(raw) as typeof parsed;\n } catch {\n return undefined;\n }\n if (parsed.type !== 'authorized_user') return undefined;\n // A truncated or half-written file is not a credential. Returning a partial one\n // would defer the failure to a token exchange whose error names the wrong cause.\n if (!parsed.client_id || !parsed.client_secret || !parsed.refresh_token) return undefined;\n return {\n clientId: parsed.client_id,\n clientSecret: parsed.client_secret,\n refreshToken: parsed.refresh_token,\n };\n}\n\n/** The quota project gcloud recorded with an ADC credential, if any. */\nexport function adcQuotaProject(raw: string): string | undefined {\n try {\n return (JSON.parse(raw) as { quota_project_id?: string }).quota_project_id || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Where gcloud keeps ADC when nothing points at it explicitly.\n *\n * Reading the well-known path is what makes \"I'm logged in\" mean something, instead\n * of asking the user to export a variable at a file gcloud already wrote.\n * `CLOUDSDK_CONFIG` wins if set, as it does for gcloud itself.\n *\n * Every input is a parameter so the platform branches are testable on one machine.\n */\nexport function adcCredentialsPath(opts: {\n env: Record<string, string | undefined>;\n platform: string;\n home: string | undefined;\n join: (...parts: string[]) => string;\n}): string | undefined {\n const { env, platform, home, join } = opts;\n const sdk = env.CLOUDSDK_CONFIG;\n if (sdk) return join(sdk, 'application_default_credentials.json');\n if (platform === 'win32') {\n return env.APPDATA\n ? join(env.APPDATA, 'gcloud', 'application_default_credentials.json')\n : undefined;\n }\n return home ? join(home, '.config', 'gcloud', 'application_default_credentials.json') : undefined;\n}\n"],"mappings":";;AAkBA,SAAgB,aAAa,KAAsC;CACjE,IAAI;CAMJ,IAAI;EACF,SAAS,KAAK,MAAM,GAAG;CACzB,QAAQ;EACN;CACF;CACA,IAAI,OAAO,SAAS,mBAAmB,OAAO;CAG9C,IAAI,CAAC,OAAO,aAAa,CAAC,OAAO,iBAAiB,CAAC,OAAO,eAAe,OAAO;CAChF,OAAO;EACL,UAAU,OAAO;EACjB,cAAc,OAAO;EACrB,cAAc,OAAO;CACvB;AACF;;AAGA,SAAgB,gBAAgB,KAAiC;CAC/D,IAAI;EACF,OAAQ,KAAK,MAAM,GAAG,CAAC,CAAmC,oBAAoB;CAChF,QAAQ;EACN;CACF;AACF;;;;;;;;;;AAWA,SAAgB,mBAAmB,MAKZ;CACrB,MAAM,EAAE,KAAK,UAAU,MAAM,SAAS;CACtC,MAAM,MAAM,IAAI;CAChB,IAAI,KAAK,OAAO,KAAK,KAAK,sCAAsC;CAChE,IAAI,aAAa,SACf,OAAO,IAAI,UACP,KAAK,IAAI,SAAS,UAAU,sCAAsC,IAClE;CAEN,OAAO,OAAO,KAAK,MAAM,WAAW,UAAU,sCAAsC,IAAI;AAC1F"}
|
package/dist/ads/index.d.mts
CHANGED
|
@@ -66,7 +66,6 @@ interface AdsConfig {
|
|
|
66
66
|
* saying rather than silently adding a second record.
|
|
67
67
|
*/
|
|
68
68
|
interface AdDomainState {
|
|
69
|
-
/** The Cloudflare zone id for the domain, if one was found. */
|
|
70
69
|
zoneId?: string | undefined;
|
|
71
70
|
/** Every apex TXT record content, unquoted. */
|
|
72
71
|
txtContents: string[];
|
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":";;;;;;AAsBA;;;;AAAsB;AAWtB;;;;AAA6B;AAG7B;;;;;;;;AAAA,KAdY,UAAA;;KAWA,iBAAA;;UAGK,eAAA;EACf,QAAA,EAAU,UAAA;EAsBH;AAAA;AAIT;;EArBE,SAAA;EAuBA;EArBA,OAAA;EAiCe;EA/Bf,eAAA;;EAEA,MAAA;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/ads/types.ts","../../src/ads/plan.ts","../../src/ads/providers.ts"],"mappings":";;;;;;AAsBA;;;;AAAsB;AAWtB;;;;AAA6B;AAG7B;;;;;;;;AAAA,KAdY,UAAA;;KAWA,iBAAA;;UAGK,eAAA;EACf,QAAA,EAAU,UAAA;EAsBH;AAAA;AAIT;;EArBE,SAAA;EAuBA;EArBA,OAAA;EAiCe;EA/Bf,eAAA;;EAEA,MAAA;EAgCW;;;;ACvDb;;;ED+BE,kBAAA;EC/BoB;EDiCpB,QAAA,GAAW,iBAAiB;EAC5B,OAAA;AAAA;;UAIe,SAAA;ECjCA;EDmCf,UAAA,EAAY,eAAe;AAAA;;;;;;;;AC9BP;AAItB;UDsCiB,aAAA;EACf,MAAA;ECtCU;EDwCV,WAAW;AAAA;;;;KCvDD,UAAA;ADHZ;AAAA,KCMY,YAAA;AAAA,UAEK,QAAA;EACf,MAAA,EAAQ,UAAA;EDTY;ECWpB,MAAA;EDA2B;ECE3B,KAAA,GAAQ,YAAY;AAAA;ADFO;AAAA,UCMZ,cAAA;EACf,QAAA,EAAU,eAAA;;EAEV,IAAA;EDLA;ECOA,MAAA;EDFA;ECIA,GAAA,EAAK,QAAA;EDAL;ECEA,GAAA,EAAK,QAAA;EDQL;ECNA,UAAA;EDQW;ECNX,SAAA;AAAA;AAAA,UAGe,OAAA;EACf,UAAA,EAAY,cAAc;EDOF;ECLxB,KAAA;AAAA;ADO2B;AAY7B;;;;AAGa;;;;AAfgB,iBCKb,YAAA,CAAa,KAAA,EAAO,eAAA,EAAiB,KAAA,EAAO,aAAA,GAAgB,cAAA;;iBAoD5D,IAAA,CAAK,MAAA,EAAQ,SAAA,EAAW,MAAA,EAAQ,GAAA,SAAY,aAAA,IAAiB,OAAA;;iBAe7D,QAAA,CAAS,CAAU,EAAP,OAAO;AA7GnC;AAAA,iBAkHgB,aAAA,CAAc,CAAA,EAAG,OAAA;EAAY,QAAA;EAAkB,KAAA,EAAO,YAAY;AAAA;AAhHlF;AAAA,iBA4HgB,UAAA,CAAW,CAAU,EAAP,OAAO;;UAcpB,gBAAA;EACf,QAAA;EACA,MAAA;EACA,GAAA;EACA,GAAA;EACA,SAAA;AAAA;AA1IoB;AAAA,iBA8IN,aAAA,CAAc,OAA2B,EAAlB,gBAAgB;;;ADhH1B;AAAA,UE9BZ,kBAAA;EACf,MAAA;;;AF4CW;;;;EErCX,SAAA;EDlBoB;ECoBpB,KAAA;EDpBoB;ECsBpB,MAAA;AAAA;;UAIe,SAAA;EDvBO;ECyBtB,KAAA;EDvBe;ECyBf,GAAA,IAAO,EAAA;;EAEP,SAAA,GAAY,EAAA,UAAY,KAAA,EAAO,eAAe;ED1B9C;;;;EC+BA,OAAA;ED3BoB;EC6BpB,UAAA;EDzBe;EC2Bf,MAAA;AAAA;AAAA,UAGe,UAAA;EDvBV;ECyBL,IAAA;EDvBa;;;;;;;;ECgCb,eAAA;EDhCK;ECkCL,aAAA;ED9BA;ECgCA,UAAA;EACA,YAAA,EAAc,kBAAA;EACd,GAAA,EAAK,SAAA;ED/BiB;ECiCtB,WAAA,GAAc,KAAA,EAAO,eAAA;AAAA;AAAA,cAaV,YAAA,EAAc,MAAA,CAAO,UAAA,EAAY,UAAA;;cAuMjC,gBAAA,EAAgD,UAAU;;cAG1D,YAAA,GAAgB,KAAA,aAAgB,KAAA,IAAS,UAAmC;ADzOzF;;;;;;;;;AAAA,iBCoPgB,SAAA,CAAU,QAAA,EAAU,UAAU,EAAE,KAAA;;iBAOhC,KAAA,CAAM,KAAsB,EAAf,eAAe;;cAM/B,UAAA,GAAc,KAAA,EAAO,eAAA,KAAkB,iBACrB;ADlQ2D;AAoD1F;;;;;;;;;;AApD0F,iBCkR1E,SAAA,CACd,KAAA,EAAO,eAAe,EACtB,QAAA"}
|
package/dist/ads/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as findZoneId, r as listApexTxt, t as createApexTxt } from "../cloudflare-
|
|
1
|
+
import { i as table, n as findZoneId, r as listApexTxt, t as createApexTxt } from "../cloudflare-CgAsXiqe.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/ads/providers.ts
|
|
4
4
|
/** `gtag` is shared by every Google tag; the id shape (`AW-…`) is what differs. */
|
|
@@ -351,28 +351,19 @@ function formatPlan(p) {
|
|
|
351
351
|
for (const n of p.notes) lines.push(`! ${n}`);
|
|
352
352
|
return lines.join("\n");
|
|
353
353
|
}
|
|
354
|
-
const pad = (s, n) => s.padEnd(n);
|
|
355
354
|
/** Render the end-of-run summary: one row per property, plus any reminders. */
|
|
356
355
|
function formatSummary(results) {
|
|
357
|
-
|
|
356
|
+
return table([
|
|
358
357
|
"PROVIDER",
|
|
359
358
|
"DOMAIN",
|
|
360
359
|
"VERIFY",
|
|
361
360
|
"TAG"
|
|
362
|
-
]
|
|
363
|
-
const rows = results.map((r) => [
|
|
361
|
+
], results.map((r) => [
|
|
364
362
|
r.provider,
|
|
365
363
|
r.domain,
|
|
366
364
|
r.txt,
|
|
367
365
|
r.tag
|
|
368
|
-
]);
|
|
369
|
-
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)));
|
|
370
|
-
const line = (cells) => cells.map((c, i) => pad(c, widths[i] ?? 0)).join(" ").trimEnd();
|
|
371
|
-
const out = [line(headers), widths.map((w) => "─".repeat(w)).join(" ")];
|
|
372
|
-
for (const row of rows) out.push(line(row));
|
|
373
|
-
const reminders = results.flatMap((r) => r.reminders.map((m) => ` ! ${r.provider}: ${m}`));
|
|
374
|
-
if (reminders.length) out.push("", "Reminders (manual — no API):", ...reminders);
|
|
375
|
-
return out.join("\n");
|
|
366
|
+
]), results.flatMap((r) => r.reminders.map((m) => ` ! ${r.provider}: ${m}`)));
|
|
376
367
|
}
|
|
377
368
|
|
|
378
369
|
//#endregion
|
package/dist/ads/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/ads/providers.ts","../../src/ads/plan.ts"],"sourcesContent":["/**\n * What each ad platform actually lets a build tool do — the table every `vitops ads`\n * subcommand reads.\n *\n * Written the way the indexing module was written: start from what the platforms\n * accept, because it is much narrower than it looks, and record the *absence* of a\n * path as a fact rather than leaving it as a gap.\n *\n * - **Four platforms verify a domain by DNS TXT** (Meta, TikTok, Pinterest,\n * Snapchat). That record is the one thing here that can be created for you.\n * - **Four have no domain verification at all** (Google Ads, LinkedIn, Reddit,\n * Microsoft Ads). Linking is the tag and the account id, nothing else. Those\n * resolve to a skip *plus* a named reason — a silent omission would read as\n * \"already done\".\n * - **No platform Marketing API is called.** Meta's Business Management\n * `verify_domain` needs a system-user token; the Google Ads API needs an\n * approved developer token and puts the consumer's own account on the line.\n * That is the same reasoning that keeps the Google Indexing API unwired: a\n * toolchain other people install must not ship a documented path to a terms\n * violation. The final \"Verify\" click stays a reminder.\n *\n * The verification token is **not a secret** — it is published in DNS, and the\n * platform fetching it back is the ownership proof, exactly like the IndexNow key.\n * So it lives in the config while credentials stay in the environment.\n *\n * Each tag is the platform's documented bootstrap reduced to its queue stub, with\n * the remote library on `data-src` for the consent gate to attach after the\n * bootstrap has run (`@getvitops/core`'s `activateScript` emits the inline body\n * first for exactly this reason). Two platforms — Microsoft and Pinterest — ship a\n * snippet that creates its own `<script>`; those declare no `src`, and their body\n * stays inert until activation, so an undecided visitor still issues no request.\n */\nimport type { AdConsentCategory, AdProvider, AdPropertySetup } from './types.ts';\n\n/** How a platform proves you own the domain. */\nexport interface AdVerificationSpec {\n method: 'dns-txt' | 'none';\n /**\n * Prefix prepended to a bare token to form the record content. Owned here so a\n * config holds the token the platform shows, not a hand-assembled record — but\n * see `txtRecord`: a value already containing `=` passes through whole, which is\n * the escape hatch when a platform changes its spelling.\n */\n txtPrefix?: string;\n /** Where in the platform UI the token is found. Printed when asking for it. */\n where?: string;\n /** Why there is nothing to automate. Required when `method` is `none`. */\n reason?: string;\n}\n\n/** The tag, and which config field carries its id. */\nexport interface AdTagSpec {\n /** Which field the tag id comes from — and therefore what to ask for when absent. */\n needs: 'pixelId' | 'accountId';\n /** The remote library. Absent when the platform's own bootstrap loads itself. */\n src?: (id: string) => string;\n /** The inline bootstrap, which the gate runs before attaching `src`. */\n bootstrap: (id: string, entry: AdPropertySetup) => string;\n /**\n * Cookies the tag sets, for `data-consent-cookies` (what a revoke clears) and\n * for the generated cookie notice. A `*` suffix is a prefix match.\n */\n cookies: string[];\n /** The provider's own privacy policy, for the cookie notice. */\n privacyUrl: string;\n /** A visitor-facing opt-out, where the platform offers one. */\n optOut?: string;\n}\n\nexport interface AdPlatform {\n /** The platform as a reader would recognise it. */\n name: string;\n /**\n * The jurisdiction that can compel this platform to produce what it receives —\n * where the contracting entity is established. Feeds the generated privacy\n * policy's cross-border transfer disclosure, which is a claim about foreign\n * *access*, not foreign storage (see `Processor.operatorCountry` in the\n * generator's `legal/providers.ts`). A consumer whose contracting entity differs\n * declares their own processor entry instead.\n */\n operatorCountry: string;\n /** Click-ID parameters this platform stamps on an ad landing URL. */\n clickIdParams: string[];\n /** Where the operator manages the property. */\n consoleUrl: string;\n verification: AdVerificationSpec;\n tag: AdTagSpec;\n /** Follow-ups no API covers, given this entry. */\n manualSteps: (entry: AdPropertySetup) => string[];\n}\n\n/** `gtag` is shared by every Google tag; the id shape (`AW-…`) is what differs. */\nconst gtagBootstrap = (id: string): string =>\n `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}` +\n `gtag('js',new Date());gtag('config','${id}');`;\n\n/** The queue-stub shape shared by fbq/ttq/rdt/snaptr/pintrk. */\nconst queueStub = (global: string, methods: string): string =>\n `window.${global}=window.${global}||function(){(window.${global}.q=window.${global}.q||[]).push(arguments)};` +\n `window.${global}.methods=${methods};`;\n\nexport const AD_PLATFORMS: Record<AdProvider, AdPlatform> = {\n google: {\n name: 'Google Ads',\n operatorCountry: 'the United States',\n clickIdParams: ['gclid', 'gbraid', 'wbraid'],\n consoleUrl: 'https://ads.google.com/',\n verification: {\n method: 'none',\n reason:\n 'Google Ads has no domain verification — the conversion tag is the link. (Merchant Center claims a website; that is a different product.)',\n },\n tag: {\n // The customer id (`123-456-7890`) is not the tag id: the conversion tag is\n // `AW-…`, which is why this asks for `pixelId` rather than reusing accountId.\n needs: 'pixelId',\n src: (id) => `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}`,\n bootstrap: gtagBootstrap,\n cookies: ['_gcl_au', '_gcl_aw', '_gcl_dc', '_gac_*'],\n privacyUrl: 'https://policies.google.com/privacy',\n optOut: 'https://adssettings.google.com/',\n },\n manualSteps: (e) => [\n ...(e.conversionLabel\n ? []\n : ['create a conversion action in Google Ads and record its label as `conversionLabel`']),\n 'link the Google Ads account to Search Console / GA4 in the UI if you want imported conversions',\n ],\n },\n\n meta: {\n name: 'Meta',\n operatorCountry: 'the United States',\n clickIdParams: ['fbclid'],\n consoleUrl: 'https://business.facebook.com/settings/owned-domains',\n verification: {\n method: 'dns-txt',\n txtPrefix: 'facebook-domain-verification=',\n where: 'Business settings → Brand safety → Domains → Add → DNS verification',\n },\n tag: {\n needs: 'pixelId',\n src: () => 'https://connect.facebook.net/en_US/fbevents.js',\n bootstrap: (id) =>\n `${queueStub('fbq', `['init','track','trackCustom','consent']`)}` +\n `fbq('init','${id}');fbq('track','PageView');`,\n cookies: ['_fbp', '_fbc'],\n privacyUrl: 'https://www.facebook.com/privacy/policy/',\n optOut: 'https://www.facebook.com/adpreferences/ad_settings',\n },\n manualSteps: () => [\n 'click Verify on the domain in Business settings once the TXT record has propagated — the Business Management API needs a system-user token and is deliberately not called',\n ],\n },\n\n linkedin: {\n name: 'LinkedIn',\n operatorCountry: 'the United States',\n clickIdParams: ['li_fat_id'],\n consoleUrl: 'https://www.linkedin.com/campaignmanager/',\n verification: {\n method: 'none',\n reason:\n 'LinkedIn Ads has no domain verification — the Insight Tag is the link. (Verifying a LinkedIn *Page* is a separate, unrelated flow.)',\n },\n tag: {\n needs: 'accountId',\n src: () => 'https://snap.licdn.com/li.lms-analytics/insight.min.js',\n bootstrap: (id) =>\n `window._linkedin_partner_id='${id}';` +\n `window._linkedin_data_partner_ids=window._linkedin_data_partner_ids||[];` +\n `window._linkedin_data_partner_ids.push('${id}');`,\n cookies: ['li_sugr', 'li_gc', 'bcookie', 'lidc', 'UserMatchHistory'],\n privacyUrl: 'https://www.linkedin.com/legal/privacy-policy',\n optOut: 'https://www.linkedin.com/psettings/guest-controls/retargeting-opt-out',\n },\n manualSteps: () => [\n 'add conversions in Campaign Manager — the Insight Tag alone records page visits, not conversions',\n ],\n },\n\n reddit: {\n name: 'Reddit',\n operatorCountry: 'the United States',\n clickIdParams: ['rdt_cid'],\n consoleUrl: 'https://ads.reddit.com/',\n verification: {\n method: 'none',\n reason: 'Reddit Ads has no domain verification — the Reddit Pixel is the link.',\n },\n tag: {\n needs: 'pixelId',\n src: () => 'https://www.redditstatic.com/ads/pixel.js',\n bootstrap: (id) =>\n `${queueStub('rdt', `['init','track']`)}rdt('init','${id}');rdt('track','PageVisit');`,\n cookies: ['_rdt_uuid'],\n privacyUrl: 'https://www.reddit.com/policies/privacy-policy',\n },\n manualSteps: () => [],\n },\n\n tiktok: {\n name: 'TikTok',\n operatorCountry: 'Singapore',\n clickIdParams: ['ttclid'],\n consoleUrl: 'https://ads.tiktok.com/',\n verification: {\n method: 'dns-txt',\n txtPrefix: 'tiktok-developers-site-verification=',\n where: 'Ads Manager → Assets → Events → Web events → Verify domain → DNS',\n },\n tag: {\n needs: 'pixelId',\n src: (id) =>\n `https://analytics.tiktok.com/i18n/pixel/events.js?sdkid=${encodeURIComponent(id)}&lib=ttq`,\n bootstrap: (id) =>\n `window.TiktokAnalyticsObject='ttq';` +\n `${queueStub('ttq', `['page','track','identify','instances','debug','on','off','once','ready','alias','group','enableCookie','disableCookie']`)}` +\n `ttq('init','${id}');ttq('page');`,\n cookies: ['_ttp'],\n privacyUrl: 'https://www.tiktok.com/legal/privacy-policy',\n },\n manualSteps: () => [],\n },\n\n microsoft: {\n name: 'Microsoft Ads',\n operatorCountry: 'the United States',\n clickIdParams: ['msclkid'],\n consoleUrl: 'https://ads.microsoft.com/',\n verification: {\n method: 'none',\n reason: 'Microsoft Ads has no domain verification — the UET tag is the link.',\n },\n tag: {\n needs: 'pixelId',\n // No `src`: UET must be constructed *after* bat.js evaluates, so the\n // platform's own snippet attaches an onload handler to a script it creates.\n // Inert until activation all the same — the body is never parsed before then.\n bootstrap: (id) =>\n `(function(w,d,t,r,u){var f,n,i;w[u]=w[u]||[],f=function(){var o={ti:'${id}',enableAutoSpaTracking:true};` +\n `o.q=w[u],w[u]=new UET(o),w[u].push('pageLoad')},n=d.createElement(t),n.src=r,n.async=1,` +\n `n.onload=n.onreadystatechange=function(){var s=this.readyState;s&&s!=='loaded'&&s!=='complete'||(f(),n.onload=n.onreadystatechange=null)},` +\n `i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)})(window,document,'script','https://bat.bing.com/bat.js','uetq');`,\n cookies: ['_uetsid', '_uetvid', 'MUID'],\n privacyUrl: 'https://privacy.microsoft.com/privacystatement',\n optOut: 'https://account.microsoft.com/privacy/ad-settings',\n },\n manualSteps: () => [],\n },\n\n pinterest: {\n name: 'Pinterest',\n operatorCountry: 'the United States',\n clickIdParams: ['epik'],\n consoleUrl: 'https://ads.pinterest.com/',\n verification: {\n method: 'dns-txt',\n txtPrefix: 'pinterest-site-verification=',\n where: 'Business hub → Claim → Websites → Claim → Add TXT record',\n },\n tag: {\n needs: 'pixelId',\n // Pinterest's stub creates the script itself once `pintrk('load')` runs.\n bootstrap: (id) =>\n `${queueStub('pintrk', `['load','page','track']`)}` +\n `!function(){var t=document.createElement('script');t.async=!0,t.src='https://s.pinimg.com/ct/core.js';` +\n `var e=document.getElementsByTagName('script')[0];e.parentNode.insertBefore(t,e)}();` +\n `pintrk('load','${id}');pintrk('page');`,\n cookies: ['_pinterest_ct_ua', '_pin_unauth', '_epik'],\n privacyUrl: 'https://policy.pinterest.com/privacy-policy',\n },\n manualSteps: () => [\n 'confirm the claim in Business hub → Claim once the TXT record has propagated',\n ],\n },\n\n snapchat: {\n name: 'Snapchat',\n operatorCountry: 'the United States',\n clickIdParams: ['ScCid'],\n consoleUrl: 'https://ads.snapchat.com/',\n verification: {\n method: 'dns-txt',\n txtPrefix: 'snap-domain-verification=',\n where: 'Business Manager → Public Profiles / Domains → Verify domain → DNS',\n },\n tag: {\n needs: 'pixelId',\n src: () => 'https://sc-static.net/scevent.min.js',\n bootstrap: (id) =>\n `${queueStub('snaptr', `['init','track']`)}snaptr('init','${id}');snaptr('track','PAGE_VIEW');`,\n cookies: ['_scid', '_scid_r', 'sc_at'],\n privacyUrl: 'https://snap.com/privacy/privacy-policy',\n },\n manualSteps: () => [],\n },\n};\n\n/** Every provider, in table order — the one place iteration order is decided. */\nexport const AD_PROVIDER_KEYS = Object.keys(AD_PLATFORMS) as AdProvider[];\n\n/** Whether a string names a known provider (for CLI arg validation). */\nexport const isAdProvider = (value: string): value is AdProvider => value in AD_PLATFORMS;\n\n/**\n * The DNS TXT record content for a token.\n *\n * A value already containing `=` is taken as the whole record and left alone. That\n * is the same escape hatch an icon name containing `:` gets: a platform can change\n * its prefix spelling without waiting on a toolchain release, and an operator who\n * pasted the full record from the UI gets what they meant rather than a\n * double-prefixed record that verifies against nothing.\n */\nexport function txtRecord(provider: AdProvider, token: string): string {\n const trimmed = token.trim();\n if (trimmed.includes('=')) return trimmed;\n return `${AD_PLATFORMS[provider].verification.txtPrefix ?? ''}${trimmed}`;\n}\n\n/** The tag id for an entry, or undefined when the field it needs is unset. */\nexport function tagId(entry: AdPropertySetup): string | undefined {\n const field = AD_PLATFORMS[entry.provider].tag.needs;\n return entry[field];\n}\n\n/** The consent category gating an entry's tag. `marketing` unless stated otherwise. */\nexport const categoryOf = (entry: AdPropertySetup): AdConsentCategory =>\n entry.category ?? 'marketing';\n\nconst escapeAttr = (value: string): string =>\n value.replace(/&/g, '&').replace(/\"/g, '"').replace(/</g, '<');\n\n/**\n * One provider's tag, rendered as a **consent-gated, inert** `<script>`.\n *\n * `type=\"text/plain\"` with the URL on `data-src` is what makes the gate real: the\n * browser neither parses the body nor fetches the library, so an undecided\n * visitor's page issues no third-party request. Never give one of these a live\n * `src` — a gate that instead asks a loaded tracker not to track is a promise.\n *\n * Returns null when the entry has no tag id, so a caller can report the gap rather\n * than emit a snippet that initialises `undefined`.\n */\nexport function renderTag(\n entry: AdPropertySetup,\n strategy: 'idle' | 'async' | 'interaction' = 'idle',\n): string | null {\n const platform = AD_PLATFORMS[entry.provider];\n const id = tagId(entry);\n if (!id) return null;\n const attrs = [\n 'type=\"text/plain\"',\n 'data-vitops-tag',\n `data-consent=\"${categoryOf(entry)}\"`,\n `data-strategy=\"${strategy}\"`,\n ...(platform.tag.src ? [`data-src=\"${escapeAttr(platform.tag.src(id))}\"`] : []),\n `data-consent-cookies=\"${escapeAttr(platform.tag.cookies.join(','))}\"`,\n ];\n return `<!-- ${platform.name} — gated on ${categoryOf(entry)} consent -->\\n<script ${attrs.join(' ')}>${platform.tag.bootstrap(id, entry)}</script>`;\n}\n","/**\n * Every decision `vitops ads setup` makes, as one pure function.\n *\n * Pure in the load-bearing sense: no `fetch`, no `fs`, no clock, no stdin. The\n * executor gathers each domain's live DNS state and applies what it is told; this\n * module decides, from that state, what is left to do. The same split\n * `onboarding/plan.ts` makes, for the same two payoffs: `--dry` is a *complete*\n * account of a run, and `--check` is an honest drift report.\n *\n * Idempotency lives here — a step whose desired state already holds resolves to\n * `skip`, so a re-run of a linked property is all skips by construction rather than\n * by the executor checking twice.\n *\n * One thing this planner adds over the onboarding one: a step that cannot proceed\n * for want of a **config value** resolves to `blocked` carrying the field name in\n * `needs`. That is a machine-readable gap, which is what lets the CLI *ask* for the\n * missing token and re-plan, while the planner itself stays pure and knows nothing\n * about a terminal. A blocked step is drift, never a skip — the first run of a\n * property whose token has not been fetched yet must not read as \"already linked\".\n */\nimport { AD_PLATFORMS, tagId, txtRecord } from './providers.ts';\nimport type { AdDomainState, AdPropertySetup, AdsConfig } from './types.ts';\n\n/** A step's decision. `blocked` means the config is missing something. */\nexport type StepAction = 'create' | 'skip' | 'blocked';\n\n/** A config field the operator can supply to unblock a step. */\nexport type MissingField = 'domainVerification' | 'accountId' | 'pixelId';\n\nexport interface StepPlan {\n action: StepAction;\n /** Why it's a skip, or what the mutation will do, or what is missing. */\n detail: string;\n /** Set only on `blocked`: which config field would unblock it. */\n needs?: MissingField | undefined;\n}\n\n/** The steps for one ad property, decided against its live state. */\nexport interface AdPropertyPlan {\n provider: AdPropertySetup['provider'];\n /** The platform name, so a formatter needn't reach back into the table. */\n name: string;\n /** The domain being verified, when the platform verifies one. */\n domain?: string | undefined;\n /** Ensure the apex verification TXT. */\n txt: StepPlan;\n /** Whether the tag can be emitted (`ads tags`). */\n tag: StepPlan;\n /** The exact TXT content to create, when `txt.action` is `create`. */\n txtContent?: string | undefined;\n /** Manual follow-ups — including *why* a platform has no verification step. */\n reminders: string[];\n}\n\nexport interface AdsPlan {\n properties: AdPropertyPlan[];\n /** Operator-facing warnings that span the whole run. */\n notes: string[];\n}\n\n/**\n * Decide one property's steps.\n *\n * The `dns-txt` branch has three outcomes, and keeping them distinct is the point:\n * our record is present (skip), a *different* record from the same platform is\n * present (create, and say so — a token rotation leaves the old record in place,\n * since this command never edits or deletes DNS), or no token is configured yet\n * (blocked, naming the field).\n */\nexport function planProperty(setup: AdPropertySetup, state: AdDomainState): AdPropertyPlan {\n const platform = AD_PLATFORMS[setup.provider];\n const reminders = [...platform.manualSteps(setup)];\n\n let txt: StepPlan;\n let txtContent: string | undefined;\n\n if (platform.verification.method === 'none') {\n txt = { action: 'skip', detail: platform.verification.reason ?? 'no domain verification' };\n } else if (!setup.domainVerification) {\n txt = {\n action: 'blocked',\n detail: `needs the verification token from ${platform.verification.where ?? platform.consoleUrl}`,\n needs: 'domainVerification',\n };\n } else {\n const desired = txtRecord(setup.provider, setup.domainVerification);\n const prefix = platform.verification.txtPrefix;\n const stale =\n prefix != null && state.txtContents.some((c) => c.startsWith(prefix) && c !== desired);\n txt = state.txtContents.includes(desired)\n ? { action: 'skip', detail: 'verification TXT already present' }\n : {\n action: 'create',\n detail: stale\n ? 'create verification TXT (an older token from this platform is still present — remove it yourself; this command never deletes DNS)'\n : 'create verification TXT',\n };\n if (txt.action === 'create') txtContent = desired;\n }\n\n const id = tagId(setup);\n const tag: StepPlan = id\n ? { action: 'skip', detail: `tag ready (${platform.tag.needs} ${id})` }\n : {\n action: 'blocked',\n detail: `needs ${platform.tag.needs} from ${platform.consoleUrl} to emit the tag`,\n needs: platform.tag.needs,\n };\n\n return {\n provider: setup.provider,\n name: platform.name,\n ...(setup.domain ? { domain: setup.domain } : {}),\n txt,\n tag,\n ...(txtContent ? { txtContent } : {}),\n reminders,\n };\n}\n\n/** Plan every configured property. */\nexport function plan(config: AdsConfig, states: Map<string, AdDomainState>): AdsPlan {\n const notes: string[] = [];\n const properties = config.properties.map((setup) => {\n const key = setup.domain ?? '';\n const state = states.get(key);\n if (!state && AD_PLATFORMS[setup.provider].verification.method === 'dns-txt')\n // Should not happen — the CLI observes every verifiable domain before\n // planning — but a missing observation must not read as \"nothing to do\".\n notes.push(`${setup.provider}: no observed DNS state for ${key || '(no domain)'}`);\n return planProperty(setup, state ?? { txtContents: [] });\n });\n return { properties, notes };\n}\n\n/** Any step that isn't a skip — i.e. the live state or the config has drifted. */\nexport function hasDrift(p: AdsPlan): boolean {\n return p.properties.some((x) => x.txt.action !== 'skip' || x.tag.action !== 'skip');\n}\n\n/** Every field a run still needs, per provider — what the CLI asks for. */\nexport function missingFields(p: AdsPlan): { provider: string; needs: MissingField }[] {\n const out: { provider: string; needs: MissingField }[] = [];\n for (const x of p.properties) {\n for (const step of [x.txt, x.tag]) {\n if (step.action === 'blocked' && step.needs)\n out.push({ provider: x.provider, needs: step.needs });\n }\n }\n return out;\n}\n\n/** Render a plan as the block `--dry` and `--check` print. */\nexport function formatPlan(p: AdsPlan): string {\n const glyph = (s: StepPlan) => (s.action === 'skip' ? '·' : s.action === 'blocked' ? '?' : '+');\n const lines: string[] = [];\n for (const x of p.properties) {\n lines.push(`${x.provider}${x.domain ? ` (${x.domain})` : ''}`);\n lines.push(` ${glyph(x.txt)} verify ${x.txt.detail}`);\n lines.push(` ${glyph(x.tag)} tag ${x.tag.detail}`);\n for (const r of x.reminders) lines.push(` ! ${r}`);\n }\n for (const n of p.notes) lines.push(`! ${n}`);\n return lines.join('\\n');\n}\n\n/** The outcome of one property's run, for the summary table. */\nexport interface AdPropertyResult {\n provider: string;\n domain: string;\n txt: 'created' | 'present' | 'blocked' | 'failed' | 'n/a';\n tag: 'ready' | 'blocked';\n reminders: string[];\n}\n\nconst pad = (s: string, n: number) => s.padEnd(n);\n\n/** Render the end-of-run summary: one row per property, plus any reminders. */\nexport function formatSummary(results: AdPropertyResult[]): string {\n const headers = ['PROVIDER', 'DOMAIN', 'VERIFY', 'TAG'];\n const rows = results.map((r) => [r.provider, r.domain, r.txt, r.tag]);\n const widths = headers.map((h, i) =>\n Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)),\n );\n const line = (cells: string[]) =>\n cells\n .map((c, i) => pad(c, widths[i] ?? 0))\n .join(' ')\n .trimEnd();\n\n const out = [line(headers), widths.map((w) => '─'.repeat(w)).join(' ')];\n for (const row of rows) out.push(line(row));\n\n const reminders = results.flatMap((r) => r.reminders.map((m) => ` ! ${r.provider}: ${m}`));\n if (reminders.length) out.push('', 'Reminders (manual — no API):', ...reminders);\n return out.join('\\n');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/ads/providers.ts","../../src/ads/plan.ts"],"sourcesContent":["/**\n * What each ad platform actually lets a build tool do — the table every `vitops ads`\n * subcommand reads.\n *\n * Written the way the indexing module was written: start from what the platforms\n * accept, because it is much narrower than it looks, and record the *absence* of a\n * path as a fact rather than leaving it as a gap.\n *\n * - **Four platforms verify a domain by DNS TXT** (Meta, TikTok, Pinterest,\n * Snapchat). That record is the one thing here that can be created for you.\n * - **Four have no domain verification at all** (Google Ads, LinkedIn, Reddit,\n * Microsoft Ads). Linking is the tag and the account id, nothing else. Those\n * resolve to a skip *plus* a named reason — a silent omission would read as\n * \"already done\".\n * - **No platform Marketing API is called.** Meta's Business Management\n * `verify_domain` needs a system-user token; the Google Ads API needs an\n * approved developer token and puts the consumer's own account on the line.\n * That is the same reasoning that keeps the Google Indexing API unwired: a\n * toolchain other people install must not ship a documented path to a terms\n * violation. The final \"Verify\" click stays a reminder.\n *\n * The verification token is **not a secret** — it is published in DNS, and the\n * platform fetching it back is the ownership proof, exactly like the IndexNow key.\n * So it lives in the config while credentials stay in the environment.\n *\n * Each tag is the platform's documented bootstrap reduced to its queue stub, with\n * the remote library on `data-src` for the consent gate to attach after the\n * bootstrap has run (`@getvitops/core`'s `activateScript` emits the inline body\n * first for exactly this reason). Two platforms — Microsoft and Pinterest — ship a\n * snippet that creates its own `<script>`; those declare no `src`, and their body\n * stays inert until activation, so an undecided visitor still issues no request.\n */\nimport type { AdConsentCategory, AdProvider, AdPropertySetup } from './types.ts';\n\n/** How a platform proves you own the domain. */\nexport interface AdVerificationSpec {\n method: 'dns-txt' | 'none';\n /**\n * Prefix prepended to a bare token to form the record content. Owned here so a\n * config holds the token the platform shows, not a hand-assembled record — but\n * see `txtRecord`: a value already containing `=` passes through whole, which is\n * the escape hatch when a platform changes its spelling.\n */\n txtPrefix?: string;\n /** Where in the platform UI the token is found. Printed when asking for it. */\n where?: string;\n /** Why there is nothing to automate. Required when `method` is `none`. */\n reason?: string;\n}\n\n/** The tag, and which config field carries its id. */\nexport interface AdTagSpec {\n /** Which field the tag id comes from — and therefore what to ask for when absent. */\n needs: 'pixelId' | 'accountId';\n /** The remote library. Absent when the platform's own bootstrap loads itself. */\n src?: (id: string) => string;\n /** The inline bootstrap, which the gate runs before attaching `src`. */\n bootstrap: (id: string, entry: AdPropertySetup) => string;\n /**\n * Cookies the tag sets, for `data-consent-cookies` (what a revoke clears) and\n * for the generated cookie notice. A `*` suffix is a prefix match.\n */\n cookies: string[];\n /** The provider's own privacy policy, for the cookie notice. */\n privacyUrl: string;\n /** A visitor-facing opt-out, where the platform offers one. */\n optOut?: string;\n}\n\nexport interface AdPlatform {\n /** The platform as a reader would recognise it. */\n name: string;\n /**\n * The jurisdiction that can compel this platform to produce what it receives —\n * where the contracting entity is established. Feeds the generated privacy\n * policy's cross-border transfer disclosure, which is a claim about foreign\n * *access*, not foreign storage (see `Processor.operatorCountry` in the\n * generator's `legal/providers.ts`). A consumer whose contracting entity differs\n * declares their own processor entry instead.\n */\n operatorCountry: string;\n /** Click-ID parameters this platform stamps on an ad landing URL. */\n clickIdParams: string[];\n /** Where the operator manages the property. */\n consoleUrl: string;\n verification: AdVerificationSpec;\n tag: AdTagSpec;\n /** Follow-ups no API covers, given this entry. */\n manualSteps: (entry: AdPropertySetup) => string[];\n}\n\n/** `gtag` is shared by every Google tag; the id shape (`AW-…`) is what differs. */\nconst gtagBootstrap = (id: string): string =>\n `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}` +\n `gtag('js',new Date());gtag('config','${id}');`;\n\n/** The queue-stub shape shared by fbq/ttq/rdt/snaptr/pintrk. */\nconst queueStub = (global: string, methods: string): string =>\n `window.${global}=window.${global}||function(){(window.${global}.q=window.${global}.q||[]).push(arguments)};` +\n `window.${global}.methods=${methods};`;\n\nexport const AD_PLATFORMS: Record<AdProvider, AdPlatform> = {\n google: {\n name: 'Google Ads',\n operatorCountry: 'the United States',\n clickIdParams: ['gclid', 'gbraid', 'wbraid'],\n consoleUrl: 'https://ads.google.com/',\n verification: {\n method: 'none',\n reason:\n 'Google Ads has no domain verification — the conversion tag is the link. (Merchant Center claims a website; that is a different product.)',\n },\n tag: {\n // The customer id (`123-456-7890`) is not the tag id: the conversion tag is\n // `AW-…`, which is why this asks for `pixelId` rather than reusing accountId.\n needs: 'pixelId',\n src: (id) => `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}`,\n bootstrap: gtagBootstrap,\n cookies: ['_gcl_au', '_gcl_aw', '_gcl_dc', '_gac_*'],\n privacyUrl: 'https://policies.google.com/privacy',\n optOut: 'https://adssettings.google.com/',\n },\n manualSteps: (e) => [\n ...(e.conversionLabel\n ? []\n : ['create a conversion action in Google Ads and record its label as `conversionLabel`']),\n 'link the Google Ads account to Search Console / GA4 in the UI if you want imported conversions',\n ],\n },\n\n meta: {\n name: 'Meta',\n operatorCountry: 'the United States',\n clickIdParams: ['fbclid'],\n consoleUrl: 'https://business.facebook.com/settings/owned-domains',\n verification: {\n method: 'dns-txt',\n txtPrefix: 'facebook-domain-verification=',\n where: 'Business settings → Brand safety → Domains → Add → DNS verification',\n },\n tag: {\n needs: 'pixelId',\n src: () => 'https://connect.facebook.net/en_US/fbevents.js',\n bootstrap: (id) =>\n `${queueStub('fbq', `['init','track','trackCustom','consent']`)}` +\n `fbq('init','${id}');fbq('track','PageView');`,\n cookies: ['_fbp', '_fbc'],\n privacyUrl: 'https://www.facebook.com/privacy/policy/',\n optOut: 'https://www.facebook.com/adpreferences/ad_settings',\n },\n manualSteps: () => [\n 'click Verify on the domain in Business settings once the TXT record has propagated — the Business Management API needs a system-user token and is deliberately not called',\n ],\n },\n\n linkedin: {\n name: 'LinkedIn',\n operatorCountry: 'the United States',\n clickIdParams: ['li_fat_id'],\n consoleUrl: 'https://www.linkedin.com/campaignmanager/',\n verification: {\n method: 'none',\n reason:\n 'LinkedIn Ads has no domain verification — the Insight Tag is the link. (Verifying a LinkedIn *Page* is a separate, unrelated flow.)',\n },\n tag: {\n needs: 'accountId',\n src: () => 'https://snap.licdn.com/li.lms-analytics/insight.min.js',\n bootstrap: (id) =>\n `window._linkedin_partner_id='${id}';` +\n `window._linkedin_data_partner_ids=window._linkedin_data_partner_ids||[];` +\n `window._linkedin_data_partner_ids.push('${id}');`,\n cookies: ['li_sugr', 'li_gc', 'bcookie', 'lidc', 'UserMatchHistory'],\n privacyUrl: 'https://www.linkedin.com/legal/privacy-policy',\n optOut: 'https://www.linkedin.com/psettings/guest-controls/retargeting-opt-out',\n },\n manualSteps: () => [\n 'add conversions in Campaign Manager — the Insight Tag alone records page visits, not conversions',\n ],\n },\n\n reddit: {\n name: 'Reddit',\n operatorCountry: 'the United States',\n clickIdParams: ['rdt_cid'],\n consoleUrl: 'https://ads.reddit.com/',\n verification: {\n method: 'none',\n reason: 'Reddit Ads has no domain verification — the Reddit Pixel is the link.',\n },\n tag: {\n needs: 'pixelId',\n src: () => 'https://www.redditstatic.com/ads/pixel.js',\n bootstrap: (id) =>\n `${queueStub('rdt', `['init','track']`)}rdt('init','${id}');rdt('track','PageVisit');`,\n cookies: ['_rdt_uuid'],\n privacyUrl: 'https://www.reddit.com/policies/privacy-policy',\n },\n manualSteps: () => [],\n },\n\n tiktok: {\n name: 'TikTok',\n operatorCountry: 'Singapore',\n clickIdParams: ['ttclid'],\n consoleUrl: 'https://ads.tiktok.com/',\n verification: {\n method: 'dns-txt',\n txtPrefix: 'tiktok-developers-site-verification=',\n where: 'Ads Manager → Assets → Events → Web events → Verify domain → DNS',\n },\n tag: {\n needs: 'pixelId',\n src: (id) =>\n `https://analytics.tiktok.com/i18n/pixel/events.js?sdkid=${encodeURIComponent(id)}&lib=ttq`,\n bootstrap: (id) =>\n `window.TiktokAnalyticsObject='ttq';` +\n `${queueStub('ttq', `['page','track','identify','instances','debug','on','off','once','ready','alias','group','enableCookie','disableCookie']`)}` +\n `ttq('init','${id}');ttq('page');`,\n cookies: ['_ttp'],\n privacyUrl: 'https://www.tiktok.com/legal/privacy-policy',\n },\n manualSteps: () => [],\n },\n\n microsoft: {\n name: 'Microsoft Ads',\n operatorCountry: 'the United States',\n clickIdParams: ['msclkid'],\n consoleUrl: 'https://ads.microsoft.com/',\n verification: {\n method: 'none',\n reason: 'Microsoft Ads has no domain verification — the UET tag is the link.',\n },\n tag: {\n needs: 'pixelId',\n // No `src`: UET must be constructed *after* bat.js evaluates, so the\n // platform's own snippet attaches an onload handler to a script it creates.\n // Inert until activation all the same — the body is never parsed before then.\n bootstrap: (id) =>\n `(function(w,d,t,r,u){var f,n,i;w[u]=w[u]||[],f=function(){var o={ti:'${id}',enableAutoSpaTracking:true};` +\n `o.q=w[u],w[u]=new UET(o),w[u].push('pageLoad')},n=d.createElement(t),n.src=r,n.async=1,` +\n `n.onload=n.onreadystatechange=function(){var s=this.readyState;s&&s!=='loaded'&&s!=='complete'||(f(),n.onload=n.onreadystatechange=null)},` +\n `i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)})(window,document,'script','https://bat.bing.com/bat.js','uetq');`,\n cookies: ['_uetsid', '_uetvid', 'MUID'],\n privacyUrl: 'https://privacy.microsoft.com/privacystatement',\n optOut: 'https://account.microsoft.com/privacy/ad-settings',\n },\n manualSteps: () => [],\n },\n\n pinterest: {\n name: 'Pinterest',\n operatorCountry: 'the United States',\n clickIdParams: ['epik'],\n consoleUrl: 'https://ads.pinterest.com/',\n verification: {\n method: 'dns-txt',\n txtPrefix: 'pinterest-site-verification=',\n where: 'Business hub → Claim → Websites → Claim → Add TXT record',\n },\n tag: {\n needs: 'pixelId',\n // Pinterest's stub creates the script itself once `pintrk('load')` runs.\n bootstrap: (id) =>\n `${queueStub('pintrk', `['load','page','track']`)}` +\n `!function(){var t=document.createElement('script');t.async=!0,t.src='https://s.pinimg.com/ct/core.js';` +\n `var e=document.getElementsByTagName('script')[0];e.parentNode.insertBefore(t,e)}();` +\n `pintrk('load','${id}');pintrk('page');`,\n cookies: ['_pinterest_ct_ua', '_pin_unauth', '_epik'],\n privacyUrl: 'https://policy.pinterest.com/privacy-policy',\n },\n manualSteps: () => [\n 'confirm the claim in Business hub → Claim once the TXT record has propagated',\n ],\n },\n\n snapchat: {\n name: 'Snapchat',\n operatorCountry: 'the United States',\n clickIdParams: ['ScCid'],\n consoleUrl: 'https://ads.snapchat.com/',\n verification: {\n method: 'dns-txt',\n txtPrefix: 'snap-domain-verification=',\n where: 'Business Manager → Public Profiles / Domains → Verify domain → DNS',\n },\n tag: {\n needs: 'pixelId',\n src: () => 'https://sc-static.net/scevent.min.js',\n bootstrap: (id) =>\n `${queueStub('snaptr', `['init','track']`)}snaptr('init','${id}');snaptr('track','PAGE_VIEW');`,\n cookies: ['_scid', '_scid_r', 'sc_at'],\n privacyUrl: 'https://snap.com/privacy/privacy-policy',\n },\n manualSteps: () => [],\n },\n};\n\n/** Every provider, in table order — the one place iteration order is decided. */\nexport const AD_PROVIDER_KEYS = Object.keys(AD_PLATFORMS) as AdProvider[];\n\n/** Whether a string names a known provider (for CLI arg validation). */\nexport const isAdProvider = (value: string): value is AdProvider => value in AD_PLATFORMS;\n\n/**\n * The DNS TXT record content for a token.\n *\n * A value already containing `=` is taken as the whole record and left alone. That\n * is the same escape hatch an icon name containing `:` gets: a platform can change\n * its prefix spelling without waiting on a toolchain release, and an operator who\n * pasted the full record from the UI gets what they meant rather than a\n * double-prefixed record that verifies against nothing.\n */\nexport function txtRecord(provider: AdProvider, token: string): string {\n const trimmed = token.trim();\n if (trimmed.includes('=')) return trimmed;\n return `${AD_PLATFORMS[provider].verification.txtPrefix ?? ''}${trimmed}`;\n}\n\n/** The tag id for an entry, or undefined when the field it needs is unset. */\nexport function tagId(entry: AdPropertySetup): string | undefined {\n const field = AD_PLATFORMS[entry.provider].tag.needs;\n return entry[field];\n}\n\n/** The consent category gating an entry's tag. `marketing` unless stated otherwise. */\nexport const categoryOf = (entry: AdPropertySetup): AdConsentCategory =>\n entry.category ?? 'marketing';\n\nconst escapeAttr = (value: string): string =>\n value.replace(/&/g, '&').replace(/\"/g, '"').replace(/</g, '<');\n\n/**\n * One provider's tag, rendered as a **consent-gated, inert** `<script>`.\n *\n * `type=\"text/plain\"` with the URL on `data-src` is what makes the gate real: the\n * browser neither parses the body nor fetches the library, so an undecided\n * visitor's page issues no third-party request. Never give one of these a live\n * `src` — a gate that instead asks a loaded tracker not to track is a promise.\n *\n * Returns null when the entry has no tag id, so a caller can report the gap rather\n * than emit a snippet that initialises `undefined`.\n */\nexport function renderTag(\n entry: AdPropertySetup,\n strategy: 'idle' | 'async' | 'interaction' = 'idle',\n): string | null {\n const platform = AD_PLATFORMS[entry.provider];\n const id = tagId(entry);\n if (!id) return null;\n const attrs = [\n 'type=\"text/plain\"',\n 'data-vitops-tag',\n `data-consent=\"${categoryOf(entry)}\"`,\n `data-strategy=\"${strategy}\"`,\n ...(platform.tag.src ? [`data-src=\"${escapeAttr(platform.tag.src(id))}\"`] : []),\n `data-consent-cookies=\"${escapeAttr(platform.tag.cookies.join(','))}\"`,\n ];\n return `<!-- ${platform.name} — gated on ${categoryOf(entry)} consent -->\\n<script ${attrs.join(' ')}>${platform.tag.bootstrap(id, entry)}</script>`;\n}\n","/**\n * Every decision `vitops ads setup` makes, as one pure function.\n *\n * Pure in the load-bearing sense: no `fetch`, no `fs`, no clock, no stdin. The\n * executor gathers each domain's live DNS state and applies what it is told; this\n * module decides, from that state, what is left to do. The same split\n * `onboarding/plan.ts` makes, for the same two payoffs: `--dry` is a *complete*\n * account of a run, and `--check` is an honest drift report.\n *\n * Idempotency lives here — a step whose desired state already holds resolves to\n * `skip`, so a re-run of a linked property is all skips by construction rather than\n * by the executor checking twice.\n *\n * One thing this planner adds over the onboarding one: a step that cannot proceed\n * for want of a **config value** resolves to `blocked` carrying the field name in\n * `needs`. That is a machine-readable gap, which is what lets the CLI *ask* for the\n * missing token and re-plan, while the planner itself stays pure and knows nothing\n * about a terminal. A blocked step is drift, never a skip — the first run of a\n * property whose token has not been fetched yet must not read as \"already linked\".\n */\nimport { AD_PLATFORMS, tagId, txtRecord } from './providers.ts';\nimport type { AdDomainState, AdPropertySetup, AdsConfig } from './types.ts';\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":";;;;AA4FA,MAAM,iBAAiB,OACrB,wHACwC,GAAG;;AAG7C,MAAM,aAAa,QAAgB,YACjC,UAAU,OAAO,UAAU,OAAO,uBAAuB,OAAO,YAAY,OAAO,kCACzE,OAAO,WAAW,QAAQ;AAEtC,MAAa,eAA+C;CAC1D,QAAQ;EACN,MAAM;EACN,iBAAiB;EACjB,eAAe;GAAC;GAAS;GAAU;EAAQ;EAC3C,YAAY;EACZ,cAAc;GACZ,QAAQ;GACR,QACE;EACJ;EACA,KAAK;GAGH,OAAO;GACP,MAAM,OAAO,+CAA+C,mBAAmB,EAAE;GACjF,WAAW;GACX,SAAS;IAAC;IAAW;IAAW;IAAW;GAAQ;GACnD,YAAY;GACZ,QAAQ;EACV;EACA,cAAc,MAAM,CAClB,GAAI,EAAE,kBACF,CAAC,IACD,CAAC,oFAAoF,GACzF,gGACF;CACF;CAEA,MAAM;EACJ,MAAM;EACN,iBAAiB;EACjB,eAAe,CAAC,QAAQ;EACxB,YAAY;EACZ,cAAc;GACZ,QAAQ;GACR,WAAW;GACX,OAAO;EACT;EACA,KAAK;GACH,OAAO;GACP,WAAW;GACX,YAAY,OACV,GAAG,UAAU,OAAO,0CAA0C,gBAC/C,GAAG;GACpB,SAAS,CAAC,QAAQ,MAAM;GACxB,YAAY;GACZ,QAAQ;EACV;EACA,mBAAmB,CACjB,2KACF;CACF;CAEA,UAAU;EACR,MAAM;EACN,iBAAiB;EACjB,eAAe,CAAC,WAAW;EAC3B,YAAY;EACZ,cAAc;GACZ,QAAQ;GACR,QACE;EACJ;EACA,KAAK;GACH,OAAO;GACP,WAAW;GACX,YAAY,OACV,gCAAgC,GAAG,oHAEQ,GAAG;GAChD,SAAS;IAAC;IAAW;IAAS;IAAW;IAAQ;GAAkB;GACnE,YAAY;GACZ,QAAQ;EACV;EACA,mBAAmB,CACjB,kGACF;CACF;CAEA,QAAQ;EACN,MAAM;EACN,iBAAiB;EACjB,eAAe,CAAC,SAAS;EACzB,YAAY;EACZ,cAAc;GACZ,QAAQ;GACR,QAAQ;EACV;EACA,KAAK;GACH,OAAO;GACP,WAAW;GACX,YAAY,OACV,GAAG,UAAU,OAAO,kBAAkB,EAAE,cAAc,GAAG;GAC3D,SAAS,CAAC,WAAW;GACrB,YAAY;EACd;EACA,mBAAmB,CAAC;CACtB;CAEA,QAAQ;EACN,MAAM;EACN,iBAAiB;EACjB,eAAe,CAAC,QAAQ;EACxB,YAAY;EACZ,cAAc;GACZ,QAAQ;GACR,WAAW;GACX,OAAO;EACT;EACA,KAAK;GACH,OAAO;GACP,MAAM,OACJ,2DAA2D,mBAAmB,EAAE,EAAE;GACpF,YAAY,OACV,sCACG,UAAU,OAAO,0HAA0H,gBAC/H,GAAG;GACpB,SAAS,CAAC,MAAM;GAChB,YAAY;EACd;EACA,mBAAmB,CAAC;CACtB;CAEA,WAAW;EACT,MAAM;EACN,iBAAiB;EACjB,eAAe,CAAC,SAAS;EACzB,YAAY;EACZ,cAAc;GACZ,QAAQ;GACR,QAAQ;EACV;EACA,KAAK;GACH,OAAO;GAIP,YAAY,OACV,wEAAwE,GAAG;GAI7E,SAAS;IAAC;IAAW;IAAW;GAAM;GACtC,YAAY;GACZ,QAAQ;EACV;EACA,mBAAmB,CAAC;CACtB;CAEA,WAAW;EACT,MAAM;EACN,iBAAiB;EACjB,eAAe,CAAC,MAAM;EACtB,YAAY;EACZ,cAAc;GACZ,QAAQ;GACR,WAAW;GACX,OAAO;EACT;EACA,KAAK;GACH,OAAO;GAEP,YAAY,OACV,GAAG,UAAU,UAAU,yBAAyB,4MAG9B,GAAG;GACvB,SAAS;IAAC;IAAoB;IAAe;GAAO;GACpD,YAAY;EACd;EACA,mBAAmB,CACjB,8EACF;CACF;CAEA,UAAU;EACR,MAAM;EACN,iBAAiB;EACjB,eAAe,CAAC,OAAO;EACvB,YAAY;EACZ,cAAc;GACZ,QAAQ;GACR,WAAW;GACX,OAAO;EACT;EACA,KAAK;GACH,OAAO;GACP,WAAW;GACX,YAAY,OACV,GAAG,UAAU,UAAU,kBAAkB,EAAE,iBAAiB,GAAG;GACjE,SAAS;IAAC;IAAS;IAAW;GAAO;GACrC,YAAY;EACd;EACA,mBAAmB,CAAC;CACtB;AACF;;AAGA,MAAa,mBAAmB,OAAO,KAAK,YAAY;;AAGxD,MAAa,gBAAgB,UAAuC,SAAS;;;;;;;;;;AAW7E,SAAgB,UAAU,UAAsB,OAAuB;CACrE,MAAM,UAAU,MAAM,KAAK;CAC3B,IAAI,QAAQ,SAAS,GAAG,GAAG,OAAO;CAClC,OAAO,GAAG,aAAa,SAAS,CAAC,aAAa,aAAa,KAAK;AAClE;;AAGA,SAAgB,MAAM,OAA4C;CAEhE,OAAO,MADO,aAAa,MAAM,SAAS,CAAC,IAAI;AAEjD;;AAGA,MAAa,cAAc,UACzB,MAAM,YAAY;AAEpB,MAAM,cAAc,UAClB,MAAM,QAAQ,MAAM,OAAO,CAAC,CAAC,QAAQ,MAAM,QAAQ,CAAC,CAAC,QAAQ,MAAM,MAAM;;;;;;;;;;;;AAa3E,SAAgB,UACd,OACA,WAA6C,QAC9B;CACf,MAAM,WAAW,aAAa,MAAM;CACpC,MAAM,KAAK,MAAM,KAAK;CACtB,IAAI,CAAC,IAAI,OAAO;CAChB,MAAM,QAAQ;EACZ;EACA;EACA,iBAAiB,WAAW,KAAK,EAAE;EACnC,kBAAkB,SAAS;EAC3B,GAAI,SAAS,IAAI,MAAM,CAAC,aAAa,WAAW,SAAS,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,IAAI,CAAC;EAC7E,yBAAyB,WAAW,SAAS,IAAI,QAAQ,KAAK,GAAG,CAAC,EAAE;CACtE;CACA,OAAO,QAAQ,SAAS,KAAK,cAAc,WAAW,KAAK,EAAE,wBAAwB,MAAM,KAAK,GAAG,EAAE,GAAG,SAAS,IAAI,UAAU,IAAI,KAAK,EAAE;AAC5I;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AClSA,SAAgB,aAAa,OAAwB,OAAsC;CACzF,MAAM,WAAW,aAAa,MAAM;CACpC,MAAM,YAAY,CAAC,GAAG,SAAS,YAAY,KAAK,CAAC;CAEjD,IAAI;CACJ,IAAI;CAEJ,IAAI,SAAS,aAAa,WAAW,QACnC,MAAM;EAAE,QAAQ;EAAQ,QAAQ,SAAS,aAAa,UAAU;CAAyB;MACpF,IAAI,CAAC,MAAM,oBAChB,MAAM;EACJ,QAAQ;EACR,QAAQ,qCAAqC,SAAS,aAAa,SAAS,SAAS;EACrF,OAAO;CACT;MACK;EACL,MAAM,UAAU,UAAU,MAAM,UAAU,MAAM,kBAAkB;EAClE,MAAM,SAAS,SAAS,aAAa;EACrC,MAAM,QACJ,UAAU,QAAQ,MAAM,YAAY,MAAM,MAAM,EAAE,WAAW,MAAM,KAAK,MAAM,OAAO;EACvF,MAAM,MAAM,YAAY,SAAS,OAAO,IACpC;GAAE,QAAQ;GAAQ,QAAQ;EAAmC,IAC7D;GACE,QAAQ;GACR,QAAQ,QACJ,sIACA;EACN;EACJ,IAAI,IAAI,WAAW,UAAU,aAAa;CAC5C;CAEA,MAAM,KAAK,MAAM,KAAK;CACtB,MAAM,MAAgB,KAClB;EAAE,QAAQ;EAAQ,QAAQ,cAAc,SAAS,IAAI,MAAM,GAAG,GAAG;CAAG,IACpE;EACE,QAAQ;EACR,QAAQ,SAAS,SAAS,IAAI,MAAM,QAAQ,SAAS,WAAW;EAChE,OAAO,SAAS,IAAI;CACtB;CAEJ,OAAO;EACL,UAAU,MAAM;EAChB,MAAM,SAAS;EACf,GAAI,MAAM,SAAS,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;EAC/C;EACA;EACA,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;EACnC;CACF;AACF;;AAGA,SAAgB,KAAK,QAAmB,QAA6C;CACnF,MAAM,QAAkB,CAAC;CAUzB,OAAO;EAAE,YATU,OAAO,WAAW,KAAK,UAAU;GAClD,MAAM,MAAM,MAAM,UAAU;GAC5B,MAAM,QAAQ,OAAO,IAAI,GAAG;GAC5B,IAAI,CAAC,SAAS,aAAa,MAAM,SAAS,CAAC,aAAa,WAAW,WAGjE,MAAM,KAAK,GAAG,MAAM,SAAS,8BAA8B,OAAO,eAAe;GACnF,OAAO,aAAa,OAAO,SAAS,EAAE,aAAa,CAAC,EAAE,CAAC;EACzD,CACkB;EAAG;CAAM;AAC7B;;AAGA,SAAgB,SAAS,GAAqB;CAC5C,OAAO,EAAE,WAAW,MAAM,MAAM,EAAE,IAAI,WAAW,UAAU,EAAE,IAAI,WAAW,MAAM;AACpF;;AAGA,SAAgB,cAAc,GAAyD;CACrF,MAAM,MAAmD,CAAC;CAC1D,KAAK,MAAM,KAAK,EAAE,YAChB,KAAK,MAAM,QAAQ,CAAC,EAAE,KAAK,EAAE,GAAG,GAC9B,IAAI,KAAK,WAAW,aAAa,KAAK,OACpC,IAAI,KAAK;EAAE,UAAU,EAAE;EAAU,OAAO,KAAK;CAAM,CAAC;CAG1D,OAAO;AACT;;AAGA,SAAgB,WAAW,GAAoB;CAC7C,MAAM,SAAS,MAAiB,EAAE,WAAW,SAAS,MAAM,EAAE,WAAW,YAAY,MAAM;CAC3F,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,KAAK,EAAE,YAAY;EAC5B,MAAM,KAAK,GAAG,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE,OAAO,KAAK,IAAI;EAC9D,MAAM,KAAK,KAAK,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,QAAQ;EACtD,MAAM,KAAK,KAAK,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,QAAQ;EACtD,KAAK,MAAM,KAAK,EAAE,WAAW,MAAM,KAAK,OAAO,GAAG;CACpD;CACA,KAAK,MAAM,KAAK,EAAE,OAAO,MAAM,KAAK,KAAK,GAAG;CAC5C,OAAO,MAAM,KAAK,IAAI;AACxB;;AAYA,SAAgB,cAAc,SAAqC;CAIjE,OAAO,MAAM;EAHI;EAAY;EAAU;EAAU;CAG9B,GAFN,QAAQ,KAAK,MAAM;EAAC,EAAE;EAAU,EAAE;EAAQ,EAAE;EAAK,EAAE;CAAG,CAE1C,GADP,QAAQ,SAAS,MAAM,EAAE,UAAU,KAAK,MAAM,OAAO,EAAE,SAAS,IAAI,GAAG,CACrD,CAAC;AACvC"}
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
//#region src/plan-format.ts
|
|
2
|
+
/**
|
|
3
|
+
* The end-of-run summary table shared by `onboarding/plan.ts`'s and `ads/plan.ts`'s
|
|
4
|
+
* `formatSummary` — same header row, same column padding, same "Reminders
|
|
5
|
+
* (manual — no API):" tail. Their `StepAction`/`StepPlan`/`formatPlan` glyph
|
|
6
|
+
* renderers stay separate: onboarding's steps are `create | update | skip`
|
|
7
|
+
* with no `blocked` state, ads' are `create | skip | blocked` with a `needs`
|
|
8
|
+
* field, and collapsing those two shapes into one would blur a real
|
|
9
|
+
* difference rather than remove duplication.
|
|
10
|
+
*/
|
|
11
|
+
const pad = (s, n) => s.padEnd(n);
|
|
12
|
+
/** Render a fixed-width table, then a "Reminders" section if any were passed. */
|
|
13
|
+
function table(headers, rows, reminders) {
|
|
14
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)));
|
|
15
|
+
const line = (cells) => cells.map((c, i) => pad(c, widths[i] ?? 0)).join(" ").trimEnd();
|
|
16
|
+
const out = [line(headers), widths.map((w) => "─".repeat(w)).join(" ")];
|
|
17
|
+
for (const row of rows) out.push(line(row));
|
|
18
|
+
if (reminders.length) out.push("", "Reminders (manual — no API):", ...reminders);
|
|
19
|
+
return out.join("\n");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
1
23
|
//#region src/onboarding/cloudflare.ts
|
|
2
24
|
/**
|
|
3
25
|
* Cloudflare DNS — the one thing this command writes.
|
|
@@ -88,5 +110,5 @@ async function createApexTxt(token, zoneId, domain, content, fetchImpl = fetch)
|
|
|
88
110
|
}
|
|
89
111
|
|
|
90
112
|
//#endregion
|
|
91
|
-
export { findZoneId as n, listApexTxt as r, createApexTxt as t };
|
|
92
|
-
//# sourceMappingURL=cloudflare-
|
|
113
|
+
export { table as i, findZoneId as n, listApexTxt as r, createApexTxt as t };
|
|
114
|
+
//# sourceMappingURL=cloudflare-CgAsXiqe.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudflare-CgAsXiqe.mjs","names":[],"sources":["../src/plan-format.ts","../src/onboarding/cloudflare.ts"],"sourcesContent":["/**\n * The end-of-run summary table shared by `onboarding/plan.ts`'s and `ads/plan.ts`'s\n * `formatSummary` — same header row, same column padding, same \"Reminders\n * (manual — no API):\" tail. Their `StepAction`/`StepPlan`/`formatPlan` glyph\n * renderers stay separate: onboarding's steps are `create | update | skip`\n * with no `blocked` state, ads' are `create | skip | blocked` with a `needs`\n * field, and collapsing those two shapes into one would blur a real\n * difference rather than remove duplication.\n */\nconst pad = (s: string, n: number) => s.padEnd(n);\n\n/** Render a fixed-width table, then a \"Reminders\" section if any were passed. */\nexport function table(headers: string[], rows: string[][], reminders: string[]): string {\n const widths = headers.map((h, i) =>\n Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)),\n );\n const line = (cells: string[]) =>\n cells\n .map((c, i) => pad(c, widths[i] ?? 0))\n .join(' ')\n .trimEnd();\n\n const out = [line(headers), widths.map((w) => '─'.repeat(w)).join(' ')];\n for (const row of rows) out.push(line(row));\n if (reminders.length) out.push('', 'Reminders (manual — no API):', ...reminders);\n return out.join('\\n');\n}\n","/**\n * Cloudflare DNS — the one thing this command writes.\n *\n * Scoped to exactly what onboarding needs and nothing more: find a zone by name,\n * read the apex TXT records, and *create* the verification TXT when it is missing.\n * There is deliberately no update and no delete — the command's contract is that it\n * never removes a DNS record, so the destructive verbs are simply absent rather\n * than guarded.\n *\n * The token is a `Zone:DNS:Edit` credential from the environment\n * (`CLOUDFLARE_API_TOKEN`); listing a zone by name also needs `Zone:Read`, which\n * Cloudflare's standard \"Edit zone DNS\" token template already bundles. As with the\n * indexing executors, `fetchImpl` is injected so the request shape is testable\n * without the network, and every call returns a structured result rather than\n * throwing — the caller decides what a failure means per domain.\n */\nconst API = 'https://api.cloudflare.com/client/v4';\n\ninterface CfEnvelope<T> {\n success: boolean;\n errors?: { code: number; message: string }[];\n result?: T;\n}\n\nconst cfError = (status: number, body: CfEnvelope<unknown>): string => {\n const first = body.errors?.[0];\n return first ? `${first.code} ${first.message}` : `HTTP ${status}`;\n};\n\n/** Find the zone id for a domain (`GET /zones?name=`). */\nexport async function findZoneId(\n token: string,\n domain: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; zoneId?: string; message?: string }> {\n const res = await fetchImpl(`${API}/zones?name=${encodeURIComponent(domain)}`, {\n headers: { authorization: `Bearer ${token}` },\n });\n const body = (await res.json()) as CfEnvelope<{ id: string; name: string }[]>;\n if (!res.ok || !body.success)\n return { ok: false, status: res.status, message: cfError(res.status, body) };\n const zone = body.result?.[0];\n if (!zone)\n return {\n ok: false,\n status: res.status,\n // Name the scope: this lookup is a GET /zones?name=, which needs\n // `Zone:Read` — a token carrying only `Zone:DNS:Edit` reaches here and\n // reads as \"the zone isn't in this account\", which sends you to the\n // wrong dashboard.\n message: `no Cloudflare zone named \"${domain}\" (is it in this account, and does the token carry Zone:Read as well as Zone:DNS:Edit? The \"Edit zone DNS\" template covers both.)`,\n };\n return { ok: true, status: res.status, zoneId: zone.id };\n}\n\n/** List the apex TXT record contents for a zone. */\nexport async function listApexTxt(\n token: string,\n zoneId: string,\n domain: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; contents: string[]; message?: string }> {\n const res = await fetchImpl(\n `${API}/zones/${zoneId}/dns_records?type=TXT&name=${encodeURIComponent(domain)}`,\n { headers: { authorization: `Bearer ${token}` } },\n );\n const body = (await res.json()) as CfEnvelope<{ content: string }[]>;\n if (!res.ok || !body.success)\n return { ok: false, status: res.status, contents: [], message: cfError(res.status, body) };\n // Cloudflare returns TXT content quoted; normalise so an exact compare works.\n const contents = (body.result ?? []).map((r) => r.content.replace(/^\"|\"$/g, ''));\n return { ok: true, status: res.status, contents };\n}\n\n/**\n * Create an apex TXT record. **Create only** — never called when one already\n * exists (the planner decides that), and there is no sibling that edits or removes.\n */\nexport async function createApexTxt(\n token: string,\n zoneId: string,\n domain: string,\n content: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; message?: string }> {\n const res = await fetchImpl(`${API}/zones/${zoneId}/dns_records`, {\n method: 'POST',\n headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },\n body: JSON.stringify({ type: 'TXT', name: domain, content }),\n });\n const body = (await res.json()) as CfEnvelope<{ id: string }>;\n if (!res.ok || !body.success)\n return { ok: false, status: res.status, message: cfError(res.status, body) };\n return { ok: true, status: res.status };\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,OAAO,GAAW,MAAc,EAAE,OAAO,CAAC;;AAGhD,SAAgB,MAAM,SAAmB,MAAkB,WAA6B;CACtF,MAAM,SAAS,QAAQ,KAAK,GAAG,MAC7B,KAAK,IAAI,EAAE,QAAQ,GAAG,KAAK,KAAK,QAAQ,IAAI,EAAE,EAAE,UAAU,CAAC,CAAC,CAC9D;CACA,MAAM,QAAQ,UACZ,MACG,KAAK,GAAG,MAAM,IAAI,GAAG,OAAO,MAAM,CAAC,CAAC,CAAC,CACrC,KAAK,IAAI,CAAC,CACV,QAAQ;CAEb,MAAM,MAAM,CAAC,KAAK,OAAO,GAAG,OAAO,KAAK,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;CACvE,KAAK,MAAM,OAAO,MAAM,IAAI,KAAK,KAAK,GAAG,CAAC;CAC1C,IAAI,UAAU,QAAQ,IAAI,KAAK,IAAI,gCAAgC,GAAG,SAAS;CAC/E,OAAO,IAAI,KAAK,IAAI;AACtB;;;;;;;;;;;;;;;;;;;;ACVA,MAAM,MAAM;AAQZ,MAAM,WAAW,QAAgB,SAAsC;CACrE,MAAM,QAAQ,KAAK,SAAS;CAC5B,OAAO,QAAQ,GAAG,MAAM,KAAK,GAAG,MAAM,YAAY,QAAQ;AAC5D;;AAGA,eAAsB,WACpB,OACA,QACA,YAA0B,OACmD;CAC7E,MAAM,MAAM,MAAM,UAAU,GAAG,IAAI,cAAc,mBAAmB,MAAM,KAAK,EAC7E,SAAS,EAAE,eAAe,UAAU,QAAQ,EAC9C,CAAC;CACD,MAAM,OAAQ,MAAM,IAAI,KAAK;CAC7B,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,SACnB,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ,SAAS,QAAQ,IAAI,QAAQ,IAAI;CAAE;CAC7E,MAAM,OAAO,KAAK,SAAS;CAC3B,IAAI,CAAC,MACH,OAAO;EACL,IAAI;EACJ,QAAQ,IAAI;EAKZ,SAAS,6BAA6B,OAAO;CAC/C;CACF,OAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;EAAQ,QAAQ,KAAK;CAAG;AACzD;;AAGA,eAAsB,YACpB,OACA,QACA,QACA,YAA0B,OACsD;CAChF,MAAM,MAAM,MAAM,UAChB,GAAG,IAAI,SAAS,OAAO,6BAA6B,mBAAmB,MAAM,KAC7E,EAAE,SAAS,EAAE,eAAe,UAAU,QAAQ,EAAE,CAClD;CACA,MAAM,OAAQ,MAAM,IAAI,KAAK;CAC7B,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,SACnB,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ,UAAU,CAAC;EAAG,SAAS,QAAQ,IAAI,QAAQ,IAAI;CAAE;CAE3F,MAAM,YAAY,KAAK,UAAU,CAAC,EAAC,CAAE,KAAK,MAAM,EAAE,QAAQ,QAAQ,UAAU,EAAE,CAAC;CAC/E,OAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;EAAQ;CAAS;AAClD;;;;;AAMA,eAAsB,cACpB,OACA,QACA,QACA,SACA,YAA0B,OACkC;CAC5D,MAAM,MAAM,MAAM,UAAU,GAAG,IAAI,SAAS,OAAO,eAAe;EAChE,QAAQ;EACR,SAAS;GAAE,eAAe,UAAU;GAAS,gBAAgB;EAAmB;EAChF,MAAM,KAAK,UAAU;GAAE,MAAM;GAAO,MAAM;GAAQ;EAAQ,CAAC;CAC7D,CAAC;CACD,MAAM,OAAQ,MAAM,IAAI,KAAK;CAC7B,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,SACnB,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ,SAAS,QAAQ,IAAI,QAAQ,IAAI;CAAE;CAC7E,OAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;CAAO;AACxC"}
|
package/dist/color/index.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/color/colors.ts","../../src/color/color-harmony.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/color/colors.ts","../../src/color/color-harmony.ts"],"mappings":";UAiBiB,SAAA;EACf,CAAA;EACA,CAAA;EACA,CAAA;EACA,OAAA;EAHA;EAKA,aAAA;AAAA;;;;AAAa;iBAOC,WAAA,CAAY,CAAA,UAAW,CAAA,UAAW,CAAA,WAAY,SAAS;;;;;iBAmBvD,SAAA,CAAU,CAAA,UAAW,CAAA,UAAW,CAAA,WAAY,SAAS;;;;AAnBE;iBAsCvD,kBAAA,CACd,CAAA,UACA,CAAA,UACA,CAAA,UACA,KAAA;AAAA,cAUW,UAAA;AAAA,KACD,QAAA,WAAmB,UAAU;AAAA,cAE5B,YAAA;AAAA,KAWD,UAAA,WAAqB,YAAY;;;;iBAK7B,UAAA,CAAW,GAAA;EAAgB,CAAA;EAAW,CAAA;EAAW,CAAA;AAAA;AAAA,iBAUjD,UAAA,CAAW,KAAA;EAAS,CAAA;EAAW,CAAA;EAAW,CAAA;AAAA;;AAvC3B;AAU/B;;iBA0CgB,gBAAA,CAAiB,WAAmB;;AA1CqC;AACzF;;iBA0DgB,eAAA,CAAgB,IAAY;;AA1DH;AAEzC;iBAsKgB,YAAA,CAAa,GAAW;;;AA5J9B;iBAkLM,mBAAA,CAAoB,GAAW;AAAA,cAclC,aAAA;AAAA,KASD,WAAA,WAAsB,aAAa;AAAA,UAE9B,iBAAA;EACf,KAAA;EACA,OAAA;EACA,YAAA;AAAA;AAAA,cAGW,mBAAA,EAAqB,MAAA,CAAO,WAAA,EAAa,iBAAA;;;;;;AA3MY;AAUlE;;;;;;;;cAqPa,gBAAA,EAAkB,MAAM,CAAC,QAAA;AArP+B;AAarE;;;;AAAoD;AAiBpD;;;;AAA4C;AA8G5C;;;;AAAwC;AAsBxC;;;AAlKqE,cAsRxD,eAAA;EAAA,SAAwD,KAAA;EAAA,SAAA,IAAA;AAAA;AAAA,UAE3D,UAAA;EACR,CAAA;EACA,CAAA;EACA,CAAA;AAAA;;;AAlG6C;AAE/C;;iBAwGgB,UAAA,CAAW,WAAA,WAAsB,UAAU;;;;;iBAgC3C,qBAAA,CAAsB,SAAA,WAAoB,QAAQ;AArIpD;AAGd;;;;;;;AAHc,iBA4JE,eAAA,CAAgB,IAAA,EAAM,QAAQ,EAAE,SAAA;;;;;AAzJuB;AAoDvE;;;;AAA8C;AAiC9C;;;;;AAAsE;;;iBA6FtD,oBAAA,CACd,KAAA,sBAA2B,MAAA,mBAC1B,MAAA,CAAO,QAAA;;iBAsFM,UAAA,CAAW,EAAA,UAAY,EAAU;;;AAhL9C;AAQH;iBAgLgB,MAAA,CAAO,EAAA,UAAY,GAAY;;;AAhLY;AAgC3D;;;;AAAkE;AAuBlE;;iBA0IgB,eAAA,CAAgB,GAAA,UAAa,MAAA;EAAoB,KAAA;EAAe,IAAA;AAAA;;;;AAlkBhF;;KCbY,eAAA;AAAA,UAWK,aAAA;EACf,EAAA,EAAI,eAAe;EACnB,KAAA;EACA,WAAA;EACA,QAAA;AAAA;AAAA,cAGW,kBAAA,EAAoB,aAAa;ADC/B;AAOf;;AAPe,iBC2DC,oBAAA,CAAqB,OAAA,UAAiB,QAAA,EAAU,eAAe;;;;iBAgB/D,qBAAA,CACd,OAAA,WACC,KAAK;EAAG,QAAA,EAAU,eAAA;EAAiB,GAAA;EAAa,KAAA;EAAe,WAAA;AAAA;;;;cAYrD,uBAAA;EAAA;;;;;;;;iBAUG,yBAAA,CACd,IAAA,eAAmB,uBAAuB,EAC1C,YAAA"}
|