@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
@@ -1,6 +1,4 @@
1
- import { createSign } from "node:crypto";
2
-
3
- //#region src/google/token.ts
1
+ // src/google/token.ts
4
2
  /**
5
3
  * Google OAuth2 token exchange — one request, two grants.
6
4
  *
@@ -29,11 +27,29 @@ import { createSign } from "node:crypto";
29
27
  * it, so it lands in a shared chunk, which is free for a Node library. (The
30
28
  * "don't create a shared chunk" rule in AGENTS.md is about browser bundles and
31
29
  * the consent gate.)
30
+ *
31
+ * **Worker-safe: Web Crypto only, no Node builtins.** `./notify` re-exports the Google Ads
32
+ * upload, which signs a service-account assertion from inside a Worker, and `./notify` is
33
+ * documented as free of Node builtins. `crypto.subtle` and `btoa` exist in Node, workerd and
34
+ * browsers alike; `node:crypto`'s `createSign` and `Buffer` do not exist in a bare Worker.
32
35
  */
33
36
  const TOKEN_URL = "https://oauth2.googleapis.com/token";
34
- const b64url = (input) => Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
37
+ const b64url = (input) => {
38
+ const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
39
+ let binary = "";
40
+ for (const byte of bytes) binary += String.fromCharCode(byte);
41
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
42
+ };
43
+ /** Decode a PKCS#8 PEM ("-----BEGIN PRIVATE KEY-----") to its DER bytes. */
44
+ function pemToDer(pem) {
45
+ const body = pem.replace(/\\n/g, "\n").replace(/-----(BEGIN|END) [A-Z ]+-----/g, "").replace(/\s+/g, "");
46
+ const binary = atob(body);
47
+ const der = new Uint8Array(binary.length);
48
+ for (let i = 0; i < binary.length; i++) der[i] = binary.charCodeAt(i);
49
+ return der;
50
+ }
35
51
  /** Sign the RS256 JWT assertion a service-account grant carries. */
36
- function assertionFor(account, scope, now) {
52
+ async function assertionFor(account, scope, now) {
37
53
  const iat = Math.floor(now / 1e3);
38
54
  const header = b64url(JSON.stringify({
39
55
  alg: "RS256",
@@ -46,10 +62,12 @@ function assertionFor(account, scope, now) {
46
62
  iat,
47
63
  exp: iat + 3600
48
64
  }));
49
- const signer = createSign("RSA-SHA256");
50
- signer.update(`${header}.${claims}`);
51
- const pem = account.private_key.replace(/\\n/g, "\n");
52
- return `${header}.${claims}.${b64url(signer.sign(pem))}`;
65
+ const key = await crypto.subtle.importKey("pkcs8", pemToDer(account.private_key), {
66
+ name: "RSASSA-PKCS1-v1_5",
67
+ hash: "SHA-256"
68
+ }, false, ["sign"]);
69
+ const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, new TextEncoder().encode(`${header}.${claims}`));
70
+ return `${header}.${claims}.${b64url(new Uint8Array(signature))}`;
53
71
  }
54
72
  /** The half that never differs: POST the grant, surface the cause, return the token. */
55
73
  async function requestToken(body, fetchImpl) {
@@ -72,7 +90,7 @@ async function requestToken(body, fetchImpl) {
72
90
  *
73
91
  * `now` is a parameter so the assertion is testable without mocking the clock.
74
92
  */
75
- function googleAccessToken(credential, fetchImpl = fetch, now = Date.now()) {
93
+ async function googleAccessToken(credential, fetchImpl = fetch, now = Date.now()) {
76
94
  return requestToken(credential.kind === "oauth" ? new URLSearchParams({
77
95
  grant_type: "refresh_token",
78
96
  client_id: credential.oauth.clientId,
@@ -80,7 +98,7 @@ function googleAccessToken(credential, fetchImpl = fetch, now = Date.now()) {
80
98
  refresh_token: credential.oauth.refreshToken
81
99
  }) : new URLSearchParams({
82
100
  grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
83
- assertion: assertionFor(credential.account, credential.scope, now)
101
+ assertion: await assertionFor(credential.account, credential.scope, now)
84
102
  }), fetchImpl);
85
103
  }
86
104
  /** Auth headers for a Google REST call, with the quota project when there is one. */
@@ -110,9 +128,10 @@ const SCOPES = {
110
128
  /** Site Verification API. */
111
129
  siteVerification: "https://www.googleapis.com/auth/siteverification",
112
130
  /** Business Profile (reviews). The GCP project must be allowlisted by Google. */
113
- businessProfile: "https://www.googleapis.com/auth/business.manage"
131
+ businessProfile: "https://www.googleapis.com/auth/business.manage",
132
+ /** Google Ads API (offline conversion upload). Needs an approved developer token too. */
133
+ adwords: "https://www.googleapis.com/auth/adwords"
114
134
  };
115
135
 
116
- //#endregion
117
136
  export { googleHeaders as a, googleAccessToken as i, encodeSite as n, errorDetail as r, SCOPES as t };
118
- //# sourceMappingURL=token-DkKC5m23.mjs.map
137
+ //# sourceMappingURL=token-CJSl9eIS.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-CJSl9eIS.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 *\n * **Worker-safe: Web Crypto only, no Node builtins.** `./notify` re-exports the Google Ads\n * upload, which signs a service-account assertion from inside a Worker, and `./notify` is\n * documented as free of Node builtins. `crypto.subtle` and `btoa` exist in Node, workerd and\n * browsers alike; `node:crypto`'s `createSign` and `Buffer` do not exist in a bare Worker.\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: Uint8Array | string): string => {\n const bytes = typeof input === 'string' ? new TextEncoder().encode(input) : input;\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n};\n\n/** Decode a PKCS#8 PEM (\"-----BEGIN PRIVATE KEY-----\") to its DER bytes. */\nfunction pemToDer(pem: string): Uint8Array<ArrayBuffer> {\n // `\\n` survives most secret stores as a literal; normalise so a pasted key works.\n const body = pem\n .replace(/\\\\n/g, '\\n')\n .replace(/-----(BEGIN|END) [A-Z ]+-----/g, '')\n .replace(/\\s+/g, '');\n const binary = atob(body);\n const der = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) der[i] = binary.charCodeAt(i);\n return der;\n}\n\n/** Sign the RS256 JWT assertion a service-account grant carries. */\nasync function assertionFor(account: ServiceAccount, scope: string, now: number): Promise<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 // Google issues service-account keys as PKCS#8, which is what Web Crypto imports.\n const key = await crypto.subtle.importKey(\n 'pkcs8',\n pemToDer(account.private_key),\n { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n const signature = await crypto.subtle.sign(\n 'RSASSA-PKCS1-v1_5',\n key,\n new TextEncoder().encode(`${header}.${claims}`),\n );\n return `${header}.${claims}.${b64url(new Uint8Array(signature))}`;\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 async 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: await 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 /** Google Ads API (offline conversion upload). Needs an approved developer token too. */\n adwords: 'https://www.googleapis.com/auth/adwords',\n} as const;\n"],"mappings":"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"}
@@ -1,5 +1,4 @@
1
- import { i as TrackingData, n as TrackingCategory, r as TrackingConfig, t as ConversionEvent } from "../types-7ELci8RQ.mjs";
2
- //#region src/tracking/params.d.ts
1
+ import { i as TrackingData, n as TrackingCategory, r as TrackingConfig, t as ConversionEvent } from "../types-oac2P5or.mjs";
3
2
  /**
4
3
  * The ad-platform vocabulary — the one place that says which URL parameters mean
5
4
  * "this visit came from an ad", and which platform each belongs to.
@@ -40,8 +39,6 @@ export declare const TRACKING_COOKIE = "_ac";
40
39
  export declare const TRACKING_COOKIE_DAYS = 90;
41
40
  /** The A/B variant cookie, assigned by the router when a site runs experiments. */
42
41
  export declare const AB_COOKIE = "_ab";
43
- //#endregion
44
- //#region src/tracking/cookie.d.ts
45
42
  /** Read one cookie out of a `document.cookie`-shaped string. */
46
43
  export declare function readCookie(cookieString: string | null | undefined, name: string): string | null;
47
44
  /**
@@ -73,8 +70,6 @@ export declare function cookieAttributes(now: number, secure: boolean): string;
73
70
  * outlive the click it represents.
74
71
  */
75
72
  export declare function mergeTracking(existing: TrackingData | null, captured: TrackingData, now: number): TrackingData;
76
- //#endregion
77
- //#region src/tracking/platform.d.ts
78
73
  /**
79
74
  * Which platform sent this visitor.
80
75
  *
@@ -88,8 +83,6 @@ export declare function getPrimaryClickId(data: TrackingData): {
88
83
  param: string;
89
84
  value: string;
90
85
  } | null;
91
- //#endregion
92
- //#region src/tracking/identity.d.ts
93
86
  /**
94
87
  * Pulling a submitter's identity out of a form's fields.
95
88
  *
@@ -115,6 +108,5 @@ interface SubmitterIdentity {
115
108
  company?: string | undefined;
116
109
  }
117
110
  export declare function identifySubmitter(formData: Record<string, string>): SubmitterIdentity;
118
- //#endregion
119
111
  export type { ConversionEvent, SubmitterIdentity, TrackingCategory, TrackingConfig, TrackingData };
120
112
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/tracking/params.ts","../../src/tracking/cookie.ts","../../src/tracking/platform.ts","../../src/tracking/identity.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;qBAuBa,iBAAiB,SAAS;;qBAc1B;qBAEA;;qBASA;;;;;;;;qBASA;;qBAGA;;qBAGA;;;;wBChDG,WAAW,yCAAyC;;;;;;;;;wBAiBpD,oBAAoB,0CAA0C;;wBAa9D,wBAAwB,MAAM;;;;;;;;;wBAe9B,iBAAiB,aAAa;;;;;;;;;wBAa9B,cACd,UAAU,qBACV,UAAU,cACV,cACC;;;;;;;;;;wBC3Da,iBAAiB,MAAM;;wBASvB,kBAAkB,MAAM;EAAiB;EAAe;;;;;;;;;;;;;;;;;;UCZvD;EACf;EACA;;;EAGA;EACA;EACA;EACA;;wBAWc,kBAAkB,UAAU,yBAAyB"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/tracking/params.ts","../../src/tracking/cookie.ts","../../src/tracking/platform.ts","../../src/tracking/identity.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;qBAuBa,iBAAiB,SAAS;;qBAc1B;qBAEA;;qBASA;;;;;;;;qBASA;;qBAGA;;qBAGA;;wBChDG,WAAW,yCAAyC;;;;;;;;;wBAiBpD,oBAAoB,0CAA0C;;wBAa9D,wBAAwB,MAAM;;;;;;;;;wBAe9B,iBAAiB,aAAa;;;;;;;;;wBAa9B,cACd,UAAU,qBACV,UAAU,cACV,cACC;;;;;;;;wBC3Da,iBAAiB,MAAM;;wBASvB,kBAAkB,MAAM;EAAiB;EAAe;;;;;;;;;;;;;;;;UCZvD;EACf;EACA;;;EAGA;EACA;EACA;EACA;;wBAWc,kBAAkB,UAAU,yBAAyB"}
@@ -1,7 +1,7 @@
1
- import { t as identifySubmitter } from "../identity-BoRddTx3.mjs";
2
- import { a as PLATFORM_PARAMS, c as TRACKING_COOKIE_DAYS, i as CLICK_ID_PARAMS, l as UTM_PARAMS, n as identifyPlatform, o as TRACKED_PARAMS, r as AB_COOKIE, s as TRACKING_COOKIE, t as getPrimaryClickId } from "../platform-0nlOIy8h.mjs";
1
+ import { t as identifySubmitter } from "../identity-C4yInICW.mjs";
2
+ import { a as PLATFORM_PARAMS, c as TRACKING_COOKIE_DAYS, i as CLICK_ID_PARAMS, l as UTM_PARAMS, n as identifyPlatform, o as TRACKED_PARAMS, r as AB_COOKIE, s as TRACKING_COOKIE, t as getPrimaryClickId } from "../platform-Cb_RtsF1.mjs";
3
3
 
4
- //#region src/tracking/cookie.ts
4
+ // src/tracking/cookie.ts
5
5
  /**
6
6
  * Reading and writing the `_ac` cookie, as functions over a cookie **string**.
7
7
  *
@@ -73,6 +73,5 @@ function mergeTracking(existing, captured, now) {
73
73
  };
74
74
  }
75
75
 
76
- //#endregion
77
76
  export { AB_COOKIE, CLICK_ID_PARAMS, PLATFORM_PARAMS, TRACKED_PARAMS, TRACKING_COOKIE, TRACKING_COOKIE_DAYS, UTM_PARAMS, cookieAttributes, getPrimaryClickId, identifyPlatform, identifySubmitter, mergeTracking, parseTrackingCookie, readCookie, serializeTrackingCookie };
78
77
  //# sourceMappingURL=index.mjs.map
@@ -0,0 +1,105 @@
1
+ /** A Turnstile widget as the list endpoint returns it. The list never carries the secret. */
2
+ interface WidgetSummary {
3
+ sitekey: string;
4
+ name: string;
5
+ domains: string[];
6
+ mode: string;
7
+ }
8
+ /** What the config asks for. */
9
+ interface TurnstileSetup {
10
+ /** Widget display name (not unique in Cloudflare). */
11
+ name: string;
12
+ /** Hostnames to protect; the widget also covers their subdomains. */
13
+ domains: string[];
14
+ /** `site.security.turnstile.siteKey`, when already set. */
15
+ siteKey?: string | undefined;
16
+ }
17
+ /** What the executor observed. */
18
+ interface TurnstileState {
19
+ /** Undefined when no account could be resolved. */
20
+ accountId?: string | undefined;
21
+ widgets: WidgetSummary[];
22
+ }
23
+ type StepAction = 'create' | 'skip' | 'blocked';
24
+ interface StepPlan {
25
+ action: StepAction;
26
+ detail: string;
27
+ }
28
+ interface TurnstilePlan {
29
+ /** Create the widget in Cloudflare. */
30
+ widget: StepPlan;
31
+ /** Write the public site key to `site.security.turnstile.siteKey`. */
32
+ config: StepPlan;
33
+ /** The widget the config should point at, when one already exists (adopted). */
34
+ adopt?: WidgetSummary | undefined;
35
+ /** Hostnames the created widget will cover. */
36
+ domains: string[];
37
+ /** What only a human can do — most importantly, where the secret goes. */
38
+ reminders: string[];
39
+ }
40
+ /** Cloudflare caps a widget at 10 domains; more is a plan-time refusal, not an API 400. */
41
+ export declare const MAX_DOMAINS = 10;
42
+ export declare function plan(setup: TurnstileSetup, state: TurnstileState): TurnstilePlan;
43
+ /** Any step that isn't a skip — the live state or the config has drifted. */
44
+ export declare function hasDrift(p: TurnstilePlan): boolean;
45
+ export declare function formatPlan(p: TurnstilePlan): string;
46
+ type StepStatus = 'created' | 'present' | 'blocked' | 'failed';
47
+ interface TurnstileResult {
48
+ step: string;
49
+ status: StepStatus;
50
+ detail: string;
51
+ }
52
+ export declare function formatSummary(results: TurnstileResult[]): string;
53
+ /**
54
+ * The account to create the widget in. `CLOUDFLARE_ACCOUNT_ID` wins; otherwise the token's
55
+ * only account. Cloudflare's docs state no permission for `GET /accounts`, so a token scoped
56
+ * to Turnstile alone may be refused here: the message says to set the id instead.
57
+ */
58
+ export declare function resolveAccountId(token: string, explicit: string | undefined, fetchImpl?: typeof fetch): Promise<{
59
+ ok: boolean;
60
+ accountId?: string;
61
+ message?: string;
62
+ }>;
63
+ /** Every widget in the account, following pagination. */
64
+ export declare function listWidgets(token: string, accountId: string, fetchImpl?: typeof fetch): Promise<{
65
+ ok: boolean;
66
+ widgets: WidgetSummary[];
67
+ message?: string;
68
+ }>;
69
+ /**
70
+ * Create a managed widget. **Create only** — the planner decides whether one is needed. The
71
+ * returned `secret` is the only time Cloudflare shows it; the caller must store it as a Worker
72
+ * secret and must not write it to a config file.
73
+ */
74
+ export declare function createWidget(token: string, accountId: string, widget: {
75
+ name: string;
76
+ domains: string[];
77
+ }, fetchImpl?: typeof fetch): Promise<{
78
+ ok: boolean;
79
+ sitekey?: string;
80
+ secret?: string;
81
+ message?: string;
82
+ }>;
83
+ /**
84
+ * Hostnames a widget should cover, from the site config: the canonical domain and every
85
+ * environment's URL. Cloudflare matches subdomains too, so `example.com` already covers
86
+ * `dev.example.com`. Deduplicated, lowercase, no scheme or port. `localhost` is never added:
87
+ * Cloudflare publishes always-pass test keys for local development.
88
+ */
89
+ export declare function deriveDomains(site: {
90
+ domains?: {
91
+ canonical?: string | undefined;
92
+ } | undefined;
93
+ environments?: Record<string, {
94
+ url?: string | undefined;
95
+ } | undefined> | undefined;
96
+ }): string[];
97
+ /**
98
+ * Merge the site key into a **raw, on-disk** config object — the rule `ads setup`'s
99
+ * `patchRaw` follows: never write from the resolved object, so `extends` and shorthand
100
+ * shapes are not normalised into a copy nothing builds from. Everything outside
101
+ * `site.security.turnstile.siteKey` is left exactly as it was.
102
+ */
103
+ export declare function patchSiteKey(raw: unknown, siteKey: string): unknown;
104
+ export type { StepAction, StepPlan, StepStatus, TurnstilePlan, TurnstileResult, TurnstileSetup, TurnstileState, WidgetSummary };
105
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/turnstile/types.ts","../../src/turnstile/plan.ts","../../src/turnstile/cloudflare.ts","../../src/turnstile/index.ts"],"mappings":";UACiB;EACf;EACA;EACA;EACA;;;UAIe;;EAEf;;EAEA;;EAEA;;;UAIe;;EAEf;EACA,SAAS;;KCJC;UAEK;EACf,QAAQ;EACR;;UAGe;;EAEf,QAAQ;;EAER,QAAQ;;EAER,QAAQ;;EAER;;EAEA;;;qBAIW;wBAKG,KAAK,OAAO,gBAAgB,OAAO,iBAAiB;;wBA4FpD,SAAS,GAAG;wBAMZ,WAAW,GAAG;KAUlB;UAEK;EACf;EACA,QAAQ;EACR;;wBAGc,cAAc,SAAS;;;;;;wBCpHjB,iBACpB,eACA,8BACA,mBAAkB,QACjB;EAAU;EAAa;EAAoB;;;wBAuBxB,YACpB,eACA,mBACA,mBAAkB,QACjB;EAAU;EAAa,SAAS;EAAiB;;;;;;;wBA2B9B,aACpB,eACA,mBACA;EAAU;EAAc;GACxB,mBAAkB,QACjB;EAAU;EAAa;EAAkB;EAAiB;;;;;;;;wBCrF7C,cAAc;EAC5B;IAAY;;EACZ,eAAe;IAAiB;;;;;;;;;wBA0BlB,aAAa,cAAc"}
@@ -0,0 +1,281 @@
1
+ import { t as table } from "../plan-format-CIz6Feg2.mjs";
2
+
3
+ // src/turnstile/plan.ts
4
+ /**
5
+ * Every decision `vitops setup turnstile` makes, as one pure function.
6
+ *
7
+ * Pure in the load-bearing sense: no `fetch`, no `fs`, no clock, no stdin. The
8
+ * executor (`cloudflare.ts`) lists the account's widgets and creates the missing one;
9
+ * this module decides, from that observation, what is left to do. The same split
10
+ * `ads/plan.ts` and `onboarding/plan.ts` make, for the same two payoffs: `--dry` is a
11
+ * *complete* account of a run, and `--check` is an honest drift report.
12
+ *
13
+ * Idempotency lives here. A widget that already covers the site's domains resolves
14
+ * to `skip`, so a re-run is all skips by construction rather than by the executor
15
+ * checking twice. And there is no update or delete verb anywhere in this module's
16
+ * vocabulary: an existing widget is only ever *adopted* (its public site key is
17
+ * written to the config), never edited, rotated or removed.
18
+ */
19
+ /** Cloudflare caps a widget at 10 domains; more is a plan-time refusal, not an API 400. */
20
+ const MAX_DOMAINS = 10;
21
+ const covers = (w, domains) => domains.every((d) => w.domains.some((wd) => d === wd || d.endsWith(`.${wd}`)));
22
+ function plan(setup, state) {
23
+ const reminders = ["Set the secret on the Worker: `npx wrangler secret put VITOPS_TURNSTILE_SECRET` (and on the dev worker with `--env`/`--config` as your setup uses). It is shown once, at creation, and never written to site.json."];
24
+ if (setup.domains.length === 0) return {
25
+ widget: {
26
+ action: "blocked",
27
+ detail: "no hostnames to protect — set site.domains.canonical or site.environments.<env>.url"
28
+ },
29
+ config: {
30
+ action: "blocked",
31
+ detail: "needs a widget first"
32
+ },
33
+ domains: [],
34
+ reminders: []
35
+ };
36
+ if (setup.domains.length > 10) return {
37
+ widget: {
38
+ action: "blocked",
39
+ detail: `${setup.domains.length} hostnames, but Cloudflare allows ${10} per widget`
40
+ },
41
+ config: {
42
+ action: "blocked",
43
+ detail: "needs a widget first"
44
+ },
45
+ domains: setup.domains,
46
+ reminders: []
47
+ };
48
+ if (!state.accountId) return {
49
+ widget: {
50
+ action: "blocked",
51
+ detail: "no Cloudflare account id — set CLOUDFLARE_ACCOUNT_ID (or use a token that can list accounts)"
52
+ },
53
+ config: {
54
+ action: "blocked",
55
+ detail: "needs a widget first"
56
+ },
57
+ domains: setup.domains,
58
+ reminders: []
59
+ };
60
+ const configured = setup.siteKey ? state.widgets.find((w) => w.sitekey === setup.siteKey) : void 0;
61
+ if (setup.siteKey && !configured) return {
62
+ widget: {
63
+ action: "blocked",
64
+ detail: `site.security.turnstile.siteKey ${setup.siteKey} matches no widget in this account — remove it to let setup adopt or create one`
65
+ },
66
+ config: {
67
+ action: "blocked",
68
+ detail: "config key is unverified"
69
+ },
70
+ domains: setup.domains,
71
+ reminders: []
72
+ };
73
+ const existing = configured ?? state.widgets.find((w) => covers(w, setup.domains));
74
+ if (existing) {
75
+ const uncovered = setup.domains.filter((d) => !covers(existing, [d]));
76
+ return {
77
+ widget: {
78
+ action: "skip",
79
+ detail: uncovered.length === 0 ? `widget "${existing.name}" already covers ${setup.domains.join(", ")}` : `widget "${existing.name}" exists but does not cover ${uncovered.join(", ")} — add them in the dashboard; this command never edits a widget`
80
+ },
81
+ config: setup.siteKey === existing.sitekey ? {
82
+ action: "skip",
83
+ detail: "site key already in site.json"
84
+ } : {
85
+ action: "create",
86
+ detail: `write ${existing.sitekey} to site.json`
87
+ },
88
+ adopt: existing,
89
+ domains: setup.domains,
90
+ reminders: ["The secret of an existing widget cannot be read back. Lost it? Rotate it in the dashboard (Turnstile > the widget > Rotate secret), then `wrangler secret put VITOPS_TURNSTILE_SECRET`."]
91
+ };
92
+ }
93
+ return {
94
+ widget: {
95
+ action: "create",
96
+ detail: `create "${setup.name}" (managed) for ${setup.domains.join(", ")}`
97
+ },
98
+ config: {
99
+ action: "create",
100
+ detail: "write the new site key to site.json"
101
+ },
102
+ domains: setup.domains,
103
+ reminders
104
+ };
105
+ }
106
+ /** Any step that isn't a skip — the live state or the config has drifted. */
107
+ function hasDrift(p) {
108
+ return p.widget.action !== "skip" || p.config.action !== "skip";
109
+ }
110
+ const GLYPH = {
111
+ create: "+",
112
+ skip: "·",
113
+ blocked: "!"
114
+ };
115
+ function formatPlan(p) {
116
+ const rows = [["widget", p.widget], ["config", p.config]].map(([name, step]) => [
117
+ GLYPH[step.action],
118
+ name,
119
+ step.detail
120
+ ]);
121
+ return table([
122
+ "",
123
+ "step",
124
+ "detail"
125
+ ], rows, p.reminders);
126
+ }
127
+ function formatSummary(results) {
128
+ return table([
129
+ "step",
130
+ "status",
131
+ "detail"
132
+ ], results.map((r) => [
133
+ r.step,
134
+ r.status,
135
+ r.detail
136
+ ]), []);
137
+ }
138
+
139
+ // src/turnstile/cloudflare.ts
140
+ const API = "https://api.cloudflare.com/client/v4";
141
+ const cfError = (status, body) => {
142
+ const first = body.errors?.[0];
143
+ return first ? `${first.code} ${first.message}` : `HTTP ${status}`;
144
+ };
145
+ const PERMISSION_HINT = " (does the token carry Account > Turnstile > Edit?)";
146
+ async function json(res) {
147
+ try {
148
+ return await res.json();
149
+ } catch {
150
+ return { success: false };
151
+ }
152
+ }
153
+ /**
154
+ * The account to create the widget in. `CLOUDFLARE_ACCOUNT_ID` wins; otherwise the token's
155
+ * only account. Cloudflare's docs state no permission for `GET /accounts`, so a token scoped
156
+ * to Turnstile alone may be refused here: the message says to set the id instead.
157
+ */
158
+ async function resolveAccountId(token, explicit, fetchImpl = fetch) {
159
+ if (explicit) return {
160
+ ok: true,
161
+ accountId: explicit
162
+ };
163
+ const res = await fetchImpl(`${API}/accounts?per_page=5`, { headers: { authorization: `Bearer ${token}` } });
164
+ const body = await json(res);
165
+ if (!res.ok || !body.success) return {
166
+ ok: false,
167
+ message: `${cfError(res.status, body)} — set CLOUDFLARE_ACCOUNT_ID to skip the account lookup`
168
+ };
169
+ const accounts = body.result ?? [];
170
+ if (accounts.length === 1) return {
171
+ ok: true,
172
+ accountId: accounts[0].id
173
+ };
174
+ return {
175
+ ok: false,
176
+ message: accounts.length === 0 ? "the token can see no accounts — set CLOUDFLARE_ACCOUNT_ID" : `the token can see ${accounts.length} accounts — set CLOUDFLARE_ACCOUNT_ID to choose one`
177
+ };
178
+ }
179
+ /** Every widget in the account, following pagination. */
180
+ async function listWidgets(token, accountId, fetchImpl = fetch) {
181
+ const widgets = [];
182
+ for (let page = 1; page <= 20; page++) {
183
+ const res = await fetchImpl(`${API}/accounts/${encodeURIComponent(accountId)}/challenges/widgets?page=${page}&per_page=100`, { headers: { authorization: `Bearer ${token}` } });
184
+ const body = await json(res);
185
+ if (!res.ok || !body.success) return {
186
+ ok: false,
187
+ widgets: [],
188
+ message: cfError(res.status, body) + (res.status === 403 ? PERMISSION_HINT : "")
189
+ };
190
+ for (const w of body.result ?? []) widgets.push({
191
+ sitekey: w.sitekey,
192
+ name: w.name,
193
+ domains: w.domains ?? [],
194
+ mode: w.mode
195
+ });
196
+ const info = body.result_info;
197
+ if (!info || page * info.per_page >= info.total_count) break;
198
+ }
199
+ return {
200
+ ok: true,
201
+ widgets
202
+ };
203
+ }
204
+ /**
205
+ * Create a managed widget. **Create only** — the planner decides whether one is needed. The
206
+ * returned `secret` is the only time Cloudflare shows it; the caller must store it as a Worker
207
+ * secret and must not write it to a config file.
208
+ */
209
+ async function createWidget(token, accountId, widget, fetchImpl = fetch) {
210
+ const res = await fetchImpl(`${API}/accounts/${encodeURIComponent(accountId)}/challenges/widgets`, {
211
+ method: "POST",
212
+ headers: {
213
+ authorization: `Bearer ${token}`,
214
+ "content-type": "application/json"
215
+ },
216
+ body: JSON.stringify({
217
+ name: widget.name,
218
+ domains: widget.domains,
219
+ mode: "managed"
220
+ })
221
+ });
222
+ const body = await json(res);
223
+ if (!res.ok || !body.success || !body.result?.sitekey || !body.result.secret) return {
224
+ ok: false,
225
+ message: cfError(res.status, body) + (res.status === 403 ? PERMISSION_HINT : "")
226
+ };
227
+ return {
228
+ ok: true,
229
+ sitekey: body.result.sitekey,
230
+ secret: body.result.secret
231
+ };
232
+ }
233
+
234
+ // src/turnstile/index.ts
235
+ /**
236
+ * Hostnames a widget should cover, from the site config: the canonical domain and every
237
+ * environment's URL. Cloudflare matches subdomains too, so `example.com` already covers
238
+ * `dev.example.com`. Deduplicated, lowercase, no scheme or port. `localhost` is never added:
239
+ * Cloudflare publishes always-pass test keys for local development.
240
+ */
241
+ function deriveDomains(site) {
242
+ const urls = [site.domains?.canonical, ...Object.values(site.environments ?? {}).map((e) => e?.url)];
243
+ const hosts = /* @__PURE__ */ new Set();
244
+ for (const url of urls) {
245
+ if (!url) continue;
246
+ try {
247
+ const host = new URL(url).hostname.toLowerCase();
248
+ if (host && host !== "localhost") hosts.add(host);
249
+ } catch {}
250
+ }
251
+ return [...hosts].filter((h) => ![...hosts].some((p) => p !== h && h.endsWith(`.${p}`)));
252
+ }
253
+ /**
254
+ * Merge the site key into a **raw, on-disk** config object — the rule `ads setup`'s
255
+ * `patchRaw` follows: never write from the resolved object, so `extends` and shorthand
256
+ * shapes are not normalised into a copy nothing builds from. Everything outside
257
+ * `site.security.turnstile.siteKey` is left exactly as it was.
258
+ */
259
+ function patchSiteKey(raw, siteKey) {
260
+ if (typeof raw !== "object" || raw == null || Array.isArray(raw)) throw new Error("config is not an object");
261
+ const root = raw;
262
+ const site = root["site"] ?? {};
263
+ const security = site["security"] ?? {};
264
+ const turnstile = security["turnstile"] ?? {};
265
+ return {
266
+ ...root,
267
+ site: {
268
+ ...site,
269
+ security: {
270
+ ...security,
271
+ turnstile: {
272
+ ...turnstile,
273
+ siteKey
274
+ }
275
+ }
276
+ }
277
+ };
278
+ }
279
+
280
+ export { MAX_DOMAINS, createWidget, deriveDomains, formatPlan, formatSummary, hasDrift, listWidgets, patchSiteKey, plan, resolveAccountId };
281
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/turnstile/plan.ts","../../src/turnstile/cloudflare.ts","../../src/turnstile/index.ts"],"sourcesContent":["/**\n * Every decision `vitops setup turnstile` makes, as one pure function.\n *\n * Pure in the load-bearing sense: no `fetch`, no `fs`, no clock, no stdin. The\n * executor (`cloudflare.ts`) lists the account's widgets and creates the missing one;\n * this module decides, from that observation, what is left to do. The same split\n * `ads/plan.ts` and `onboarding/plan.ts` make, for the same two payoffs: `--dry` is a\n * *complete* account of a run, and `--check` is an honest drift report.\n *\n * Idempotency lives here. A widget that already covers the site's domains resolves\n * to `skip`, so a re-run is all skips by construction rather than by the executor\n * checking twice. And there is no update or delete verb anywhere in this module's\n * vocabulary: an existing widget is only ever *adopted* (its public site key is\n * written to the config), never edited, rotated or removed.\n */\nimport { table } from '../plan-format.ts';\nimport type { TurnstileSetup, TurnstileState, WidgetSummary } from './types.ts';\n\nexport type StepAction = 'create' | 'skip' | 'blocked';\n\nexport interface StepPlan {\n action: StepAction;\n detail: string;\n}\n\nexport interface TurnstilePlan {\n /** Create the widget in Cloudflare. */\n widget: StepPlan;\n /** Write the public site key to `site.security.turnstile.siteKey`. */\n config: StepPlan;\n /** The widget the config should point at, when one already exists (adopted). */\n adopt?: WidgetSummary | undefined;\n /** Hostnames the created widget will cover. */\n domains: string[];\n /** What only a human can do — most importantly, where the secret goes. */\n reminders: string[];\n}\n\n/** Cloudflare caps a widget at 10 domains; more is a plan-time refusal, not an API 400. */\nexport const MAX_DOMAINS = 10;\n\nconst covers = (w: WidgetSummary, domains: string[]): boolean =>\n domains.every((d) => w.domains.some((wd) => d === wd || d.endsWith(`.${wd}`)));\n\nexport function plan(setup: TurnstileSetup, state: TurnstileState): TurnstilePlan {\n const reminders = [\n 'Set the secret on the Worker: `npx wrangler secret put VITOPS_TURNSTILE_SECRET` (and on the dev worker with `--env`/`--config` as your setup uses). It is shown once, at creation, and never written to site.json.',\n ];\n\n if (setup.domains.length === 0)\n return {\n widget: {\n action: 'blocked',\n detail:\n 'no hostnames to protect — set site.domains.canonical or site.environments.<env>.url',\n },\n config: { action: 'blocked', detail: 'needs a widget first' },\n domains: [],\n reminders: [],\n };\n if (setup.domains.length > MAX_DOMAINS)\n return {\n widget: {\n action: 'blocked',\n detail: `${setup.domains.length} hostnames, but Cloudflare allows ${MAX_DOMAINS} per widget`,\n },\n config: { action: 'blocked', detail: 'needs a widget first' },\n domains: setup.domains,\n reminders: [],\n };\n\n if (!state.accountId)\n return {\n widget: {\n action: 'blocked',\n detail:\n 'no Cloudflare account id — set CLOUDFLARE_ACCOUNT_ID (or use a token that can list accounts)',\n },\n config: { action: 'blocked', detail: 'needs a widget first' },\n domains: setup.domains,\n reminders: [],\n };\n\n // The config's own key wins: if it names a widget in this account, that is the one.\n const configured = setup.siteKey\n ? state.widgets.find((w) => w.sitekey === setup.siteKey)\n : undefined;\n if (setup.siteKey && !configured) {\n // A key that no widget in this account owns is drift (typo, another account, a deleted\n // widget). Creating a second widget would hide it, so stop and say so.\n return {\n widget: {\n action: 'blocked',\n detail: `site.security.turnstile.siteKey ${setup.siteKey} matches no widget in this account — remove it to let setup adopt or create one`,\n },\n config: { action: 'blocked', detail: 'config key is unverified' },\n domains: setup.domains,\n reminders: [],\n };\n }\n\n const existing = configured ?? state.widgets.find((w) => covers(w, setup.domains));\n if (existing) {\n const uncovered = setup.domains.filter((d) => !covers(existing, [d]));\n return {\n widget: {\n action: 'skip',\n detail:\n uncovered.length === 0\n ? `widget \"${existing.name}\" already covers ${setup.domains.join(', ')}`\n : `widget \"${existing.name}\" exists but does not cover ${uncovered.join(', ')} — add them in the dashboard; this command never edits a widget`,\n },\n config:\n setup.siteKey === existing.sitekey\n ? { action: 'skip', detail: 'site key already in site.json' }\n : { action: 'create', detail: `write ${existing.sitekey} to site.json` },\n adopt: existing,\n domains: setup.domains,\n reminders: [\n 'The secret of an existing widget cannot be read back. Lost it? Rotate it in the dashboard (Turnstile > the widget > Rotate secret), then `wrangler secret put VITOPS_TURNSTILE_SECRET`.',\n ],\n };\n }\n\n return {\n widget: {\n action: 'create',\n detail: `create \"${setup.name}\" (managed) for ${setup.domains.join(', ')}`,\n },\n config: { action: 'create', detail: 'write the new site key to site.json' },\n domains: setup.domains,\n reminders,\n };\n}\n\n/** Any step that isn't a skip — the live state or the config has drifted. */\nexport function hasDrift(p: TurnstilePlan): boolean {\n return p.widget.action !== 'skip' || p.config.action !== 'skip';\n}\n\nconst GLYPH: Record<StepAction, string> = { create: '+', skip: '·', blocked: '!' };\n\nexport function formatPlan(p: TurnstilePlan): string {\n const rows = (\n [\n ['widget', p.widget],\n ['config', p.config],\n ] as const\n ).map(([name, step]) => [GLYPH[step.action], name, step.detail]);\n return table(['', 'step', 'detail'], rows, p.reminders);\n}\n\nexport type StepStatus = 'created' | 'present' | 'blocked' | 'failed';\n\nexport interface TurnstileResult {\n step: string;\n status: StepStatus;\n detail: string;\n}\n\nexport function formatSummary(results: TurnstileResult[]): string {\n return table(\n ['step', 'status', 'detail'],\n results.map((r) => [r.step, r.status, r.detail]),\n [],\n );\n}\n","/**\n * Cloudflare's Turnstile widget API — the one thing `vitops setup turnstile` writes.\n *\n * Verified against developers.cloudflare.com (2026-11): list is\n * `GET /accounts/{id}/challenges/widgets` (paged, `per_page` 5 to 1000), create is\n * `POST` to the same path with `name`, `domains` (max 10) and `mode` required, and only\n * the create response carries `secret`. The token needs the **Account > Turnstile > Edit**\n * permission (API reference names: `Turnstile Sites Write`, or `Account Settings Write`).\n *\n * As in `onboarding/cloudflare.ts` there is deliberately no update, rotate or delete verb:\n * the destructive operations are absent rather than guarded. `fetchImpl` is injected and\n * every call returns a structured result rather than throwing.\n */\nimport type { WidgetSummary } from './types.ts';\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 result_info?: { page: number; per_page: number; total_count: number };\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\nconst PERMISSION_HINT = ' (does the token carry Account > Turnstile > Edit?)';\n\nasync function json<T>(res: Response): Promise<CfEnvelope<T>> {\n try {\n return (await res.json()) as CfEnvelope<T>;\n } catch {\n return { success: false };\n }\n}\n\n/**\n * The account to create the widget in. `CLOUDFLARE_ACCOUNT_ID` wins; otherwise the token's\n * only account. Cloudflare's docs state no permission for `GET /accounts`, so a token scoped\n * to Turnstile alone may be refused here: the message says to set the id instead.\n */\nexport async function resolveAccountId(\n token: string,\n explicit: string | undefined,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; accountId?: string; message?: string }> {\n if (explicit) return { ok: true, accountId: explicit };\n const res = await fetchImpl(`${API}/accounts?per_page=5`, {\n headers: { authorization: `Bearer ${token}` },\n });\n const body = await json<{ id: string; name: string }[]>(res);\n if (!res.ok || !body.success)\n return {\n ok: false,\n message: `${cfError(res.status, body)} — set CLOUDFLARE_ACCOUNT_ID to skip the account lookup`,\n };\n const accounts = body.result ?? [];\n if (accounts.length === 1) return { ok: true, accountId: accounts[0]!.id };\n return {\n ok: false,\n message:\n accounts.length === 0\n ? 'the token can see no accounts — set CLOUDFLARE_ACCOUNT_ID'\n : `the token can see ${accounts.length} accounts — set CLOUDFLARE_ACCOUNT_ID to choose one`,\n };\n}\n\n/** Every widget in the account, following pagination. */\nexport async function listWidgets(\n token: string,\n accountId: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; widgets: WidgetSummary[]; message?: string }> {\n const widgets: WidgetSummary[] = [];\n for (let page = 1; page <= 20; page++) {\n const res = await fetchImpl(\n `${API}/accounts/${encodeURIComponent(accountId)}/challenges/widgets?page=${page}&per_page=100`,\n { headers: { authorization: `Bearer ${token}` } },\n );\n const body = await json<WidgetSummary[]>(res);\n if (!res.ok || !body.success)\n return {\n ok: false,\n widgets: [],\n message: cfError(res.status, body) + (res.status === 403 ? PERMISSION_HINT : ''),\n };\n for (const w of body.result ?? [])\n widgets.push({ sitekey: w.sitekey, name: w.name, domains: w.domains ?? [], mode: w.mode });\n const info = body.result_info;\n if (!info || page * info.per_page >= info.total_count) break;\n }\n return { ok: true, widgets };\n}\n\n/**\n * Create a managed widget. **Create only** — the planner decides whether one is needed. The\n * returned `secret` is the only time Cloudflare shows it; the caller must store it as a Worker\n * secret and must not write it to a config file.\n */\nexport async function createWidget(\n token: string,\n accountId: string,\n widget: { name: string; domains: string[] },\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; sitekey?: string; secret?: string; message?: string }> {\n const res = await fetchImpl(\n `${API}/accounts/${encodeURIComponent(accountId)}/challenges/widgets`,\n {\n method: 'POST',\n headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },\n body: JSON.stringify({ name: widget.name, domains: widget.domains, mode: 'managed' }),\n },\n );\n const body = await json<{ sitekey: string; secret: string }>(res);\n if (!res.ok || !body.success || !body.result?.sitekey || !body.result.secret)\n return {\n ok: false,\n message: cfError(res.status, body) + (res.status === 403 ? PERMISSION_HINT : ''),\n };\n return { ok: true, sitekey: body.result.sitekey, secret: body.result.secret };\n}\n","/**\n * `vitops setup turnstile` — create a Cloudflare Turnstile widget and record its public site\n * key in `site.json`.\n *\n * A separate subpath (`@getvitops/utils/turnstile`), matching `./ads` and `./hosting`. It is\n * build-time tooling, unlike `@getvitops/utils/security` (the Worker-side verifier): keep the\n * two apart so a conversion route never pulls this in.\n *\n * The layering is the repo's usual one: `plan.ts` decides everything and touches nothing;\n * `cloudflare.ts` executes and decides nothing.\n */\nexport { MAX_DOMAINS, formatPlan, formatSummary, hasDrift, plan } from './plan.ts';\nexport type { StepAction, StepPlan, StepStatus, TurnstilePlan, TurnstileResult } from './plan.ts';\nexport { createWidget, listWidgets, resolveAccountId } from './cloudflare.ts';\nexport type { TurnstileSetup, TurnstileState, WidgetSummary } from './types.ts';\n\n/**\n * Hostnames a widget should cover, from the site config: the canonical domain and every\n * environment's URL. Cloudflare matches subdomains too, so `example.com` already covers\n * `dev.example.com`. Deduplicated, lowercase, no scheme or port. `localhost` is never added:\n * Cloudflare publishes always-pass test keys for local development.\n */\nexport function deriveDomains(site: {\n domains?: { canonical?: string | undefined } | undefined;\n environments?: Record<string, { url?: string | undefined } | undefined> | undefined;\n}): string[] {\n const urls = [\n site.domains?.canonical,\n ...Object.values(site.environments ?? {}).map((e) => e?.url),\n ];\n const hosts = new Set<string>();\n for (const url of urls) {\n if (!url) continue;\n try {\n const host = new URL(url).hostname.toLowerCase();\n if (host && host !== 'localhost') hosts.add(host);\n } catch {\n // a malformed URL is the config validator's business, not a reason to create a widget for it\n }\n }\n // Drop hostnames already covered by a parent in the set (dev.example.com under example.com).\n return [...hosts].filter((h) => ![...hosts].some((p) => p !== h && h.endsWith(`.${p}`)));\n}\n\n/**\n * Merge the site key into a **raw, on-disk** config object — the rule `ads setup`'s\n * `patchRaw` follows: never write from the resolved object, so `extends` and shorthand\n * shapes are not normalised into a copy nothing builds from. Everything outside\n * `site.security.turnstile.siteKey` is left exactly as it was.\n */\nexport function patchSiteKey(raw: unknown, siteKey: string): unknown {\n if (typeof raw !== 'object' || raw == null || Array.isArray(raw))\n throw new Error('config is not an object');\n const root = raw as Record<string, unknown>;\n const site = (root['site'] ?? {}) as Record<string, unknown>;\n const security = (site['security'] ?? {}) as Record<string, unknown>;\n const turnstile = (security['turnstile'] ?? {}) as Record<string, unknown>;\n return {\n ...root,\n site: { ...site, security: { ...security, turnstile: { ...turnstile, siteKey } } },\n };\n}\n"],"mappings":"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"}
@@ -1,25 +1,6 @@
1
- //#region src/tracking/types.d.ts
2
- /**
3
- * The tracking module's own option surface.
4
- *
5
- * Deliberately structural rather than `Config['site']['tracking']` imported from
6
- * `@getvitops/generator`: the generator already depends on this package, so
7
- * importing back would be a cycle. The vocabulary mirrors that block field for
8
- * field — the same arrangement, and for the same reason, as `IndexingConfig`.
9
- */
1
+ import { TrackingConfig } from "@getvitops/config";
10
2
  /** Which consent category the `_ac` cookie waits on. */
11
- type TrackingCategory = 'marketing' | 'analytics';
12
- interface TrackingConfig {
13
- enabled?: boolean | undefined;
14
- /**
15
- * Default `marketing`. `_ac` is a 90-day identifier tying a visitor to the ad
16
- * that brought them, which is what advertising consent covers — but a site
17
- * using click IDs purely for its own campaign reporting may reasonably file it
18
- * under `analytics`, so this is stated rather than assumed.
19
- */
20
- category?: TrackingCategory | undefined;
21
- platforms?: string[] | undefined;
22
- }
3
+ type TrackingCategory = NonNullable<TrackingConfig['category']>;
23
4
  /**
24
5
  * What the `_ac` cookie holds: where a visitor came from, captured once on the
25
6
  * landing page and carried until it expires.
@@ -65,6 +46,5 @@ interface ConversionEvent {
65
46
  /** Epoch ms. */
66
47
  at: number;
67
48
  }
68
- //#endregion
69
49
  export { TrackingData as i, TrackingCategory as n, TrackingConfig as r, ConversionEvent as t };
70
- //# sourceMappingURL=types-7ELci8RQ.d.mts.map
50
+ //# sourceMappingURL=types-oac2P5or.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-oac2P5or.d.mts","names":[],"sources":["../src/tracking/types.ts"],"mappings":";;KAsBY,mBAAmB,YAAY;;;;;;;;;UAU1B;GACd;;EAED;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;;;UAee;EACf;;EAEA,WAAW;EACX;;EAEA;;EAEA,UAAU;EACV;EACA;;EAEA"}