@getvitops/utils 8.2.0-alpha.0 → 8.3.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +399 -23
  2. package/dist/{adc-D6aw2qgq.mjs → adc-BiW9BzqA.mjs} +2 -3
  3. package/dist/{adc-D6aw2qgq.mjs.map → adc-BiW9BzqA.mjs.map} +1 -1
  4. package/dist/{adc-BoEoogyo.d.mts → adc-DvX3-P7a.d.mts} +2 -4
  5. package/dist/adc-DvX3-P7a.d.mts.map +1 -0
  6. package/dist/ads/index.d.mts +37 -57
  7. package/dist/ads/index.d.mts.map +1 -1
  8. package/dist/ads/index.mjs +21 -10
  9. package/dist/ads/index.mjs.map +1 -1
  10. package/dist/audit/index.d.mts +180 -0
  11. package/dist/audit/index.d.mts.map +1 -0
  12. package/dist/audit/index.mjs +509 -0
  13. package/dist/audit/index.mjs.map +1 -0
  14. package/dist/color/index.d.mts +7 -5
  15. package/dist/color/index.d.mts.map +1 -1
  16. package/dist/color/index.mjs +10 -5
  17. package/dist/color/index.mjs.map +1 -1
  18. package/dist/content.d.mts +78 -0
  19. package/dist/content.d.mts.map +1 -0
  20. package/dist/content.mjs +45 -0
  21. package/dist/content.mjs.map +1 -0
  22. package/dist/crm/index.d.mts +5 -87
  23. package/dist/crm/index.d.mts.map +1 -1
  24. package/dist/crm/index.mjs +10 -19
  25. package/dist/crm/index.mjs.map +1 -1
  26. package/dist/email/index.d.mts +1 -11
  27. package/dist/email/index.d.mts.map +1 -1
  28. package/dist/email/index.mjs +6 -10
  29. package/dist/email/index.mjs.map +1 -1
  30. package/dist/favicon.d.mts +0 -2
  31. package/dist/favicon.d.mts.map +1 -1
  32. package/dist/favicon.mjs +1 -2
  33. package/dist/git/index.d.mts +0 -2
  34. package/dist/git/index.d.mts.map +1 -1
  35. package/dist/git/index.mjs +1 -2
  36. package/dist/hosting/index.d.mts +0 -8
  37. package/dist/hosting/index.d.mts.map +1 -1
  38. package/dist/hosting/index.mjs +4 -7
  39. package/dist/hosting/index.mjs.map +1 -1
  40. package/dist/{identity-BoRddTx3.mjs → identity-C4yInICW.mjs} +2 -3
  41. package/dist/{identity-BoRddTx3.mjs.map → identity-C4yInICW.mjs.map} +1 -1
  42. package/dist/index.d.mts +2 -88
  43. package/dist/index.d.mts.map +1 -1
  44. package/dist/index.mjs +36 -52
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/indexing/index.d.mts +3 -42
  47. package/dist/indexing/index.d.mts.map +1 -1
  48. package/dist/indexing/index.mjs +8 -13
  49. package/dist/indexing/index.mjs.map +1 -1
  50. package/dist/{json-store-30CFCbZZ.mjs → json-store-VJKEaoVX.mjs} +2 -3
  51. package/dist/{json-store-30CFCbZZ.mjs.map → json-store-VJKEaoVX.mjs.map} +1 -1
  52. package/dist/media/index.d.mts +0 -12
  53. package/dist/media/index.d.mts.map +1 -1
  54. package/dist/media/index.mjs +7 -13
  55. package/dist/media/index.mjs.map +1 -1
  56. package/dist/notify/index.d.mts +64 -44
  57. package/dist/notify/index.d.mts.map +1 -1
  58. package/dist/notify/index.mjs +138 -11
  59. package/dist/notify/index.mjs.map +1 -1
  60. package/dist/onboarding/index.d.mts +26 -6
  61. package/dist/onboarding/index.d.mts.map +1 -1
  62. package/dist/onboarding/index.mjs +92 -5
  63. package/dist/onboarding/index.mjs.map +1 -1
  64. package/dist/{plan-BLKikoOG.mjs → plan-B9USuc17.mjs} +3 -4
  65. package/dist/{plan-BLKikoOG.mjs.map → plan-B9USuc17.mjs.map} +1 -1
  66. package/dist/{plan-Hq8Pb0Uw.d.mts → plan-CH3Wt5ZV.d.mts} +8 -30
  67. package/dist/plan-CH3Wt5ZV.d.mts.map +1 -0
  68. package/dist/plan-CddqsmJJ.d.mts +82 -0
  69. package/dist/plan-CddqsmJJ.d.mts.map +1 -0
  70. package/dist/plan-DpHv6NO8.mjs +93 -0
  71. package/dist/plan-DpHv6NO8.mjs.map +1 -0
  72. package/dist/{plan-format-A18SvJ7C.mjs → plan-format-CIz6Feg2.mjs} +2 -3
  73. package/dist/{plan-format-A18SvJ7C.mjs.map → plan-format-CIz6Feg2.mjs.map} +1 -1
  74. package/dist/{platform-0nlOIy8h.mjs → platform-Cb_RtsF1.mjs} +3 -5
  75. package/dist/{platform-0nlOIy8h.mjs.map → platform-Cb_RtsF1.mjs.map} +1 -1
  76. package/dist/reviews/index.d.mts +4 -120
  77. package/dist/reviews/index.d.mts.map +1 -1
  78. package/dist/reviews/index.mjs +6 -99
  79. package/dist/reviews/index.mjs.map +1 -1
  80. package/dist/reviews/select.d.mts +2 -0
  81. package/dist/reviews/select.mjs +3 -0
  82. package/dist/schema/index.d.mts +0 -12
  83. package/dist/schema/index.d.mts.map +1 -1
  84. package/dist/schema/index.mjs +6 -12
  85. package/dist/schema/index.mjs.map +1 -1
  86. package/dist/security/index.d.mts +0 -2
  87. package/dist/security/index.d.mts.map +1 -1
  88. package/dist/security/index.mjs +1 -2
  89. package/dist/{token-DmmoQ7um.d.mts → token-CHR1O4w5.d.mts} +3 -3
  90. package/dist/token-CHR1O4w5.d.mts.map +1 -0
  91. package/dist/{token-DkKC5m23.mjs → token-CJSl9eIS.mjs} +33 -14
  92. package/dist/token-CJSl9eIS.mjs.map +1 -0
  93. package/dist/tracking/index.d.mts +1 -9
  94. package/dist/tracking/index.d.mts.map +1 -1
  95. package/dist/tracking/index.mjs +3 -4
  96. package/dist/turnstile/index.d.mts +105 -0
  97. package/dist/turnstile/index.d.mts.map +1 -0
  98. package/dist/turnstile/index.mjs +281 -0
  99. package/dist/turnstile/index.mjs.map +1 -0
  100. package/dist/{types-7ELci8RQ.d.mts → types-oac2P5or.d.mts} +3 -23
  101. package/dist/types-oac2P5or.d.mts.map +1 -0
  102. package/dist/url/index.d.mts +18 -0
  103. package/dist/url/index.d.mts.map +1 -0
  104. package/dist/url/index.mjs +34 -0
  105. package/dist/url/index.mjs.map +1 -0
  106. package/package.json +43 -2
  107. package/dist/adc-BoEoogyo.d.mts.map +0 -1
  108. package/dist/cloudflare-BiK61Oq4.d.mts +0 -27
  109. package/dist/cloudflare-BiK61Oq4.d.mts.map +0 -1
  110. package/dist/cloudflare-CD8vwrgj.mjs +0 -92
  111. package/dist/cloudflare-CD8vwrgj.mjs.map +0 -1
  112. package/dist/plan-Hq8Pb0Uw.d.mts.map +0 -1
  113. package/dist/token-DkKC5m23.mjs.map +0 -1
  114. package/dist/token-DmmoQ7um.d.mts.map +0 -1
  115. package/dist/types-7ELci8RQ.d.mts.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,389 @@
1
1
  # @getvitops/utils
2
2
 
3
+ ## 8.3.0-alpha.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 161c7f3: A contact form that works end to end: `vitops setup turnstile`, `<wc-turnstile>`, the `vitops.form` EmDash block, and the route the emdash template ships to receive it.
8
+
9
+ **CSS, all formats** (no existing class changes): `forms.css` gains `.form-notice--target:not(:target)` (a message revealed only when the URL fragment names it, which is how a prerendered page shows a no-JS form's result) and `:where(wc-turnstile) { display: block }`.
10
+
11
+ **`@getvitops/core`:** new `<wc-turnstile sitekey="…">` in `elements.js`. It loads Cloudflare Turnstile on the first interaction with its form (no request to `challenges.cloudflare.com` for a visitor who only reads the page), holds an early submit until the token arrives, and leaves its slotted no-JS explanation in place until JS takes over. Turnstile is category `necessary`, so it is not behind the consent banner.
12
+
13
+ **`@getvitops/utils` / `@getvitops/cli`:** new `@getvitops/utils/turnstile` and `vitops setup turnstile [--dry] [--check] [--no-write] [--no-secret]`. It plans from the site's hostnames, creates a managed widget unless one already covers them (an existing widget is adopted, never edited, rotated or deleted), writes the public key to `site.security.turnstile.siteKey`, and pipes the one-time secret to `wrangler secret put VITOPS_TURNSTILE_SECRET` without writing it to any file. Needs `CLOUDFLARE_API_TOKEN` with **Account > Turnstile > Edit** and `CLOUDFLARE_ACCOUNT_ID`.
14
+
15
+ **`@getvitops/astro`:** `virtual:getvitops/head` gains `turnstileSiteKey` (read from `site.security.turnstile.siteKey`) so blocks can render the widget. No other change.
16
+
17
+ **`@getvitops/generator`:** `TIERS` gains a `turnstile` entry (wc-turnstile, claimed by `vitops.form`); `wc-multi-field`'s `emdashExempt` reason is reworded now the form block exists.
18
+
19
+ **`@getvitops/emdash`:** new `vitops.form` block: a field repeater (text, email, phone, long text, choose one, tick box), required flags, help text, success/error messages, optional Turnstile. It renders a semantic `<form method="post">` that works with no JavaScript, with a honeypot named per `createConversionRoute`'s defaults. A field named `phone` submits as `tel`, because the route reserves `phone` and `event`.
20
+
21
+ **`@getvitops/create`:** the emdash template ships `src/pages/api/track.ts` (`prerender = false`, `createConversionRoute()`), a seeded contact form, and `challenges.cloudflare.com` in both templates' Report-Only CSP (`script-src`, `frame-src`). The route refuses a post with nowhere to deliver and fails closed when a site key exists without `VITOPS_TURNSTILE_SECRET`. SETUP.html gains the Turnstile step. Existing sites: copy the route and the CSP change, then run `vitops setup turnstile`.
22
+
23
+ - 175d16e: **Added: `vitops.reviews` and `vitops.nav` EmDash blocks.** Non-breaking.
24
+
25
+ - **`vitops.reviews`** renders Google reviews from the committed `.vitops/reviews.json` cache (`vitops reviews`) as a grid, carousel or marquee of `<ReviewCard>`. Fields: location key, layout, columns, limit, lowest rating, order, featured ids, reviewer photos (off by default), tone/colour. Which reviews show is `selectReviews()`, the one shared decision. Review JSON-LD is not offered (Google treats self-marked-up reviews as ineligible for rich results). A new review reaches the page after `vitops reviews` is re-run, committed and the site rebuilt, not when an editor saves; with no cache file the block renders nothing (a note in `astro dev`).
26
+ - **`vitops.nav`** renders a named EmDash menu inside page content (site map, section links, footer-style columns), never the header. Fields: menu (picked from the site's menus via a new `menus/list` plugin route, with `footer-sitemap`/`footer-legal`/`primary` as the static fallback), accessible name, visible heading and level, layout `list | inline | columns | sitenav`, depth 1-3, tone/colour. It is a `<nav>` with a name that is never `Primary`, `Site map` or `Legal` (the layout's landmarks), `aria-current="page"`, no runtime JS; a missing or empty menu renders nothing.
27
+ - `@getvitops/astro`: **`virtual:getvitops/reviews`**, served by `vitops()`, is `.vitops/reviews.json` as a build-time import (an empty cache when absent or unreadable), because a Worker cannot read the file. `<ReviewCard>` now renders the review's source link (`.review__source`, `rel="nofollow noopener noreferrer"`), which the pattern already styled. **Fix:** `src/json-ld.ts` was missing from the published `files`, so any consumer importing `<ReviewCard>` (which composes `schemas/Review.astro`) failed to build with an unresolved import. `@getvitops/emdash` now needs this version of `@getvitops/astro` for the `vitops.reviews` block.
28
+ - `@getvitops/utils`: new Worker-safe subpath **`@getvitops/utils/reviews/select`** (`selectReviews` and the review types, no `node:fs`). `selectReviews` now takes only the display fields (`featured`, `minRating`, `limit`, `sort`), so a caller with no configured `locationId` can use it; existing calls are unchanged.
29
+ - `@getvitops/core`: new `.navlist` pattern in `patterns/sitenav.css` (`.navlist`, `__item`, `__link`, `__label`, `__sub`, `--inline`, `--columns`). **All formats** (css, bricks, tailwind). No existing class changes.
30
+ - `@getvitops/generator`: `TIERS.review` lists `vitops.reviews` and `TIERS.sitenav` lists `vitops.nav` (and `.navlist`), so `vitops docs components` and the docsite show them.
31
+
32
+ - 83d6f3c: Theming escape hatches, so a real palette builds without raw ramp steps. All additive; existing configs build unchanged.
33
+
34
+ - **Dark-only sites.** `designSystem.defaultColorScheme: "dark"` (accepted by the schema before, but ignored) now makes the dark tokens the `:root` appearance with `color-scheme: dark`, emits no light block or `data-theme` flip, and skips light-mode contrast checks. Reaches css, bricks and tailwind formats. Do not ship `<wc-color-scheme-toggle>` on such a site.
35
+ - **Role solid step and foreground.** `colors.roles.<role>` may be `{ hue, kind?, solid?, foreground? }`. `solid: 400` starts the solid family at step 400 (bold and x-bold walk one and two rungs darker); `foreground: "dark" | "light"` pins `text-on-<role>`. The APCA check (Lc 60) still runs on the pair emitted. The string shorthand is unchanged. All formats. The live editor's `roleTokens` still assume the default 600 family when re-pointing a role.
36
+ - **Split shadows.** `shadows.<name>` may be `{ box?, drop? }`: `box` (spread, inset, layers) is `--shadow-<name>`; `drop` is `--shadow-<name>-drop` and feeds `.drop-shadow-<name>` and the `shadow:` state shortcut. The string form is unchanged. css/bricks emit `.drop-shadow-*`; tailwind emits the `-drop` token only.
37
+ - **Layout tokens.** New optional `layout` block (`widths.{measure,breakout,spotlight,nav}`, `regionSpace.{min,preferred,max}`) emitted as `--width-*` / `--region-space*` on `:root` in css, bricks and tailwind formats, and exposed in `design-manifest.json` (`layout`, `reverseIndex`). Keys you omit keep the framework defaults.
38
+ - **`acknowledgeOffLadder: true`** on a palette hue silences that hue's off-ladder anchor warnings.
39
+ - **Typography:** `transform` and `decoration` are now accepted as aliases of `text-transform` and `text-decoration` (canonical wins when both are present) instead of warning and being dropped.
40
+ - **Docs strings** (`authoring.md`, JSON Schema hovers) state the Lc 60 muted-text requirement against the page plane and give a type-scale recipe for marketing headings.
41
+ - `@getvitops/utils`: `pickOn` takes an optional `prefer: 'light' | 'dark'`.
42
+
43
+ - b47df4d: **Added: `vitops audit` — Lighthouse + axe budgets over a built site.** Non-breaking, opt-in.
44
+
45
+ `vitops audit [--build] [--url <base>] [--pages /a,/b] [--warn] [--out <dir>]` serves the existing build with `astro preview` on a dedicated port (4398; it aborts rather than attach to something else already there), audits `/` plus `/privacy`, `/terms`, `/cookies`, `/contact` and every link in `seed/seed.json`'s `primary*`/`footer*` menus, and skips any route that 404s (a 404 on `/` fails the run). Per page it runs axe (tags wcag2a, wcag2aa, wcag21a, wcag21aa) and Lighthouse on the same Chromium. Default budgets: LCP < 2500 ms, CLS < 0.1, TBT < 200 ms (the INP proxy), no serious or critical axe violations; override with `--lcp`, `--cls`, `--tbt`, `--impact`. Writes `report.json`, `report.md` and `pages/<page>.{json,md}` to `.vitops/audit/`; exits non-zero on failure unless `--warn`. Every report states that a pass is **not** AODA certification and lists the manual checks.
46
+
47
+ `playwright`, `@axe-core/playwright` and `lighthouse` are **optional peer dependencies** of `@getvitops/utils`, loaded lazily — nothing is installed or loaded unless you run the command, and a missing one fails with the install command. Pure planner and report rendering are `@getvitops/utils/audit` (new subpath).
48
+
49
+ CI: the reusable `site-ci.yml` gains opt-in `run-audit` (default `false`) and `audit-args` inputs; callers pinned to `@v1` that pass neither are unaffected.
50
+
51
+ ### Patch Changes
52
+
53
+ - @getvitops/config@8.3.0-alpha.0
54
+
55
+ ## 8.2.0
56
+
57
+ ### Minor Changes
58
+
59
+ - 89265af: **New package: `@getvitops/config`** — the site-config provider blocks (`site.crm`, `site.ads`,
60
+ `site.reviews`, `site.seo.indexing`, `site.searchConsole`, `site.notifications`, `site.tracking`,
61
+ `site.security`, `site.deployment`) as zod schemas and inferred types, with the closed provider lists
62
+ (`AD_PROVIDERS`, `CRM_PROVIDERS`, `BIGIN_DATA_CENTERS`) on a zod-free `./providers` entry. It joins the
63
+ `fixed` group.
64
+
65
+ Why: `@getvitops/utils` holds the provider code but could not import its configuration types — the schema
66
+ lived in `@getvitops/generator`, a layer above — so it kept a hand-copied mirror beside each module,
67
+ an adapter converting into it, and a test holding the copy to the schema. The mirrors are gone.
68
+
69
+ - **`@getvitops/generator`**: `AD_PROVIDERS`, `CRM_PROVIDERS`, `BIGIN_DATA_CENTERS`, `AdProvider` and
70
+ `CrmProviderConfig` are re-exported unchanged. No import path changes.
71
+ - **`@getvitops/utils`**: the config types in `./crm`, `./ads`, `./indexing`, `./onboarding`, `./reviews`,
72
+ `./tracking` and `./notify` (`CrmConfig`, `HubSpotConfig`, `BiginConfig`, `JobberConfig`,
73
+ `IndexNowConfig`, `SearchConsoleConfig`, `ReviewsConfig`, `ReviewLocationConfig`, `TrackingConfig`,
74
+ `NotificationsConfig`, `EmailChannelConfig`, `DomainSetup`, `AdPropertySetup`) are now inferred from the
75
+ schema. Same names, same fields, but **optional fields now follow zod's inference**, so a hand-written
76
+ object literal typed against them may need `| undefined` handled. `AdPropertySetup` gains the fields the
77
+ schema always had (`conversionActionId`, `reporting`).
78
+ - **`@getvitops/astro`**: `GetvitopsAdProperty` and `GetvitopsTrackingOptions` are aliases of the schema
79
+ types. `toCrmConfig()` and `toGoogleAdsConfig()` are **deprecated** — they returned their argument's
80
+ field unchanged; pass `config.site.crm` / `config.site.ads?.google` directly.
81
+ - **`@getvitops/cli`**: `vitops ads` now passes `conversionActionId` and `reporting` through to the planner;
82
+ it previously copied a hand-picked subset of the block and dropped them.
83
+
84
+ Formats reached: none — no CSS change.
85
+
86
+ - 0574388: Add `site.crm` — push form submissions into HubSpot, Zoho Bigin, or Jobber.
87
+
88
+ Client sites need form submissions to land in the client's CRM. The options available before this
89
+ — a HubSpot/Bigin/Jobber iframe or JS embed — fail on three counts: the markup can't be
90
+ white-labelled against this design system, nothing can operate on the submission before it leaves
91
+ the page, and the submission never touched `createConversionRoute()`, so `_ac` ad-click attribution
92
+ was lost and no conversion notification was planned.
93
+
94
+ This adds one server-side path for all three CRMs, hung off the conversion route that already
95
+ exists. `createConversionRoute()` gets a new `crm` option (`{ config, credentials, fetch?,
96
+ timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the existing `binding`
97
+ option, and are never config. A failed CRM write never fails the request and is folded into the
98
+ notification email's new "Delivery" section, so a broken CRM token is never silent.
99
+
100
+ New `@getvitops/utils/crm` subpath: `deliverToCrm()`, `mapSubmission()`, `deriveLastName()` (Bigin
101
+ requires a last name; this derives one from an explicit field, a full name, or an email address,
102
+ refusing rather than fabricating one), `submitToHubSpot()`, `submitToBigin()`, `submitToJobber()`.
103
+ `identifySubmitter()` was promoted out of `notify`'s private name-guessing cascade into
104
+ `@getvitops/utils/tracking`, since the CRM mapper needed the whole identity (first/last name,
105
+ email, phone, company), not just a display name.
106
+
107
+ Jobber is GraphQL, not REST, and answers `200 OK` for failures — a rejected `clientCreate` returns
108
+ `userErrors[]`, a malformed request returns top-level `errors[]`. `submitToJobber()` classifies on
109
+ the response body, not just the status. Its refresh tokens also rotate by default: **Refresh Token
110
+ Rotation must be turned off** in the app's Developer Center settings, since a Worker reading a
111
+ static `VITOPS_JOBBER_REFRESH_TOKEN` cannot persist a rotated one — a rotated token is detected at
112
+ exchange time and reported with the fix, rather than surfacing an hour later as an opaque 401.
113
+ Attribution and unmapped extra fields render into a best-effort note by default (works on every
114
+ Jobber plan); a resubmitting visitor creates a second Jobber client unless `crm.jobber.dedupe: true`
115
+ is set once its lookup query has been verified against a live account (defaults `false` pending
116
+ that verification — see `jobber.ts`'s docblock).
117
+
118
+ New `site.crm` config block (`provider`, `hubspot`/`bigin`/`jobber`, `fieldMap`, `trackingFields`)
119
+ and two CLI commands: `vitops crm check` (fetches the live HubSpot property list or Bigin field
120
+ list and reports every mapped name that doesn't exist there — HubSpot rejects an unknown field
121
+ outright, Bigin silently drops it; Jobber has nothing live to check yet, since its mapped values
122
+ currently render as note text) and `vitops crm setup` (creates missing HubSpot properties; Bigin
123
+ and Jobber have no such API, so both always print a manual reminder).
124
+
125
+ The generated privacy policy now names HubSpot, Zoho Bigin, or Jobber when `site.crm` configures
126
+ one. Bigin gets the same cross-border-transfer derivation Matomo Cloud already gets: a
127
+ `dataCenter: 'ca'` asserts no transfer outside Canada, any other data centre does. Jobber's
128
+ operator (Octopusapp Inc. d/b/a Jobber) is Canadian, but its own privacy policy names the United
129
+ States as a processing location too, so the policy always asserts a cross-border transfer for
130
+ Jobber regardless of config — a Canadian operator is not read as Canadian-only storage.
131
+
132
+ Credentials: `VITOPS_HUBSPOT_ACCESS_TOKEN` for HubSpot; `VITOPS_ZOHO_CLIENT_ID` /
133
+ `VITOPS_ZOHO_CLIENT_SECRET` / `VITOPS_ZOHO_REFRESH_TOKEN` for Bigin; `VITOPS_JOBBER_CLIENT_ID` /
134
+ `VITOPS_JOBBER_CLIENT_SECRET` / `VITOPS_JOBBER_REFRESH_TOKEN` for Jobber. Never config.
135
+
136
+ No consumer-facing change in `@getvitops/astro` beyond the new `crm` route option and a build
137
+ warning when `site.crm` is configured with no route answering `TRACKING_ENDPOINT` — CSS/Bricks/
138
+ tailwind outputs are unaffected.
139
+
140
+ - c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
141
+ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
142
+ './site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
143
+ `<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
144
+ offered, now usable by the client directly, no dev server required. An edit is visible
145
+ site-wide immediately (a runtime `:root` override, served via a new plugin API route and
146
+ `page:fragments`) and — once `site.deployment.repo`/`branch` and a `VITOPS_GITHUB_TOKEN`
147
+ secret are set (see the scaffold's `SETUP.html`) — auto-commits to your `dev` branch, so it
148
+ reaches the git-tracked `site.json` through your repo's normal review path rather than
149
+ becoming a second, unreviewed source of truth. Until that's configured, the live half still
150
+ works; only the auto-commit half is skipped (and says so in the admin page). The save route
151
+ runs the exact same accessibility (APCA contrast) gate `generate()` enforces at build time,
152
+ synchronously, before persisting — nothing non-compliant ever goes live even momentarily.
153
+
154
+ Requires `@astrojs/react` + `react` + `react-dom` (already scaffold dependencies, for
155
+ EmDash's own admin) and `@getvitops/core` (added as a new scaffold dependency this release).
156
+ Declares two new plugin capabilities when enabled: `network:request` (for the GitHub
157
+ auto-commit) and `hooks.page-fragments:register` (for the live override) — reviewed once at
158
+ plugin-install time.
159
+
160
+ **Breaking for `@getvitops/emdash`**: `vitopsEmdash()`'s `hooks.page-fragments:register`
161
+ capability is now also granted whenever `designSystemEditor` is set, independent of the
162
+ existing `scripts: 'fragments'` option (which governs something unrelated — web-component
163
+ script delivery). No change if you don't use `designSystemEditor`.
164
+
165
+ Also new, supporting the above:
166
+
167
+ - `@getvitops/generator`: `validateColorContract()` and `buildReverseIndex()` (both
168
+ `/tokens` subpath, Worker-safe) — the monotonicity/contrast gate and the
169
+ `design-manifest.json` reverse-index, each extracted from `generate()`'s `build()` into
170
+ standalone, non-throwing functions. `patchToOverrideCss()` turns a design-system patch
171
+ into the `:root` override `<style>` block. `designSystemPath`/`getAt`/`setAt`/
172
+ `mergePatch`/`formatConfig` moved from `@getvitops/vite` into `@getvitops/generator/config`
173
+ (re-exported from `@getvitops/vite` unchanged) — one "patch a config file's design-system
174
+ subtree" implementation shared by the dev endpoint and the new plugin route. New
175
+ `site.deployment.repo`/`branch` config fields.
176
+ - `@getvitops/utils`: new `/git` subpath — `commitFile()`, a Worker-safe (fetch-only)
177
+ wrapper over GitHub's Contents API.
178
+ - `@getvitops/core`: `<wc-theme-editor>` gains a `save-endpoint` attribute (default
179
+ unchanged — `/__vitops/design-system`) so the same element can target the new plugin
180
+ route instead of the dev-only Vite endpoint.
181
+
182
+ - 0574388: Add `vitops email setup|check` — connect `site.notifications.email.from` to whether its domain is
183
+ actually onboarded onto an outbound email provider.
184
+
185
+ Before this, nothing checked that. `EMAIL.send()` throws `E_SENDER_NOT_VERIFIED` at runtime with no
186
+ build or deploy-time warning — the failure mode is silent and production-only.
187
+
188
+ `vitops email setup [domain]` onboards a sending domain (currently Cloudflare Email Sending only)
189
+ and polls DNS until its SPF/DKIM/DMARC records resolve or a timeout is reached; `vitops email
190
+ check` is the non-blocking deploy guard both `@getvitops/create` templates' `deploy`/`deploy:dev`
191
+ scripts now run first (`--strict` makes it a real gate; opt out per-site with
192
+ `site.notifications.email.verifySender: false`, or globally with `--skip` /
193
+ `VITOPS_SKIP_EMAIL_CHECK`).
194
+
195
+ Two things worth knowing before running it:
196
+
197
+ - **The onboarded entity is an exact name, and it is usually a subdomain**
198
+ (`send.yourdomain.com`), never the apex your `from` address names. When the exact domain isn't
199
+ onboarded but a sibling in the same zone already is, the command reports that as the fix — send
200
+ from the onboarded subdomain — rather than silently trying to onboard the apex, which rewrites
201
+ `_dmarc.<domain>` and can tighten DMARC policy on every other mail your domain sends.
202
+ - Candidates come from `site.notifications.email.from` (or its derived `noreply@<domains
203
+ .canonical>` default) first; a best-effort scan of `--src` only supplements it, and never guesses
204
+ at a computed `from` value.
205
+
206
+ New `@getvitops/utils/email` subpath and `site.notifications.email.verifySender` config field.
207
+ Needs `CLOUDFLARE_API_TOKEN` with an Email Sending scope in addition to the `Zone:Read` other
208
+ commands already use.
209
+
210
+ - 8d8aad7: **New: server-side Google Ads offline conversion upload.** Set
211
+ `site.ads.google.reporting: "server"` and a matching
212
+ `conversionActionId` (a numeric id or full
213
+ `customers/…/conversionActions/…` resource name — **not** the same value
214
+ as `conversionLabel`, which is the opaque label the web tag uses and
215
+ which the Ads API's upload endpoint doesn't accept) and
216
+ `createConversionRoute`'s new `googleAdsOffline` option uploads the
217
+ conversion via `ConversionUploadService.UploadClickConversions` instead
218
+ of relying on the visitor's browser to fire `gtag.js`. An action with no
219
+ matching `conversionActionId` keeps firing client-side regardless of
220
+ `reporting`, so turning it on before the id is filled in never silently
221
+ drops a conversion. `@getvitops/astro/routes` exports a matching
222
+ `toGoogleAdsConfig(config)` adapter, alongside `toCrmConfig`.
223
+
224
+ Structurally typed against Google's documented REST shape, not verified
225
+ against a live Ads account — no developer-token/MCC access exists
226
+ anywhere in this toolchain's own setup to test against. Wrapped so a
227
+ wrong guess degrades to a stated failure reason (folded into the
228
+ conversion email's "Delivery" section, same as a failed CRM write) rather
229
+ than breaking, the same caveat `@getvitops/utils/crm`'s Jobber executor
230
+ already carries for its own unverified surfaces.
231
+
232
+ `vitops ads lint` now flags a `reporting: "server"` property whose
233
+ configured actions have no matching `conversionActionId` — not a
234
+ failure, since that action keeps firing client-side, but worth surfacing
235
+ rather than leaving someone to wonder why nothing changed.
236
+
237
+ Not built: `vitops ads provision` (upserting conversion actions via
238
+ `ConversionActionService` and printing back both the label and the
239
+ resource id) — same credential blocker, and deliberately left unscoped
240
+ until there's a live account to verify the request/response shape
241
+ against.
242
+
243
+ - 0574388: Fire a Google Ads conversion on a tel:/mailto: click, and fix a rate-limit bug on the conversion
244
+ route that could silently drop one of the two.
245
+
246
+ `site.ads.google.conversionLabel` used to be accepted by the config schema and typed all the way
247
+ through to the rendered tag, but nothing actually read it — no `gtag('event', 'conversion', …)`
248
+ was ever emitted. When set, the click tracker `<Tracking />` inlines now fires the conversion at
249
+ the moment a visitor taps a `tel:` or `mailto:` link, using the `AW-…/label` target Google Ads
250
+ expects. A `conversionLabel` that can't safely land inside the inline bootstrap script (the same
251
+ check `pixelId`/`accountId` already get) is dropped with a warning rather than firing something
252
+ malformed — the ad tag itself still loads.
253
+
254
+ Also: `createConversionRoute()`'s rate limiter previously keyed on IP address alone, so a call
255
+ beacon and an email-click beacon from the same visitor, seconds apart, competed for one window —
256
+ the second could 429 and vanish with no error anywhere. It's now keyed on IP _and_ event type, so
257
+ distinct conversion types never starve each other; repeats of the same type are still throttled
258
+ exactly as before.
259
+
260
+ No config changes — a site with `site.ads.google.conversionLabel` already set picks this up
261
+ automatically on the next build.
262
+
263
+ - 0574388: Track `mailto:` link clicks as conversions, alongside the existing `tel:` link tracking.
264
+
265
+ Before this, the capture script only beaconed `tel:` clicks; a `mailto:` link on the same page was
266
+ invisible to attribution — a visitor who emailed the business instead of calling generated no
267
+ conversion event at all. `<Tracking />`'s inlined script now listens for both, sending `{ event:
268
+ 'email', email }` via `navigator.sendBeacon` the same way it already sends `{ event: 'call', phone
269
+ }`.
270
+
271
+ `ConversionEvent['type']` gains an `'email'` member alongside `'form'`/`'call'`, with a new
272
+ `email` field. `createConversionRoute()` handles it exactly like a call: no honeypot, Turnstile or
273
+ CRM checks (there are no form fields to validate or map), a 204 response, and a notification email
274
+ subject reading `[Email Click] Visitor emailing ...`.
275
+
276
+ No config changes and no new options — a site with `<Tracking />` already mounted picks this up
277
+ automatically the next time it rebuilds.
278
+
279
+ - dde3df3: **`site.ads.google.conversionLabel` now accepts a per-action map** —
280
+ `{ call?, email?, form? }` — instead of one bare string shared by every
281
+ interaction. A bare string still works (back-compat, treated as `call`), but a
282
+ call and an email click used to report as the **same** Google Ads conversion
283
+ action, indistinguishable in the account; each action now gets its own
284
+ `send_to` target.
285
+
286
+ **New: `vitopsFormConversion(formEl)`**, exposed as `window.vitopsFormConversion`
287
+ by `<Tracking />`'s capture script — call it from your own fetch-based form
288
+ submit handler once the POST to `createConversionRoute()` succeeds, to fire the
289
+ `form` Google Ads conversion action client-side (mirroring the existing
290
+ tel:/mailto: click handler, which now fires `call`/`email` instead of sharing
291
+ one label). Idempotent per form element. There's deliberately no server-side
292
+ equivalent for a no-JS `<form method="post">` submission — that's a full-page
293
+ redirect with no script to fire from, and a real server-side Google Ads
294
+ conversion needs the Ads API's click-conversion upload (developer token + MCC
295
+ access), tracked separately in the vNext backlog.
296
+
297
+ **Fixed: the conversion route's rate limiter no longer trusts the client's
298
+ raw `event` field.** It used to key on whatever string a client sent
299
+ (`` `${ip}:${body.event}` ``), so varying that value on every request let a
300
+ form submission dodge the per-IP rate limit entirely — only `'call'`/`'email'`
301
+ were ever recognized, and anything else got its own fresh bucket. The key now
302
+ uses the server's own classification (`call` | `email` | `form`), closing that
303
+ bypass without changing the existing call-vs-email-don't-compete behavior.
304
+
305
+ **New: `site.notifications.actionUrl`** — an optional action-link template
306
+ added to the conversion-notification email as an "Actions" section.
307
+ `{clickId}` is replaced with the ad click ID captured on `_ac` (gclid, fbclid,
308
+ etc.); the section is omitted when the event has no click ID to interpolate,
309
+ rather than emailing a broken link.
310
+
311
+ - 7f4f50e: `@getvitops/utils/url` exports `safeHref()` (a Worker-safe scheme allowlist: http, https, mailto, tel and relative), shared by the EmDash blocks and the Astro components. It now also guards the EmDash image-compare block, the review avatar and the cookie-notice policy link. `nodesToHtml`/`toHtml` throw on invalid tag or attribute names and on `on*` event-handler attributes, and drop `href`/`src`-style values that fail `safeHref`. EmDash cards link text now names the card for screen readers.
312
+ - ceb8310: **`@getvitops/utils/notify` is Worker-safe again.** It re-exports the Google Ads offline-conversion
313
+ upload, which reached `google/token.ts` and its `node:crypto` import, so the subpath could not load in
314
+ a bare Worker. The service-account JWT is now signed with Web Crypto (`crypto.subtle`, RS256) — same
315
+ token, same PKCS#8 key Google issues, no Node builtin. A new `worker-safe.test.ts` walks each Worker
316
+ subpath's import graph and fails on any `node:` specifier.
317
+
318
+ **Breaking, narrow:** `@getvitops/utils/ads` no longer re-exports `createApexTxt`, `findZoneId` and
319
+ `listApexTxt`. Import them from `@getvitops/utils/onboarding`, where they were always defined. The
320
+ re-export tied the ad-platform capability table (imported by the generator and by Worker code) to the
321
+ Cloudflare network executors. `vitops` itself already imported them from `onboarding`.
322
+
323
+ Other packages in the `fixed` group: No consumer-facing change in this package.
324
+
325
+ ### Patch Changes
326
+
327
+ - 0574388: Bump `emdash`/`@emdash-cms/cloudflare` peers to `>=1.0.0 <2` and `vite-plus` to `^1.0.0`, following
328
+ both projects' GA releases.
329
+
330
+ **Breaking for `@getvitops/emdash` consumers on EmDash 0.x**: the peer range no longer admits
331
+ `emdash@0.31.x`/`@emdash-cms/cloudflare@0.31.x`. Upgrade `emdash` and `@emdash-cms/cloudflare` to
332
+ `^1.0.1` first — nothing in `@getvitops/emdash`'s own plugin surface (`admin.portableTextBlocks`,
333
+ the `hooks.page-fragments:register` capability, `definePlugin()`) changed against EmDash 1.0, so no
334
+ code here needed to change, only the declared range.
335
+
336
+ The `emdash` scaffold template (`vp create @getvitops:emdash`) picks up several fixes alongside
337
+ the version bump:
338
+
339
+ - `tailwindcss`/`@tailwindcss/vite` are now real dependencies — the template selected
340
+ `css.format: 'tailwind'` without installing either, which threw on build.
341
+ - `site.json` now sets `site.icons.sprite: true` plus the full `semantic` list the
342
+ `vitops.actionLink` block's icon picker offers — previously its sprite icons pointed at a file
343
+ the build never generated.
344
+ - `astro.config.mjs` sets `migrations: { runtime: 'check', dev: 'auto' }`, and
345
+ `site-deploy-emdash.yml` runs EmDash 1.0's documented `build → status → apply → deploy → check`
346
+ migration sequence explicitly, rather than relying solely on first-request auto-migration. This
347
+ needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
348
+ which only a human can set — run `emdash migrate --status` against the real deployed database,
349
+ review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
350
+ <value>`. Deploys fail clearly on the migration-apply step until it's set.
351
+ - `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
352
+ not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
353
+ errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
354
+ Uncomment it once scaffolded into its own repo.
355
+
356
+ No config schema changes. `@getvitops/generator` also switches from the `zod/mini` subpath import
357
+ to the standalone `@zod/mini` package (zod 4.5+; same API) and fixes `validateConfig()` silently
358
+ losing the offending key name on an `unrecognized_keys` error (e.g. a typo'd `site.ads` platform
359
+ now names the bad key instead of reporting a bare "Invalid input").
360
+
361
+ Also fixed: `vp check`'s `tsconfig-error` for `packages/create/templates/emdash/tsconfig.json`
362
+ (`extends: "astro/tsconfigs/strict"`, unresolvable from inside this monorepo since the template
363
+ has no `node_modules` of its own — a pre-existing, structural gap, newly surfaced as a hard error
364
+ by oxlint 1.85's deeper config discovery, and blocking on every commit since this repo's pre-commit
365
+ hook runs `vp check --fix`). Fixed with a new `scripts/link-template-deps.mjs`, wired as the root
366
+ `postinstall` script: symlinks `astro`/`emdash`/`@emdash-cms/cloudflare` into the template's own
367
+ `node_modules/` (resolved fresh from sibling packages that already depend on them for real, not a
368
+ hardcoded `.pnpm` hash) and writes a minimal `astro:content` ambient shim at `.astro/types.d.ts`
369
+ (the same path `astro sync` would generate for real). Both are gitignored and only ever exist
370
+ inside this monorepo's copy of the template — a real scaffold installs its own real copies instead.
371
+ Considered and rejected first: inlining astro's strict preset (drift risk against astro's own
372
+ evolving default) and making the template a real pnpm workspace member (would change how its
373
+ `@getvitops/*: "latest"` deps resolve locally, which `templates.test.ts` exists to protect).
374
+
375
+ **Known gap, not fixed here**: `packages/astro/src/components/container.test.ts` (the
376
+ `AstroContainer`-based render tests) fails to even load under vite-plus 1.0's Vite 8 — direct
377
+ `.astro` imports in a Vitest file need the Astro Vite plugin registered, which nothing in this
378
+ repo's test config actually does (Astro's own docs call for `getViteConfig()`, which doesn't
379
+ compose with vite-plus's own `defineConfig`). A type-level shim
380
+ (`packages/astro/src/components/astro-modules.d.ts`) fixes `vp check`'s type-aware pass, but the
381
+ runtime `vp test` failure for this one file is still open.
382
+
383
+ - Updated dependencies [89265af]
384
+ - Updated dependencies [7fd5f8f]
385
+ - @getvitops/config@8.2.0
386
+
3
387
  ## 8.2.0-alpha.0
4
388
 
5
389
  ### Minor Changes
@@ -14,7 +398,7 @@
14
398
 
15
399
  This adds one server-side path for all three CRMs, hung off the conversion route that already
16
400
  exists. `createConversionRoute()` gets a new `crm` option (`{ config, credentials, fetch?,
17
- timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the existing `binding`
401
+ timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the existing `binding`
18
402
  option, and are never config. A failed CRM write never fails the request and is folded into the
19
403
  notification email's new "Delivery" section, so a broken CRM token is never silent.
20
404
 
@@ -59,8 +443,8 @@ timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the e
59
443
  tailwind outputs are unaffected.
60
444
 
61
445
  - c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
62
- designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
63
- './site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
446
+ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
447
+ './site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
64
448
  `<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
65
449
  offered, now usable by the client directly, no dev server required. An edit is visible
66
450
  site-wide immediately (a runtime `:root` override, served via a new plugin API route and
@@ -107,7 +491,7 @@ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import c
107
491
 
108
492
  `vitops email setup [domain]` onboards a sending domain (currently Cloudflare Email Sending only)
109
493
  and polls DNS until its SPF/DKIM/DMARC records resolve or a timeout is reached; `vitops email
110
- check` is the non-blocking deploy guard both `@getvitops/create` templates' `deploy`/`deploy:dev`
494
+ check` is the non-blocking deploy guard both `@getvitops/create` templates' `deploy`/`deploy:dev`
111
495
  scripts now run first (`--strict` makes it a real gate; opt out per-site with
112
496
  `site.notifications.email.verifySender: false`, or globally with `--skip` /
113
497
  `VITOPS_SKIP_EMAIL_CHECK`).
@@ -119,7 +503,7 @@ check` is the non-blocking deploy guard both `@getvitops/create` templates' `dep
119
503
  from the onboarded subdomain — rather than silently trying to onboard the apex, which rewrites
120
504
  `_dmarc.<domain>` and can tighten DMARC policy on every other mail your domain sends.
121
505
  - Candidates come from `site.notifications.email.from` (or its derived `noreply@<domains
122
- .canonical>` default) first; a best-effort scan of `--src` only supplements it, and never guesses
506
+ .canonical>` default) first; a best-effort scan of `--src` only supplements it, and never guesses
123
507
  at a computed `from` value.
124
508
 
125
509
  New `@getvitops/utils/email` subpath and `site.notifications.email.verifySender` config field.
@@ -151,8 +535,8 @@ check` is the non-blocking deploy guard both `@getvitops/create` templates' `dep
151
535
  Before this, the capture script only beaconed `tel:` clicks; a `mailto:` link on the same page was
152
536
  invisible to attribution — a visitor who emailed the business instead of calling generated no
153
537
  conversion event at all. `<Tracking />`'s inlined script now listens for both, sending `{ event:
154
- 'email', email }` via `navigator.sendBeacon` the same way it already sends `{ event: 'call', phone
155
- }`.
538
+ 'email', email }` via `navigator.sendBeacon` the same way it already sends `{ event: 'call', phone
539
+ }`.
156
540
 
157
541
  `ConversionEvent['type']` gains an `'email'` member alongside `'form'`/`'call'`, with a new
158
542
  `email` field. `createConversionRoute()` handles it exactly like a call: no honeypot, Turnstile or
@@ -218,7 +602,7 @@ check` is the non-blocking deploy guard both `@getvitops/create` templates' `dep
218
602
  needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
219
603
  which only a human can set — run `emdash migrate --status` against the real deployed database,
220
604
  review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
221
- <value>`. Deploys fail clearly on the migration-apply step until it's set.
605
+ <value>`. Deploys fail clearly on the migration-apply step until it's set.
222
606
  - `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
223
607
  not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
224
608
  errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
@@ -432,7 +816,7 @@ check` is the non-blocking deploy guard both `@getvitops/create` templates' `dep
432
816
  It is **required for a user credential and wrong for a service account**, which is why it is not
433
817
  sent unconditionally. A user credential authenticates through a shared OAuth client that owns no
434
818
  project, so Google refuses the call outright — measured: `403 "requires a quota project, which is
435
- not set by default"`. A service account already belongs to a project, and pointing it at a
819
+ not set by default"`. A service account already belongs to a project, and pointing it at a
436
820
  different one would newly require `serviceusage.services.use` there, turning a working CI
437
821
  credential into a 403 for no benefit.
438
822
 
@@ -457,7 +841,7 @@ not set by default"`. A service account already belongs to a project, and pointi
457
841
  **Fixed: an ADC file in `GOOGLE_APPLICATION_CREDENTIALS` no longer hard-exits.** That variable is
458
842
  Google's own convention _and_ where gcloud writes ADC, so the same variable carries two kinds of
459
843
  credential. Parsing an `authorized_user` file as a service account failed with `service account
460
- JSON is missing client_email or private_key` — while holding a credential the command already knew
844
+ JSON is missing client_email or private_key` — while holding a credential the command already knew
461
845
  how to use. It is now discriminated on `type`, the field Google's own libraries switch on.
462
846
 
463
847
  **Also fixed — `search setup --dry` and `--check` are not offline.** The docs implied otherwise. The
@@ -794,28 +1178,20 @@ here changed, and no code that imports `@getvitops/utils` needs editing.
794
1178
  {
795
1179
  "designSystem": {
796
1180
  "themes": {
797
- "default": {
798
- /* … */
799
- },
1181
+ "default": {/* … */},
800
1182
  },
801
1183
  },
802
1184
  "organization": {
803
1185
  "name": "Acme",
804
1186
  "contact": "hq",
805
1187
  "locations": {
806
- "hq": {
807
- /* … */
808
- },
1188
+ "hq": {/* … */},
809
1189
  },
810
1190
  },
811
1191
  "site": {
812
1192
  "defaultLocale": "en",
813
- "domains": {
814
- /* … */
815
- },
816
- "analytics": {
817
- /* … */
818
- },
1193
+ "domains": {/* … */},
1194
+ "analytics": {/* … */},
819
1195
  },
820
1196
  }
821
1197
  ```
@@ -950,7 +1326,7 @@ here changed, and no code that imports `@getvitops/utils` needs editing.
950
1326
  toolchain bump. Use `--force` when you mean to re-encode.
951
1327
 
952
1328
  **`ffmpeg` is an external tool, not an npm dependency** — install it yourself (`brew install
953
- ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails without it rather
1329
+ ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails without it rather
954
1330
  than skipping: a page referencing a video that was never encoded is broken, not degraded.
955
1331
 
956
1332
  Two defaults worth knowing about. The MP4 exists because older iOS and the in-app webviews inside
@@ -1,4 +1,4 @@
1
- //#region src/google/adc.ts
1
+ // src/google/adc.ts
2
2
  /** A parsed ADC user credential, or `undefined` if `raw` is not one. */
3
3
  function parseAdcUser(raw) {
4
4
  let parsed;
@@ -40,6 +40,5 @@ function adcCredentialsPath(opts) {
40
40
  return home ? join(home, ".config", "gcloud", "application_default_credentials.json") : void 0;
41
41
  }
42
42
 
43
- //#endregion
44
43
  export { adcQuotaProject as n, parseAdcUser as r, adcCredentialsPath as t };
45
- //# sourceMappingURL=adc-D6aw2qgq.mjs.map
44
+ //# sourceMappingURL=adc-BiW9BzqA.mjs.map
@@ -1 +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"}
1
+ {"version":3,"file":"adc-BiW9BzqA.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"}
@@ -1,5 +1,4 @@
1
- import { i as GoogleOAuth } from "./token-DmmoQ7um.mjs";
2
- //#region src/google/adc.d.ts
1
+ import { i as GoogleOAuth } from "./token-CHR1O4w5.mjs";
3
2
  /** A parsed ADC user credential, or `undefined` if `raw` is not one. */
4
3
  declare function parseAdcUser(raw: string): GoogleOAuth | undefined;
5
4
  /** The quota project gcloud recorded with an ADC credential, if any. */
@@ -19,6 +18,5 @@ declare function adcCredentialsPath(opts: {
19
18
  home: string | undefined;
20
19
  join: (...parts: string[]) => string;
21
20
  }): string | undefined;
22
- //#endregion
23
21
  export { adcQuotaProject as n, parseAdcUser as r, adcCredentialsPath as t };
24
- //# sourceMappingURL=adc-BoEoogyo.d.mts.map
22
+ //# sourceMappingURL=adc-DvX3-P7a.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adc-DvX3-P7a.d.mts","names":[],"sources":["../src/google/adc.ts"],"mappings":";;iBAkBgB,aAAa,cAAc;;iBAwB3B,gBAAgB;;;;;;;;;;iBAiBhB,mBAAmB;EACjC,KAAK;EACL;EACA;EACA,UAAU"}