@getvitops/utils 8.0.1 → 8.2.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 (77) hide show
  1. package/CHANGELOG.md +289 -8
  2. package/dist/{adc-BFrzpMHq.d.mts → adc-BoEoogyo.d.mts} +2 -3
  3. package/dist/adc-BoEoogyo.d.mts.map +1 -0
  4. package/dist/ads/index.d.mts +50 -17
  5. package/dist/ads/index.d.mts.map +1 -1
  6. package/dist/ads/index.mjs +77 -8
  7. package/dist/ads/index.mjs.map +1 -1
  8. package/dist/cloudflare-BiK61Oq4.d.mts.map +1 -1
  9. package/dist/{cloudflare-CgAsXiqe.mjs → cloudflare-CD8vwrgj.mjs} +2 -24
  10. package/dist/cloudflare-CD8vwrgj.mjs.map +1 -0
  11. package/dist/color/index.d.mts +34 -35
  12. package/dist/color/index.d.mts.map +1 -1
  13. package/dist/color/index.mjs +3 -1
  14. package/dist/color/index.mjs.map +1 -1
  15. package/dist/crm/index.d.mts +288 -0
  16. package/dist/crm/index.d.mts.map +1 -0
  17. package/dist/crm/index.mjs +731 -0
  18. package/dist/crm/index.mjs.map +1 -0
  19. package/dist/email/index.d.mts +216 -0
  20. package/dist/email/index.d.mts.map +1 -0
  21. package/dist/email/index.mjs +331 -0
  22. package/dist/email/index.mjs.map +1 -0
  23. package/dist/favicon.d.mts +8 -9
  24. package/dist/favicon.d.mts.map +1 -1
  25. package/dist/git/index.d.mts +65 -0
  26. package/dist/git/index.d.mts.map +1 -0
  27. package/dist/git/index.mjs +107 -0
  28. package/dist/git/index.mjs.map +1 -0
  29. package/dist/hosting/index.d.mts +184 -0
  30. package/dist/hosting/index.d.mts.map +1 -0
  31. package/dist/hosting/index.mjs +553 -0
  32. package/dist/hosting/index.mjs.map +1 -0
  33. package/dist/identity-BoRddTx3.mjs +35 -0
  34. package/dist/identity-BoRddTx3.mjs.map +1 -0
  35. package/dist/index.d.mts +24 -25
  36. package/dist/index.d.mts.map +1 -1
  37. package/dist/index.mjs +5 -5
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/indexing/index.d.mts +23 -24
  40. package/dist/indexing/index.d.mts.map +1 -1
  41. package/dist/media/index.d.mts +24 -24
  42. package/dist/media/index.d.mts.map +1 -1
  43. package/dist/media/index.mjs +3 -3
  44. package/dist/media/index.mjs.map +1 -1
  45. package/dist/notify/index.d.mts +26 -15
  46. package/dist/notify/index.d.mts.map +1 -1
  47. package/dist/notify/index.mjs +50 -20
  48. package/dist/notify/index.mjs.map +1 -1
  49. package/dist/onboarding/index.d.mts +10 -141
  50. package/dist/onboarding/index.d.mts.map +1 -1
  51. package/dist/onboarding/index.mjs +2 -144
  52. package/dist/onboarding/index.mjs.map +1 -1
  53. package/dist/plan-BLKikoOG.mjs +150 -0
  54. package/dist/plan-BLKikoOG.mjs.map +1 -0
  55. package/dist/plan-Hq8Pb0Uw.d.mts +133 -0
  56. package/dist/plan-Hq8Pb0Uw.d.mts.map +1 -0
  57. package/dist/plan-format-A18SvJ7C.mjs +24 -0
  58. package/dist/plan-format-A18SvJ7C.mjs.map +1 -0
  59. package/dist/reviews/index.d.mts +12 -13
  60. package/dist/reviews/index.d.mts.map +1 -1
  61. package/dist/schema/index.d.mts +7 -7
  62. package/dist/schema/index.d.mts.map +1 -1
  63. package/dist/security/index.d.mts +2 -2
  64. package/dist/security/index.d.mts.map +1 -1
  65. package/dist/{token-Bz_-igw-.d.mts → token-DmmoQ7um.d.mts} +6 -3
  66. package/dist/token-DmmoQ7um.d.mts.map +1 -0
  67. package/dist/tracking/index.d.mts +43 -17
  68. package/dist/tracking/index.d.mts.map +1 -1
  69. package/dist/tracking/index.mjs +2 -1
  70. package/dist/tracking/index.mjs.map +1 -1
  71. package/dist/{types-CHHjzRrv.d.mts → types-7ELci8RQ.d.mts} +4 -2
  72. package/dist/types-7ELci8RQ.d.mts.map +1 -0
  73. package/package.json +18 -4
  74. package/dist/adc-BFrzpMHq.d.mts.map +0 -1
  75. package/dist/cloudflare-CgAsXiqe.mjs.map +0 -1
  76. package/dist/token-Bz_-igw-.d.mts.map +0 -1
  77. package/dist/types-CHHjzRrv.d.mts.map +0 -1
@@ -1,149 +1,7 @@
1
1
  import { a as googleHeaders, i as googleAccessToken, n as encodeSite, r as errorDetail } from "../token-DkKC5m23.mjs";
2
- import { i as table, n as findZoneId, r as listApexTxt, t as createApexTxt } from "../cloudflare-CgAsXiqe.mjs";
2
+ import { a as ownerUnion, c as siteUrlFor, i as hasDrift, n as formatPlan, o as plan, r as formatSummary, s as planDomain, t as backoffSchedule } from "../plan-BLKikoOG.mjs";
3
+ import { n as findZoneId, r as listApexTxt, t as createApexTxt } from "../cloudflare-CD8vwrgj.mjs";
3
4
 
4
- //#region src/onboarding/plan.ts
5
- /** The `sc-domain:` property identifier for a bare domain. */
6
- const siteUrlFor = (domain) => `sc-domain:${domain}`;
7
- /**
8
- * Backoff ladder for the verification retry, in milliseconds.
9
- *
10
- * DNS_TXT verification fails until the record has propagated, which is a matter of
11
- * seconds-to-minutes, not a real error — so the schedule is a *policy*, decided
12
- * here and asserted in the tests, while the actual sleeping is the CLI's job.
13
- * Capped at `maxAttempts`; a domain still unverified after the last attempt is
14
- * reported PENDING, never failed.
15
- */
16
- function backoffSchedule(maxAttempts = 5) {
17
- const out = [];
18
- for (let i = 1; i <= maxAttempts; i++) out.push(2 ** i * 1e3);
19
- return out;
20
- }
21
- /** Decide the four steps for one domain from its desired config and live state. */
22
- function planDomain(setup, state) {
23
- const reminders = [];
24
- if (setup.fullUserGroup) reminders.push(`add ${setup.fullUserGroup} as a Full User in Search Console — no API exists for this, do it in the UI`);
25
- const txt = state.txtPresent ? {
26
- action: "skip",
27
- detail: "apex TXT already present"
28
- } : {
29
- action: "create",
30
- detail: "create apex TXT verification record"
31
- };
32
- const verify = state.verified ? {
33
- action: "skip",
34
- detail: "ownership already verified"
35
- } : {
36
- action: "create",
37
- detail: "verify ownership (DNS_TXT)"
38
- };
39
- const property = state.propertyExists ? {
40
- action: "skip",
41
- detail: "property already exists"
42
- } : {
43
- action: "create",
44
- detail: `add property ${siteUrlFor(setup.domain)}`
45
- };
46
- const desired = setup.delegatedOwners ?? [];
47
- const present = new Set(state.currentOwners.map((o) => o.toLowerCase()));
48
- const ownersToAdd = desired.filter((o) => !present.has(o.toLowerCase()));
49
- const owners = desired.length === 0 ? {
50
- action: "skip",
51
- detail: "no delegated owners"
52
- } : ownersToAdd.length === 0 ? {
53
- action: "skip",
54
- detail: "all delegated owners already present"
55
- } : {
56
- action: "update",
57
- detail: `add ${ownersToAdd.length} owner(s): ${ownersToAdd.join(", ")}`
58
- };
59
- return {
60
- domain: setup.domain,
61
- siteUrl: siteUrlFor(setup.domain),
62
- txt,
63
- verify,
64
- property,
65
- owners,
66
- ownersToAdd,
67
- reminders
68
- };
69
- }
70
- /**
71
- * The owner set to PUT: everyone already there, plus everyone being added.
72
- *
73
- * `updateOwners` REPLACES the owner list, so this is the one computation standing
74
- * between a delegation and locking yourself out — pass a bare `ownersToAdd` and
75
- * the account that just verified the domain is removed from it. It belongs here
76
- * rather than at the call site for exactly that reason: it is a decision, it is
77
- * the dangerous one, and the executor is not allowed to make it.
78
- *
79
- * `current` comes FIRST so the API's own casing survives dedupe; matching is
80
- * case-insensitive, mirroring `planDomain`'s `present` check, because Google
81
- * echoes back addresses in whatever case the account uses and a case-sensitive
82
- * `Set` would re-add an owner who is already there.
83
- */
84
- function ownerUnion(current, toAdd) {
85
- const out = [];
86
- const seen = /* @__PURE__ */ new Set();
87
- for (const o of [...current, ...toAdd]) {
88
- const key = o.toLowerCase();
89
- if (seen.has(key)) continue;
90
- seen.add(key);
91
- out.push(o);
92
- }
93
- return out;
94
- }
95
- /** Plan every domain. */
96
- function plan(config, states) {
97
- const notes = [];
98
- return {
99
- domains: config.domains.map((setup) => {
100
- const state = states.get(setup.domain);
101
- if (!state) notes.push(`${setup.domain}: no observed state; treating everything as pending`);
102
- return planDomain(setup, state ?? {
103
- txtPresent: false,
104
- verified: false,
105
- currentOwners: [],
106
- propertyExists: false
107
- });
108
- }),
109
- notes
110
- };
111
- }
112
- /** Any step on any domain is a mutation — i.e. the live state has drifted. */
113
- function hasDrift(p) {
114
- return p.domains.some((d) => d.txt.action !== "skip" || d.verify.action !== "skip" || d.property.action !== "skip" || d.owners.action !== "skip");
115
- }
116
- /** Render a plan as the block `--dry` and `--check` print. */
117
- function formatPlan(p) {
118
- const lines = [];
119
- const glyph = (s) => s.action === "skip" ? "·" : "+";
120
- for (const d of p.domains) {
121
- lines.push(d.domain);
122
- lines.push(` ${glyph(d.txt)} TXT ${d.txt.detail}`);
123
- lines.push(` ${glyph(d.verify)} verify ${d.verify.detail}`);
124
- lines.push(` ${glyph(d.property)} property ${d.property.detail}`);
125
- lines.push(` ${glyph(d.owners)} owners ${d.owners.detail}`);
126
- for (const r of d.reminders) lines.push(` ! ${r}`);
127
- }
128
- for (const n of p.notes) lines.push(`! ${n}`);
129
- return lines.join("\n");
130
- }
131
- /** Render the end-of-run summary: one row per domain, plus any reminders. */
132
- function formatSummary(results) {
133
- return table([
134
- "DOMAIN",
135
- "TXT",
136
- "VERIFIED",
137
- "PROPERTY"
138
- ], results.map((r) => [
139
- r.domain,
140
- r.txt,
141
- r.verified,
142
- r.property
143
- ]), results.flatMap((r) => r.reminders.map((m) => ` ! ${r.domain}: ${m}`)));
144
- }
145
-
146
- //#endregion
147
5
  //#region src/onboarding/google.ts
148
6
  /**
149
7
  * Google — the two APIs onboarding drives, and the user-OAuth token behind both.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/onboarding/plan.ts","../../src/onboarding/google.ts"],"sourcesContent":["/**\n * Every decision `vitops search setup` makes, as one pure function.\n *\n * Pure in the load-bearing sense: no `fetch`, no `fs`, no clock. The executors\n * beside this one gather each domain's live state and apply what they are told;\n * this module decides, from that state, what is left to do. That split is what\n * makes `--dry` a complete account of a run and `--check` an honest drift report —\n * the same arrangement the indexing planner makes against `gsc.ts`/`indexnow.ts`.\n *\n * Idempotency lives here too: a step whose desired state already holds resolves to\n * `skip`, so a re-run of a fully-onboarded domain is all skips — a no-op, by\n * construction rather than by the executors checking twice.\n */\nimport type { DomainSetup, DomainState, OnboardingConfig } from './types.ts';\nimport { table } from '../plan-format.ts';\n\n/** A step's decision: do it, or skip it because it already holds. */\nexport type StepAction = 'create' | 'update' | 'skip';\n\nexport interface StepPlan {\n action: StepAction;\n /** Why it's a skip, or what the mutation will do. */\n detail: string;\n}\n\n/** The four onboarding steps for one domain, decided against its live state. */\nexport interface DomainPlan {\n domain: string;\n /** `sc-domain:` property identifier this domain maps to. */\n siteUrl: string;\n /** Ensure the apex TXT verification record. */\n txt: StepPlan;\n /** Verify ownership via the Site Verification API (DNS_TXT). */\n verify: StepPlan;\n /** Add the Search Console domain property. */\n property: StepPlan;\n /** Add delegated owners to the verified web resource. */\n owners: StepPlan;\n /** Owners not yet present that the run should add. */\n ownersToAdd: string[];\n /** Manual follow-ups (e.g. a Full-User group). */\n reminders: string[];\n}\n\nexport interface OnboardingPlan {\n domains: DomainPlan[];\n /** Operator-facing warnings that span the whole run. */\n notes: string[];\n}\n\n/** The `sc-domain:` property identifier for a bare domain. */\nexport const siteUrlFor = (domain: string): string => `sc-domain:${domain}`;\n\n/**\n * Backoff ladder for the verification retry, in milliseconds.\n *\n * DNS_TXT verification fails until the record has propagated, which is a matter of\n * seconds-to-minutes, not a real error — so the schedule is a *policy*, decided\n * here and asserted in the tests, while the actual sleeping is the CLI's job.\n * Capped at `maxAttempts`; a domain still unverified after the last attempt is\n * reported PENDING, never failed.\n */\nexport function backoffSchedule(maxAttempts = 5): number[] {\n const out: number[] = [];\n for (let i = 1; i <= maxAttempts; i++) out.push(2 ** i * 1000);\n return out;\n}\n\n/** Decide the four steps for one domain from its desired config and live state. */\nexport function planDomain(setup: DomainSetup, state: DomainState): DomainPlan {\n const reminders: string[] = [];\n if (setup.fullUserGroup)\n reminders.push(\n `add ${setup.fullUserGroup} as a Full User in Search Console — no API exists for this, do it in the UI`,\n );\n\n const txt: StepPlan = state.txtPresent\n ? { action: 'skip', detail: 'apex TXT already present' }\n : { action: 'create', detail: 'create apex TXT verification record' };\n\n const verify: StepPlan = state.verified\n ? { action: 'skip', detail: 'ownership already verified' }\n : { action: 'create', detail: 'verify ownership (DNS_TXT)' };\n\n const property: StepPlan = state.propertyExists\n ? { action: 'skip', detail: 'property already exists' }\n : { action: 'create', detail: `add property ${siteUrlFor(setup.domain)}` };\n\n // Owners can only be read once verified; before that, treat every delegated\n // owner as still-to-add so --check reports the drift rather than hiding it.\n const desired = setup.delegatedOwners ?? [];\n const present = new Set(state.currentOwners.map((o) => o.toLowerCase()));\n const ownersToAdd = desired.filter((o) => !present.has(o.toLowerCase()));\n const owners: StepPlan =\n desired.length === 0\n ? { action: 'skip', detail: 'no delegated owners' }\n : ownersToAdd.length === 0\n ? { action: 'skip', detail: 'all delegated owners already present' }\n : {\n action: 'update',\n detail: `add ${ownersToAdd.length} owner(s): ${ownersToAdd.join(', ')}`,\n };\n\n return {\n domain: setup.domain,\n siteUrl: siteUrlFor(setup.domain),\n txt,\n verify,\n property,\n owners,\n ownersToAdd,\n reminders,\n };\n}\n\n/**\n * The owner set to PUT: everyone already there, plus everyone being added.\n *\n * `updateOwners` REPLACES the owner list, so this is the one computation standing\n * between a delegation and locking yourself out — pass a bare `ownersToAdd` and\n * the account that just verified the domain is removed from it. It belongs here\n * rather than at the call site for exactly that reason: it is a decision, it is\n * the dangerous one, and the executor is not allowed to make it.\n *\n * `current` comes FIRST so the API's own casing survives dedupe; matching is\n * case-insensitive, mirroring `planDomain`'s `present` check, because Google\n * echoes back addresses in whatever case the account uses and a case-sensitive\n * `Set` would re-add an owner who is already there.\n */\nexport function ownerUnion(current: string[], toAdd: string[]): string[] {\n const out: string[] = [];\n const seen = new Set<string>();\n for (const o of [...current, ...toAdd]) {\n const key = o.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(o);\n }\n return out;\n}\n\n/** Plan every domain. */\nexport function plan(config: OnboardingConfig, states: Map<string, DomainState>): OnboardingPlan {\n const notes: string[] = [];\n const domains = config.domains.map((setup) => {\n const state = states.get(setup.domain);\n if (!state)\n // Should not happen — the CLI observes every domain before planning — but a\n // missing observation must not silently read as \"nothing to do\".\n notes.push(`${setup.domain}: no observed state; treating everything as pending`);\n return planDomain(\n setup,\n state ?? { txtPresent: false, verified: false, currentOwners: [], propertyExists: false },\n );\n });\n return { domains, notes };\n}\n\n/** Any step on any domain is a mutation — i.e. the live state has drifted. */\nexport function hasDrift(p: OnboardingPlan): boolean {\n return p.domains.some(\n (d) =>\n d.txt.action !== 'skip' ||\n d.verify.action !== 'skip' ||\n d.property.action !== 'skip' ||\n d.owners.action !== 'skip',\n );\n}\n\n/** Render a plan as the block `--dry` and `--check` print. */\nexport function formatPlan(p: OnboardingPlan): string {\n const lines: string[] = [];\n const glyph = (s: StepPlan) => (s.action === 'skip' ? '·' : '+');\n for (const d of p.domains) {\n lines.push(d.domain);\n lines.push(` ${glyph(d.txt)} TXT ${d.txt.detail}`);\n lines.push(` ${glyph(d.verify)} verify ${d.verify.detail}`);\n lines.push(` ${glyph(d.property)} property ${d.property.detail}`);\n lines.push(` ${glyph(d.owners)} owners ${d.owners.detail}`);\n for (const r of d.reminders) lines.push(` ! ${r}`);\n }\n for (const n of p.notes) lines.push(`! ${n}`);\n return lines.join('\\n');\n}\n\n/** The outcome of one domain's run, for the summary table. */\nexport interface DomainResult {\n domain: string;\n txt: 'created' | 'present' | 'failed' | '—';\n verified: 'yes' | 'pending' | 'failed' | '—';\n property: 'added' | 'present' | 'failed' | '—';\n reminders: string[];\n}\n\n/** Render the end-of-run summary: one row per domain, plus any reminders. */\nexport function formatSummary(results: DomainResult[]): string {\n const headers = ['DOMAIN', 'TXT', 'VERIFIED', 'PROPERTY'];\n const rows = results.map((r) => [r.domain, r.txt, r.verified, r.property]);\n const reminders = results.flatMap((r) => r.reminders.map((m) => ` ! ${r.domain}: ${m}`));\n return table(headers, rows, reminders);\n}\n","/**\n * Google — the two APIs onboarding drives, and the user-OAuth token behind both.\n *\n * Auth here is a **user OAuth refresh token**, not a service account: verifying a\n * site and adding a Search Console property act *as a person* who can own them, not\n * as a project robot. That makes the token exchange simpler than the indexing\n * module's — a `refresh_token` grant, no JWT and no `node:crypto` — but it needs\n * broader scope, granted when the refresh token is minted:\n * - `https://www.googleapis.com/auth/siteverification` (Site Verification API)\n * - `https://www.googleapis.com/auth/webmasters` (Search Console API)\n *\n * The exchange itself lives in `../google/token.ts`, shared with `indexing/gsc.ts`\n * — the grants differ, everything after building the form body did not. It throws\n * (nothing downstream can run without a token) while every API call below returns\n * a structured result and lets the caller decide per domain. `googleapis` is\n * deliberately not a dependency — these are a handful of REST endpoints in a CLI\n * that installs into every consumer.\n */\nimport {\n encodeSite,\n errorDetail,\n googleAccessToken,\n googleHeaders,\n type GoogleAuthLike,\n type GoogleOAuth,\n} from '../google/token.ts';\n\nconst SITEVERIFICATION = 'https://www.googleapis.com/siteVerification/v1';\nconst WEBMASTERS = 'https://www.googleapis.com/webmasters/v3';\n\n/** Exchange a user OAuth refresh token for a short-lived access token. */\nexport function refreshTokenGrant(\n oauth: GoogleOAuth,\n fetchImpl: typeof fetch = fetch,\n): Promise<string> {\n return googleAccessToken({ kind: 'oauth', oauth }, fetchImpl);\n}\n\nconst inetSite = (domain: string) => ({ type: 'INET_DOMAIN', identifier: domain });\n\n/**\n * Ask the Site Verification API for the DNS_TXT token for a domain.\n *\n * Returns the full `google-site-verification=…` value that goes in the apex TXT\n * record. It is deterministic per (user, domain), so re-fetching it and comparing\n * against DNS is what makes the TXT step idempotent.\n */\nexport async function getVerificationToken(\n auth: GoogleAuthLike,\n domain: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; token?: string; message?: string }> {\n const res = await fetchImpl(`${SITEVERIFICATION}/token`, {\n method: 'POST',\n headers: googleHeaders(auth, { 'content-type': 'application/json' }),\n body: JSON.stringify({ verificationMethod: 'DNS_TXT', site: inetSite(domain) }),\n });\n if (!res.ok) return { ok: false, status: res.status, message: await res.text() };\n const body = (await res.json()) as { token?: string };\n if (!body.token) return { ok: false, status: res.status, message: 'no token in response' };\n return { ok: true, status: res.status, token: body.token };\n}\n\n/**\n * Look up the verified web resource for a domain, if any.\n *\n * Uses `webResource.list` rather than a `get` by id, because the id format for an\n * INET_DOMAIN resource is awkward to construct by hand and the list only ever\n * contains resources the user already owns — so a hit *is* the verified state, and\n * carries the id and owners the later steps need.\n */\nexport async function getWebResource(\n auth: GoogleAuthLike,\n domain: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{\n ok: boolean;\n status: number;\n exists: boolean;\n id?: string;\n owners: string[];\n message?: string;\n}> {\n const res = await fetchImpl(`${SITEVERIFICATION}/webResource`, {\n headers: googleHeaders(auth),\n });\n if (!res.ok)\n return { ok: false, status: res.status, exists: false, owners: [], message: await res.text() };\n const body = (await res.json()) as {\n items?: { id?: string; site?: { type?: string; identifier?: string }; owners?: string[] }[];\n };\n const match = body.items?.find(\n (it) => it.site?.type === 'INET_DOMAIN' && it.site?.identifier === domain,\n );\n if (!match) return { ok: true, status: res.status, exists: false, owners: [] };\n return {\n ok: true,\n status: res.status,\n exists: true,\n ...(match.id ? { id: match.id } : {}),\n owners: match.owners ?? [],\n };\n}\n\n/** Verify ownership (DNS_TXT). Succeeds only once the TXT record has propagated. */\nexport async function verifyWebResource(\n auth: GoogleAuthLike,\n domain: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; id?: string; owners: string[]; message?: string }> {\n const res = await fetchImpl(`${SITEVERIFICATION}/webResource?verificationMethod=DNS_TXT`, {\n method: 'POST',\n headers: googleHeaders(auth, { 'content-type': 'application/json' }),\n body: JSON.stringify({ site: inetSite(domain), verificationMethod: 'DNS_TXT' }),\n });\n if (!res.ok) return { ok: false, status: res.status, owners: [], message: await res.text() };\n const body = (await res.json()) as { id?: string; owners?: string[] };\n return {\n ok: true,\n status: res.status,\n ...(body.id ? { id: body.id } : {}),\n owners: body.owners ?? [],\n };\n}\n\n/**\n * Set the owners of a verified web resource to a given list.\n *\n * The API replaces the owner set, so the caller passes the **union** of the\n * current owners and the ones to add — never a bare list of new owners, which\n * would drop everyone already there.\n */\nexport async function updateOwners(\n auth: GoogleAuthLike,\n id: string,\n domain: string,\n owners: string[],\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; message?: string }> {\n const res = await fetchImpl(`${SITEVERIFICATION}/webResource/${encodeURIComponent(id)}`, {\n method: 'PUT',\n headers: googleHeaders(auth, { 'content-type': 'application/json' }),\n body: JSON.stringify({ id, site: inetSite(domain), owners }),\n });\n if (!res.ok) return { ok: false, status: res.status, message: await res.text() };\n return { ok: true, status: res.status };\n}\n\n/** Whether a Search Console property already exists (`GET sites/{siteUrl}`). */\nexport async function getSite(\n auth: GoogleAuthLike,\n siteUrl: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; exists: boolean; message?: string }> {\n const res = await fetchImpl(`${WEBMASTERS}/sites/${encodeSite(siteUrl)}`, {\n headers: googleHeaders(auth),\n });\n if (res.ok) return { ok: true, status: res.status, exists: true };\n if (res.status === 404) return { ok: true, status: res.status, exists: false };\n return { ok: false, status: res.status, exists: false, message: await res.text() };\n}\n\n/**\n * Add a Search Console property (`PUT sites/{siteUrl}`).\n *\n * A `403` here almost always means the domain is not yet verified for this user\n * rather than a transient error — property creation is gated on ownership.\n */\nexport async function addSite(\n auth: GoogleAuthLike,\n siteUrl: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; message?: string }> {\n const res = await fetchImpl(`${WEBMASTERS}/sites/${encodeSite(siteUrl)}`, {\n method: 'PUT',\n headers: googleHeaders(auth),\n });\n if (res.ok) return { ok: true, status: res.status };\n const message = await errorDetail(res, (status) =>\n status === 403\n ? `403 — \"${siteUrl}\" is not verified for this account yet, or the OAuth token lacks the webmasters scope`\n : undefined,\n );\n return { ok: false, status: res.status, message };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/onboarding/google.ts"],"sourcesContent":["/**\n * Google — the two APIs onboarding drives, and the user-OAuth token behind both.\n *\n * Auth here is a **user OAuth refresh token**, not a service account: verifying a\n * site and adding a Search Console property act *as a person* who can own them, not\n * as a project robot. That makes the token exchange simpler than the indexing\n * module's — a `refresh_token` grant, no JWT and no `node:crypto` — but it needs\n * broader scope, granted when the refresh token is minted:\n * - `https://www.googleapis.com/auth/siteverification` (Site Verification API)\n * - `https://www.googleapis.com/auth/webmasters` (Search Console API)\n *\n * The exchange itself lives in `../google/token.ts`, shared with `indexing/gsc.ts`\n * — the grants differ, everything after building the form body did not. It throws\n * (nothing downstream can run without a token) while every API call below returns\n * a structured result and lets the caller decide per domain. `googleapis` is\n * deliberately not a dependency — these are a handful of REST endpoints in a CLI\n * that installs into every consumer.\n */\nimport {\n encodeSite,\n errorDetail,\n googleAccessToken,\n googleHeaders,\n type GoogleAuthLike,\n type GoogleOAuth,\n} from '../google/token.ts';\n\nconst SITEVERIFICATION = 'https://www.googleapis.com/siteVerification/v1';\nconst WEBMASTERS = 'https://www.googleapis.com/webmasters/v3';\n\n/** Exchange a user OAuth refresh token for a short-lived access token. */\nexport function refreshTokenGrant(\n oauth: GoogleOAuth,\n fetchImpl: typeof fetch = fetch,\n): Promise<string> {\n return googleAccessToken({ kind: 'oauth', oauth }, fetchImpl);\n}\n\nconst inetSite = (domain: string) => ({ type: 'INET_DOMAIN', identifier: domain });\n\n/**\n * Ask the Site Verification API for the DNS_TXT token for a domain.\n *\n * Returns the full `google-site-verification=…` value that goes in the apex TXT\n * record. It is deterministic per (user, domain), so re-fetching it and comparing\n * against DNS is what makes the TXT step idempotent.\n */\nexport async function getVerificationToken(\n auth: GoogleAuthLike,\n domain: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; token?: string; message?: string }> {\n const res = await fetchImpl(`${SITEVERIFICATION}/token`, {\n method: 'POST',\n headers: googleHeaders(auth, { 'content-type': 'application/json' }),\n body: JSON.stringify({ verificationMethod: 'DNS_TXT', site: inetSite(domain) }),\n });\n if (!res.ok) return { ok: false, status: res.status, message: await res.text() };\n const body = (await res.json()) as { token?: string };\n if (!body.token) return { ok: false, status: res.status, message: 'no token in response' };\n return { ok: true, status: res.status, token: body.token };\n}\n\n/**\n * Look up the verified web resource for a domain, if any.\n *\n * Uses `webResource.list` rather than a `get` by id, because the id format for an\n * INET_DOMAIN resource is awkward to construct by hand and the list only ever\n * contains resources the user already owns — so a hit *is* the verified state, and\n * carries the id and owners the later steps need.\n */\nexport async function getWebResource(\n auth: GoogleAuthLike,\n domain: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{\n ok: boolean;\n status: number;\n exists: boolean;\n id?: string;\n owners: string[];\n message?: string;\n}> {\n const res = await fetchImpl(`${SITEVERIFICATION}/webResource`, {\n headers: googleHeaders(auth),\n });\n if (!res.ok)\n return { ok: false, status: res.status, exists: false, owners: [], message: await res.text() };\n const body = (await res.json()) as {\n items?: { id?: string; site?: { type?: string; identifier?: string }; owners?: string[] }[];\n };\n const match = body.items?.find(\n (it) => it.site?.type === 'INET_DOMAIN' && it.site?.identifier === domain,\n );\n if (!match) return { ok: true, status: res.status, exists: false, owners: [] };\n return {\n ok: true,\n status: res.status,\n exists: true,\n ...(match.id ? { id: match.id } : {}),\n owners: match.owners ?? [],\n };\n}\n\n/** Verify ownership (DNS_TXT). Succeeds only once the TXT record has propagated. */\nexport async function verifyWebResource(\n auth: GoogleAuthLike,\n domain: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; id?: string; owners: string[]; message?: string }> {\n const res = await fetchImpl(`${SITEVERIFICATION}/webResource?verificationMethod=DNS_TXT`, {\n method: 'POST',\n headers: googleHeaders(auth, { 'content-type': 'application/json' }),\n body: JSON.stringify({ site: inetSite(domain), verificationMethod: 'DNS_TXT' }),\n });\n if (!res.ok) return { ok: false, status: res.status, owners: [], message: await res.text() };\n const body = (await res.json()) as { id?: string; owners?: string[] };\n return {\n ok: true,\n status: res.status,\n ...(body.id ? { id: body.id } : {}),\n owners: body.owners ?? [],\n };\n}\n\n/**\n * Set the owners of a verified web resource to a given list.\n *\n * The API replaces the owner set, so the caller passes the **union** of the\n * current owners and the ones to add — never a bare list of new owners, which\n * would drop everyone already there.\n */\nexport async function updateOwners(\n auth: GoogleAuthLike,\n id: string,\n domain: string,\n owners: string[],\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; message?: string }> {\n const res = await fetchImpl(`${SITEVERIFICATION}/webResource/${encodeURIComponent(id)}`, {\n method: 'PUT',\n headers: googleHeaders(auth, { 'content-type': 'application/json' }),\n body: JSON.stringify({ id, site: inetSite(domain), owners }),\n });\n if (!res.ok) return { ok: false, status: res.status, message: await res.text() };\n return { ok: true, status: res.status };\n}\n\n/** Whether a Search Console property already exists (`GET sites/{siteUrl}`). */\nexport async function getSite(\n auth: GoogleAuthLike,\n siteUrl: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; exists: boolean; message?: string }> {\n const res = await fetchImpl(`${WEBMASTERS}/sites/${encodeSite(siteUrl)}`, {\n headers: googleHeaders(auth),\n });\n if (res.ok) return { ok: true, status: res.status, exists: true };\n if (res.status === 404) return { ok: true, status: res.status, exists: false };\n return { ok: false, status: res.status, exists: false, message: await res.text() };\n}\n\n/**\n * Add a Search Console property (`PUT sites/{siteUrl}`).\n *\n * A `403` here almost always means the domain is not yet verified for this user\n * rather than a transient error — property creation is gated on ownership.\n */\nexport async function addSite(\n auth: GoogleAuthLike,\n siteUrl: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; message?: string }> {\n const res = await fetchImpl(`${WEBMASTERS}/sites/${encodeSite(siteUrl)}`, {\n method: 'PUT',\n headers: googleHeaders(auth),\n });\n if (res.ok) return { ok: true, status: res.status };\n const message = await errorDetail(res, (status) =>\n status === 403\n ? `403 — \"${siteUrl}\" is not verified for this account yet, or the OAuth token lacks the webmasters scope`\n : undefined,\n );\n return { ok: false, status: res.status, message };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAM,mBAAmB;AACzB,MAAM,aAAa;;AAGnB,SAAgB,kBACd,OACA,YAA0B,OACT;CACjB,OAAO,kBAAkB;EAAE,MAAM;EAAS;CAAM,GAAG,SAAS;AAC9D;AAEA,MAAM,YAAY,YAAoB;CAAE,MAAM;CAAe,YAAY;AAAO;;;;;;;;AAShF,eAAsB,qBACpB,MACA,QACA,YAA0B,OACkD;CAC5E,MAAM,MAAM,MAAM,UAAU,GAAG,iBAAiB,SAAS;EACvD,QAAQ;EACR,SAAS,cAAc,MAAM,EAAE,gBAAgB,mBAAmB,CAAC;EACnE,MAAM,KAAK,UAAU;GAAE,oBAAoB;GAAW,MAAM,SAAS,MAAM;EAAE,CAAC;CAChF,CAAC;CACD,IAAI,CAAC,IAAI,IAAI,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ,SAAS,MAAM,IAAI,KAAK;CAAE;CAC/E,MAAM,OAAQ,MAAM,IAAI,KAAK;CAC7B,IAAI,CAAC,KAAK,OAAO,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ,SAAS;CAAuB;CACzF,OAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;EAAQ,OAAO,KAAK;CAAM;AAC3D;;;;;;;;;AAUA,eAAsB,eACpB,MACA,QACA,YAA0B,OAQzB;CACD,MAAM,MAAM,MAAM,UAAU,GAAG,iBAAiB,eAAe,EAC7D,SAAS,cAAc,IAAI,EAC7B,CAAC;CACD,IAAI,CAAC,IAAI,IACP,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ,QAAQ;EAAO,QAAQ,CAAC;EAAG,SAAS,MAAM,IAAI,KAAK;CAAE;CAI/F,MAAM,SAAQ,MAHM,IAAI,KAAK,EAGX,CAAC,OAAO,MACvB,OAAO,GAAG,MAAM,SAAS,iBAAiB,GAAG,MAAM,eAAe,MACrE;CACA,IAAI,CAAC,OAAO,OAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;EAAQ,QAAQ;EAAO,QAAQ,CAAC;CAAE;CAC7E,OAAO;EACL,IAAI;EACJ,QAAQ,IAAI;EACZ,QAAQ;EACR,GAAI,MAAM,KAAK,EAAE,IAAI,MAAM,GAAG,IAAI,CAAC;EACnC,QAAQ,MAAM,UAAU,CAAC;CAC3B;AACF;;AAGA,eAAsB,kBACpB,MACA,QACA,YAA0B,OACiE;CAC3F,MAAM,MAAM,MAAM,UAAU,GAAG,iBAAiB,0CAA0C;EACxF,QAAQ;EACR,SAAS,cAAc,MAAM,EAAE,gBAAgB,mBAAmB,CAAC;EACnE,MAAM,KAAK,UAAU;GAAE,MAAM,SAAS,MAAM;GAAG,oBAAoB;EAAU,CAAC;CAChF,CAAC;CACD,IAAI,CAAC,IAAI,IAAI,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ,QAAQ,CAAC;EAAG,SAAS,MAAM,IAAI,KAAK;CAAE;CAC3F,MAAM,OAAQ,MAAM,IAAI,KAAK;CAC7B,OAAO;EACL,IAAI;EACJ,QAAQ,IAAI;EACZ,GAAI,KAAK,KAAK,EAAE,IAAI,KAAK,GAAG,IAAI,CAAC;EACjC,QAAQ,KAAK,UAAU,CAAC;CAC1B;AACF;;;;;;;;AASA,eAAsB,aACpB,MACA,IACA,QACA,QACA,YAA0B,OACkC;CAC5D,MAAM,MAAM,MAAM,UAAU,GAAG,iBAAiB,eAAe,mBAAmB,EAAE,KAAK;EACvF,QAAQ;EACR,SAAS,cAAc,MAAM,EAAE,gBAAgB,mBAAmB,CAAC;EACnE,MAAM,KAAK,UAAU;GAAE;GAAI,MAAM,SAAS,MAAM;GAAG;EAAO,CAAC;CAC7D,CAAC;CACD,IAAI,CAAC,IAAI,IAAI,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ,SAAS,MAAM,IAAI,KAAK;CAAE;CAC/E,OAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;CAAO;AACxC;;AAGA,eAAsB,QACpB,MACA,SACA,YAA0B,OACmD;CAC7E,MAAM,MAAM,MAAM,UAAU,GAAG,WAAW,SAAS,WAAW,OAAO,KAAK,EACxE,SAAS,cAAc,IAAI,EAC7B,CAAC;CACD,IAAI,IAAI,IAAI,OAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;EAAQ,QAAQ;CAAK;CAChE,IAAI,IAAI,WAAW,KAAK,OAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;EAAQ,QAAQ;CAAM;CAC7E,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ,QAAQ;EAAO,SAAS,MAAM,IAAI,KAAK;CAAE;AACnF;;;;;;;AAQA,eAAsB,QACpB,MACA,SACA,YAA0B,OACkC;CAC5D,MAAM,MAAM,MAAM,UAAU,GAAG,WAAW,SAAS,WAAW,OAAO,KAAK;EACxE,QAAQ;EACR,SAAS,cAAc,IAAI;CAC7B,CAAC;CACD,IAAI,IAAI,IAAI,OAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;CAAO;CAClD,MAAM,UAAU,MAAM,YAAY,MAAM,WACtC,WAAW,MACP,UAAU,QAAQ,yFAClB,MACN;CACA,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ;CAAQ;AAClD"}
@@ -0,0 +1,150 @@
1
+ import { t as table } from "./plan-format-A18SvJ7C.mjs";
2
+
3
+ //#region src/onboarding/plan.ts
4
+ /** The `sc-domain:` property identifier for a bare domain. */
5
+ const siteUrlFor = (domain) => `sc-domain:${domain}`;
6
+ /**
7
+ * Backoff ladder for the verification retry, in milliseconds.
8
+ *
9
+ * DNS_TXT verification fails until the record has propagated, which is a matter of
10
+ * seconds-to-minutes, not a real error — so the schedule is a *policy*, decided
11
+ * here and asserted in the tests, while the actual sleeping is the CLI's job.
12
+ * Capped at `maxAttempts`; a domain still unverified after the last attempt is
13
+ * reported PENDING, never failed.
14
+ */
15
+ function backoffSchedule(maxAttempts = 5) {
16
+ const out = [];
17
+ for (let i = 1; i <= maxAttempts; i++) out.push(2 ** i * 1e3);
18
+ return out;
19
+ }
20
+ /** Decide the four steps for one domain from its desired config and live state. */
21
+ function planDomain(setup, state) {
22
+ const reminders = [];
23
+ if (setup.fullUserGroup) reminders.push(`add ${setup.fullUserGroup} as a Full User in Search Console — no API exists for this, do it in the UI`);
24
+ const txt = state.txtPresent ? {
25
+ action: "skip",
26
+ detail: "apex TXT already present"
27
+ } : {
28
+ action: "create",
29
+ detail: "create apex TXT verification record"
30
+ };
31
+ const verify = state.verified ? {
32
+ action: "skip",
33
+ detail: "ownership already verified"
34
+ } : {
35
+ action: "create",
36
+ detail: "verify ownership (DNS_TXT)"
37
+ };
38
+ const property = state.propertyExists ? {
39
+ action: "skip",
40
+ detail: "property already exists"
41
+ } : {
42
+ action: "create",
43
+ detail: `add property ${siteUrlFor(setup.domain)}`
44
+ };
45
+ const desired = setup.delegatedOwners ?? [];
46
+ const present = new Set(state.currentOwners.map((o) => o.toLowerCase()));
47
+ const ownersToAdd = desired.filter((o) => !present.has(o.toLowerCase()));
48
+ const owners = desired.length === 0 ? {
49
+ action: "skip",
50
+ detail: "no delegated owners"
51
+ } : ownersToAdd.length === 0 ? {
52
+ action: "skip",
53
+ detail: "all delegated owners already present"
54
+ } : {
55
+ action: "update",
56
+ detail: `add ${ownersToAdd.length} owner(s): ${ownersToAdd.join(", ")}`
57
+ };
58
+ return {
59
+ domain: setup.domain,
60
+ siteUrl: siteUrlFor(setup.domain),
61
+ txt,
62
+ verify,
63
+ property,
64
+ owners,
65
+ ownersToAdd,
66
+ reminders
67
+ };
68
+ }
69
+ /**
70
+ * The owner set to PUT: everyone already there, plus everyone being added.
71
+ *
72
+ * `updateOwners` REPLACES the owner list, so this is the one computation standing
73
+ * between a delegation and locking yourself out — pass a bare `ownersToAdd` and
74
+ * the account that just verified the domain is removed from it. It belongs here
75
+ * rather than at the call site for exactly that reason: it is a decision, it is
76
+ * the dangerous one, and the executor is not allowed to make it.
77
+ *
78
+ * `current` comes FIRST so the API's own casing survives dedupe; matching is
79
+ * case-insensitive, mirroring `planDomain`'s `present` check, because Google
80
+ * echoes back addresses in whatever case the account uses and a case-sensitive
81
+ * `Set` would re-add an owner who is already there.
82
+ */
83
+ function ownerUnion(current, toAdd) {
84
+ const out = [];
85
+ const seen = /* @__PURE__ */ new Set();
86
+ for (const o of [...current, ...toAdd]) {
87
+ const key = o.toLowerCase();
88
+ if (seen.has(key)) continue;
89
+ seen.add(key);
90
+ out.push(o);
91
+ }
92
+ return out;
93
+ }
94
+ /** Plan every domain. */
95
+ function plan(config, states) {
96
+ const notes = [];
97
+ return {
98
+ domains: config.domains.map((setup) => {
99
+ const state = states.get(setup.domain);
100
+ if (!state) notes.push(`${setup.domain}: no observed state; treating everything as pending`);
101
+ return planDomain(setup, state ?? {
102
+ txtPresent: false,
103
+ verified: false,
104
+ currentOwners: [],
105
+ propertyExists: false
106
+ });
107
+ }),
108
+ notes
109
+ };
110
+ }
111
+ /** Any step on any domain is a mutation — i.e. the live state has drifted. */
112
+ function hasDrift(p) {
113
+ return p.domains.some((d) => d.txt.action !== "skip" || d.verify.action !== "skip" || d.property.action !== "skip" || d.owners.action !== "skip");
114
+ }
115
+ /** Render a plan as the block `--dry` and `--check` print. */
116
+ function formatPlan(p) {
117
+ const lines = [];
118
+ const glyph = (s) => s.action === "skip" ? "·" : "+";
119
+ for (const d of p.domains) {
120
+ lines.push(d.domain);
121
+ lines.push(` ${glyph(d.txt)} TXT ${d.txt.detail}`);
122
+ lines.push(` ${glyph(d.verify)} verify ${d.verify.detail}`);
123
+ lines.push(` ${glyph(d.property)} property ${d.property.detail}`);
124
+ lines.push(` ${glyph(d.owners)} owners ${d.owners.detail}`);
125
+ for (const r of d.reminders) lines.push(` ! ${r}`);
126
+ }
127
+ for (const n of p.notes) lines.push(`! ${n}`);
128
+ return lines.join("\n");
129
+ }
130
+ /** Render the end-of-run summary: one row per domain, plus any reminders. */
131
+ function formatSummary(results) {
132
+ const headers = [
133
+ "DOMAIN",
134
+ "TXT",
135
+ "VERIFIED",
136
+ "PROPERTY"
137
+ ];
138
+ const rows = results.map((r) => [
139
+ r.domain,
140
+ r.txt,
141
+ r.verified,
142
+ r.property
143
+ ]);
144
+ const reminders = results.flatMap((r) => r.reminders.map((m) => ` ! ${r.domain}: ${m}`));
145
+ return table(headers, rows, reminders);
146
+ }
147
+
148
+ //#endregion
149
+ export { ownerUnion as a, siteUrlFor as c, hasDrift as i, formatPlan as n, plan as o, formatSummary as r, planDomain as s, backoffSchedule as t };
150
+ //# sourceMappingURL=plan-BLKikoOG.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-BLKikoOG.mjs","names":[],"sources":["../src/onboarding/plan.ts"],"sourcesContent":["/**\n * Every decision `vitops search setup` makes, as one pure function.\n *\n * Pure in the load-bearing sense: no `fetch`, no `fs`, no clock. The executors\n * beside this one gather each domain's live state and apply what they are told;\n * this module decides, from that state, what is left to do. That split is what\n * makes `--dry` a complete account of a run and `--check` an honest drift report —\n * the same arrangement the indexing planner makes against `gsc.ts`/`indexnow.ts`.\n *\n * Idempotency lives here too: a step whose desired state already holds resolves to\n * `skip`, so a re-run of a fully-onboarded domain is all skips — a no-op, by\n * construction rather than by the executors checking twice.\n */\nimport type { DomainSetup, DomainState, OnboardingConfig } from './types.ts';\nimport { table } from '../plan-format.ts';\n\n/** A step's decision: do it, or skip it because it already holds. */\nexport type StepAction = 'create' | 'update' | 'skip';\n\nexport interface StepPlan {\n action: StepAction;\n /** Why it's a skip, or what the mutation will do. */\n detail: string;\n}\n\n/** The four onboarding steps for one domain, decided against its live state. */\nexport interface DomainPlan {\n domain: string;\n /** `sc-domain:` property identifier this domain maps to. */\n siteUrl: string;\n /** Ensure the apex TXT verification record. */\n txt: StepPlan;\n /** Verify ownership via the Site Verification API (DNS_TXT). */\n verify: StepPlan;\n /** Add the Search Console domain property. */\n property: StepPlan;\n /** Add delegated owners to the verified web resource. */\n owners: StepPlan;\n /** Owners not yet present that the run should add. */\n ownersToAdd: string[];\n /** Manual follow-ups (e.g. a Full-User group). */\n reminders: string[];\n}\n\nexport interface OnboardingPlan {\n domains: DomainPlan[];\n /** Operator-facing warnings that span the whole run. */\n notes: string[];\n}\n\n/** The `sc-domain:` property identifier for a bare domain. */\nexport const siteUrlFor = (domain: string): string => `sc-domain:${domain}`;\n\n/**\n * Backoff ladder for the verification retry, in milliseconds.\n *\n * DNS_TXT verification fails until the record has propagated, which is a matter of\n * seconds-to-minutes, not a real error — so the schedule is a *policy*, decided\n * here and asserted in the tests, while the actual sleeping is the CLI's job.\n * Capped at `maxAttempts`; a domain still unverified after the last attempt is\n * reported PENDING, never failed.\n */\nexport function backoffSchedule(maxAttempts = 5): number[] {\n const out: number[] = [];\n for (let i = 1; i <= maxAttempts; i++) out.push(2 ** i * 1000);\n return out;\n}\n\n/** Decide the four steps for one domain from its desired config and live state. */\nexport function planDomain(setup: DomainSetup, state: DomainState): DomainPlan {\n const reminders: string[] = [];\n if (setup.fullUserGroup)\n reminders.push(\n `add ${setup.fullUserGroup} as a Full User in Search Console — no API exists for this, do it in the UI`,\n );\n\n const txt: StepPlan = state.txtPresent\n ? { action: 'skip', detail: 'apex TXT already present' }\n : { action: 'create', detail: 'create apex TXT verification record' };\n\n const verify: StepPlan = state.verified\n ? { action: 'skip', detail: 'ownership already verified' }\n : { action: 'create', detail: 'verify ownership (DNS_TXT)' };\n\n const property: StepPlan = state.propertyExists\n ? { action: 'skip', detail: 'property already exists' }\n : { action: 'create', detail: `add property ${siteUrlFor(setup.domain)}` };\n\n // Owners can only be read once verified; before that, treat every delegated\n // owner as still-to-add so --check reports the drift rather than hiding it.\n const desired = setup.delegatedOwners ?? [];\n const present = new Set(state.currentOwners.map((o) => o.toLowerCase()));\n const ownersToAdd = desired.filter((o) => !present.has(o.toLowerCase()));\n const owners: StepPlan =\n desired.length === 0\n ? { action: 'skip', detail: 'no delegated owners' }\n : ownersToAdd.length === 0\n ? { action: 'skip', detail: 'all delegated owners already present' }\n : {\n action: 'update',\n detail: `add ${ownersToAdd.length} owner(s): ${ownersToAdd.join(', ')}`,\n };\n\n return {\n domain: setup.domain,\n siteUrl: siteUrlFor(setup.domain),\n txt,\n verify,\n property,\n owners,\n ownersToAdd,\n reminders,\n };\n}\n\n/**\n * The owner set to PUT: everyone already there, plus everyone being added.\n *\n * `updateOwners` REPLACES the owner list, so this is the one computation standing\n * between a delegation and locking yourself out — pass a bare `ownersToAdd` and\n * the account that just verified the domain is removed from it. It belongs here\n * rather than at the call site for exactly that reason: it is a decision, it is\n * the dangerous one, and the executor is not allowed to make it.\n *\n * `current` comes FIRST so the API's own casing survives dedupe; matching is\n * case-insensitive, mirroring `planDomain`'s `present` check, because Google\n * echoes back addresses in whatever case the account uses and a case-sensitive\n * `Set` would re-add an owner who is already there.\n */\nexport function ownerUnion(current: string[], toAdd: string[]): string[] {\n const out: string[] = [];\n const seen = new Set<string>();\n for (const o of [...current, ...toAdd]) {\n const key = o.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(o);\n }\n return out;\n}\n\n/** Plan every domain. */\nexport function plan(config: OnboardingConfig, states: Map<string, DomainState>): OnboardingPlan {\n const notes: string[] = [];\n const domains = config.domains.map((setup) => {\n const state = states.get(setup.domain);\n if (!state)\n // Should not happen — the CLI observes every domain before planning — but a\n // missing observation must not silently read as \"nothing to do\".\n notes.push(`${setup.domain}: no observed state; treating everything as pending`);\n return planDomain(\n setup,\n state ?? { txtPresent: false, verified: false, currentOwners: [], propertyExists: false },\n );\n });\n return { domains, notes };\n}\n\n/** Any step on any domain is a mutation — i.e. the live state has drifted. */\nexport function hasDrift(p: OnboardingPlan): boolean {\n return p.domains.some(\n (d) =>\n d.txt.action !== 'skip' ||\n d.verify.action !== 'skip' ||\n d.property.action !== 'skip' ||\n d.owners.action !== 'skip',\n );\n}\n\n/** Render a plan as the block `--dry` and `--check` print. */\nexport function formatPlan(p: OnboardingPlan): string {\n const lines: string[] = [];\n const glyph = (s: StepPlan) => (s.action === 'skip' ? '·' : '+');\n for (const d of p.domains) {\n lines.push(d.domain);\n lines.push(` ${glyph(d.txt)} TXT ${d.txt.detail}`);\n lines.push(` ${glyph(d.verify)} verify ${d.verify.detail}`);\n lines.push(` ${glyph(d.property)} property ${d.property.detail}`);\n lines.push(` ${glyph(d.owners)} owners ${d.owners.detail}`);\n for (const r of d.reminders) lines.push(` ! ${r}`);\n }\n for (const n of p.notes) lines.push(`! ${n}`);\n return lines.join('\\n');\n}\n\n/** The outcome of one domain's run, for the summary table. */\nexport interface DomainResult {\n domain: string;\n txt: 'created' | 'present' | 'failed' | '—';\n verified: 'yes' | 'pending' | 'failed' | '—';\n property: 'added' | 'present' | 'failed' | '—';\n reminders: string[];\n}\n\n/** Render the end-of-run summary: one row per domain, plus any reminders. */\nexport function formatSummary(results: DomainResult[]): string {\n const headers = ['DOMAIN', 'TXT', 'VERIFIED', 'PROPERTY'];\n const rows = results.map((r) => [r.domain, r.txt, r.verified, r.property]);\n const reminders = results.flatMap((r) => r.reminders.map((m) => ` ! ${r.domain}: ${m}`));\n return table(headers, rows, reminders);\n}\n"],"mappings":"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"}
@@ -0,0 +1,133 @@
1
+ //#region src/onboarding/types.d.ts
2
+ /**
3
+ * The onboarding module's own option surface.
4
+ *
5
+ * Deliberately structural rather than `Config['site']['searchConsole']` imported
6
+ * from `@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, so the CLI's adapter is a flat map — the same arrangement, and for the
9
+ * same reason, as `IndexingConfig` beside it.
10
+ *
11
+ * Optional fields are written `?: T | undefined` throughout: `exactOptionalPropertyTypes`
12
+ * is on, and these types describe a *parsed JSON config* where an absent key and an
13
+ * explicit `undefined` are the same thing.
14
+ */
15
+ /** One domain to onboard, as the config declares it. */
16
+ interface DomainSetup {
17
+ /** The domain, bare (`acme.ca`). Becomes the `sc-domain:` property and the DNS zone name. */
18
+ domain: string;
19
+ /**
20
+ * Emails added as verification owners on the Site Verification web resource.
21
+ * Additive: existing owners are never removed.
22
+ */
23
+ delegatedOwners?: string[] | undefined;
24
+ /**
25
+ * A Google Group to grant Full-User access in Search Console. Surfaced as a
26
+ * reminder only — Search Console has no user/permission API.
27
+ */
28
+ fullUserGroup?: string | undefined;
29
+ }
30
+ /** Everything `plan()` needs, once the CLI has read the config and creds. */
31
+ interface OnboardingConfig {
32
+ /** The domains to onboard. Already filtered to `--domain` if that was passed. */
33
+ domains: DomainSetup[];
34
+ }
35
+ /**
36
+ * One domain's observed live state, gathered by the executors before planning.
37
+ *
38
+ * `plan()` compares this against the desired `DomainSetup` to decide each step —
39
+ * the same shape the indexing planner takes its sitemap snapshot in, for the same
40
+ * reason: the decision is pure, the observation is not.
41
+ */
42
+ interface DomainState {
43
+ /** The Cloudflare zone id for the domain, if one was found. */
44
+ zoneId?: string | undefined;
45
+ /** An apex TXT record already carrying the verification token exists. */
46
+ txtPresent: boolean;
47
+ /** The Site Verification web resource reports this domain as verified. */
48
+ verified: boolean;
49
+ /** Owners currently on the web resource (empty when not yet verified). */
50
+ currentOwners: string[];
51
+ /** A `sc-domain:` property already exists in Search Console. */
52
+ propertyExists: boolean;
53
+ }
54
+ //#endregion
55
+ //#region src/onboarding/plan.d.ts
56
+ /** A step's decision: do it, or skip it because it already holds. */
57
+ type StepAction = 'create' | 'update' | 'skip';
58
+ interface StepPlan {
59
+ action: StepAction;
60
+ /** Why it's a skip, or what the mutation will do. */
61
+ detail: string;
62
+ }
63
+ /** The four onboarding steps for one domain, decided against its live state. */
64
+ interface DomainPlan {
65
+ domain: string;
66
+ /** `sc-domain:` property identifier this domain maps to. */
67
+ siteUrl: string;
68
+ /** Ensure the apex TXT verification record. */
69
+ txt: StepPlan;
70
+ /** Verify ownership via the Site Verification API (DNS_TXT). */
71
+ verify: StepPlan;
72
+ /** Add the Search Console domain property. */
73
+ property: StepPlan;
74
+ /** Add delegated owners to the verified web resource. */
75
+ owners: StepPlan;
76
+ /** Owners not yet present that the run should add. */
77
+ ownersToAdd: string[];
78
+ /** Manual follow-ups (e.g. a Full-User group). */
79
+ reminders: string[];
80
+ }
81
+ interface OnboardingPlan {
82
+ domains: DomainPlan[];
83
+ /** Operator-facing warnings that span the whole run. */
84
+ notes: string[];
85
+ }
86
+ /** The `sc-domain:` property identifier for a bare domain. */
87
+ declare const siteUrlFor: (domain: string) => string;
88
+ /**
89
+ * Backoff ladder for the verification retry, in milliseconds.
90
+ *
91
+ * DNS_TXT verification fails until the record has propagated, which is a matter of
92
+ * seconds-to-minutes, not a real error — so the schedule is a *policy*, decided
93
+ * here and asserted in the tests, while the actual sleeping is the CLI's job.
94
+ * Capped at `maxAttempts`; a domain still unverified after the last attempt is
95
+ * reported PENDING, never failed.
96
+ */
97
+ declare function backoffSchedule(maxAttempts?: number): number[];
98
+ /** Decide the four steps for one domain from its desired config and live state. */
99
+ declare function planDomain(setup: DomainSetup, state: DomainState): DomainPlan;
100
+ /**
101
+ * The owner set to PUT: everyone already there, plus everyone being added.
102
+ *
103
+ * `updateOwners` REPLACES the owner list, so this is the one computation standing
104
+ * between a delegation and locking yourself out — pass a bare `ownersToAdd` and
105
+ * the account that just verified the domain is removed from it. It belongs here
106
+ * rather than at the call site for exactly that reason: it is a decision, it is
107
+ * the dangerous one, and the executor is not allowed to make it.
108
+ *
109
+ * `current` comes FIRST so the API's own casing survives dedupe; matching is
110
+ * case-insensitive, mirroring `planDomain`'s `present` check, because Google
111
+ * echoes back addresses in whatever case the account uses and a case-sensitive
112
+ * `Set` would re-add an owner who is already there.
113
+ */
114
+ declare function ownerUnion(current: string[], toAdd: string[]): string[];
115
+ /** Plan every domain. */
116
+ declare function plan(config: OnboardingConfig, states: Map<string, DomainState>): OnboardingPlan;
117
+ /** Any step on any domain is a mutation — i.e. the live state has drifted. */
118
+ declare function hasDrift(p: OnboardingPlan): boolean;
119
+ /** Render a plan as the block `--dry` and `--check` print. */
120
+ declare function formatPlan(p: OnboardingPlan): string;
121
+ /** The outcome of one domain's run, for the summary table. */
122
+ interface DomainResult {
123
+ domain: string;
124
+ txt: 'created' | 'present' | 'failed' | '—';
125
+ verified: 'yes' | 'pending' | 'failed' | '—';
126
+ property: 'added' | 'present' | 'failed' | '—';
127
+ reminders: string[];
128
+ }
129
+ /** Render the end-of-run summary: one row per domain, plus any reminders. */
130
+ declare function formatSummary(results: DomainResult[]): string;
131
+ //#endregion
132
+ export { StepPlan as a, formatSummary as c, plan as d, planDomain as f, OnboardingConfig as g, DomainState as h, StepAction as i, hasDrift as l, DomainSetup as m, DomainResult as n, backoffSchedule as o, siteUrlFor as p, OnboardingPlan as r, formatPlan as s, DomainPlan as t, ownerUnion as u };
133
+ //# sourceMappingURL=plan-Hq8Pb0Uw.d.mts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,24 @@
1
+ //#region src/plan-format.ts
2
+ /**
3
+ * The end-of-run summary table shared by `onboarding/plan.ts`'s and `ads/plan.ts`'s
4
+ * `formatSummary` — same header row, same column padding, same "Reminders
5
+ * (manual — no API):" tail. Their `StepAction`/`StepPlan`/`formatPlan` glyph
6
+ * renderers stay separate: onboarding's steps are `create | update | skip`
7
+ * with no `blocked` state, ads' are `create | skip | blocked` with a `needs`
8
+ * field, and collapsing those two shapes into one would blur a real
9
+ * difference rather than remove duplication.
10
+ */
11
+ const pad = (s, n) => s.padEnd(n);
12
+ /** Render a fixed-width table, then a "Reminders" section if any were passed. */
13
+ function table(headers, rows, reminders) {
14
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)));
15
+ const line = (cells) => cells.map((c, i) => pad(c, widths[i] ?? 0)).join(" ").trimEnd();
16
+ const out = [line(headers), widths.map((w) => "─".repeat(w)).join(" ")];
17
+ for (const row of rows) out.push(line(row));
18
+ if (reminders.length) out.push("", "Reminders (manual — no API):", ...reminders);
19
+ return out.join("\n");
20
+ }
21
+
22
+ //#endregion
23
+ export { table as t };
24
+ //# sourceMappingURL=plan-format-A18SvJ7C.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-format-A18SvJ7C.mjs","names":[],"sources":["../src/plan-format.ts"],"sourcesContent":["/**\n * The end-of-run summary table shared by `onboarding/plan.ts`'s and `ads/plan.ts`'s\n * `formatSummary` — same header row, same column padding, same \"Reminders\n * (manual — no API):\" tail. Their `StepAction`/`StepPlan`/`formatPlan` glyph\n * renderers stay separate: onboarding's steps are `create | update | skip`\n * with no `blocked` state, ads' are `create | skip | blocked` with a `needs`\n * field, and collapsing those two shapes into one would blur a real\n * difference rather than remove duplication.\n */\nconst pad = (s: string, n: number) => s.padEnd(n);\n\n/** Render a fixed-width table, then a \"Reminders\" section if any were passed. */\nexport function table(headers: string[], rows: string[][], reminders: string[]): string {\n const widths = headers.map((h, i) =>\n Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)),\n );\n const line = (cells: string[]) =>\n cells\n .map((c, i) => pad(c, widths[i] ?? 0))\n .join(' ')\n .trimEnd();\n\n const out = [line(headers), widths.map((w) => '─'.repeat(w)).join(' ')];\n for (const row of rows) out.push(line(row));\n if (reminders.length) out.push('', 'Reminders (manual — no API):', ...reminders);\n return out.join('\\n');\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,OAAO,GAAW,MAAc,EAAE,OAAO,CAAC;;AAGhD,SAAgB,MAAM,SAAmB,MAAkB,WAA6B;CACtF,MAAM,SAAS,QAAQ,KAAK,GAAG,MAC7B,KAAK,IAAI,EAAE,QAAQ,GAAG,KAAK,KAAK,QAAQ,IAAI,EAAE,EAAE,UAAU,CAAC,CAAC,CAC9D;CACA,MAAM,QAAQ,UACZ,MACG,KAAK,GAAG,MAAM,IAAI,GAAG,OAAO,MAAM,CAAC,CAAC,CAAC,CACrC,KAAK,IAAI,CAAC,CACV,QAAQ;CAEb,MAAM,MAAM,CAAC,KAAK,OAAO,GAAG,OAAO,KAAK,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;CACvE,KAAK,MAAM,OAAO,MAAM,IAAI,KAAK,KAAK,GAAG,CAAC;CAC1C,IAAI,UAAU,QAAQ,IAAI,KAAK,IAAI,gCAAgC,GAAG,SAAS;CAC/E,OAAO,IAAI,KAAK,IAAI;AACtB"}
@@ -1,6 +1,5 @@
1
- import { a as SCOPES, c as googleHeaders, i as GoogleOAuth, n as GoogleAuthLike, r as GoogleCredential, s as googleAccessToken, t as GoogleAuth } from "../token-Bz_-igw-.mjs";
2
- import { n as adcQuotaProject, r as parseAdcUser, t as adcCredentialsPath } from "../adc-BFrzpMHq.mjs";
3
-
1
+ import { a as SCOPES, c as googleHeaders, i as GoogleOAuth, n as GoogleAuthLike, r as GoogleCredential, s as googleAccessToken, t as GoogleAuth } from "../token-DmmoQ7um.mjs";
2
+ import { n as adcQuotaProject, r as parseAdcUser, t as adcCredentialsPath } from "../adc-BoEoogyo.mjs";
4
3
  //#region src/reviews/types.d.ts
5
4
  /**
6
5
  * The reviews module's own option surface.
@@ -91,9 +90,9 @@ interface PlanInput {
91
90
  hasCredential: boolean;
92
91
  }
93
92
  /** Decide which locations to fetch, and why any are skipped. */
94
- declare function plan(input: PlanInput): ReviewsPlan;
93
+ export declare function plan(input: PlanInput): ReviewsPlan;
95
94
  /** Render a plan as the human-readable block `--dry` prints. */
96
- declare function formatPlan(p: ReviewsPlan): string;
95
+ export declare function formatPlan(p: ReviewsPlan): string;
97
96
  /**
98
97
  * Filter and order one location's reviews for display.
99
98
  *
@@ -103,7 +102,7 @@ declare function formatPlan(p: ReviewsPlan): string;
103
102
  * is a design choice a config field should not have to spell out every time),
104
103
  * sorted, then capped by `limit`.
105
104
  */
106
- declare function selectReviews(reviews: Review[], config: ReviewLocationConfig): Review[];
105
+ export declare function selectReviews(reviews: Review[], config: ReviewLocationConfig): Review[];
107
106
  /**
108
107
  * Merge a fresh fetch into the existing cache.
109
108
  *
@@ -112,11 +111,11 @@ declare function selectReviews(reviews: Review[], config: ReviewLocationConfig):
112
111
  * failure (one location's fetch errors) non-destructive — `cmdReviews` merges
113
112
  * only the locations that succeeded and leaves the rest of the cache alone.
114
113
  */
115
- declare function mergeCache(existing: ReviewCache | undefined, fetched: Record<string, LocationReviews>): ReviewCache;
114
+ export declare function mergeCache(existing: ReviewCache | undefined, fetched: Record<string, LocationReviews>): ReviewCache;
116
115
  //#endregion
117
116
  //#region src/reviews/cache.d.ts
118
117
  /** Default location, relative to the consumer's project root. */
119
- declare const REVIEWS_CACHE_PATH = ".vitops/reviews.json";
118
+ export declare const REVIEWS_CACHE_PATH = ".vitops/reviews.json";
120
119
  /**
121
120
  * Read the cache, or `undefined` when there isn't a usable one.
122
121
  *
@@ -125,11 +124,11 @@ declare const REVIEWS_CACHE_PATH = ".vitops/reviews.json";
125
124
  * from "this business genuinely has no reviews" and silently ship an empty
126
125
  * carousel.
127
126
  */
128
- declare function readCache(path: string): ReviewCache | undefined;
127
+ export declare function readCache(path: string): ReviewCache | undefined;
129
128
  /** Build a cache with its keys and each location's reviews in a stable order. */
130
- declare function toCache(locations: Record<string, LocationReviews>): ReviewCache;
129
+ export declare function toCache(locations: Record<string, LocationReviews>): ReviewCache;
131
130
  /** Write the cache, creating its directory. */
132
- declare function writeCache(path: string, cache: ReviewCache): void;
131
+ export declare function writeCache(path: string, cache: ReviewCache): void;
133
132
  //#endregion
134
133
  //#region src/reviews/gbp.d.ts
135
134
  /** One page (or, having paginated, the whole set) of a location's reviews. */
@@ -148,7 +147,7 @@ interface FetchReviewsResult {
148
147
  * several locations can fetch what it can and report the rest, the same contract
149
148
  * `../indexing/gsc.ts`'s `inspectUrl` keeps.
150
149
  */
151
- declare function fetchReviews(auth: GoogleAuthLike, accountId: string, locationId: string, fetchImpl?: typeof fetch): Promise<FetchReviewsResult>;
150
+ export declare function fetchReviews(auth: GoogleAuthLike, accountId: string, locationId: string, fetchImpl?: typeof fetch): Promise<FetchReviewsResult>;
152
151
  //#endregion
153
- export { type FetchReviewsResult, type GoogleAuth, type GoogleAuthLike, type GoogleCredential, type GoogleOAuth, type LocationPlan, type LocationReviews, type PlanInput, REVIEWS_CACHE_PATH, type Review, type ReviewCache, type ReviewLocationConfig, type ReviewsConfig, type ReviewsPlan, SCOPES, adcCredentialsPath, adcQuotaProject, fetchReviews, formatPlan, googleAccessToken, googleHeaders, mergeCache, parseAdcUser, plan, readCache, selectReviews, toCache, writeCache };
152
+ export { type FetchReviewsResult, type GoogleAuth, type GoogleAuthLike, type GoogleCredential, type GoogleOAuth, type LocationPlan, type LocationReviews, type PlanInput, type Review, type ReviewCache, type ReviewLocationConfig, type ReviewsConfig, type ReviewsPlan, SCOPES, adcCredentialsPath, adcQuotaProject, googleAccessToken, googleHeaders, parseAdcUser };
154
153
  //# sourceMappingURL=index.d.mts.map