@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
@@ -0,0 +1,18 @@
1
+ /**
2
+ * URL safety for values that end up in an `href`/`src`.
3
+ *
4
+ * Worker-safe, dependency-free: used by the EmDash blocks, the Astro components that render
5
+ * data from outside the repo (Google reviews), and `nodesToHtml`. It is an allowlist rather
6
+ * than a sanitizer on purpose — nothing here takes HTML, and the only dangerous input is a URL
7
+ * whose scheme runs script when followed.
8
+ */
9
+ /**
10
+ * Return `href` only when it is safe to put in an anchor or image, otherwise `undefined`.
11
+ *
12
+ * Editors are semi-trusted, but `javascript:` and `data:` URLs run script on click, so only
13
+ * web/mail/phone schemes and scheme-less (relative, `#`, `?`) values pass. Browsers ignore
14
+ * ASCII whitespace and control characters inside a scheme (`java\tscript:`), so they are
15
+ * stripped before the check rather than trusted to be absent.
16
+ */
17
+ export declare function safeHref(href: unknown): string | undefined;
18
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/url/index.ts"],"mappings":";;;;;;;;;;;;;;;;wBAmBgB,SAAS"}
@@ -0,0 +1,34 @@
1
+ // src/url/index.ts
2
+ /**
3
+ * URL safety for values that end up in an `href`/`src`.
4
+ *
5
+ * Worker-safe, dependency-free: used by the EmDash blocks, the Astro components that render
6
+ * data from outside the repo (Google reviews), and `nodesToHtml`. It is an allowlist rather
7
+ * than a sanitizer on purpose — nothing here takes HTML, and the only dangerous input is a URL
8
+ * whose scheme runs script when followed.
9
+ */
10
+ const SAFE_SCHEMES = /* @__PURE__ */ new Set([
11
+ "http:",
12
+ "https:",
13
+ "mailto:",
14
+ "tel:"
15
+ ]);
16
+ /**
17
+ * Return `href` only when it is safe to put in an anchor or image, otherwise `undefined`.
18
+ *
19
+ * Editors are semi-trusted, but `javascript:` and `data:` URLs run script on click, so only
20
+ * web/mail/phone schemes and scheme-less (relative, `#`, `?`) values pass. Browsers ignore
21
+ * ASCII whitespace and control characters inside a scheme (`java\tscript:`), so they are
22
+ * stripped before the check rather than trusted to be absent.
23
+ */
24
+ function safeHref(href) {
25
+ if (typeof href !== "string") return void 0;
26
+ const trimmed = href.trim();
27
+ const probe = trimmed.replace(/[\u0000- ]/g, "");
28
+ const scheme = /^([a-z][a-z0-9+.-]*:)/i.exec(probe)?.[1]?.toLowerCase();
29
+ if (scheme && !SAFE_SCHEMES.has(scheme)) return void 0;
30
+ return trimmed || void 0;
31
+ }
32
+
33
+ export { safeHref };
34
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/url/index.ts"],"sourcesContent":["/**\n * URL safety for values that end up in an `href`/`src`.\n *\n * Worker-safe, dependency-free: used by the EmDash blocks, the Astro components that render\n * data from outside the repo (Google reviews), and `nodesToHtml`. It is an allowlist rather\n * than a sanitizer on purpose — nothing here takes HTML, and the only dangerous input is a URL\n * whose scheme runs script when followed.\n */\n\nconst SAFE_SCHEMES = new Set(['http:', 'https:', 'mailto:', 'tel:']);\n\n/**\n * Return `href` only when it is safe to put in an anchor or image, otherwise `undefined`.\n *\n * Editors are semi-trusted, but `javascript:` and `data:` URLs run script on click, so only\n * web/mail/phone schemes and scheme-less (relative, `#`, `?`) values pass. Browsers ignore\n * ASCII whitespace and control characters inside a scheme (`java\\tscript:`), so they are\n * stripped before the check rather than trusted to be absent.\n */\nexport function safeHref(href: unknown): string | undefined {\n if (typeof href !== 'string') return undefined;\n const trimmed = href.trim();\n // eslint-disable-next-line no-control-regex\n const probe = trimmed.replace(/[\\u0000- ]/g, '');\n const scheme = /^([a-z][a-z0-9+.-]*:)/i.exec(probe)?.[1]?.toLowerCase();\n if (scheme && !SAFE_SCHEMES.has(scheme)) return undefined;\n return trimmed || undefined;\n}\n"],"mappings":";;;;;;;;;AASA,MAAM,+BAAe,IAAI,IAAI;CAAC;CAAS;CAAU;CAAW;AAAM,CAAC;;;;;;;;;AAUnE,SAAgB,SAAS,MAAmC;CAC1D,IAAI,OAAO,SAAS,UAAU,OAAO;CACrC,MAAM,UAAU,KAAK,KAAK;CAE1B,MAAM,QAAQ,QAAQ,QAAQ,eAAe,EAAE;CAC/C,MAAM,SAAS,yBAAyB,KAAK,KAAK,CAAC,GAAG,EAAE,EAAE,YAAY;CACtE,IAAI,UAAU,CAAC,aAAa,IAAI,MAAM,GAAG,OAAO;CAChD,OAAO,WAAW;AACpB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getvitops/utils",
3
- "version": "8.2.0-alpha.0",
3
+ "version": "8.3.0-alpha.0",
4
4
  "description": "Shared build-time utilities for the Vitops toolchain — favicon generation and friends.",
5
5
  "homepage": "https://docs.vitops.ca",
6
6
  "bugs": "https://github.com/getvitops/design-system/issues",
@@ -26,6 +26,14 @@
26
26
  "types": "./dist/favicon.d.mts",
27
27
  "import": "./dist/favicon.mjs"
28
28
  },
29
+ "./content": {
30
+ "types": "./dist/content.d.mts",
31
+ "import": "./dist/content.mjs"
32
+ },
33
+ "./url": {
34
+ "types": "./dist/url/index.d.mts",
35
+ "import": "./dist/url/index.mjs"
36
+ },
29
37
  "./color": {
30
38
  "types": "./dist/color/index.d.mts",
31
39
  "import": "./dist/color/index.mjs"
@@ -42,10 +50,22 @@
42
50
  "types": "./dist/reviews/index.d.mts",
43
51
  "import": "./dist/reviews/index.mjs"
44
52
  },
53
+ "./turnstile": {
54
+ "types": "./dist/turnstile/index.d.mts",
55
+ "import": "./dist/turnstile/index.mjs"
56
+ },
57
+ "./reviews/select": {
58
+ "types": "./dist/reviews/select.d.mts",
59
+ "import": "./dist/reviews/select.mjs"
60
+ },
45
61
  "./ads": {
46
62
  "types": "./dist/ads/index.d.mts",
47
63
  "import": "./dist/ads/index.mjs"
48
64
  },
65
+ "./audit": {
66
+ "types": "./dist/audit/index.d.mts",
67
+ "import": "./dist/audit/index.mjs"
68
+ },
49
69
  "./media": {
50
70
  "types": "./dist/media/index.d.mts",
51
71
  "import": "./dist/media/index.mjs"
@@ -87,12 +107,33 @@
87
107
  "access": "public"
88
108
  },
89
109
  "dependencies": {
110
+ "@getvitops/config": "8.3.0-alpha.0",
90
111
  "colorjs.io": "^0.5.2",
91
112
  "parse5": "^7.3.0",
92
113
  "png-to-ico": "^3.0.2",
93
114
  "sharp": "^0.35.3"
94
115
  },
95
- "devDependencies": {},
116
+ "devDependencies": {
117
+ "@axe-core/playwright": "^4.10.0",
118
+ "lighthouse": "^13.0.0",
119
+ "playwright": "^1.62.1"
120
+ },
121
+ "peerDependencies": {
122
+ "@axe-core/playwright": ">=4.10.0",
123
+ "lighthouse": ">=13.0.0",
124
+ "playwright": ">=1.50.0"
125
+ },
126
+ "peerDependenciesMeta": {
127
+ "@axe-core/playwright": {
128
+ "optional": true
129
+ },
130
+ "lighthouse": {
131
+ "optional": true
132
+ },
133
+ "playwright": {
134
+ "optional": true
135
+ }
136
+ },
96
137
  "engines": {
97
138
  "node": ">=24"
98
139
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"adc-BoEoogyo.d.mts","names":[],"sources":["../src/google/adc.ts"],"mappings":";;;iBAkBgB,aAAa,cAAc;;iBAwB3B,gBAAgB;;;;;;;;;;iBAiBhB,mBAAmB;EACjC,KAAK;EACL;EACA;EACA,UAAU"}
@@ -1,27 +0,0 @@
1
- //#region src/onboarding/cloudflare.d.ts
2
- /** Find the zone id for a domain (`GET /zones?name=`). */
3
- declare function findZoneId(token: string, domain: string, fetchImpl?: typeof fetch): Promise<{
4
- ok: boolean;
5
- status: number;
6
- zoneId?: string;
7
- message?: string;
8
- }>;
9
- /** List the apex TXT record contents for a zone. */
10
- declare function listApexTxt(token: string, zoneId: string, domain: string, fetchImpl?: typeof fetch): Promise<{
11
- ok: boolean;
12
- status: number;
13
- contents: string[];
14
- message?: string;
15
- }>;
16
- /**
17
- * Create an apex TXT record. **Create only** — never called when one already
18
- * exists (the planner decides that), and there is no sibling that edits or removes.
19
- */
20
- declare function createApexTxt(token: string, zoneId: string, domain: string, content: string, fetchImpl?: typeof fetch): Promise<{
21
- ok: boolean;
22
- status: number;
23
- message?: string;
24
- }>;
25
- //#endregion
26
- export { findZoneId as n, listApexTxt as r, createApexTxt as t };
27
- //# sourceMappingURL=cloudflare-BiK61Oq4.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cloudflare-BiK61Oq4.d.mts","names":[],"sources":["../src/onboarding/cloudflare.ts"],"mappings":";;iBA8BsB,WACpB,eACA,gBACA,mBAAkB,QACjB;EAAU;EAAa;EAAgB;EAAiB;;;iBAsBrC,YACpB,eACA,gBACA,gBACA,mBAAkB,QACjB;EAAU;EAAa;EAAgB;EAAoB;;;;;;iBAiBxC,cACpB,eACA,gBACA,gBACA,iBACA,mBAAkB,QACjB;EAAU;EAAa;EAAgB"}
@@ -1,92 +0,0 @@
1
- //#region src/onboarding/cloudflare.ts
2
- /**
3
- * Cloudflare DNS — the one thing this command writes.
4
- *
5
- * Scoped to exactly what onboarding needs and nothing more: find a zone by name,
6
- * read the apex TXT records, and *create* the verification TXT when it is missing.
7
- * There is deliberately no update and no delete — the command's contract is that it
8
- * never removes a DNS record, so the destructive verbs are simply absent rather
9
- * than guarded.
10
- *
11
- * The token is a `Zone:DNS:Edit` credential from the environment
12
- * (`CLOUDFLARE_API_TOKEN`); listing a zone by name also needs `Zone:Read`, which
13
- * Cloudflare's standard "Edit zone DNS" token template already bundles. As with the
14
- * indexing executors, `fetchImpl` is injected so the request shape is testable
15
- * without the network, and every call returns a structured result rather than
16
- * throwing — the caller decides what a failure means per domain.
17
- */
18
- const API = "https://api.cloudflare.com/client/v4";
19
- const cfError = (status, body) => {
20
- const first = body.errors?.[0];
21
- return first ? `${first.code} ${first.message}` : `HTTP ${status}`;
22
- };
23
- /** Find the zone id for a domain (`GET /zones?name=`). */
24
- async function findZoneId(token, domain, fetchImpl = fetch) {
25
- const res = await fetchImpl(`${API}/zones?name=${encodeURIComponent(domain)}`, { headers: { authorization: `Bearer ${token}` } });
26
- const body = await res.json();
27
- if (!res.ok || !body.success) return {
28
- ok: false,
29
- status: res.status,
30
- message: cfError(res.status, body)
31
- };
32
- const zone = body.result?.[0];
33
- if (!zone) return {
34
- ok: false,
35
- status: res.status,
36
- 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.)`
37
- };
38
- return {
39
- ok: true,
40
- status: res.status,
41
- zoneId: zone.id
42
- };
43
- }
44
- /** List the apex TXT record contents for a zone. */
45
- async function listApexTxt(token, zoneId, domain, fetchImpl = fetch) {
46
- const res = await fetchImpl(`${API}/zones/${zoneId}/dns_records?type=TXT&name=${encodeURIComponent(domain)}`, { headers: { authorization: `Bearer ${token}` } });
47
- const body = await res.json();
48
- if (!res.ok || !body.success) return {
49
- ok: false,
50
- status: res.status,
51
- contents: [],
52
- message: cfError(res.status, body)
53
- };
54
- const contents = (body.result ?? []).map((r) => r.content.replace(/^"|"$/g, ""));
55
- return {
56
- ok: true,
57
- status: res.status,
58
- contents
59
- };
60
- }
61
- /**
62
- * Create an apex TXT record. **Create only** — never called when one already
63
- * exists (the planner decides that), and there is no sibling that edits or removes.
64
- */
65
- async function createApexTxt(token, zoneId, domain, content, fetchImpl = fetch) {
66
- const res = await fetchImpl(`${API}/zones/${zoneId}/dns_records`, {
67
- method: "POST",
68
- headers: {
69
- authorization: `Bearer ${token}`,
70
- "content-type": "application/json"
71
- },
72
- body: JSON.stringify({
73
- type: "TXT",
74
- name: domain,
75
- content
76
- })
77
- });
78
- const body = await res.json();
79
- if (!res.ok || !body.success) return {
80
- ok: false,
81
- status: res.status,
82
- message: cfError(res.status, body)
83
- };
84
- return {
85
- ok: true,
86
- status: res.status
87
- };
88
- }
89
-
90
- //#endregion
91
- export { findZoneId as n, listApexTxt as r, createApexTxt as t };
92
- //# sourceMappingURL=cloudflare-CD8vwrgj.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cloudflare-CD8vwrgj.mjs","names":[],"sources":["../src/onboarding/cloudflare.ts"],"sourcesContent":["/**\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":";;;;;;;;;;;;;;;;;AAgBA,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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"plan-Hq8Pb0Uw.d.mts","names":[],"sources":["../src/onboarding/types.ts","../src/onboarding/plan.ts"],"mappings":";;;;;;;;;;;;;;;UAeiB;;EAEf;;;;;EAKA;;;;;EAKA;;;UAIe;;EAEf,SAAS;;;;;;;;;UAiBM;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;;;KC3CU;UAEK;EACf,QAAQ;;EAER;;;UAIe;EACf;;EAEA;;EAEA,KAAK;;EAEL,QAAQ;;EAER,UAAU;;EAEV,QAAQ;;EAER;;EAEA;;UAGe;EACf,SAAS;;EAET;;;cAIW,aAAc;;;;;;;;;;iBAWX,gBAAgB;;iBAOhB,WAAW,OAAO,aAAa,OAAO,cAAc;;;;;;;;;;;;;;;iBA4DpD,WAAW,mBAAmB;;iBAa9B,KAAK,QAAQ,kBAAkB,QAAQ,YAAY,eAAe;;iBAiBlE,SAAS,GAAG;;iBAWZ,WAAW,GAAG;;UAgBb;EACf;EACA;EACA;EACA;EACA;;;iBAIc,cAAc,SAAS"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"token-DkKC5m23.mjs","names":[],"sources":["../src/google/token.ts"],"sourcesContent":["/**\n * Google OAuth2 token exchange — one request, two grants.\n *\n * Both of this package's Google modules need a bearer token, and both used to\n * mint one themselves. The two copies looked different because the *credentials*\n * differ, but everything after building the form body was identical: same\n * endpoint, same content-type, same \"surface the response body as the error\"\n * rule, same `access_token` extraction. So the seam is precise —\n * **credential → form params differs, params → token does not.**\n *\n * The two grants are kept because they are not accidental duplication; they track\n * how each command is actually run:\n *\n * - **service account** (RS256 JWT bearer) for `search notify`, which runs on\n * every deploy in CI. It never expires. A refresh token can be revoked, and\n * for an OAuth client still in *Testing* publishing status Google expires it\n * after 7 days — a bad property for a deploy step.\n * - **user OAuth** (refresh-token grant) for `search setup`, which is a\n * one-time human operation. Site verification makes the calling identity an\n * *owner* of the property, and that should be a person, not a project robot.\n *\n * `googleapis` is deliberately not a dependency — this is one REST endpoint in a\n * CLI that installs into every consumer project. See AGENTS.md.\n *\n * Internal: not a published subpath. Both `./indexing` and `./onboarding` import\n * it, so it lands in a shared chunk, which is free for a Node library. (The\n * \"don't create a shared chunk\" rule in AGENTS.md is about browser bundles and\n * the consent gate.)\n */\nimport { createSign } from 'node:crypto';\n\nconst TOKEN_URL = 'https://oauth2.googleapis.com/token';\n\n/** The fields we need from a service-account JSON key. */\nexport interface ServiceAccount {\n client_email: string;\n private_key: string;\n}\n\n/** A user OAuth credential (refresh-token flow — never a service account). */\nexport interface GoogleOAuth {\n clientId: string;\n clientSecret: string;\n refreshToken: string;\n}\n\n/**\n * Which identity to act as.\n *\n * `scope` rides on the service-account variant because that grant carries its\n * scope in the signed assertion. A user OAuth token's scopes are fixed when the\n * refresh token is minted, so there is nothing to pass at exchange time — the\n * asymmetry is the protocol's, not ours.\n */\nexport type GoogleCredential =\n | { kind: 'service-account'; account: ServiceAccount; scope: string }\n | { kind: 'oauth'; oauth: GoogleOAuth };\n\nconst b64url = (input: Buffer | string): string =>\n Buffer.from(input).toString('base64').replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n\n/** Sign the RS256 JWT assertion a service-account grant carries. */\nfunction assertionFor(account: ServiceAccount, scope: string, now: number): string {\n const iat = Math.floor(now / 1000);\n const header = b64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));\n const claims = b64url(\n JSON.stringify({ iss: account.client_email, scope, aud: TOKEN_URL, iat, exp: iat + 3600 }),\n );\n const signer = createSign('RSA-SHA256');\n signer.update(`${header}.${claims}`);\n // `\\n` survives most secret stores as a literal; normalise so a pasted key works.\n const pem = account.private_key.replace(/\\\\n/g, '\\n');\n return `${header}.${claims}.${b64url(signer.sign(pem))}`;\n}\n\n/** The half that never differs: POST the grant, surface the cause, return the token. */\nasync function requestToken(body: URLSearchParams, fetchImpl: typeof fetch): Promise<string> {\n const res = await fetchImpl(TOKEN_URL, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n if (!res.ok) {\n // The body names the cause — clock skew, wrong audience or a revoked key for a\n // service account; `invalid_grant` / `invalid_client` for OAuth — and carries\n // no secret of ours: the assertion travels in the request, not the response.\n throw new Error(`token exchange failed (${res.status}): ${await res.text()}`);\n }\n const { access_token } = (await res.json()) as { access_token?: string };\n if (!access_token) throw new Error('token exchange returned no access_token');\n return access_token;\n}\n\n/**\n * Exchange either credential for a short-lived access token.\n *\n * Throws rather than returning a result: nothing downstream can run without a\n * token, so there is no per-item decision for a caller to make. (The API calls\n * themselves do return structured results — those fail per domain.)\n *\n * `now` is a parameter so the assertion is testable without mocking the clock.\n */\nexport function googleAccessToken(\n credential: GoogleCredential,\n fetchImpl: typeof fetch = fetch,\n now: number = Date.now(),\n): Promise<string> {\n const body =\n credential.kind === 'oauth'\n ? new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: credential.oauth.clientId,\n client_secret: credential.oauth.clientSecret,\n refresh_token: credential.oauth.refreshToken,\n })\n : new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion: assertionFor(credential.account, credential.scope, now),\n });\n return requestToken(body, fetchImpl);\n}\n\n/**\n * A bearer token, plus the project its usage is attributed to.\n *\n * `quotaProject` becomes the `x-goog-user-project` header, and it is **required**\n * for a user credential — an ADC login or a `VITOPS_GOOGLE_*` refresh token\n * authenticates through a shared OAuth client that carries no project, so Google\n * refuses the call with a 403 naming the missing quota project rather than\n * guessing one. A service account already belongs to a project, so it needs none.\n *\n * It is deliberately per-call rather than baked into the token: one agency\n * identity administers many sites, each with its own project, so the identity is\n * shared and the attribution is not.\n */\nexport interface GoogleAuth {\n token: string;\n quotaProject?: string;\n}\n\n/**\n * A bare token still works everywhere an auth does.\n *\n * The token-only form is what every caller passed before per-site projects\n * existed, and a service-account caller never needs more, so widening the\n * parameter beats a breaking signature change across seven exported functions.\n */\nexport type GoogleAuthLike = string | GoogleAuth;\n\n/** Auth headers for a Google REST call, with the quota project when there is one. */\nexport function googleHeaders(\n auth: GoogleAuthLike,\n extra?: Record<string, string>,\n): Record<string, string> {\n const { token, quotaProject } = typeof auth === 'string' ? { token: auth } : auth;\n return {\n authorization: `Bearer ${token}`,\n ...(quotaProject ? { 'x-goog-user-project': quotaProject } : {}),\n ...extra,\n };\n}\n\n/** Percent-encode a Search Console `siteUrl` (e.g. `sc-domain:acme.ca`) for a path segment. */\nexport const encodeSite = (siteUrl: string): string => encodeURIComponent(siteUrl);\n\n/**\n * Render a failed response's detail: a caller-supplied hint on a specific status\n * (e.g. what a `403` almost always means for this endpoint), or the raw body\n * otherwise. Shared by every REST call here that follows the \"PUT and report\n * why\" shape — `gsc.ts`'s `submitSitemap`, `onboarding/google.ts`'s `addSite`.\n */\nexport async function errorDetail(\n res: Response,\n hint?: (status: number) => string | undefined,\n): Promise<string> {\n const named = hint?.(res.status);\n return named ?? `${res.status}: ${await res.text()}`;\n}\n\n/** Scopes, named once so a caller never spells one out. */\nexport const SCOPES = {\n /** Search Console (`sitemaps.submit`, `urlInspection`, `sites`). */\n webmasters: 'https://www.googleapis.com/auth/webmasters',\n /** Site Verification API. */\n siteVerification: 'https://www.googleapis.com/auth/siteverification',\n /** Business Profile (reviews). The GCP project must be allowlisted by Google. */\n businessProfile: 'https://www.googleapis.com/auth/business.manage',\n} as const;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAM,YAAY;AA2BlB,MAAM,UAAU,UACd,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,QAAQ,OAAO,GAAG,CAAC,CAAC,QAAQ,OAAO,GAAG,CAAC,CAAC,QAAQ,OAAO,EAAE;;AAGjG,SAAS,aAAa,SAAyB,OAAe,KAAqB;CACjF,MAAM,MAAM,KAAK,MAAM,MAAM,GAAI;CACjC,MAAM,SAAS,OAAO,KAAK,UAAU;EAAE,KAAK;EAAS,KAAK;CAAM,CAAC,CAAC;CAClE,MAAM,SAAS,OACb,KAAK,UAAU;EAAE,KAAK,QAAQ;EAAc;EAAO,KAAK;EAAW;EAAK,KAAK,MAAM;CAAK,CAAC,CAC3F;CACA,MAAM,SAAS,WAAW,YAAY;CACtC,OAAO,OAAO,GAAG,OAAO,GAAG,QAAQ;CAEnC,MAAM,MAAM,QAAQ,YAAY,QAAQ,QAAQ,IAAI;CACpD,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,OAAO,KAAK,GAAG,CAAC;AACvD;;AAGA,eAAe,aAAa,MAAuB,WAA0C;CAC3F,MAAM,MAAM,MAAM,UAAU,WAAW;EACrC,QAAQ;EACR,SAAS,EAAE,gBAAgB,oCAAoC;EAC/D,MAAM,KAAK,SAAS;CACtB,CAAC;CACD,IAAI,CAAC,IAAI,IAIP,MAAM,IAAI,MAAM,0BAA0B,IAAI,OAAO,KAAK,MAAM,IAAI,KAAK,GAAG;CAE9E,MAAM,EAAE,iBAAkB,MAAM,IAAI,KAAK;CACzC,IAAI,CAAC,cAAc,MAAM,IAAI,MAAM,yCAAyC;CAC5E,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,kBACd,YACA,YAA0B,OAC1B,MAAc,KAAK,IAAI,GACN;CAajB,OAAO,aAXL,WAAW,SAAS,UAChB,IAAI,gBAAgB;EAClB,YAAY;EACZ,WAAW,WAAW,MAAM;EAC5B,eAAe,WAAW,MAAM;EAChC,eAAe,WAAW,MAAM;CAClC,CAAC,IACD,IAAI,gBAAgB;EAClB,YAAY;EACZ,WAAW,aAAa,WAAW,SAAS,WAAW,OAAO,GAAG;CACnE,CAAC,GACmB,SAAS;AACrC;;AA8BA,SAAgB,cACd,MACA,OACwB;CACxB,MAAM,EAAE,OAAO,iBAAiB,OAAO,SAAS,WAAW,EAAE,OAAO,KAAK,IAAI;CAC7E,OAAO;EACL,eAAe,UAAU;EACzB,GAAI,eAAe,EAAE,uBAAuB,aAAa,IAAI,CAAC;EAC9D,GAAG;CACL;AACF;;AAGA,MAAa,cAAc,YAA4B,mBAAmB,OAAO;;;;;;;AAQjF,eAAsB,YACpB,KACA,MACiB;CAEjB,OADc,OAAO,IAAI,MAAM,KACf,GAAG,IAAI,OAAO,IAAI,MAAM,IAAI,KAAK;AACnD;;AAGA,MAAa,SAAS;;CAEpB,YAAY;;CAEZ,kBAAkB;;CAElB,iBAAiB;AACnB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"token-DmmoQ7um.d.mts","names":[],"sources":["../src/google/token.ts"],"mappings":";;UAkCiB;EACf;EACA;;;UAIe;EACf;EACA;EACA;;;;;;;;;;KAWU;EACN;EAAyB,SAAS;EAAgB;;EAClD;EAAe,OAAO;;;;;;;;;;;iBA8CZ,kBACd,YAAY,kBACZ,mBAAkB,OAClB,eACC;;;;;;;;;;;;;;UA6Bc;EACf;EACA;;;;;;;;;KAUU,0BAA0B;;iBAGtB,cACd,MAAM,gBACN,QAAQ,yBACP;;cA2BU"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"types-7ELci8RQ.d.mts","names":[],"sources":["../src/tracking/types.ts"],"mappings":";;;;;;;;;;KAiBY;UAEK;EACf;;;;;;;EAOA,WAAW;EACX;;;;;;;;;;UAWe;GACd;;EAED;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;;;UAee;EACf;;EAEA,WAAW;EACX;;EAEA;;EAEA,UAAU;EACV;EACA;;EAEA"}