@getvitops/utils 7.0.0 → 8.0.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 (57) hide show
  1. package/CHANGELOG.md +131 -13
  2. package/dist/adc-BFrzpMHq.d.mts +25 -0
  3. package/dist/adc-BFrzpMHq.d.mts.map +1 -0
  4. package/dist/adc-D6aw2qgq.mjs +45 -0
  5. package/dist/adc-D6aw2qgq.mjs.map +1 -0
  6. package/dist/ads/index.d.mts +0 -1
  7. package/dist/ads/index.d.mts.map +1 -1
  8. package/dist/ads/index.mjs +4 -13
  9. package/dist/ads/index.mjs.map +1 -1
  10. package/dist/{cloudflare-CD8vwrgj.mjs → cloudflare-CgAsXiqe.mjs} +24 -2
  11. package/dist/cloudflare-CgAsXiqe.mjs.map +1 -0
  12. package/dist/color/index.d.mts +0 -3
  13. package/dist/color/index.d.mts.map +1 -1
  14. package/dist/color/index.mjs +0 -3
  15. package/dist/color/index.mjs.map +1 -1
  16. package/dist/index.d.mts +5 -190
  17. package/dist/index.d.mts.map +1 -1
  18. package/dist/index.mjs +5 -240
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/indexing/index.d.mts +2 -24
  21. package/dist/indexing/index.d.mts.map +1 -1
  22. package/dist/indexing/index.mjs +7 -69
  23. package/dist/indexing/index.mjs.map +1 -1
  24. package/dist/json-store-30CFCbZZ.mjs +38 -0
  25. package/dist/json-store-30CFCbZZ.mjs.map +1 -0
  26. package/dist/media/index.d.mts +0 -2
  27. package/dist/media/index.d.mts.map +1 -1
  28. package/dist/media/index.mjs +4 -20
  29. package/dist/media/index.mjs.map +1 -1
  30. package/dist/notify/index.d.mts +1 -2
  31. package/dist/notify/index.d.mts.map +1 -1
  32. package/dist/onboarding/index.d.mts +1 -1
  33. package/dist/onboarding/index.d.mts.map +1 -1
  34. package/dist/onboarding/index.mjs +7 -17
  35. package/dist/onboarding/index.mjs.map +1 -1
  36. package/dist/reviews/index.d.mts +154 -0
  37. package/dist/reviews/index.d.mts.map +1 -0
  38. package/dist/reviews/index.mjs +218 -0
  39. package/dist/reviews/index.mjs.map +1 -0
  40. package/dist/schema/index.d.mts +191 -0
  41. package/dist/schema/index.d.mts.map +1 -0
  42. package/dist/schema/index.mjs +241 -0
  43. package/dist/schema/index.mjs.map +1 -0
  44. package/dist/security/index.d.mts +48 -0
  45. package/dist/security/index.d.mts.map +1 -0
  46. package/dist/security/index.mjs +80 -0
  47. package/dist/security/index.mjs.map +1 -0
  48. package/dist/{token-BlShEM6J.d.mts → token-Bz_-igw-.d.mts} +3 -2
  49. package/dist/{token-BlShEM6J.d.mts.map → token-Bz_-igw-.d.mts.map} +1 -1
  50. package/dist/{token-CuyjDo6I.mjs → token-DkKC5m23.mjs} +16 -3
  51. package/dist/{token-CuyjDo6I.mjs.map → token-DkKC5m23.mjs.map} +1 -1
  52. package/dist/tracking/index.d.mts +1 -1
  53. package/dist/{types-C3lHaSrU.d.mts → types-CHHjzRrv.d.mts} +1 -2
  54. package/dist/types-CHHjzRrv.d.mts.map +1 -0
  55. package/package.json +25 -2
  56. package/dist/cloudflare-CD8vwrgj.mjs.map +0 -1
  57. package/dist/types-C3lHaSrU.d.mts.map +0 -1
@@ -0,0 +1,241 @@
1
+ //#region src/schema/article.ts
2
+ /** Build a schema.org Article JSON-LD graph. */
3
+ function articleGraph(options) {
4
+ const { headline, description, image, datePublished, dateModified, author, publisher, mainEntityOfPage, articleSection, wordCount, keywords } = options;
5
+ const authors = Array.isArray(author) ? author : [author];
6
+ return {
7
+ "@context": "https://schema.org",
8
+ "@type": "Article",
9
+ headline,
10
+ ...description && { description },
11
+ ...image && { image: Array.isArray(image) ? image : [image] },
12
+ datePublished,
13
+ ...dateModified && { dateModified },
14
+ author: authors.map((a) => ({
15
+ "@type": "Person",
16
+ name: a.name,
17
+ ...a.url && { url: a.url }
18
+ })),
19
+ ...publisher && { publisher: {
20
+ "@type": "Organization",
21
+ name: publisher.name,
22
+ ...publisher.logo && { logo: {
23
+ "@type": "ImageObject",
24
+ url: publisher.logo
25
+ } }
26
+ } },
27
+ ...mainEntityOfPage && { mainEntityOfPage: {
28
+ "@type": "WebPage",
29
+ "@id": mainEntityOfPage
30
+ } },
31
+ ...articleSection && { articleSection },
32
+ ...wordCount && { wordCount },
33
+ ...keywords && { keywords: keywords.join(", ") }
34
+ };
35
+ }
36
+
37
+ //#endregion
38
+ //#region src/schema/breadcrumb.ts
39
+ /** Build a schema.org BreadcrumbList JSON-LD graph. */
40
+ function breadcrumbGraph(options) {
41
+ return {
42
+ "@context": "https://schema.org",
43
+ "@type": "BreadcrumbList",
44
+ itemListElement: options.items.map((item, index) => ({
45
+ "@type": "ListItem",
46
+ position: index + 1,
47
+ name: item.name,
48
+ item: item.url
49
+ }))
50
+ };
51
+ }
52
+
53
+ //#endregion
54
+ //#region src/schema/faq.ts
55
+ /**
56
+ * Build a schema.org FAQPage JSON-LD graph.
57
+ *
58
+ * NOTE: Google recommends FAQ markup only for authoritative government
59
+ * and health sites. For other sites, consider using QAPage schema instead.
60
+ */
61
+ function faqGraph(options) {
62
+ return {
63
+ "@context": "https://schema.org",
64
+ "@type": "FAQPage",
65
+ mainEntity: options.items.map((item) => ({
66
+ "@type": "Question",
67
+ name: item.question,
68
+ acceptedAnswer: {
69
+ "@type": "Answer",
70
+ text: item.answer
71
+ }
72
+ }))
73
+ };
74
+ }
75
+
76
+ //#endregion
77
+ //#region src/schema/itemList.ts
78
+ /** Build a schema.org ItemList JSON-LD graph (e.g. for a carousel rich result). */
79
+ function itemListGraph(options) {
80
+ const { items, name } = options;
81
+ return {
82
+ "@context": "https://schema.org",
83
+ "@type": "ItemList",
84
+ ...name && { name },
85
+ itemListElement: items.map((item, index) => ({
86
+ "@type": "ListItem",
87
+ position: index + 1,
88
+ item: {
89
+ "@type": item.type,
90
+ name: item.name,
91
+ url: item.url,
92
+ ...item.image && { image: item.image },
93
+ ...item.description && { description: item.description }
94
+ }
95
+ }))
96
+ };
97
+ }
98
+
99
+ //#endregion
100
+ //#region src/schema/localBusiness.ts
101
+ /** Build a schema.org LocalBusiness (or subtype) JSON-LD graph. */
102
+ function localBusinessGraph(options) {
103
+ const { type = "LocalBusiness", name, description, url, telephone, email, image, priceRange, address, geo, openingHoursSpecification, specialOpeningHoursSpecification, aggregateRating, review, paymentAccepted, currenciesAccepted, areaServed, sameAs, knowsLanguage } = options;
104
+ const hours = [...(openingHoursSpecification ?? []).map((spec) => ({
105
+ "@type": "OpeningHoursSpecification",
106
+ dayOfWeek: spec.dayOfWeek,
107
+ opens: spec.opens,
108
+ closes: spec.closes
109
+ })), ...(specialOpeningHoursSpecification ?? []).map((spec) => ({
110
+ "@type": "OpeningHoursSpecification",
111
+ validFrom: spec.validFrom,
112
+ ...spec.validThrough && { validThrough: spec.validThrough },
113
+ ...spec.opens && { opens: spec.opens },
114
+ ...spec.closes && { closes: spec.closes }
115
+ }))];
116
+ return {
117
+ "@context": "https://schema.org",
118
+ "@type": type,
119
+ name,
120
+ ...description && { description },
121
+ ...url && { url },
122
+ ...telephone && { telephone },
123
+ ...email && { email },
124
+ ...image && { image: Array.isArray(image) ? image : [image] },
125
+ ...priceRange && { priceRange },
126
+ ...address && { address: {
127
+ "@type": "PostalAddress",
128
+ ...address.streetAddress && { streetAddress: address.streetAddress },
129
+ ...address.addressLocality && { addressLocality: address.addressLocality },
130
+ ...address.addressRegion && { addressRegion: address.addressRegion },
131
+ ...address.postalCode && { postalCode: address.postalCode },
132
+ ...address.addressCountry && { addressCountry: address.addressCountry }
133
+ } },
134
+ ...geo && { geo: {
135
+ "@type": "GeoCoordinates",
136
+ latitude: geo.latitude,
137
+ longitude: geo.longitude
138
+ } },
139
+ ...hours.length > 0 && { openingHoursSpecification: hours },
140
+ ...aggregateRating && { aggregateRating: {
141
+ "@type": "AggregateRating",
142
+ ratingValue: aggregateRating.ratingValue,
143
+ ...aggregateRating.reviewCount && { reviewCount: aggregateRating.reviewCount },
144
+ ...aggregateRating.ratingCount && { ratingCount: aggregateRating.ratingCount },
145
+ bestRating: aggregateRating.bestRating ?? 5,
146
+ worstRating: aggregateRating.worstRating ?? 1
147
+ } },
148
+ ...review && { review: review.map((r) => ({
149
+ "@type": "Review",
150
+ author: {
151
+ "@type": "Person",
152
+ name: r.author
153
+ },
154
+ datePublished: r.datePublished,
155
+ reviewBody: r.reviewBody,
156
+ ...r.reviewRating && { reviewRating: {
157
+ "@type": "Rating",
158
+ ratingValue: r.reviewRating.ratingValue,
159
+ bestRating: r.reviewRating.bestRating ?? 5,
160
+ worstRating: r.reviewRating.worstRating ?? 1
161
+ } }
162
+ })) },
163
+ ...paymentAccepted && { paymentAccepted },
164
+ ...currenciesAccepted && { currenciesAccepted },
165
+ ...areaServed && { areaServed },
166
+ ...sameAs && { sameAs },
167
+ ...knowsLanguage && { knowsLanguage }
168
+ };
169
+ }
170
+
171
+ //#endregion
172
+ //#region src/schema/organization.ts
173
+ /** Build a schema.org Organization JSON-LD graph. */
174
+ function organizationGraph(options) {
175
+ const { name, url, logo, description, email, telephone, address, sameAs, foundingDate, founders, numberOfEmployees, parentOrganization, subOrganization, contactPoint, areaServed, brand, award, memberOf } = options;
176
+ return {
177
+ "@context": "https://schema.org",
178
+ "@type": "Organization",
179
+ name,
180
+ ...url && { url },
181
+ ...logo && { logo },
182
+ ...description && { description },
183
+ ...email && { email },
184
+ ...telephone && { telephone },
185
+ ...address && { address: {
186
+ "@type": "PostalAddress",
187
+ ...address.streetAddress && { streetAddress: address.streetAddress },
188
+ ...address.addressLocality && { addressLocality: address.addressLocality },
189
+ ...address.addressRegion && { addressRegion: address.addressRegion },
190
+ ...address.postalCode && { postalCode: address.postalCode },
191
+ ...address.addressCountry && { addressCountry: address.addressCountry }
192
+ } },
193
+ ...sameAs && { sameAs },
194
+ ...foundingDate && { foundingDate },
195
+ ...founders && { founders: founders.map((f) => ({
196
+ "@type": "Person",
197
+ name: f
198
+ })) },
199
+ ...numberOfEmployees && { numberOfEmployees: typeof numberOfEmployees === "number" ? {
200
+ "@type": "QuantitativeValue",
201
+ value: numberOfEmployees
202
+ } : {
203
+ "@type": "QuantitativeValue",
204
+ minValue: numberOfEmployees.minValue,
205
+ maxValue: numberOfEmployees.maxValue
206
+ } },
207
+ ...parentOrganization && { parentOrganization: {
208
+ "@type": "Organization",
209
+ name: parentOrganization.name,
210
+ ...parentOrganization.url && { url: parentOrganization.url }
211
+ } },
212
+ ...subOrganization && { subOrganization: subOrganization.map((sub) => ({
213
+ "@type": "Organization",
214
+ name: sub.name,
215
+ ...sub.url && { url: sub.url }
216
+ })) },
217
+ ...contactPoint && { contactPoint: contactPoint.map((cp) => ({
218
+ "@type": "ContactPoint",
219
+ telephone: cp.telephone,
220
+ contactType: cp.contactType,
221
+ ...cp.email && { email: cp.email },
222
+ ...cp.availableLanguage && { availableLanguage: cp.availableLanguage },
223
+ ...cp.areaServed && { areaServed: cp.areaServed }
224
+ })) },
225
+ ...areaServed && { areaServed },
226
+ ...brand && { brand: {
227
+ "@type": "Brand",
228
+ name: brand
229
+ } },
230
+ ...award && { award },
231
+ ...memberOf && { memberOf: memberOf.map((m) => ({
232
+ "@type": "Organization",
233
+ name: m.name,
234
+ ...m.url && { url: m.url }
235
+ })) }
236
+ };
237
+ }
238
+
239
+ //#endregion
240
+ export { articleGraph, breadcrumbGraph, faqGraph, itemListGraph, localBusinessGraph, organizationGraph };
241
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/schema/article.ts","../../src/schema/breadcrumb.ts","../../src/schema/faq.ts","../../src/schema/itemList.ts","../../src/schema/localBusiness.ts","../../src/schema/organization.ts"],"sourcesContent":["export interface ArticleAuthor {\n name: string;\n url?: string;\n}\n\nexport interface ArticleGraphOptions {\n headline: string;\n description?: string;\n image?: string | string[];\n datePublished: string;\n dateModified?: string;\n author: ArticleAuthor | ArticleAuthor[];\n publisher?: {\n name: string;\n logo?: string;\n };\n mainEntityOfPage?: string;\n articleSection?: string;\n wordCount?: number;\n keywords?: string[];\n}\n\n/** Build a schema.org Article JSON-LD graph. */\nexport function articleGraph(options: ArticleGraphOptions): Record<string, unknown> {\n const {\n headline,\n description,\n image,\n datePublished,\n dateModified,\n author,\n publisher,\n mainEntityOfPage,\n articleSection,\n wordCount,\n keywords,\n } = options;\n\n const authors = Array.isArray(author) ? author : [author];\n\n return {\n '@context': 'https://schema.org',\n '@type': 'Article',\n headline,\n ...(description && { description }),\n ...(image && { image: Array.isArray(image) ? image : [image] }),\n datePublished,\n ...(dateModified && { dateModified }),\n author: authors.map((a) => ({\n '@type': 'Person',\n name: a.name,\n ...(a.url && { url: a.url }),\n })),\n ...(publisher && {\n publisher: {\n '@type': 'Organization',\n name: publisher.name,\n ...(publisher.logo && { logo: { '@type': 'ImageObject', url: publisher.logo } }),\n },\n }),\n ...(mainEntityOfPage && { mainEntityOfPage: { '@type': 'WebPage', '@id': mainEntityOfPage } }),\n ...(articleSection && { articleSection }),\n ...(wordCount && { wordCount }),\n ...(keywords && { keywords: keywords.join(', ') }),\n };\n}\n","export interface BreadcrumbItem {\n name: string;\n url: string;\n}\n\nexport interface BreadcrumbGraphOptions {\n items: BreadcrumbItem[];\n}\n\n/** Build a schema.org BreadcrumbList JSON-LD graph. */\nexport function breadcrumbGraph(options: BreadcrumbGraphOptions): Record<string, unknown> {\n return {\n '@context': 'https://schema.org',\n '@type': 'BreadcrumbList',\n itemListElement: options.items.map((item, index) => ({\n '@type': 'ListItem',\n position: index + 1,\n name: item.name,\n item: item.url,\n })),\n };\n}\n","export interface FAQItem {\n question: string;\n answer: string;\n}\n\nexport interface FAQGraphOptions {\n items: FAQItem[];\n}\n\n/**\n * Build a schema.org FAQPage JSON-LD graph.\n *\n * NOTE: Google recommends FAQ markup only for authoritative government\n * and health sites. For other sites, consider using QAPage schema instead.\n */\nexport function faqGraph(options: FAQGraphOptions): Record<string, unknown> {\n return {\n '@context': 'https://schema.org',\n '@type': 'FAQPage',\n mainEntity: options.items.map((item) => ({\n '@type': 'Question',\n name: item.question,\n acceptedAnswer: {\n '@type': 'Answer',\n text: item.answer,\n },\n })),\n };\n}\n","export type ItemListEntryType =\n | 'Article'\n | 'Recipe'\n | 'Movie'\n | 'Course'\n | 'Restaurant'\n | 'Product'\n // Anything else is still valid ItemList markup, just not a Google\n // carousel rich-result candidate — those six are the full supported set.\n | (string & {});\n\nexport interface ItemListEntry {\n type: ItemListEntryType;\n name: string;\n /**\n * Summary-page format: the item's own detail page (unique, same domain).\n * All-in-one-page format: this page's URL plus an HTML anchor pointing at\n * the item on it.\n */\n url: string;\n image?: string;\n description?: string;\n}\n\nexport interface ItemListGraphOptions {\n items: ItemListEntry[];\n /** Recommended, not required by the spec — names the list itself. */\n name?: string;\n}\n\n/** Build a schema.org ItemList JSON-LD graph (e.g. for a carousel rich result). */\nexport function itemListGraph(options: ItemListGraphOptions): Record<string, unknown> {\n const { items, name } = options;\n\n return {\n '@context': 'https://schema.org',\n '@type': 'ItemList',\n ...(name && { name }),\n itemListElement: items.map((item, index) => ({\n '@type': 'ListItem',\n position: index + 1,\n item: {\n '@type': item.type,\n name: item.name,\n url: item.url,\n ...(item.image && { image: item.image }),\n ...(item.description && { description: item.description }),\n },\n })),\n };\n}\n","export interface OpeningHoursSpecification {\n dayOfWeek: ('Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday')[];\n opens: string;\n closes: string;\n}\n\n/**\n * A dated deviation from the recurring `openingHoursSpecification` — holiday\n * hours, a one-off closure, a temporary schedule. Same schema.org shape\n * (`OpeningHoursSpecification`), distinguished by carrying `validFrom`. A full\n * closure omits `opens`/`closes` rather than encoding it as an open interval.\n */\nexport interface SpecialHoursSpecification {\n validFrom: string;\n validThrough?: string;\n opens?: string;\n closes?: string;\n}\n\nexport interface LocalBusinessGraphOptions {\n /** schema.org `LocalBusiness` subtype, e.g. `'Restaurant'`, `'ProfessionalService'`. Defaults to `'LocalBusiness'`. */\n type?: string;\n name: string;\n description?: string;\n url?: string;\n telephone?: string;\n email?: string;\n image?: string | string[];\n priceRange?: string;\n address?: {\n streetAddress?: string;\n addressLocality?: string;\n addressRegion?: string;\n postalCode?: string;\n addressCountry?: string;\n };\n geo?: {\n latitude: number;\n longitude: number;\n };\n openingHoursSpecification?: OpeningHoursSpecification[];\n specialOpeningHoursSpecification?: SpecialHoursSpecification[];\n aggregateRating?: {\n ratingValue: number;\n reviewCount?: number;\n ratingCount?: number;\n bestRating?: number;\n worstRating?: number;\n };\n review?: {\n author: string;\n datePublished: string;\n reviewBody: string;\n reviewRating?: {\n ratingValue: number;\n bestRating?: number;\n worstRating?: number;\n };\n }[];\n paymentAccepted?: string[];\n currenciesAccepted?: string;\n areaServed?: string[];\n sameAs?: string[];\n knowsLanguage?: string[];\n}\n\n/** Build a schema.org LocalBusiness (or subtype) JSON-LD graph. */\nexport function localBusinessGraph(options: LocalBusinessGraphOptions): Record<string, unknown> {\n const {\n type = 'LocalBusiness',\n name,\n description,\n url,\n telephone,\n email,\n image,\n priceRange,\n address,\n geo,\n openingHoursSpecification,\n specialOpeningHoursSpecification,\n aggregateRating,\n review,\n paymentAccepted,\n currenciesAccepted,\n areaServed,\n sameAs,\n knowsLanguage,\n } = options;\n\n const hours = [\n ...(openingHoursSpecification ?? []).map((spec) => ({\n '@type': 'OpeningHoursSpecification',\n dayOfWeek: spec.dayOfWeek,\n opens: spec.opens,\n closes: spec.closes,\n })),\n ...(specialOpeningHoursSpecification ?? []).map((spec) => ({\n '@type': 'OpeningHoursSpecification',\n validFrom: spec.validFrom,\n ...(spec.validThrough && { validThrough: spec.validThrough }),\n ...(spec.opens && { opens: spec.opens }),\n ...(spec.closes && { closes: spec.closes }),\n })),\n ];\n\n return {\n '@context': 'https://schema.org',\n '@type': type,\n name,\n ...(description && { description }),\n ...(url && { url }),\n ...(telephone && { telephone }),\n ...(email && { email }),\n ...(image && { image: Array.isArray(image) ? image : [image] }),\n ...(priceRange && { priceRange }),\n ...(address && {\n address: {\n '@type': 'PostalAddress',\n ...(address.streetAddress && { streetAddress: address.streetAddress }),\n ...(address.addressLocality && { addressLocality: address.addressLocality }),\n ...(address.addressRegion && { addressRegion: address.addressRegion }),\n ...(address.postalCode && { postalCode: address.postalCode }),\n ...(address.addressCountry && { addressCountry: address.addressCountry }),\n },\n }),\n ...(geo && {\n geo: {\n '@type': 'GeoCoordinates',\n latitude: geo.latitude,\n longitude: geo.longitude,\n },\n }),\n ...(hours.length > 0 && { openingHoursSpecification: hours }),\n ...(aggregateRating && {\n aggregateRating: {\n '@type': 'AggregateRating',\n ratingValue: aggregateRating.ratingValue,\n ...(aggregateRating.reviewCount && { reviewCount: aggregateRating.reviewCount }),\n ...(aggregateRating.ratingCount && { ratingCount: aggregateRating.ratingCount }),\n bestRating: aggregateRating.bestRating ?? 5,\n worstRating: aggregateRating.worstRating ?? 1,\n },\n }),\n ...(review && {\n review: review.map((r) => ({\n '@type': 'Review',\n author: { '@type': 'Person', name: r.author },\n datePublished: r.datePublished,\n reviewBody: r.reviewBody,\n ...(r.reviewRating && {\n reviewRating: {\n '@type': 'Rating',\n ratingValue: r.reviewRating.ratingValue,\n bestRating: r.reviewRating.bestRating ?? 5,\n worstRating: r.reviewRating.worstRating ?? 1,\n },\n }),\n })),\n }),\n ...(paymentAccepted && { paymentAccepted }),\n ...(currenciesAccepted && { currenciesAccepted }),\n ...(areaServed && { areaServed }),\n ...(sameAs && { sameAs }),\n ...(knowsLanguage && { knowsLanguage }),\n };\n}\n","export interface OrganizationGraphOptions {\n name: string;\n url?: string;\n logo?: string;\n description?: string;\n email?: string;\n telephone?: string;\n address?: {\n streetAddress?: string;\n addressLocality?: string;\n addressRegion?: string;\n postalCode?: string;\n addressCountry?: string;\n };\n sameAs?: string[];\n foundingDate?: string;\n founders?: string[];\n numberOfEmployees?: number | { minValue: number; maxValue: number };\n parentOrganization?: {\n name: string;\n url?: string;\n };\n subOrganization?: {\n name: string;\n url?: string;\n }[];\n contactPoint?: {\n type?: 'customer service' | 'technical support' | 'sales' | 'billing support' | 'reservations';\n telephone: string;\n email?: string;\n contactType: string;\n availableLanguage?: string[];\n areaServed?: string[];\n }[];\n areaServed?: string[];\n brand?: string;\n award?: string[];\n memberOf?: {\n name: string;\n url?: string;\n }[];\n}\n\n/** Build a schema.org Organization JSON-LD graph. */\nexport function organizationGraph(options: OrganizationGraphOptions): Record<string, unknown> {\n const {\n name,\n url,\n logo,\n description,\n email,\n telephone,\n address,\n sameAs,\n foundingDate,\n founders,\n numberOfEmployees,\n parentOrganization,\n subOrganization,\n contactPoint,\n areaServed,\n brand,\n award,\n memberOf,\n } = options;\n\n return {\n '@context': 'https://schema.org',\n '@type': 'Organization',\n name,\n ...(url && { url }),\n ...(logo && { logo }),\n ...(description && { description }),\n ...(email && { email }),\n ...(telephone && { telephone }),\n ...(address && {\n address: {\n '@type': 'PostalAddress',\n ...(address.streetAddress && { streetAddress: address.streetAddress }),\n ...(address.addressLocality && { addressLocality: address.addressLocality }),\n ...(address.addressRegion && { addressRegion: address.addressRegion }),\n ...(address.postalCode && { postalCode: address.postalCode }),\n ...(address.addressCountry && { addressCountry: address.addressCountry }),\n },\n }),\n ...(sameAs && { sameAs }),\n ...(foundingDate && { foundingDate }),\n ...(founders && {\n founders: founders.map((f) => ({\n '@type': 'Person',\n name: f,\n })),\n }),\n ...(numberOfEmployees && {\n numberOfEmployees:\n typeof numberOfEmployees === 'number'\n ? { '@type': 'QuantitativeValue', value: numberOfEmployees }\n : {\n '@type': 'QuantitativeValue',\n minValue: numberOfEmployees.minValue,\n maxValue: numberOfEmployees.maxValue,\n },\n }),\n ...(parentOrganization && {\n parentOrganization: {\n '@type': 'Organization',\n name: parentOrganization.name,\n ...(parentOrganization.url && { url: parentOrganization.url }),\n },\n }),\n ...(subOrganization && {\n subOrganization: subOrganization.map((sub) => ({\n '@type': 'Organization',\n name: sub.name,\n ...(sub.url && { url: sub.url }),\n })),\n }),\n ...(contactPoint && {\n contactPoint: contactPoint.map((cp) => ({\n '@type': 'ContactPoint',\n telephone: cp.telephone,\n contactType: cp.contactType,\n ...(cp.email && { email: cp.email }),\n ...(cp.availableLanguage && { availableLanguage: cp.availableLanguage }),\n ...(cp.areaServed && { areaServed: cp.areaServed }),\n })),\n }),\n ...(areaServed && { areaServed }),\n ...(brand && { brand: { '@type': 'Brand', name: brand } }),\n ...(award && { award }),\n ...(memberOf && {\n memberOf: memberOf.map((m) => ({\n '@type': 'Organization',\n name: m.name,\n ...(m.url && { url: m.url }),\n })),\n }),\n };\n}\n"],"mappings":";;AAuBA,SAAgB,aAAa,SAAuD;CAClF,MAAM,EACJ,UACA,aACA,OACA,eACA,cACA,QACA,WACA,kBACA,gBACA,WACA,aACE;CAEJ,MAAM,UAAU,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAM;CAExD,OAAO;EACL,YAAY;EACZ,SAAS;EACT;EACA,GAAI,eAAe,EAAE,YAAY;EACjC,GAAI,SAAS,EAAE,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,EAAE;EAC7D;EACA,GAAI,gBAAgB,EAAE,aAAa;EACnC,QAAQ,QAAQ,KAAK,OAAO;GAC1B,SAAS;GACT,MAAM,EAAE;GACR,GAAI,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI;EAC5B,EAAE;EACF,GAAI,aAAa,EACf,WAAW;GACT,SAAS;GACT,MAAM,UAAU;GAChB,GAAI,UAAU,QAAQ,EAAE,MAAM;IAAE,SAAS;IAAe,KAAK,UAAU;GAAK,EAAE;EAChF,EACF;EACA,GAAI,oBAAoB,EAAE,kBAAkB;GAAE,SAAS;GAAW,OAAO;EAAiB,EAAE;EAC5F,GAAI,kBAAkB,EAAE,eAAe;EACvC,GAAI,aAAa,EAAE,UAAU;EAC7B,GAAI,YAAY,EAAE,UAAU,SAAS,KAAK,IAAI,EAAE;CAClD;AACF;;;;;ACvDA,SAAgB,gBAAgB,SAA0D;CACxF,OAAO;EACL,YAAY;EACZ,SAAS;EACT,iBAAiB,QAAQ,MAAM,KAAK,MAAM,WAAW;GACnD,SAAS;GACT,UAAU,QAAQ;GAClB,MAAM,KAAK;GACX,MAAM,KAAK;EACb,EAAE;CACJ;AACF;;;;;;;;;;ACNA,SAAgB,SAAS,SAAmD;CAC1E,OAAO;EACL,YAAY;EACZ,SAAS;EACT,YAAY,QAAQ,MAAM,KAAK,UAAU;GACvC,SAAS;GACT,MAAM,KAAK;GACX,gBAAgB;IACd,SAAS;IACT,MAAM,KAAK;GACb;EACF,EAAE;CACJ;AACF;;;;;ACGA,SAAgB,cAAc,SAAwD;CACpF,MAAM,EAAE,OAAO,SAAS;CAExB,OAAO;EACL,YAAY;EACZ,SAAS;EACT,GAAI,QAAQ,EAAE,KAAK;EACnB,iBAAiB,MAAM,KAAK,MAAM,WAAW;GAC3C,SAAS;GACT,UAAU,QAAQ;GAClB,MAAM;IACJ,SAAS,KAAK;IACd,MAAM,KAAK;IACX,KAAK,KAAK;IACV,GAAI,KAAK,SAAS,EAAE,OAAO,KAAK,MAAM;IACtC,GAAI,KAAK,eAAe,EAAE,aAAa,KAAK,YAAY;GAC1D;EACF,EAAE;CACJ;AACF;;;;;ACiBA,SAAgB,mBAAmB,SAA6D;CAC9F,MAAM,EACJ,OAAO,iBACP,MACA,aACA,KACA,WACA,OACA,OACA,YACA,SACA,KACA,2BACA,kCACA,iBACA,QACA,iBACA,oBACA,YACA,QACA,kBACE;CAEJ,MAAM,QAAQ,CACZ,IAAI,6BAA6B,CAAC,EAAC,CAAE,KAAK,UAAU;EAClD,SAAS;EACT,WAAW,KAAK;EAChB,OAAO,KAAK;EACZ,QAAQ,KAAK;CACf,EAAE,GACF,IAAI,oCAAoC,CAAC,EAAC,CAAE,KAAK,UAAU;EACzD,SAAS;EACT,WAAW,KAAK;EAChB,GAAI,KAAK,gBAAgB,EAAE,cAAc,KAAK,aAAa;EAC3D,GAAI,KAAK,SAAS,EAAE,OAAO,KAAK,MAAM;EACtC,GAAI,KAAK,UAAU,EAAE,QAAQ,KAAK,OAAO;CAC3C,EAAE,CACJ;CAEA,OAAO;EACL,YAAY;EACZ,SAAS;EACT;EACA,GAAI,eAAe,EAAE,YAAY;EACjC,GAAI,OAAO,EAAE,IAAI;EACjB,GAAI,aAAa,EAAE,UAAU;EAC7B,GAAI,SAAS,EAAE,MAAM;EACrB,GAAI,SAAS,EAAE,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,EAAE;EAC7D,GAAI,cAAc,EAAE,WAAW;EAC/B,GAAI,WAAW,EACb,SAAS;GACP,SAAS;GACT,GAAI,QAAQ,iBAAiB,EAAE,eAAe,QAAQ,cAAc;GACpE,GAAI,QAAQ,mBAAmB,EAAE,iBAAiB,QAAQ,gBAAgB;GAC1E,GAAI,QAAQ,iBAAiB,EAAE,eAAe,QAAQ,cAAc;GACpE,GAAI,QAAQ,cAAc,EAAE,YAAY,QAAQ,WAAW;GAC3D,GAAI,QAAQ,kBAAkB,EAAE,gBAAgB,QAAQ,eAAe;EACzE,EACF;EACA,GAAI,OAAO,EACT,KAAK;GACH,SAAS;GACT,UAAU,IAAI;GACd,WAAW,IAAI;EACjB,EACF;EACA,GAAI,MAAM,SAAS,KAAK,EAAE,2BAA2B,MAAM;EAC3D,GAAI,mBAAmB,EACrB,iBAAiB;GACf,SAAS;GACT,aAAa,gBAAgB;GAC7B,GAAI,gBAAgB,eAAe,EAAE,aAAa,gBAAgB,YAAY;GAC9E,GAAI,gBAAgB,eAAe,EAAE,aAAa,gBAAgB,YAAY;GAC9E,YAAY,gBAAgB,cAAc;GAC1C,aAAa,gBAAgB,eAAe;EAC9C,EACF;EACA,GAAI,UAAU,EACZ,QAAQ,OAAO,KAAK,OAAO;GACzB,SAAS;GACT,QAAQ;IAAE,SAAS;IAAU,MAAM,EAAE;GAAO;GAC5C,eAAe,EAAE;GACjB,YAAY,EAAE;GACd,GAAI,EAAE,gBAAgB,EACpB,cAAc;IACZ,SAAS;IACT,aAAa,EAAE,aAAa;IAC5B,YAAY,EAAE,aAAa,cAAc;IACzC,aAAa,EAAE,aAAa,eAAe;GAC7C,EACF;EACF,EAAE,EACJ;EACA,GAAI,mBAAmB,EAAE,gBAAgB;EACzC,GAAI,sBAAsB,EAAE,mBAAmB;EAC/C,GAAI,cAAc,EAAE,WAAW;EAC/B,GAAI,UAAU,EAAE,OAAO;EACvB,GAAI,iBAAiB,EAAE,cAAc;CACvC;AACF;;;;;AC1HA,SAAgB,kBAAkB,SAA4D;CAC5F,MAAM,EACJ,MACA,KACA,MACA,aACA,OACA,WACA,SACA,QACA,cACA,UACA,mBACA,oBACA,iBACA,cACA,YACA,OACA,OACA,aACE;CAEJ,OAAO;EACL,YAAY;EACZ,SAAS;EACT;EACA,GAAI,OAAO,EAAE,IAAI;EACjB,GAAI,QAAQ,EAAE,KAAK;EACnB,GAAI,eAAe,EAAE,YAAY;EACjC,GAAI,SAAS,EAAE,MAAM;EACrB,GAAI,aAAa,EAAE,UAAU;EAC7B,GAAI,WAAW,EACb,SAAS;GACP,SAAS;GACT,GAAI,QAAQ,iBAAiB,EAAE,eAAe,QAAQ,cAAc;GACpE,GAAI,QAAQ,mBAAmB,EAAE,iBAAiB,QAAQ,gBAAgB;GAC1E,GAAI,QAAQ,iBAAiB,EAAE,eAAe,QAAQ,cAAc;GACpE,GAAI,QAAQ,cAAc,EAAE,YAAY,QAAQ,WAAW;GAC3D,GAAI,QAAQ,kBAAkB,EAAE,gBAAgB,QAAQ,eAAe;EACzE,EACF;EACA,GAAI,UAAU,EAAE,OAAO;EACvB,GAAI,gBAAgB,EAAE,aAAa;EACnC,GAAI,YAAY,EACd,UAAU,SAAS,KAAK,OAAO;GAC7B,SAAS;GACT,MAAM;EACR,EAAE,EACJ;EACA,GAAI,qBAAqB,EACvB,mBACE,OAAO,sBAAsB,WACzB;GAAE,SAAS;GAAqB,OAAO;EAAkB,IACzD;GACE,SAAS;GACT,UAAU,kBAAkB;GAC5B,UAAU,kBAAkB;EAC9B,EACR;EACA,GAAI,sBAAsB,EACxB,oBAAoB;GAClB,SAAS;GACT,MAAM,mBAAmB;GACzB,GAAI,mBAAmB,OAAO,EAAE,KAAK,mBAAmB,IAAI;EAC9D,EACF;EACA,GAAI,mBAAmB,EACrB,iBAAiB,gBAAgB,KAAK,SAAS;GAC7C,SAAS;GACT,MAAM,IAAI;GACV,GAAI,IAAI,OAAO,EAAE,KAAK,IAAI,IAAI;EAChC,EAAE,EACJ;EACA,GAAI,gBAAgB,EAClB,cAAc,aAAa,KAAK,QAAQ;GACtC,SAAS;GACT,WAAW,GAAG;GACd,aAAa,GAAG;GAChB,GAAI,GAAG,SAAS,EAAE,OAAO,GAAG,MAAM;GAClC,GAAI,GAAG,qBAAqB,EAAE,mBAAmB,GAAG,kBAAkB;GACtE,GAAI,GAAG,cAAc,EAAE,YAAY,GAAG,WAAW;EACnD,EAAE,EACJ;EACA,GAAI,cAAc,EAAE,WAAW;EAC/B,GAAI,SAAS,EAAE,OAAO;GAAE,SAAS;GAAS,MAAM;EAAM,EAAE;EACxD,GAAI,SAAS,EAAE,MAAM;EACrB,GAAI,YAAY,EACd,UAAU,SAAS,KAAK,OAAO;GAC7B,SAAS;GACT,MAAM,EAAE;GACR,GAAI,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI;EAC5B,EAAE,EACJ;CACF;AACF"}
@@ -0,0 +1,48 @@
1
+ //#region src/security/turnstile.d.ts
2
+ /**
3
+ * Cloudflare Turnstile server-side verification.
4
+ *
5
+ * `site.security.turnstile.siteKey` is public and lives in the config; the
6
+ * matching **secret** key never does — same rule as every other credential in
7
+ * this toolchain (`CLOUDFLARE_API_TOKEN`, the Google OAuth/service-account
8
+ * grants) and the same reason: an env var, injected by the caller, never
9
+ * imported here. Before this module existed, `siteKey` was read by the legal
10
+ * templates (`legal/providers.ts`, `legal/derive.ts`) to *name* Turnstile in the
11
+ * generated privacy policy, with nothing in the toolchain that verified a token
12
+ * — a config field asserting a fact the site didn't actually implement, which is
13
+ * exactly what the legal system's own rule ("the config records facts; the
14
+ * template owns prose") argues against. `verifyTurnstileToken` plus
15
+ * `createConversionRoute()`'s `turnstile` option (`@getvitops/astro/routes`) is
16
+ * what lets that field describe something real.
17
+ *
18
+ * A separate `@getvitops/utils/security` subpath for the same reason
19
+ * `notify`/`tracking` are: this runs in a **Worker**, on every submission, and
20
+ * must not pull in anything build-time.
21
+ *
22
+ * The widget itself — `<script src="https://challenges.cloudflare.com/turnstile/
23
+ * v0/api.js" async defer></script>` plus a `<div class="cf-turnstile"
24
+ * data-sitekey="…">` — is the consumer's own markup, on their own hand-written
25
+ * form. There is no `FormRenderer` (removed 2026-07-27, see `AGENTS.md`) for this
26
+ * to hook into generically, so wiring the widget follows the same pattern a
27
+ * consumer already uses for the rest of their form: write the HTML, submit it to
28
+ * `createConversionRoute()`.
29
+ */
30
+ interface TurnstileVerifyResult {
31
+ success: boolean;
32
+ /** Cloudflare's own codes (e.g. `timeout-or-duplicate`), present iff not successful. */
33
+ errorCodes?: string[];
34
+ }
35
+ /**
36
+ * Verify a submitted `cf-turnstile-response` token against Cloudflare's
37
+ * siteverify endpoint.
38
+ *
39
+ * Never throws — a network failure or a malformed response both come back as
40
+ * `{ success: false }` with an error code the caller can log, the same
41
+ * "structured result, not an exception" rule `indexing`/`notify` follow. A
42
+ * failed verification is not this module's business to act on; the caller
43
+ * (`createConversionRoute`) decides what a failure means for the request.
44
+ */
45
+ declare function verifyTurnstileToken(token: string, secretKey: string, remoteIp?: string, fetchImpl?: typeof fetch): Promise<TurnstileVerifyResult>;
46
+ //#endregion
47
+ export { type TurnstileVerifyResult, verifyTurnstileToken };
48
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/security/turnstile.ts"],"mappings":";;AA+BA;;;;AAGY;AAaZ;;;;;;;;;;;;;;;;AAKgC;;;;;;UArBf,qBAAA;EACf,OAAA;;EAEA,UAAU;AAAA;;;;;;;;;;;iBAaU,oBAAA,CACpB,KAAA,UACA,SAAA,UACA,QAAA,WACA,SAAA,UAAkB,KAAA,GACjB,OAAA,CAAQ,qBAAA"}
@@ -0,0 +1,80 @@
1
+ //#region src/security/turnstile.ts
2
+ /**
3
+ * Cloudflare Turnstile server-side verification.
4
+ *
5
+ * `site.security.turnstile.siteKey` is public and lives in the config; the
6
+ * matching **secret** key never does — same rule as every other credential in
7
+ * this toolchain (`CLOUDFLARE_API_TOKEN`, the Google OAuth/service-account
8
+ * grants) and the same reason: an env var, injected by the caller, never
9
+ * imported here. Before this module existed, `siteKey` was read by the legal
10
+ * templates (`legal/providers.ts`, `legal/derive.ts`) to *name* Turnstile in the
11
+ * generated privacy policy, with nothing in the toolchain that verified a token
12
+ * — a config field asserting a fact the site didn't actually implement, which is
13
+ * exactly what the legal system's own rule ("the config records facts; the
14
+ * template owns prose") argues against. `verifyTurnstileToken` plus
15
+ * `createConversionRoute()`'s `turnstile` option (`@getvitops/astro/routes`) is
16
+ * what lets that field describe something real.
17
+ *
18
+ * A separate `@getvitops/utils/security` subpath for the same reason
19
+ * `notify`/`tracking` are: this runs in a **Worker**, on every submission, and
20
+ * must not pull in anything build-time.
21
+ *
22
+ * The widget itself — `<script src="https://challenges.cloudflare.com/turnstile/
23
+ * v0/api.js" async defer><\/script>` plus a `<div class="cf-turnstile"
24
+ * data-sitekey="…">` — is the consumer's own markup, on their own hand-written
25
+ * form. There is no `FormRenderer` (removed 2026-07-27, see `AGENTS.md`) for this
26
+ * to hook into generically, so wiring the widget follows the same pattern a
27
+ * consumer already uses for the rest of their form: write the HTML, submit it to
28
+ * `createConversionRoute()`.
29
+ */
30
+ const VERIFY_ENDPOINT = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
31
+ /**
32
+ * Verify a submitted `cf-turnstile-response` token against Cloudflare's
33
+ * siteverify endpoint.
34
+ *
35
+ * Never throws — a network failure or a malformed response both come back as
36
+ * `{ success: false }` with an error code the caller can log, the same
37
+ * "structured result, not an exception" rule `indexing`/`notify` follow. A
38
+ * failed verification is not this module's business to act on; the caller
39
+ * (`createConversionRoute`) decides what a failure means for the request.
40
+ */
41
+ async function verifyTurnstileToken(token, secretKey, remoteIp, fetchImpl = fetch) {
42
+ const body = new URLSearchParams({
43
+ secret: secretKey,
44
+ response: token
45
+ });
46
+ if (remoteIp) body.set("remoteip", remoteIp);
47
+ let res;
48
+ try {
49
+ res = await fetchImpl(VERIFY_ENDPOINT, {
50
+ method: "POST",
51
+ body
52
+ });
53
+ } catch (e) {
54
+ return {
55
+ success: false,
56
+ errorCodes: [`network-error: ${e.message}`]
57
+ };
58
+ }
59
+ if (!res.ok) return {
60
+ success: false,
61
+ errorCodes: [`http-${res.status}`]
62
+ };
63
+ let data;
64
+ try {
65
+ data = await res.json();
66
+ } catch {
67
+ return {
68
+ success: false,
69
+ errorCodes: ["malformed-response"]
70
+ };
71
+ }
72
+ return data.success === true ? { success: true } : {
73
+ success: false,
74
+ errorCodes: data["error-codes"] ?? ["unknown-error"]
75
+ };
76
+ }
77
+
78
+ //#endregion
79
+ export { verifyTurnstileToken };
80
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/security/turnstile.ts"],"sourcesContent":["/**\n * Cloudflare Turnstile server-side verification.\n *\n * `site.security.turnstile.siteKey` is public and lives in the config; the\n * matching **secret** key never does — same rule as every other credential in\n * this toolchain (`CLOUDFLARE_API_TOKEN`, the Google OAuth/service-account\n * grants) and the same reason: an env var, injected by the caller, never\n * imported here. Before this module existed, `siteKey` was read by the legal\n * templates (`legal/providers.ts`, `legal/derive.ts`) to *name* Turnstile in the\n * generated privacy policy, with nothing in the toolchain that verified a token\n * — a config field asserting a fact the site didn't actually implement, which is\n * exactly what the legal system's own rule (\"the config records facts; the\n * template owns prose\") argues against. `verifyTurnstileToken` plus\n * `createConversionRoute()`'s `turnstile` option (`@getvitops/astro/routes`) is\n * what lets that field describe something real.\n *\n * A separate `@getvitops/utils/security` subpath for the same reason\n * `notify`/`tracking` are: this runs in a **Worker**, on every submission, and\n * must not pull in anything build-time.\n *\n * The widget itself — `<script src=\"https://challenges.cloudflare.com/turnstile/\n * v0/api.js\" async defer></script>` plus a `<div class=\"cf-turnstile\"\n * data-sitekey=\"…\">` — is the consumer's own markup, on their own hand-written\n * form. There is no `FormRenderer` (removed 2026-07-27, see `AGENTS.md`) for this\n * to hook into generically, so wiring the widget follows the same pattern a\n * consumer already uses for the rest of their form: write the HTML, submit it to\n * `createConversionRoute()`.\n */\n\nconst VERIFY_ENDPOINT = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';\n\nexport interface TurnstileVerifyResult {\n success: boolean;\n /** Cloudflare's own codes (e.g. `timeout-or-duplicate`), present iff not successful. */\n errorCodes?: string[];\n}\n\n/**\n * Verify a submitted `cf-turnstile-response` token against Cloudflare's\n * siteverify endpoint.\n *\n * Never throws — a network failure or a malformed response both come back as\n * `{ success: false }` with an error code the caller can log, the same\n * \"structured result, not an exception\" rule `indexing`/`notify` follow. A\n * failed verification is not this module's business to act on; the caller\n * (`createConversionRoute`) decides what a failure means for the request.\n */\nexport async function verifyTurnstileToken(\n token: string,\n secretKey: string,\n remoteIp?: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<TurnstileVerifyResult> {\n const body = new URLSearchParams({ secret: secretKey, response: token });\n if (remoteIp) body.set('remoteip', remoteIp);\n\n let res: Response;\n try {\n res = await fetchImpl(VERIFY_ENDPOINT, { method: 'POST', body });\n } catch (e) {\n return { success: false, errorCodes: [`network-error: ${(e as Error).message}`] };\n }\n if (!res.ok) return { success: false, errorCodes: [`http-${res.status}`] };\n\n let data: { success?: boolean; 'error-codes'?: string[] };\n try {\n data = (await res.json()) as typeof data;\n } catch {\n return { success: false, errorCodes: ['malformed-response'] };\n }\n\n return data.success === true\n ? { success: true }\n : { success: false, errorCodes: data['error-codes'] ?? ['unknown-error'] };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAM,kBAAkB;;;;;;;;;;;AAkBxB,eAAsB,qBACpB,OACA,WACA,UACA,YAA0B,OACM;CAChC,MAAM,OAAO,IAAI,gBAAgB;EAAE,QAAQ;EAAW,UAAU;CAAM,CAAC;CACvE,IAAI,UAAU,KAAK,IAAI,YAAY,QAAQ;CAE3C,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,UAAU,iBAAiB;GAAE,QAAQ;GAAQ;EAAK,CAAC;CACjE,SAAS,GAAG;EACV,OAAO;GAAE,SAAS;GAAO,YAAY,CAAC,kBAAmB,EAAY,SAAS;EAAE;CAClF;CACA,IAAI,CAAC,IAAI,IAAI,OAAO;EAAE,SAAS;EAAO,YAAY,CAAC,QAAQ,IAAI,QAAQ;CAAE;CAEzE,IAAI;CACJ,IAAI;EACF,OAAQ,MAAM,IAAI,KAAK;CACzB,QAAQ;EACN,OAAO;GAAE,SAAS;GAAO,YAAY,CAAC,oBAAoB;EAAE;CAC9D;CAEA,OAAO,KAAK,YAAY,OACpB,EAAE,SAAS,KAAK,IAChB;EAAE,SAAS;EAAO,YAAY,KAAK,kBAAkB,CAAC,eAAe;CAAE;AAC7E"}
@@ -66,8 +66,9 @@ declare function googleHeaders(auth: GoogleAuthLike, extra?: Record<string, stri
66
66
  /** Scopes, named once so a caller never spells one out. */
67
67
  declare const SCOPES: {
68
68
  /** Search Console (`sitemaps.submit`, `urlInspection`, `sites`). */readonly webmasters: "https://www.googleapis.com/auth/webmasters"; /** Site Verification API. */
69
- readonly siteVerification: "https://www.googleapis.com/auth/siteverification";
69
+ readonly siteVerification: "https://www.googleapis.com/auth/siteverification"; /** Business Profile (reviews). The GCP project must be allowlisted by Google. */
70
+ readonly businessProfile: "https://www.googleapis.com/auth/business.manage";
70
71
  };
71
72
  //#endregion
72
73
  export { SCOPES as a, googleHeaders as c, GoogleOAuth as i, GoogleAuthLike as n, ServiceAccount as o, GoogleCredential as r, googleAccessToken as s, GoogleAuth as t };
73
- //# sourceMappingURL=token-BlShEM6J.d.mts.map
74
+ //# sourceMappingURL=token-Bz_-igw-.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"token-BlShEM6J.d.mts","names":[],"sources":["../src/google/token.ts"],"mappings":";;UAkCiB,cAAA;EACf,YAAA;EACA,WAAW;AAAA;AAAA;AAAA,UAII,WAAA;EACf,QAAA;EACA,YAAA;EACA,YAAA;AAAA;;;;AAAY;AAWd;;;;KAAY,gBAAA;EACN,IAAA;EAAyB,OAAA,EAAS,cAAA;EAAgB,KAAA;AAAA;EAClD,IAAA;EAAe,KAAA,EAAO,WAAW;AAAA;AAAA;AA8CvC;;;;;;;;AA9CuC,iBA8CvB,iBAAA,CACd,UAAA,EAAY,gBAAA,EACZ,SAAA,UAAkB,KAAA,EAClB,GAAA,YACC,OAAA;;;;;;;AAAO;AA6BV;;;;AAEc;AAUd;UAZiB,UAAA;EACf,KAAA;EACA,YAAY;AAAA;AAad;;;;;;;AAAA,KAHY,cAAA,YAA0B,UAAU;;iBAGhC,aAAA,CACd,IAAA,EAAM,cAAA,EACN,KAAA,GAAQ,MAAA,mBACP,MAAA;;cAUU,MAAA;EAVV,6EAeO,UAAA,gDAfD;EAAA,SAeC,gBAAA;AAAA"}
1
+ {"version":3,"file":"token-Bz_-igw-.d.mts","names":[],"sources":["../src/google/token.ts"],"mappings":";;UAkCiB,cAAA;EACf,YAAA;EACA,WAAW;AAAA;AAAA;AAAA,UAII,WAAA;EACf,QAAA;EACA,YAAA;EACA,YAAA;AAAA;;;;AAAY;AAWd;;;;KAAY,gBAAA;EACN,IAAA;EAAyB,OAAA,EAAS,cAAA;EAAgB,KAAA;AAAA;EAClD,IAAA;EAAe,KAAA,EAAO,WAAW;AAAA;AAAA;AA8CvC;;;;;;;;AA9CuC,iBA8CvB,iBAAA,CACd,UAAA,EAAY,gBAAA,EACZ,SAAA,UAAkB,KAAA,EAClB,GAAA,YACC,OAAA;;;;;;;AAAO;AA6BV;;;;AAEc;AAUd;UAZiB,UAAA;EACf,KAAA;EACA,YAAY;AAAA;AAad;;;;;;;AAAA,KAHY,cAAA,YAA0B,UAAU;;iBAGhC,aAAA,CACd,IAAA,EAAM,cAAA,EACN,KAAA,GAAQ,MAAA,mBACP,MAAA;;cA2BU,MAAA"}
@@ -92,14 +92,27 @@ function googleHeaders(auth, extra) {
92
92
  ...extra
93
93
  };
94
94
  }
95
+ /** Percent-encode a Search Console `siteUrl` (e.g. `sc-domain:acme.ca`) for a path segment. */
96
+ const encodeSite = (siteUrl) => encodeURIComponent(siteUrl);
97
+ /**
98
+ * Render a failed response's detail: a caller-supplied hint on a specific status
99
+ * (e.g. what a `403` almost always means for this endpoint), or the raw body
100
+ * otherwise. Shared by every REST call here that follows the "PUT and report
101
+ * why" shape — `gsc.ts`'s `submitSitemap`, `onboarding/google.ts`'s `addSite`.
102
+ */
103
+ async function errorDetail(res, hint) {
104
+ return hint?.(res.status) ?? `${res.status}: ${await res.text()}`;
105
+ }
95
106
  /** Scopes, named once so a caller never spells one out. */
96
107
  const SCOPES = {
97
108
  /** Search Console (`sitemaps.submit`, `urlInspection`, `sites`). */
98
109
  webmasters: "https://www.googleapis.com/auth/webmasters",
99
110
  /** Site Verification API. */
100
- siteVerification: "https://www.googleapis.com/auth/siteverification"
111
+ siteVerification: "https://www.googleapis.com/auth/siteverification",
112
+ /** Business Profile (reviews). The GCP project must be allowlisted by Google. */
113
+ businessProfile: "https://www.googleapis.com/auth/business.manage"
101
114
  };
102
115
 
103
116
  //#endregion
104
- export { googleAccessToken as n, googleHeaders as r, SCOPES as t };
105
- //# sourceMappingURL=token-CuyjDo6I.mjs.map
117
+ export { googleHeaders as a, googleAccessToken as i, encodeSite as n, errorDetail as r, SCOPES as t };
118
+ //# sourceMappingURL=token-DkKC5m23.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"token-CuyjDo6I.mjs","names":[],"sources":["../src/google/token.ts"],"sourcesContent":["/**\n * Google OAuth2 token exchange — one request, two grants.\n *\n * Both of this package's Google modules need a bearer token, and both used to\n * mint one themselves. The two copies looked different because the *credentials*\n * differ, but everything after building the form body was identical: same\n * endpoint, same content-type, same \"surface the response body as the error\"\n * rule, same `access_token` extraction. So the seam is precise —\n * **credential → form params differs, params → token does not.**\n *\n * The two grants are kept because they are not accidental duplication; they track\n * how each command is actually run:\n *\n * - **service account** (RS256 JWT bearer) for `search notify`, which runs on\n * every deploy in CI. It never expires. A refresh token can be revoked, and\n * for an OAuth client still in *Testing* publishing status Google expires it\n * after 7 days — a bad property for a deploy step.\n * - **user OAuth** (refresh-token grant) for `search setup`, which is a\n * one-time human operation. Site verification makes the calling identity an\n * *owner* of the property, and that should be a person, not a project robot.\n *\n * `googleapis` is deliberately not a dependency — this is one REST endpoint in a\n * CLI that installs into every consumer project. See AGENTS.md.\n *\n * Internal: not a published subpath. Both `./indexing` and `./onboarding` import\n * it, so it lands in a shared chunk, which is free for a Node library. (The\n * \"don't create a shared chunk\" rule in AGENTS.md is about browser bundles and\n * the consent gate.)\n */\nimport { createSign } from 'node:crypto';\n\nconst TOKEN_URL = 'https://oauth2.googleapis.com/token';\n\n/** The fields we need from a service-account JSON key. */\nexport interface ServiceAccount {\n client_email: string;\n private_key: string;\n}\n\n/** A user OAuth credential (refresh-token flow — never a service account). */\nexport interface GoogleOAuth {\n clientId: string;\n clientSecret: string;\n refreshToken: string;\n}\n\n/**\n * Which identity to act as.\n *\n * `scope` rides on the service-account variant because that grant carries its\n * scope in the signed assertion. A user OAuth token's scopes are fixed when the\n * refresh token is minted, so there is nothing to pass at exchange time — the\n * asymmetry is the protocol's, not ours.\n */\nexport type GoogleCredential =\n | { kind: 'service-account'; account: ServiceAccount; scope: string }\n | { kind: 'oauth'; oauth: GoogleOAuth };\n\nconst b64url = (input: Buffer | string): string =>\n Buffer.from(input).toString('base64').replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n\n/** Sign the RS256 JWT assertion a service-account grant carries. */\nfunction assertionFor(account: ServiceAccount, scope: string, now: number): string {\n const iat = Math.floor(now / 1000);\n const header = b64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));\n const claims = b64url(\n JSON.stringify({ iss: account.client_email, scope, aud: TOKEN_URL, iat, exp: iat + 3600 }),\n );\n const signer = createSign('RSA-SHA256');\n signer.update(`${header}.${claims}`);\n // `\\n` survives most secret stores as a literal; normalise so a pasted key works.\n const pem = account.private_key.replace(/\\\\n/g, '\\n');\n return `${header}.${claims}.${b64url(signer.sign(pem))}`;\n}\n\n/** The half that never differs: POST the grant, surface the cause, return the token. */\nasync function requestToken(body: URLSearchParams, fetchImpl: typeof fetch): Promise<string> {\n const res = await fetchImpl(TOKEN_URL, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n if (!res.ok) {\n // The body names the cause — clock skew, wrong audience or a revoked key for a\n // service account; `invalid_grant` / `invalid_client` for OAuth — and carries\n // no secret of ours: the assertion travels in the request, not the response.\n throw new Error(`token exchange failed (${res.status}): ${await res.text()}`);\n }\n const { access_token } = (await res.json()) as { access_token?: string };\n if (!access_token) throw new Error('token exchange returned no access_token');\n return access_token;\n}\n\n/**\n * Exchange either credential for a short-lived access token.\n *\n * Throws rather than returning a result: nothing downstream can run without a\n * token, so there is no per-item decision for a caller to make. (The API calls\n * themselves do return structured results — those fail per domain.)\n *\n * `now` is a parameter so the assertion is testable without mocking the clock.\n */\nexport function googleAccessToken(\n credential: GoogleCredential,\n fetchImpl: typeof fetch = fetch,\n now: number = Date.now(),\n): Promise<string> {\n const body =\n credential.kind === 'oauth'\n ? new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: credential.oauth.clientId,\n client_secret: credential.oauth.clientSecret,\n refresh_token: credential.oauth.refreshToken,\n })\n : new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion: assertionFor(credential.account, credential.scope, now),\n });\n return requestToken(body, fetchImpl);\n}\n\n/**\n * A bearer token, plus the project its usage is attributed to.\n *\n * `quotaProject` becomes the `x-goog-user-project` header, and it is **required**\n * for a user credential — an ADC login or a `VITOPS_GOOGLE_*` refresh token\n * authenticates through a shared OAuth client that carries no project, so Google\n * refuses the call with a 403 naming the missing quota project rather than\n * guessing one. A service account already belongs to a project, so it needs none.\n *\n * It is deliberately per-call rather than baked into the token: one agency\n * identity administers many sites, each with its own project, so the identity is\n * shared and the attribution is not.\n */\nexport interface GoogleAuth {\n token: string;\n quotaProject?: string;\n}\n\n/**\n * A bare token still works everywhere an auth does.\n *\n * The token-only form is what every caller passed before per-site projects\n * existed, and a service-account caller never needs more, so widening the\n * parameter beats a breaking signature change across seven exported functions.\n */\nexport type GoogleAuthLike = string | GoogleAuth;\n\n/** Auth headers for a Google REST call, with the quota project when there is one. */\nexport function googleHeaders(\n auth: GoogleAuthLike,\n extra?: Record<string, string>,\n): Record<string, string> {\n const { token, quotaProject } = typeof auth === 'string' ? { token: auth } : auth;\n return {\n authorization: `Bearer ${token}`,\n ...(quotaProject ? { 'x-goog-user-project': quotaProject } : {}),\n ...extra,\n };\n}\n\n/** 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} as const;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAM,YAAY;AA2BlB,MAAM,UAAU,UACd,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,QAAQ,OAAO,GAAG,CAAC,CAAC,QAAQ,OAAO,GAAG,CAAC,CAAC,QAAQ,OAAO,EAAE;;AAGjG,SAAS,aAAa,SAAyB,OAAe,KAAqB;CACjF,MAAM,MAAM,KAAK,MAAM,MAAM,GAAI;CACjC,MAAM,SAAS,OAAO,KAAK,UAAU;EAAE,KAAK;EAAS,KAAK;CAAM,CAAC,CAAC;CAClE,MAAM,SAAS,OACb,KAAK,UAAU;EAAE,KAAK,QAAQ;EAAc;EAAO,KAAK;EAAW;EAAK,KAAK,MAAM;CAAK,CAAC,CAC3F;CACA,MAAM,SAAS,WAAW,YAAY;CACtC,OAAO,OAAO,GAAG,OAAO,GAAG,QAAQ;CAEnC,MAAM,MAAM,QAAQ,YAAY,QAAQ,QAAQ,IAAI;CACpD,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,OAAO,KAAK,GAAG,CAAC;AACvD;;AAGA,eAAe,aAAa,MAAuB,WAA0C;CAC3F,MAAM,MAAM,MAAM,UAAU,WAAW;EACrC,QAAQ;EACR,SAAS,EAAE,gBAAgB,oCAAoC;EAC/D,MAAM,KAAK,SAAS;CACtB,CAAC;CACD,IAAI,CAAC,IAAI,IAIP,MAAM,IAAI,MAAM,0BAA0B,IAAI,OAAO,KAAK,MAAM,IAAI,KAAK,GAAG;CAE9E,MAAM,EAAE,iBAAkB,MAAM,IAAI,KAAK;CACzC,IAAI,CAAC,cAAc,MAAM,IAAI,MAAM,yCAAyC;CAC5E,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,kBACd,YACA,YAA0B,OAC1B,MAAc,KAAK,IAAI,GACN;CAajB,OAAO,aAXL,WAAW,SAAS,UAChB,IAAI,gBAAgB;EAClB,YAAY;EACZ,WAAW,WAAW,MAAM;EAC5B,eAAe,WAAW,MAAM;EAChC,eAAe,WAAW,MAAM;CAClC,CAAC,IACD,IAAI,gBAAgB;EAClB,YAAY;EACZ,WAAW,aAAa,WAAW,SAAS,WAAW,OAAO,GAAG;CACnE,CAAC,GACmB,SAAS;AACrC;;AA8BA,SAAgB,cACd,MACA,OACwB;CACxB,MAAM,EAAE,OAAO,iBAAiB,OAAO,SAAS,WAAW,EAAE,OAAO,KAAK,IAAI;CAC7E,OAAO;EACL,eAAe,UAAU;EACzB,GAAI,eAAe,EAAE,uBAAuB,aAAa,IAAI,CAAC;EAC9D,GAAG;CACL;AACF;;AAGA,MAAa,SAAS;;CAEpB,YAAY;;CAEZ,kBAAkB;AACpB"}
1
+ {"version":3,"file":"token-DkKC5m23.mjs","names":[],"sources":["../src/google/token.ts"],"sourcesContent":["/**\n * Google OAuth2 token exchange — one request, two grants.\n *\n * Both of this package's Google modules need a bearer token, and both used to\n * mint one themselves. The two copies looked different because the *credentials*\n * differ, but everything after building the form body was identical: same\n * endpoint, same content-type, same \"surface the response body as the error\"\n * rule, same `access_token` extraction. So the seam is precise —\n * **credential → form params differs, params → token does not.**\n *\n * The two grants are kept because they are not accidental duplication; they track\n * how each command is actually run:\n *\n * - **service account** (RS256 JWT bearer) for `search notify`, which runs on\n * every deploy in CI. It never expires. A refresh token can be revoked, and\n * for an OAuth client still in *Testing* publishing status Google expires it\n * after 7 days — a bad property for a deploy step.\n * - **user OAuth** (refresh-token grant) for `search setup`, which is a\n * one-time human operation. Site verification makes the calling identity an\n * *owner* of the property, and that should be a person, not a project robot.\n *\n * `googleapis` is deliberately not a dependency — this is one REST endpoint in a\n * CLI that installs into every consumer project. See AGENTS.md.\n *\n * Internal: not a published subpath. Both `./indexing` and `./onboarding` import\n * it, so it lands in a shared chunk, which is free for a Node library. (The\n * \"don't create a shared chunk\" rule in AGENTS.md is about browser bundles and\n * the consent gate.)\n */\nimport { createSign } from 'node:crypto';\n\nconst TOKEN_URL = 'https://oauth2.googleapis.com/token';\n\n/** The fields we need from a service-account JSON key. */\nexport interface ServiceAccount {\n client_email: string;\n private_key: string;\n}\n\n/** A user OAuth credential (refresh-token flow — never a service account). */\nexport interface GoogleOAuth {\n clientId: string;\n clientSecret: string;\n refreshToken: string;\n}\n\n/**\n * Which identity to act as.\n *\n * `scope` rides on the service-account variant because that grant carries its\n * scope in the signed assertion. A user OAuth token's scopes are fixed when the\n * refresh token is minted, so there is nothing to pass at exchange time — the\n * asymmetry is the protocol's, not ours.\n */\nexport type GoogleCredential =\n | { kind: 'service-account'; account: ServiceAccount; scope: string }\n | { kind: 'oauth'; oauth: GoogleOAuth };\n\nconst b64url = (input: Buffer | string): string =>\n Buffer.from(input).toString('base64').replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n\n/** Sign the RS256 JWT assertion a service-account grant carries. */\nfunction assertionFor(account: ServiceAccount, scope: string, now: number): string {\n const iat = Math.floor(now / 1000);\n const header = b64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));\n const claims = b64url(\n JSON.stringify({ iss: account.client_email, scope, aud: TOKEN_URL, iat, exp: iat + 3600 }),\n );\n const signer = createSign('RSA-SHA256');\n signer.update(`${header}.${claims}`);\n // `\\n` survives most secret stores as a literal; normalise so a pasted key works.\n const pem = account.private_key.replace(/\\\\n/g, '\\n');\n return `${header}.${claims}.${b64url(signer.sign(pem))}`;\n}\n\n/** The half that never differs: POST the grant, surface the cause, return the token. */\nasync function requestToken(body: URLSearchParams, fetchImpl: typeof fetch): Promise<string> {\n const res = await fetchImpl(TOKEN_URL, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n if (!res.ok) {\n // The body names the cause — clock skew, wrong audience or a revoked key for a\n // service account; `invalid_grant` / `invalid_client` for OAuth — and carries\n // no secret of ours: the assertion travels in the request, not the response.\n throw new Error(`token exchange failed (${res.status}): ${await res.text()}`);\n }\n const { access_token } = (await res.json()) as { access_token?: string };\n if (!access_token) throw new Error('token exchange returned no access_token');\n return access_token;\n}\n\n/**\n * Exchange either credential for a short-lived access token.\n *\n * Throws rather than returning a result: nothing downstream can run without a\n * token, so there is no per-item decision for a caller to make. (The API calls\n * themselves do return structured results — those fail per domain.)\n *\n * `now` is a parameter so the assertion is testable without mocking the clock.\n */\nexport function googleAccessToken(\n credential: GoogleCredential,\n fetchImpl: typeof fetch = fetch,\n now: number = Date.now(),\n): Promise<string> {\n const body =\n credential.kind === 'oauth'\n ? new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: credential.oauth.clientId,\n client_secret: credential.oauth.clientSecret,\n refresh_token: credential.oauth.refreshToken,\n })\n : new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion: assertionFor(credential.account, credential.scope, now),\n });\n return requestToken(body, fetchImpl);\n}\n\n/**\n * A bearer token, plus the project its usage is attributed to.\n *\n * `quotaProject` becomes the `x-goog-user-project` header, and it is **required**\n * for a user credential — an ADC login or a `VITOPS_GOOGLE_*` refresh token\n * authenticates through a shared OAuth client that carries no project, so Google\n * refuses the call with a 403 naming the missing quota project rather than\n * guessing one. A service account already belongs to a project, so it needs none.\n *\n * It is deliberately per-call rather than baked into the token: one agency\n * identity administers many sites, each with its own project, so the identity is\n * shared and the attribution is not.\n */\nexport interface GoogleAuth {\n token: string;\n quotaProject?: string;\n}\n\n/**\n * A bare token still works everywhere an auth does.\n *\n * The token-only form is what every caller passed before per-site projects\n * existed, and a service-account caller never needs more, so widening the\n * parameter beats a breaking signature change across seven exported functions.\n */\nexport type GoogleAuthLike = string | GoogleAuth;\n\n/** Auth headers for a Google REST call, with the quota project when there is one. */\nexport function googleHeaders(\n auth: GoogleAuthLike,\n extra?: Record<string, string>,\n): Record<string, string> {\n const { token, quotaProject } = typeof auth === 'string' ? { token: auth } : auth;\n return {\n authorization: `Bearer ${token}`,\n ...(quotaProject ? { 'x-goog-user-project': quotaProject } : {}),\n ...extra,\n };\n}\n\n/** Percent-encode a Search Console `siteUrl` (e.g. `sc-domain:acme.ca`) for a path segment. */\nexport const encodeSite = (siteUrl: string): string => encodeURIComponent(siteUrl);\n\n/**\n * Render a failed response's detail: a caller-supplied hint on a specific status\n * (e.g. what a `403` almost always means for this endpoint), or the raw body\n * otherwise. Shared by every REST call here that follows the \"PUT and report\n * why\" shape — `gsc.ts`'s `submitSitemap`, `onboarding/google.ts`'s `addSite`.\n */\nexport async function errorDetail(\n res: Response,\n hint?: (status: number) => string | undefined,\n): Promise<string> {\n const named = hint?.(res.status);\n return named ?? `${res.status}: ${await res.text()}`;\n}\n\n/** Scopes, named once so a caller never spells one out. */\nexport const SCOPES = {\n /** Search Console (`sitemaps.submit`, `urlInspection`, `sites`). */\n webmasters: 'https://www.googleapis.com/auth/webmasters',\n /** Site Verification API. */\n siteVerification: 'https://www.googleapis.com/auth/siteverification',\n /** Business Profile (reviews). The GCP project must be allowlisted by Google. */\n businessProfile: 'https://www.googleapis.com/auth/business.manage',\n} as const;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAM,YAAY;AA2BlB,MAAM,UAAU,UACd,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,QAAQ,OAAO,GAAG,CAAC,CAAC,QAAQ,OAAO,GAAG,CAAC,CAAC,QAAQ,OAAO,EAAE;;AAGjG,SAAS,aAAa,SAAyB,OAAe,KAAqB;CACjF,MAAM,MAAM,KAAK,MAAM,MAAM,GAAI;CACjC,MAAM,SAAS,OAAO,KAAK,UAAU;EAAE,KAAK;EAAS,KAAK;CAAM,CAAC,CAAC;CAClE,MAAM,SAAS,OACb,KAAK,UAAU;EAAE,KAAK,QAAQ;EAAc;EAAO,KAAK;EAAW;EAAK,KAAK,MAAM;CAAK,CAAC,CAC3F;CACA,MAAM,SAAS,WAAW,YAAY;CACtC,OAAO,OAAO,GAAG,OAAO,GAAG,QAAQ;CAEnC,MAAM,MAAM,QAAQ,YAAY,QAAQ,QAAQ,IAAI;CACpD,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,OAAO,KAAK,GAAG,CAAC;AACvD;;AAGA,eAAe,aAAa,MAAuB,WAA0C;CAC3F,MAAM,MAAM,MAAM,UAAU,WAAW;EACrC,QAAQ;EACR,SAAS,EAAE,gBAAgB,oCAAoC;EAC/D,MAAM,KAAK,SAAS;CACtB,CAAC;CACD,IAAI,CAAC,IAAI,IAIP,MAAM,IAAI,MAAM,0BAA0B,IAAI,OAAO,KAAK,MAAM,IAAI,KAAK,GAAG;CAE9E,MAAM,EAAE,iBAAkB,MAAM,IAAI,KAAK;CACzC,IAAI,CAAC,cAAc,MAAM,IAAI,MAAM,yCAAyC;CAC5E,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,kBACd,YACA,YAA0B,OAC1B,MAAc,KAAK,IAAI,GACN;CAajB,OAAO,aAXL,WAAW,SAAS,UAChB,IAAI,gBAAgB;EAClB,YAAY;EACZ,WAAW,WAAW,MAAM;EAC5B,eAAe,WAAW,MAAM;EAChC,eAAe,WAAW,MAAM;CAClC,CAAC,IACD,IAAI,gBAAgB;EAClB,YAAY;EACZ,WAAW,aAAa,WAAW,SAAS,WAAW,OAAO,GAAG;CACnE,CAAC,GACmB,SAAS;AACrC;;AA8BA,SAAgB,cACd,MACA,OACwB;CACxB,MAAM,EAAE,OAAO,iBAAiB,OAAO,SAAS,WAAW,EAAE,OAAO,KAAK,IAAI;CAC7E,OAAO;EACL,eAAe,UAAU;EACzB,GAAI,eAAe,EAAE,uBAAuB,aAAa,IAAI,CAAC;EAC9D,GAAG;CACL;AACF;;AAGA,MAAa,cAAc,YAA4B,mBAAmB,OAAO;;;;;;;AAQjF,eAAsB,YACpB,KACA,MACiB;CAEjB,OADc,OAAO,IAAI,MAAM,KACf,GAAG,IAAI,OAAO,IAAI,MAAM,IAAI,KAAK;AACnD;;AAGA,MAAa,SAAS;;CAEpB,YAAY;;CAEZ,kBAAkB;;CAElB,iBAAiB;AACnB"}
@@ -1,4 +1,4 @@
1
- import { i as TrackingData, n as TrackingCategory, r as TrackingConfig, t as ConversionEvent } from "../types-C3lHaSrU.mjs";
1
+ import { i as TrackingData, n as TrackingCategory, r as TrackingConfig, t as ConversionEvent } from "../types-CHHjzRrv.mjs";
2
2
 
3
3
  //#region src/tracking/params.d.ts
4
4
  /**
@@ -55,7 +55,6 @@ interface ConversionEvent {
55
55
  type: 'form' | 'call';
56
56
  /** Submitted fields, for `type: 'form'`. Honeypots are stripped before this. */
57
57
  formData?: Record<string, string> | undefined;
58
- /** The dialled number, for `type: 'call'`. */
59
58
  phone?: string | undefined;
60
59
  /** Attribution from the `_ac` cookie, or null when the visitor arrived cold. */
61
60
  tracking: TrackingData | null;
@@ -66,4 +65,4 @@ interface ConversionEvent {
66
65
  }
67
66
  //#endregion
68
67
  export { TrackingData as i, TrackingCategory as n, TrackingConfig as r, ConversionEvent as t };
69
- //# sourceMappingURL=types-C3lHaSrU.d.mts.map
68
+ //# sourceMappingURL=types-CHHjzRrv.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-CHHjzRrv.d.mts","names":[],"sources":["../src/tracking/types.ts"],"mappings":";;AAiBA;;;;AAA4B;AAE5B;;;KAFY,gBAAA;AAAA,UAEK,cAAA;EACf,OAAA;EAOW;;;AACF;AAWX;;EAZE,QAAA,GAAW,gBAAgB;EAC3B,SAAA;AAAA;;;;;;AAoBU;AAeZ;;UAxBiB,YAAA;EAAA,CACd,KAAA;EAwBD;EAtBA,WAAA;EAwBW;EAtBX,QAAA;EAyBA;EAvBA,EAAA;EAwBA;EAtBA,UAAA;AAAA;;AAyBE;;;;;;;;;;;UAVa,eAAA;EACf,IAAA;;EAEA,QAAA,GAAW,MAAA;EACX,KAAA;;EAEA,QAAA,EAAU,YAAY;EACtB,SAAA;EACA,EAAA;;EAEA,EAAA;AAAA"}
package/package.json CHANGED
@@ -1,13 +1,21 @@
1
1
  {
2
2
  "name": "@getvitops/utils",
3
- "version": "7.0.0",
3
+ "version": "8.0.0",
4
4
  "description": "Shared build-time utilities for the Vitops toolchain — favicon generation and friends.",
5
+ "homepage": "https://docs.vitops.ca",
6
+ "bugs": "https://github.com/getvitops/design-system/issues",
5
7
  "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/getvitops/design-system.git",
11
+ "directory": "packages/utils"
12
+ },
6
13
  "files": [
7
14
  "dist",
8
15
  "CHANGELOG.md"
9
16
  ],
10
17
  "type": "module",
18
+ "sideEffects": false,
11
19
  "types": "./dist/index.d.mts",
12
20
  "exports": {
13
21
  ".": {
@@ -30,6 +38,10 @@
30
38
  "types": "./dist/onboarding/index.d.mts",
31
39
  "import": "./dist/onboarding/index.mjs"
32
40
  },
41
+ "./reviews": {
42
+ "types": "./dist/reviews/index.d.mts",
43
+ "import": "./dist/reviews/index.mjs"
44
+ },
33
45
  "./ads": {
34
46
  "types": "./dist/ads/index.d.mts",
35
47
  "import": "./dist/ads/index.mjs"
@@ -45,6 +57,14 @@
45
57
  "./tracking": {
46
58
  "types": "./dist/tracking/index.d.mts",
47
59
  "import": "./dist/tracking/index.mjs"
60
+ },
61
+ "./security": {
62
+ "types": "./dist/security/index.d.mts",
63
+ "import": "./dist/security/index.mjs"
64
+ },
65
+ "./schema": {
66
+ "types": "./dist/schema/index.d.mts",
67
+ "import": "./dist/schema/index.mjs"
48
68
  }
49
69
  },
50
70
  "publishConfig": {
@@ -57,6 +77,9 @@
57
77
  "sharp": "^0.35.3"
58
78
  },
59
79
  "devDependencies": {
60
- "vitest": "^4.1.9"
80
+ "vitest": "4.1.9"
81
+ },
82
+ "engines": {
83
+ "node": ">=24"
61
84
  }
62
85
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"cloudflare-CD8vwrgj.mjs","names":[],"sources":["../src/onboarding/cloudflare.ts"],"sourcesContent":["/**\n * Cloudflare DNS — the one thing this command writes.\n *\n * Scoped to exactly what onboarding needs and nothing more: find a zone by name,\n * read the apex TXT records, and *create* the verification TXT when it is missing.\n * There is deliberately no update and no delete — the command's contract is that it\n * never removes a DNS record, so the destructive verbs are simply absent rather\n * than guarded.\n *\n * The token is a `Zone:DNS:Edit` credential from the environment\n * (`CLOUDFLARE_API_TOKEN`); listing a zone by name also needs `Zone:Read`, which\n * Cloudflare's standard \"Edit zone DNS\" token template already bundles. As with the\n * indexing executors, `fetchImpl` is injected so the request shape is testable\n * without the network, and every call returns a structured result rather than\n * throwing — the caller decides what a failure means per domain.\n */\nconst API = 'https://api.cloudflare.com/client/v4';\n\ninterface CfEnvelope<T> {\n success: boolean;\n errors?: { code: number; message: string }[];\n result?: T;\n}\n\nconst cfError = (status: number, body: CfEnvelope<unknown>): string => {\n const first = body.errors?.[0];\n return first ? `${first.code} ${first.message}` : `HTTP ${status}`;\n};\n\n/** Find the zone id for a domain (`GET /zones?name=`). */\nexport async function findZoneId(\n token: string,\n domain: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; zoneId?: string; message?: string }> {\n const res = await fetchImpl(`${API}/zones?name=${encodeURIComponent(domain)}`, {\n headers: { authorization: `Bearer ${token}` },\n });\n const body = (await res.json()) as CfEnvelope<{ id: string; name: string }[]>;\n if (!res.ok || !body.success)\n return { ok: false, status: res.status, message: cfError(res.status, body) };\n const zone = body.result?.[0];\n if (!zone)\n return {\n ok: false,\n status: res.status,\n // Name the scope: this lookup is a GET /zones?name=, which needs\n // `Zone:Read` — a token carrying only `Zone:DNS:Edit` reaches here and\n // reads as \"the zone isn't in this account\", which sends you to the\n // wrong dashboard.\n message: `no Cloudflare zone named \"${domain}\" (is it in this account, and does the token carry Zone:Read as well as Zone:DNS:Edit? The \"Edit zone DNS\" template covers both.)`,\n };\n return { ok: true, status: res.status, zoneId: zone.id };\n}\n\n/** List the apex TXT record contents for a zone. */\nexport async function listApexTxt(\n token: string,\n zoneId: string,\n domain: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; contents: string[]; message?: string }> {\n const res = await fetchImpl(\n `${API}/zones/${zoneId}/dns_records?type=TXT&name=${encodeURIComponent(domain)}`,\n { headers: { authorization: `Bearer ${token}` } },\n );\n const body = (await res.json()) as CfEnvelope<{ content: string }[]>;\n if (!res.ok || !body.success)\n return { ok: false, status: res.status, contents: [], message: cfError(res.status, body) };\n // Cloudflare returns TXT content quoted; normalise so an exact compare works.\n const contents = (body.result ?? []).map((r) => r.content.replace(/^\"|\"$/g, ''));\n return { ok: true, status: res.status, contents };\n}\n\n/**\n * Create an apex TXT record. **Create only** — never called when one already\n * exists (the planner decides that), and there is no sibling that edits or removes.\n */\nexport async function createApexTxt(\n token: string,\n zoneId: string,\n domain: string,\n content: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<{ ok: boolean; status: number; message?: string }> {\n const res = await fetchImpl(`${API}/zones/${zoneId}/dns_records`, {\n method: 'POST',\n headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },\n body: JSON.stringify({ type: 'TXT', name: domain, content }),\n });\n const body = (await res.json()) as CfEnvelope<{ id: string }>;\n if (!res.ok || !body.success)\n return { ok: false, status: res.status, message: cfError(res.status, body) };\n return { ok: true, status: res.status };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,MAAM,MAAM;AAQZ,MAAM,WAAW,QAAgB,SAAsC;CACrE,MAAM,QAAQ,KAAK,SAAS;CAC5B,OAAO,QAAQ,GAAG,MAAM,KAAK,GAAG,MAAM,YAAY,QAAQ;AAC5D;;AAGA,eAAsB,WACpB,OACA,QACA,YAA0B,OACmD;CAC7E,MAAM,MAAM,MAAM,UAAU,GAAG,IAAI,cAAc,mBAAmB,MAAM,KAAK,EAC7E,SAAS,EAAE,eAAe,UAAU,QAAQ,EAC9C,CAAC;CACD,MAAM,OAAQ,MAAM,IAAI,KAAK;CAC7B,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,SACnB,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ,SAAS,QAAQ,IAAI,QAAQ,IAAI;CAAE;CAC7E,MAAM,OAAO,KAAK,SAAS;CAC3B,IAAI,CAAC,MACH,OAAO;EACL,IAAI;EACJ,QAAQ,IAAI;EAKZ,SAAS,6BAA6B,OAAO;CAC/C;CACF,OAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;EAAQ,QAAQ,KAAK;CAAG;AACzD;;AAGA,eAAsB,YACpB,OACA,QACA,QACA,YAA0B,OACsD;CAChF,MAAM,MAAM,MAAM,UAChB,GAAG,IAAI,SAAS,OAAO,6BAA6B,mBAAmB,MAAM,KAC7E,EAAE,SAAS,EAAE,eAAe,UAAU,QAAQ,EAAE,CAClD;CACA,MAAM,OAAQ,MAAM,IAAI,KAAK;CAC7B,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,SACnB,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ,UAAU,CAAC;EAAG,SAAS,QAAQ,IAAI,QAAQ,IAAI;CAAE;CAE3F,MAAM,YAAY,KAAK,UAAU,CAAC,EAAC,CAAE,KAAK,MAAM,EAAE,QAAQ,QAAQ,UAAU,EAAE,CAAC;CAC/E,OAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;EAAQ;CAAS;AAClD;;;;;AAMA,eAAsB,cACpB,OACA,QACA,QACA,SACA,YAA0B,OACkC;CAC5D,MAAM,MAAM,MAAM,UAAU,GAAG,IAAI,SAAS,OAAO,eAAe;EAChE,QAAQ;EACR,SAAS;GAAE,eAAe,UAAU;GAAS,gBAAgB;EAAmB;EAChF,MAAM,KAAK,UAAU;GAAE,MAAM;GAAO,MAAM;GAAQ;EAAQ,CAAC;CAC7D,CAAC;CACD,MAAM,OAAQ,MAAM,IAAI,KAAK;CAC7B,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,SACnB,OAAO;EAAE,IAAI;EAAO,QAAQ,IAAI;EAAQ,SAAS,QAAQ,IAAI,QAAQ,IAAI;CAAE;CAC7E,OAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;CAAO;AACxC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"types-C3lHaSrU.d.mts","names":[],"sources":["../src/tracking/types.ts"],"mappings":";;AAiBA;;;;AAA4B;AAE5B;;;KAFY,gBAAA;AAAA,UAEK,cAAA;EACf,OAAA;EAOW;;;AACF;AAWX;;EAZE,QAAA,GAAW,gBAAgB;EAC3B,SAAA;AAAA;;;;;;AAoBU;AAeZ;;UAxBiB,YAAA;EAAA,CACd,KAAA;EAwBD;EAtBA,WAAA;EAwBW;EAtBX,QAAA;EA0BA;EAxBA,EAAA;EAyBA;EAvBA,UAAA;AAAA;;AA0BE;;;;;;;;;;;UAXa,eAAA;EACf,IAAA;;EAEA,QAAA,GAAW,MAAA;;EAEX,KAAA;;EAEA,QAAA,EAAU,YAAY;EACtB,SAAA;EACA,EAAA;;EAEA,EAAA;AAAA"}