@wildo-ai/saas-website 1.1.4 → 1.1.6

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 (265) hide show
  1. package/dist/esm/astro/blog-post-bridge.d.ts +23 -20
  2. package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -1
  3. package/dist/esm/astro/blog-post-bridge.js +13 -6
  4. package/dist/esm/astro/blog-post-bridge.js.map +1 -1
  5. package/dist/esm/astro/blog-post-head.renderer.d.ts.map +1 -1
  6. package/dist/esm/astro/bridge-runtime.d.ts +55 -26
  7. package/dist/esm/astro/bridge-runtime.d.ts.map +1 -1
  8. package/dist/esm/astro/bridge-runtime.js +21 -7
  9. package/dist/esm/astro/bridge-runtime.js.map +1 -1
  10. package/dist/esm/astro/collect-expected-label-keys.d.ts.map +1 -1
  11. package/dist/esm/astro/headers-renderer.d.ts.map +1 -1
  12. package/dist/esm/astro/i18n-routing.helper.d.ts.map +1 -1
  13. package/dist/esm/astro/label-pack-loader.d.ts +4 -4
  14. package/dist/esm/astro/label-pack-loader.d.ts.map +1 -1
  15. package/dist/esm/astro/label-pack-loader.js +14 -4
  16. package/dist/esm/astro/label-pack-loader.js.map +1 -1
  17. package/dist/esm/astro/llms-txt-server.d.ts.map +1 -1
  18. package/dist/esm/astro/pick-labels-for-page.d.ts.map +1 -1
  19. package/dist/esm/astro/robots-renderer.d.ts +9 -8
  20. package/dist/esm/astro/robots-renderer.d.ts.map +1 -1
  21. package/dist/esm/astro/robots-renderer.js +15 -15
  22. package/dist/esm/astro/robots-renderer.js.map +1 -1
  23. package/dist/esm/astro/sitemap-coverage.d.ts.map +1 -1
  24. package/dist/esm/astro/structured-data.renderer.d.ts.map +1 -1
  25. package/dist/esm/astro/website-page-head.renderer.d.ts +1 -1
  26. package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -1
  27. package/dist/esm/astro/website-page-head.renderer.js +2 -1
  28. package/dist/esm/astro/website-page-head.renderer.js.map +1 -1
  29. package/dist/esm/astro/website-page-runtime.helper.d.ts.map +1 -1
  30. package/dist/esm/astro/website-site-context.d.ts +5 -2
  31. package/dist/esm/astro/website-site-context.d.ts.map +1 -1
  32. package/dist/esm/astro/website-site-context.js.map +1 -1
  33. package/dist/esm/astro-island.d.ts.map +1 -1
  34. package/dist/esm/astro.d.ts.map +1 -1
  35. package/dist/esm/companion-exports.d.ts +8 -15
  36. package/dist/esm/companion-exports.d.ts.map +1 -1
  37. package/dist/esm/companion-exports.js +8 -15
  38. package/dist/esm/companion-exports.js.map +1 -1
  39. package/dist/esm/components/low-level/WebsiteInternalButton.d.ts.map +1 -1
  40. package/dist/esm/components/low-level/WebsiteInternalLink.d.ts.map +1 -1
  41. package/dist/esm/config/define-website-config.d.ts +3 -2
  42. package/dist/esm/config/define-website-config.d.ts.map +1 -1
  43. package/dist/esm/config/define-website-config.js +10 -10
  44. package/dist/esm/config/define-website-config.js.map +1 -1
  45. package/dist/esm/config/index.d.ts.map +1 -1
  46. package/dist/esm/config/load-website-config.d.ts +6 -0
  47. package/dist/esm/config/load-website-config.d.ts.map +1 -1
  48. package/dist/esm/config/load-website-config.js +20 -10
  49. package/dist/esm/config/load-website-config.js.map +1 -1
  50. package/dist/esm/config/load-website-pricing-catalog.d.ts +13 -0
  51. package/dist/esm/config/load-website-pricing-catalog.d.ts.map +1 -0
  52. package/dist/esm/config/load-website-pricing-catalog.js +34 -0
  53. package/dist/esm/config/load-website-pricing-catalog.js.map +1 -0
  54. package/dist/esm/config/load-website-site-context.d.ts +53 -0
  55. package/dist/esm/config/load-website-site-context.d.ts.map +1 -0
  56. package/dist/esm/config/load-website-site-context.js +98 -0
  57. package/dist/esm/config/load-website-site-context.js.map +1 -0
  58. package/dist/esm/config/wildo-website-config.schemas.d.ts +10 -5
  59. package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -1
  60. package/dist/esm/config/wildo-website-config.schemas.js +10 -5
  61. package/dist/esm/config/wildo-website-config.schemas.js.map +1 -1
  62. package/dist/esm/config-loader.d.ts +2 -0
  63. package/dist/esm/config-loader.d.ts.map +1 -1
  64. package/dist/esm/config-loader.js +2 -0
  65. package/dist/esm/config-loader.js.map +1 -1
  66. package/dist/esm/core/anonymous-session/InboundContactForm.d.ts +26 -6
  67. package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -1
  68. package/dist/esm/core/anonymous-session/InboundContactForm.js +51 -33
  69. package/dist/esm/core/anonymous-session/InboundContactForm.js.map +1 -1
  70. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +14 -3
  71. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -1
  72. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +28 -13
  73. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -1
  74. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +24 -1
  75. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -1
  76. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +46 -3
  77. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -1
  78. package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts +36 -0
  79. package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts.map +1 -0
  80. package/dist/esm/core/anonymous-session/website-submission-challenge.js +69 -0
  81. package/dist/esm/core/anonymous-session/website-submission-challenge.js.map +1 -0
  82. package/dist/esm/core/consent/WebsiteConsentBanner.d.ts +6 -0
  83. package/dist/esm/core/consent/WebsiteConsentBanner.d.ts.map +1 -0
  84. package/dist/esm/core/consent/WebsiteConsentBanner.js +129 -0
  85. package/dist/esm/core/consent/WebsiteConsentBanner.js.map +1 -0
  86. package/dist/esm/core/consent/WebsiteConsentContext.d.ts +85 -0
  87. package/dist/esm/core/consent/WebsiteConsentContext.d.ts.map +1 -0
  88. package/dist/esm/core/consent/WebsiteConsentContext.js +89 -0
  89. package/dist/esm/core/consent/WebsiteConsentContext.js.map +1 -0
  90. package/dist/esm/core/consent/WebsiteConsentGate.d.ts +27 -0
  91. package/dist/esm/core/consent/WebsiteConsentGate.d.ts.map +1 -0
  92. package/dist/esm/core/consent/WebsiteConsentGate.js +9 -0
  93. package/dist/esm/core/consent/WebsiteConsentGate.js.map +1 -0
  94. package/dist/esm/core/consent/useWebsiteConsent.d.ts +10 -0
  95. package/dist/esm/core/consent/useWebsiteConsent.d.ts.map +1 -0
  96. package/dist/esm/core/consent/useWebsiteConsent.js +17 -0
  97. package/dist/esm/core/consent/useWebsiteConsent.js.map +1 -0
  98. package/dist/esm/core/consent/website-consent-storage.d.ts +22 -0
  99. package/dist/esm/core/consent/website-consent-storage.d.ts.map +1 -0
  100. package/dist/esm/core/consent/website-consent-storage.js +102 -0
  101. package/dist/esm/core/consent/website-consent-storage.js.map +1 -0
  102. package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -1
  103. package/dist/esm/core/contexts/WebsitePageContext.js.map +1 -1
  104. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +33 -6
  105. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -1
  106. package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -1
  107. package/dist/esm/core/contexts/WebsiteSectionContext.d.ts.map +1 -1
  108. package/dist/esm/core/contexts/WebsiteSectionContext.js.map +1 -1
  109. package/dist/esm/core/contexts/useWebsitePage.d.ts.map +1 -1
  110. package/dist/esm/core/contexts/useWebsiteRuntime.d.ts +18 -0
  111. package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -1
  112. package/dist/esm/core/contexts/useWebsiteRuntime.js +28 -2
  113. package/dist/esm/core/contexts/useWebsiteRuntime.js.map +1 -1
  114. package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -1
  115. package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts +57 -0
  116. package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts.map +1 -0
  117. package/dist/esm/core/external-providers/WebsiteProviderComponent.js +56 -0
  118. package/dist/esm/core/external-providers/WebsiteProviderComponent.js.map +1 -0
  119. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts +1 -2
  120. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -1
  121. package/dist/esm/core/external-providers/frontend-provider-registry.website.js.map +1 -1
  122. package/dist/esm/core/external-providers/provider-component-registry.website.d.ts +63 -0
  123. package/dist/esm/core/external-providers/provider-component-registry.website.d.ts.map +1 -0
  124. package/dist/esm/core/external-providers/provider-component-registry.website.js +68 -0
  125. package/dist/esm/core/external-providers/provider-component-registry.website.js.map +1 -0
  126. package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts +36 -0
  127. package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts.map +1 -0
  128. package/dist/esm/core/external-providers/useWebsiteProviderComponent.js +59 -0
  129. package/dist/esm/core/external-providers/useWebsiteProviderComponent.js.map +1 -0
  130. package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts +43 -10
  131. package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts.map +1 -1
  132. package/dist/esm/core/external-providers/useWebsiteProviderScripts.js +92 -29
  133. package/dist/esm/core/external-providers/useWebsiteProviderScripts.js.map +1 -1
  134. package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts +11 -0
  135. package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts.map +1 -0
  136. package/dist/esm/core/external-providers/website-provider-sdk-activations.js +14 -0
  137. package/dist/esm/core/external-providers/website-provider-sdk-activations.js.map +1 -0
  138. package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -1
  139. package/dist/esm/core/factories/define-website-section.d.ts +3 -3
  140. package/dist/esm/core/factories/define-website-section.d.ts.map +1 -1
  141. package/dist/esm/core/factories/define-website-section.js +3 -3
  142. package/dist/esm/core/factories/define-website-section.js.map +1 -1
  143. package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts.map +1 -1
  144. package/dist/esm/core/hooks/useWebsiteLabel.d.ts.map +1 -1
  145. package/dist/esm/core/layouts/WebsitePageLayout.d.ts +21 -1
  146. package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -1
  147. package/dist/esm/core/layouts/WebsitePageLayout.js +39 -8
  148. package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -1
  149. package/dist/esm/core/layouts/WebsiteSection.d.ts.map +1 -1
  150. package/dist/esm/core/routing/internal-routing.utils.d.ts.map +1 -1
  151. package/dist/esm/index.d.ts +36 -65
  152. package/dist/esm/index.d.ts.map +1 -1
  153. package/dist/esm/index.js +36 -65
  154. package/dist/esm/index.js.map +1 -1
  155. package/dist/esm/mdx/BlogPost.d.ts.map +1 -1
  156. package/dist/esm/mdx/blog-post-collection.config.d.ts.map +1 -1
  157. package/dist/esm/mdx/blog-post-frontmatter-source.d.ts.map +1 -1
  158. package/dist/esm/mdx/index.d.ts.map +1 -1
  159. package/dist/esm/mdx.d.ts.map +1 -1
  160. package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts.map +1 -1
  161. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts +35 -6
  162. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -1
  163. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js +39 -5
  164. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js.map +1 -1
  165. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts +61 -0
  166. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts.map +1 -0
  167. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js +73 -0
  168. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js.map +1 -0
  169. package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -1
  170. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts +32 -0
  171. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts.map +1 -0
  172. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js +36 -0
  173. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js.map +1 -0
  174. package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts.map +1 -1
  175. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts +6 -5
  176. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -1
  177. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js +8 -7
  178. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js.map +1 -1
  179. package/dist/esm/schemas/refs/page-ref.schemas.d.ts.map +1 -1
  180. package/dist/esm/schemas/refs/section-ref.schemas.d.ts.map +1 -1
  181. package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -1
  182. package/dist/esm/schemas/sections/website-section-category.shared.js.map +1 -1
  183. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts +14 -7
  184. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -1
  185. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js +14 -7
  186. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js.map +1 -1
  187. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts +57 -0
  188. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts.map +1 -0
  189. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js +76 -0
  190. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js.map +1 -0
  191. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts +23 -0
  192. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts.map +1 -1
  193. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js +20 -2
  194. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js.map +1 -1
  195. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts +7 -5
  196. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts.map +1 -1
  197. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js.map +1 -1
  198. package/dist/esm/schemas/validators/label-pack.validator.d.ts.map +1 -1
  199. package/dist/tsconfig.build.tsbuildinfo +1 -1
  200. package/package.json +6 -29
  201. package/src/__tests__/bundle-isolation.test.ts +19 -22
  202. package/src/astro/__tests__/blog-post-bridge.test.tsx +58 -1
  203. package/src/astro/__tests__/bridge-runtime.test.tsx +44 -1
  204. package/src/astro/__tests__/label-pack-loader.test.ts +29 -1
  205. package/src/astro/blog-post-bridge.tsx +36 -26
  206. package/src/astro/bridge-runtime.tsx +79 -33
  207. package/src/astro/label-pack-loader.ts +12 -4
  208. package/src/astro/robots-renderer.ts +15 -15
  209. package/src/astro/website-page-head.renderer.ts +2 -2
  210. package/src/astro/website-site-context.ts +5 -2
  211. package/src/companion-exports.ts +8 -15
  212. package/src/config/__tests__/website-build-origins.test.ts +52 -0
  213. package/src/config/define-website-config.ts +10 -10
  214. package/src/config/load-website-config.ts +27 -15
  215. package/src/config/load-website-pricing-catalog.ts +52 -0
  216. package/src/config/load-website-site-context.ts +145 -0
  217. package/src/config/wildo-website-config.schemas.ts +10 -5
  218. package/src/config-loader.ts +2 -0
  219. package/src/core/__tests__/WebsitePageLayout.test.tsx +1 -1
  220. package/src/core/__tests__/contexts.test.tsx +20 -1
  221. package/src/core/__tests__/website-consent.test.tsx +312 -0
  222. package/src/core/anonymous-session/InboundContactForm.tsx +100 -45
  223. package/src/core/anonymous-session/__tests__/InboundContactForm.labels.test.tsx +108 -0
  224. package/src/core/anonymous-session/__tests__/InboundContactForm.submission-challenge.test.tsx +139 -0
  225. package/src/core/anonymous-session/__tests__/inbound-contact-form.schema.test.ts +11 -4
  226. package/src/core/anonymous-session/inbound-contact-form.schema.ts +30 -13
  227. package/src/core/anonymous-session/website-anonymous-session-client.ts +52 -3
  228. package/src/core/anonymous-session/website-submission-challenge.ts +99 -0
  229. package/src/core/consent/WebsiteConsentBanner.tsx +223 -0
  230. package/src/core/consent/WebsiteConsentContext.tsx +164 -0
  231. package/src/core/consent/WebsiteConsentGate.tsx +34 -0
  232. package/src/core/consent/useWebsiteConsent.ts +18 -0
  233. package/src/core/consent/website-consent-storage.ts +116 -0
  234. package/src/core/contexts/WebsiteRuntimeContext.tsx +33 -6
  235. package/src/core/contexts/useWebsiteRuntime.ts +33 -2
  236. package/src/core/external-providers/WebsiteProviderComponent.tsx +153 -0
  237. package/src/core/external-providers/__tests__/frontend-provider-registry.website.test.ts +4 -0
  238. package/src/core/external-providers/__tests__/website-provider-component.test.tsx +238 -0
  239. package/src/core/external-providers/__tests__/website-provider-sdks.test.tsx +72 -0
  240. package/src/core/external-providers/frontend-provider-registry.website.ts +1 -2
  241. package/src/core/external-providers/provider-component-registry.website.ts +115 -0
  242. package/src/core/external-providers/useWebsiteProviderComponent.ts +76 -0
  243. package/src/core/external-providers/useWebsiteProviderScripts.ts +114 -40
  244. package/src/core/external-providers/website-provider-sdk-activations.ts +23 -0
  245. package/src/core/factories/define-website-section.ts +3 -3
  246. package/src/core/layouts/WebsitePageLayout.tsx +75 -7
  247. package/src/index.ts +38 -65
  248. package/src/schemas/__tests__/design-token-css-property.test.ts +72 -0
  249. package/src/schemas/__tests__/pricing-offer-structured-data.test.ts +122 -0
  250. package/src/schemas/__tests__/structured-data-reconciliation.test.ts +35 -0
  251. package/src/schemas/design-tokens/website-design-tokens.shared.schemas.ts +45 -5
  252. package/src/schemas/label-keys/__tests__/scaffolded-consent-label-pack.parity.test.ts +83 -0
  253. package/src/schemas/label-keys/website-consent-label-keys.schemas.ts +78 -0
  254. package/src/schemas/label-keys/website-provider-component-label-keys.schemas.ts +40 -0
  255. package/src/schemas/manifests/website-root-config.shared.schemas.ts +10 -9
  256. package/src/schemas/sections/website-section-definition.shared.schemas.ts +14 -7
  257. package/src/schemas/structured-data/pricing-offer-structured-data.shared.ts +144 -0
  258. package/src/schemas/structured-data/structured-data-reconciliation.shared.ts +46 -1
  259. package/src/schemas/structured-data/website-structured-data.shared.schemas.ts +7 -5
  260. package/dist/esm/core/external-providers/provider-scripts.website.d.ts +0 -68
  261. package/dist/esm/core/external-providers/provider-scripts.website.d.ts.map +0 -1
  262. package/dist/esm/core/external-providers/provider-scripts.website.js +0 -99
  263. package/dist/esm/core/external-providers/provider-scripts.website.js.map +0 -1
  264. package/src/core/external-providers/__tests__/provider-scripts.website.test.ts +0 -146
  265. package/src/core/external-providers/provider-scripts.website.ts +0 -126
@@ -186,11 +186,16 @@ export const WildoMarketingWebsiteConfigSchema = z
186
186
  * Origin of the SaaS application the marketing site links into
187
187
  * (signup / login / pricing → checkout).
188
188
  *
189
- * **Today**: NO runtime consumer. The cross-origin handoff
190
- * surface that will read it does not exist yet. Authored
191
- * eagerly so the link-surface PR is a single framework-owned move
192
- * — until then the field is exercised only by the schema
193
- * parser.
189
+ * Read at runtime since #493: each `.astro` page forwards it to its
190
+ * bridge island, and the CTA reads it off `WebsiteRuntimeContext`.
191
+ *
192
+ * Per environment: a deployment's app lives at a different host in each
193
+ * environment, and a static site bakes the value in at build time, so the
194
+ * generated deploy workflow passes the environment's app origin as
195
+ * `PUBLIC_APP_ORIGIN` — derived from the infra config's `serviceHosts` and
196
+ * `publicDomain`, not authored a second time. An application's
197
+ * `wildo.website.config.ts` reads that first and keeps its authored value
198
+ * as the default for a build that passes none.
194
199
  *
195
200
  * Distinct from the general `WildoSaasConfig.services.<name>`
196
201
  * URL surface because the marketing site is a separate Astro
@@ -1 +1 @@
1
- {"version":3,"file":"wildo-website-config.schemas.js","sourceRoot":"","sources":["../../../src/config/wildo-website-config.schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsHG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,oDAAoD,CAAC;AAEvF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,YAAY,CAAC;IACtD,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC;IACjC,IAAI,EAAE,CAAC;SACJ,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,KAAK,CACJ,cAAc,EACd,iFAAiF,CAClF;CACJ,CAAC,CAAC;AAGH;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAAG,CAAC;KAC/C,YAAY,CAAC;IACZ;;;;;;;;;;OAUG;IACH,YAAY,EAAE,CAAC;SACZ,GAAG,EAAE;SACL,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE;QACvC,KAAK,EACH,2EAA2E;YAC3E,iFAAiF;KACpF,CAAC;IAEJ;;;;;;;;;;;;;;OAcG;IACH,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE;IAElB;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,SAAS,EAAE,CAAC;SACT,GAAG,EAAE;SACL,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE;QACvC,KAAK,EACH,6EAA6E;YAC7E,8EAA8E;YAC9E,kBAAkB;KACrB,CAAC;SACD,QAAQ,EAAE;IAEb;;;;;OAKG;IACH,GAAG,EAAE,oBAAoB,CAAC,QAAQ,EAAE;IAEpC;;;;;;;;;OASG;IACH,aAAa,EAAE,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC;IAExC;;;;;;;;;OASG;IACH,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;CACnD,CAAC;KACD,WAAW,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE;IAC3B,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7E,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC;QAC/C,GAAG,CAAC,QAAQ,CAAC;YACX,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,CAAC,eAAe,CAAC;YACvB,OAAO,EACL,kBAAkB,MAAM,CAAC,aAAa,iCAAiC;gBACvE,eAAe,CAAC,GAAG,eAAe,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,yBAAyB;gBACvE,sDAAsD;SACzD,CAAC,CAAC;IACL,CAAC;IAED,iEAAiE;IACjE,iEAAiE;IACjE,sDAAsD;IACtD,MAAM,UAAU,GAAG,IAAI,GAAG,EAA6B,CAAC;IACxD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,GAAG,CAAC,QAAQ,CAAC;gBACX,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,CAAC,SAAS,CAAC;gBACjB,OAAO,EACL,gBAAgB,KAAK,CAAC,IAAI,0BAA0B,QAAQ,QAAQ;oBACpE,IAAI,KAAK,CAAC,MAAM,qDAAqD;aACxE,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3C,CAAC;IAED,6DAA6D;IAC7D,kEAAkE;IAClE,cAAc;IACd,MAAM,YAAY,GAAG,IAAI,GAAG,EAA6B,CAAC;IAC1D,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnC,MAAM,YAAY,GAAG,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACpD,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;YAC/B,GAAG,CAAC,QAAQ,CAAC;gBACX,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,CAAC,SAAS,CAAC;gBACjB,OAAO,EACL,WAAW,KAAK,CAAC,MAAM,+BAA+B,YAAY,IAAI;oBACtE,QAAQ,KAAK,CAAC,IAAI,2CAA2C;aAChE,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC,CAAC,CAAC;AAGL,wDAAwD","sourcesContent":["/**\n * @wildo_source:part:start engine.saas-website.config facet:layer:engine facet:family:marketing-website\n *\n * Per-service marketing-website configuration block, authored in\n * the website service folder as `wildo.website.config.ts`.\n *\n * # What this file's loaded value is used for TODAY\n *\n * This schema is the single source of truth for the marketing-site routing\n * and origin surface. Every consumer reads here:\n *\n * - `defaultLocale` + `locales` — read by `buildAstroI18nConfig(...)`,\n * `assertLabelPacksValid(...)`, `loadWebsitePageRuntime(...)` and\n * `renderWebsitePageHeadHtml(...)` via the shared `WebsiteSiteContext`.\n * - `publicOrigin` — read by the head renderer to compose absolute\n * `<link rel=\"canonical\">`, `og:url`, hreflang `href` URLs and\n * the structured-data `mainEntityOfPage`. The previous\n * `WebsiteRootConfig.seo.siteOrigin` was deleted.\n * - `appOrigin` — the marketing → app handoff target, read at runtime\n * since #493. A `wildo.website.config.ts` is Node-loaded and so\n * invisible to a client island, which is why this value reaches the\n * browser the way `apiOrigin` does: the `.astro` page forwards it to\n * its bridge island, which puts it on `WebsiteRuntimeContext.appOrigin`\n * for the CTA to read.\n *\n * It previously had NO runtime consumer, and a site resolved the app\n * origin a SECOND time from a public build-time environment variable —\n * one host authored twice with no mechanical link, and a silent\n * fallback to localhost when the variable was unset. That is how a\n * marketing site comes to point at the wrong environment.\n * - `csp?` — read by the website-local `sync-nginx-csp` build step,\n * which merges the app-static posture with `infra.csp?.<serviceKey>`,\n * emits the generated artifact consumed by Astro pages, and rewrites\n * the checked-in nginx marker file used by the website Docker image.\n *\n * Future companion subprocesses that introspect the marketing\n * surface (locale-coverage audits, label-pack validation against\n * authored locales, SEO-link sanity checks) will also read here —\n * none exist today.\n *\n * # Why this is per-service website configuration\n *\n * Co-locating the schema with the engine package that owns its semantics\n * keeps three concerns separate from application-root service declarations:\n *\n * 1. **Right reuse axis.** Marketing-site content (locales, SEO origins,\n * future label-pack overrides, marketing-only CTAs) is conceptually\n * different from saas-app-level service declarations (`path`,\n * `serviceName`, `frontendType`, `sessionStorage`). The marketing\n * config travels with the website service folder, NOT with the\n * SaaS app root. Two unrelated lifecycles, two different review\n * audiences.\n * 2. **Engine boundary.** `platform-config-lib` no longer needs to\n * depend on `@wildo-ai/presets-components-models` (the home of\n * `AvailableLanguage`) just to type the marketing-site locale\n * enum. Adding a new website primitive (a new SEO field, a new\n * locale-pack id) belongs with the website package and its public\n * configuration contract.\n * 3. **Disambiguator hazard.** The previous root-level\n * `marketingWebsite?` block silently assumed \"one STATIC_WEBSITE\n * service per SaaS app\" — but nothing in the schema enforced it.\n * Two STATIC_WEBSITE services would have silently shared one\n * config block with no obvious \"which one wins?\" answer. The\n * application configuration rejects more than one STATIC_WEBSITE (and\n * TECH_DOC) service, so the per-service config-file lookup is\n * deterministic by construction.\n *\n * # Schema decisions encoded here\n *\n * - `z.strictObject` so authoring an unknown key surfaces as a parse\n * error at `defineWebsiteConfig` time instead of being silently\n * dropped (same posture as `WildoSaasConfigSchema` and\n * `WildoTechnicalDocConfigSchema`).\n *\n * - **No `enabled` field.** The gate is \"is there a `wildo.website.config.ts`\n * in a `frontendType: STATIC_WEBSITE` service folder?\" — file\n * presence under a STATIC_WEBSITE service IS the enable signal.\n * The Astro build / dev companion / future audit CLI requires the\n * file when it walks a STATIC_WEBSITE service folder, so a\n * half-authored config simply cannot exist (the loader fails\n * closed). Removing the boolean closes a foot-gun: an authored\n * `enabled: false` block was previously a silent way to keep the\n * config \"warm\" without shipping anything, and consumers had to\n * remember to read the flag in addition to the file presence — a\n * future read-the-config / forget-the-flag bug waiting to happen.\n * A separate `services.<key>.enabled` pattern (orthogonal to\n * marketing-website) is the right place for any \"temporarily\n * disable this service\" story (deferred — see DASHBOARD).\n *\n * - URL fields are `z.url()` (Zod 4 native validator) so we get\n * protocol + host parsing for free, matching the rest of the\n * codebase.\n *\n * - `csp?` is typed against `WildoCspConfigSchema` from\n * `@wildo-ai/platform-config-lib`, mirroring the docs service's\n * `wildo.tech-doc.config.ts` contract. The parsed value is consumed by\n * the shared `resolveCspPolicy()` / `emitCspArtifacts()` generator flow.\n *\n * - `defaultLocale ∈ locales` cross-check via `.superRefine` —\n * keeping `defaultLocale` as a top-level field is product-explicit\n * (\"which locale URL is the canonical one\") and decouples it from\n * list ordering. Locale-path uniqueness AND locale-value\n * uniqueness are also enforced — duplicate paths would silently\n * overwrite each other in the Astro build and corrupt the sitemap;\n * duplicate locales would produce duplicate hreflang entries.\n *\n * - Locale primitive is `AvailableLanguage`. The schema imports it\n * from `@wildo-ai/presets-components-models/public-runtime` (the\n * side-effect-free public-runtime seam), but **consumers authoring\n * `wildo.website.config.ts` must import it from\n * `@wildo-ai/saas-models/public-runtime`**. Both paths produce identical\n * values; saas-models simply re-exports the presets atom. Picking\n * one path consistently across consumer files prevents drift in\n * docs / examples / search results. Values authored in\n * `wildo.website.config.ts` are the lowercase BCP-47 strings\n * (`'en'`, `'fr'`), NOT the UPPERCASE TS enum member names\n * (`EN`, `FR`). `z.enum(AvailableLanguage)` parses against the\n * enum VALUES, which is what we want.\n */\n\nimport { WildoCspConfigSchema } from '@wildo-ai/platform-config-lib';\nimport { z } from 'zod';\nimport { AvailableLanguage } from '@wildo-ai/presets-components-models/public-runtime';\n\n/**\n * Per-locale configuration. Each authored locale gets one entry.\n *\n * `path` is the locale URL prefix segment (e.g. `'en'`, `'fr'`) — the\n * Astro builder maps `/en/...` and `/fr/...` paths via this value. We\n * do NOT auto-derive `path` from the locale string because some\n * deployments may want a flat root for the default locale (`/...`\n * instead of `/en/...`); keeping it explicit per locale leaves that\n * door open without a config-shape change.\n */\nexport const WebsiteLocaleConfigSchema = z.strictObject({\n locale: z.enum(AvailableLanguage),\n path: z\n .string()\n .min(1)\n .regex(\n /^[a-z0-9-]+$/,\n 'locale path must be lowercase alphanumeric with hyphens (becomes a URL segment)',\n ),\n});\nexport type WebsiteLocaleConfig = z.infer<typeof WebsiteLocaleConfigSchema>;\n\n/**\n * Per-service marketing-website configuration block.\n *\n * Authored as `export default defineWebsiteConfig({...})` in\n * `<website-service-root>/wildo.website.config.ts`. The Astro\n * build loads this file once at bootstrap via jiti and composes\n * a `WebsiteSiteContext`; helpers consume that composed context rather than\n * this raw value.\n *\n * @example\n * ```ts\n * import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n * import { defineWebsiteConfig } from '@wildo-ai/saas-website';\n *\n * export default defineWebsiteConfig({\n * publicOrigin: 'https://example.com',\n * appOrigin: 'https://app.example.com',\n * defaultLocale: AvailableLanguage.EN,\n * locales: [\n * { locale: AvailableLanguage.EN, path: 'en' },\n * { locale: AvailableLanguage.FR, path: 'fr' },\n * ],\n * csp: { enabled: true, reportOnly: true },\n * });\n * ```\n */\nexport const WildoMarketingWebsiteConfigSchema = z\n .strictObject({\n /**\n * Public origin of the marketing site (apex / www) — canonical\n * input the Astro head renderer emits as absolute URLs\n * (`<link rel=\"canonical\">`, `og:url`, hreflang `href`,\n * sitemap base, structured-data `mainEntityOfPage`).\n *\n * MUST NOT end with `/` — the head renderer composes\n * `${publicOrigin}${routePath}` and a trailing slash would emit\n * `https://example.com//`. Surface the constraint loudly here\n * rather than silently corrupting URLs at build time.\n */\n publicOrigin: z\n .url()\n .refine((value) => !value.endsWith('/'), {\n error:\n 'WildoMarketingWebsiteConfig.publicOrigin must NOT end with \"/\" (the head ' +\n 'renderer concatenates publicOrigin + routePath; trailing slash would emit \"//\")',\n }),\n\n /**\n * Origin of the SaaS application the marketing site links into\n * (signup / login / pricing → checkout).\n *\n * **Today**: NO runtime consumer. The cross-origin handoff\n * surface that will read it does not exist yet. Authored\n * eagerly so the link-surface PR is a single framework-owned move\n * — until then the field is exercised only by the schema\n * parser.\n *\n * Distinct from the general `WildoSaasConfig.services.<name>`\n * URL surface because the marketing site is a separate Astro\n * project that does not load the full SaaS app graph at build\n * time.\n */\n appOrigin: z.url(),\n\n /**\n * Backend API origin the marketing site POSTs to for anonymous\n * captures (lead / draft-note forms) — e.g.\n * `https://api.example.com`. Threaded through\n * `WebsiteRuntimeContext.apiOrigin` into the React island that\n * constructs the `WebsiteAnonymousSessionClient`.\n *\n * OPTIONAL because a pure brochure site with no capture forms never\n * talks to an API — only sites that host an anonymous-capture\n * section need it. When such a section renders without a resolved\n * `apiOrigin`, the section surfaces a loud misconfiguration rather\n * than silently POSTing nowhere.\n *\n * MUST NOT end with `/` — the anon client composes\n * `${apiOrigin}${MAIN_API_BASE_PATH}${path}` and a trailing slash\n * would emit a doubled `//` in the request URL. (The client also\n * defensively strips trailing slashes, but the refine surfaces the\n * mistake at authoring time.)\n *\n * Distinct from `appOrigin` (the human-facing app the marketing CTAs\n * link into) and `publicOrigin` (the marketing site's own canonical\n * origin): this is the machine-to-machine API host.\n */\n apiOrigin: z\n .url()\n .refine((value) => !value.endsWith('/'), {\n error:\n 'WildoMarketingWebsiteConfig.apiOrigin must NOT end with \"/\" (the anonymous ' +\n 'client concatenates apiOrigin + MAIN_API_BASE_PATH + path; a trailing slash ' +\n 'would emit \"//\")',\n })\n .optional(),\n\n /**\n * Optional Content-Security-Policy override for the marketing site.\n * Consumed by the website-local `sync-nginx-csp` step, which emits\n * the generated artifact for Astro page injection and rewrites the\n * website nginx marker file from the same resolved output.\n */\n csp: WildoCspConfigSchema.optional(),\n\n /**\n * Default locale the marketing site serves for unmatched URLs\n * and as the canonical hreflang signal. Must appear in\n * `locales` below (cross-check enforced by the schema-level\n * `.superRefine`).\n *\n * Read by `buildAstroI18nConfig(...)` (drives Astro's own\n * `i18n.defaultLocale`) and by the head renderer (drives\n * `<html lang=\"...\">`, `og:locale`, hreflang `x-default`).\n */\n defaultLocale: z.enum(AvailableLanguage),\n\n /**\n * Ordered list of locales the marketing site builds. Order is\n * preserved (used by the language switcher rendering and by\n * the sitemap emitter — when the sitemap surface lands).\n *\n * Read by `buildAstroI18nConfig(...)` (Astro `i18n.locales`),\n * by `assertLabelPacksValid(...)` (one label pack per locale),\n * by `loadWebsitePageRuntime(...)` (per-page locale resolution),\n * and by the head renderer (hreflang alternates).\n */\n locales: z.array(WebsiteLocaleConfigSchema).min(1),\n })\n .superRefine((config, ctx) => {\n const declaredLocales = new Set(config.locales.map((entry) => entry.locale));\n if (!declaredLocales.has(config.defaultLocale)) {\n ctx.addIssue({\n code: 'custom',\n path: ['defaultLocale'],\n message:\n `defaultLocale '${config.defaultLocale}' is not declared in 'locales' ` +\n `(declared: [${[...declaredLocales].join(', ')}]). The default locale ` +\n `must be one of the locales the site actually builds.`,\n });\n }\n\n // Locale-path uniqueness: two entries collapsing to the same URL\n // segment would silently overwrite each other in the Astro build\n // and corrupt the sitemap. Fail closed at parse time.\n const pathOwners = new Map<string, AvailableLanguage>();\n for (const entry of config.locales) {\n const previous = pathOwners.get(entry.path);\n if (previous !== undefined) {\n ctx.addIssue({\n code: 'custom',\n path: ['locales'],\n message:\n `Locale path '${entry.path}' is declared by both '${previous}' and ` +\n `'${entry.locale}'. Each locale must have a unique URL path segment.`,\n });\n continue;\n }\n pathOwners.set(entry.path, entry.locale);\n }\n\n // Locale-value uniqueness: same locale appearing twice would\n // produce duplicate hreflang entries and ambiguous default-locale\n // resolution.\n const localeOwners = new Map<AvailableLanguage, string>();\n for (const entry of config.locales) {\n const previousPath = localeOwners.get(entry.locale);\n if (previousPath !== undefined) {\n ctx.addIssue({\n code: 'custom',\n path: ['locales'],\n message:\n `Locale '${entry.locale}' is declared twice (paths '${previousPath}' ` +\n `and '${entry.path}'). Each locale must appear at most once.`,\n });\n continue;\n }\n localeOwners.set(entry.locale, entry.path);\n }\n });\nexport type WildoMarketingWebsiteConfig = z.infer<typeof WildoMarketingWebsiteConfigSchema>;\nexport type WildoMarketingWebsiteConfigInput = z.input<typeof WildoMarketingWebsiteConfigSchema>;\n/** @wildo_source:part:end engine.saas-website.config */\n"]}
1
+ {"version":3,"file":"wildo-website-config.schemas.js","sourceRoot":"","sources":["../../../../src/config/wildo-website-config.schemas.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @wildo_source:part:start engine.saas-website.config facet:layer:engine facet:family:marketing-website\n *\n * Per-service marketing-website configuration block, authored in\n * the website service folder as `wildo.website.config.ts`.\n *\n * # What this file's loaded value is used for TODAY\n *\n * This schema is the single source of truth for the marketing-site routing\n * and origin surface. Every consumer reads here:\n *\n * - `defaultLocale` + `locales` — read by `buildAstroI18nConfig(...)`,\n * `assertLabelPacksValid(...)`, `loadWebsitePageRuntime(...)` and\n * `renderWebsitePageHeadHtml(...)` via the shared `WebsiteSiteContext`.\n * - `publicOrigin` — read by the head renderer to compose absolute\n * `<link rel=\"canonical\">`, `og:url`, hreflang `href` URLs and\n * the structured-data `mainEntityOfPage`. The previous\n * `WebsiteRootConfig.seo.siteOrigin` was deleted.\n * - `appOrigin` — the marketing → app handoff target, read at runtime\n * since #493. A `wildo.website.config.ts` is Node-loaded and so\n * invisible to a client island, which is why this value reaches the\n * browser the way `apiOrigin` does: the `.astro` page forwards it to\n * its bridge island, which puts it on `WebsiteRuntimeContext.appOrigin`\n * for the CTA to read.\n *\n * It previously had NO runtime consumer, and a site resolved the app\n * origin a SECOND time from a public build-time environment variable —\n * one host authored twice with no mechanical link, and a silent\n * fallback to localhost when the variable was unset. That is how a\n * marketing site comes to point at the wrong environment.\n * - `csp?` — read by the website-local `sync-nginx-csp` build step,\n * which merges the app-static posture with `infra.csp?.<serviceKey>`,\n * emits the generated artifact consumed by Astro pages, and rewrites\n * the checked-in nginx marker file used by the website Docker image.\n *\n * Future companion subprocesses that introspect the marketing\n * surface (locale-coverage audits, label-pack validation against\n * authored locales, SEO-link sanity checks) will also read here —\n * none exist today.\n *\n * # Why this is per-service website configuration\n *\n * Co-locating the schema with the engine package that owns its semantics\n * keeps three concerns separate from application-root service declarations:\n *\n * 1. **Right reuse axis.** Marketing-site content (locales, SEO origins,\n * future label-pack overrides, marketing-only CTAs) is conceptually\n * different from saas-app-level service declarations (`path`,\n * `serviceName`, `frontendType`, `sessionStorage`). The marketing\n * config travels with the website service folder, NOT with the\n * SaaS app root. Two unrelated lifecycles, two different review\n * audiences.\n * 2. **Engine boundary.** `platform-config-lib` no longer needs to\n * depend on `@wildo-ai/presets-components-models` (the home of\n * `AvailableLanguage`) just to type the marketing-site locale\n * enum. Adding a new website primitive (a new SEO field, a new\n * locale-pack id) belongs with the website package and its public\n * configuration contract.\n * 3. **Disambiguator hazard.** The previous root-level\n * `marketingWebsite?` block silently assumed \"one STATIC_WEBSITE\n * service per SaaS app\" — but nothing in the schema enforced it.\n * Two STATIC_WEBSITE services would have silently shared one\n * config block with no obvious \"which one wins?\" answer. The\n * application configuration rejects more than one STATIC_WEBSITE (and\n * TECH_DOC) service, so the per-service config-file lookup is\n * deterministic by construction.\n *\n * # Schema decisions encoded here\n *\n * - `z.strictObject` so authoring an unknown key surfaces as a parse\n * error at `defineWebsiteConfig` time instead of being silently\n * dropped (same posture as `WildoSaasConfigSchema` and\n * `WildoTechnicalDocConfigSchema`).\n *\n * - **No `enabled` field.** The gate is \"is there a `wildo.website.config.ts`\n * in a `frontendType: STATIC_WEBSITE` service folder?\" — file\n * presence under a STATIC_WEBSITE service IS the enable signal.\n * The Astro build / dev companion / future audit CLI requires the\n * file when it walks a STATIC_WEBSITE service folder, so a\n * half-authored config simply cannot exist (the loader fails\n * closed). Removing the boolean closes a foot-gun: an authored\n * `enabled: false` block was previously a silent way to keep the\n * config \"warm\" without shipping anything, and consumers had to\n * remember to read the flag in addition to the file presence — a\n * future read-the-config / forget-the-flag bug waiting to happen.\n * A separate `services.<key>.enabled` pattern (orthogonal to\n * marketing-website) is the right place for any \"temporarily\n * disable this service\" story (deferred — see DASHBOARD).\n *\n * - URL fields are `z.url()` (Zod 4 native validator) so we get\n * protocol + host parsing for free, matching the rest of the\n * codebase.\n *\n * - `csp?` is typed against `WildoCspConfigSchema` from\n * `@wildo-ai/platform-config-lib`, mirroring the docs service's\n * `wildo.tech-doc.config.ts` contract. The parsed value is consumed by\n * the shared `resolveCspPolicy()` / `emitCspArtifacts()` generator flow.\n *\n * - `defaultLocale ∈ locales` cross-check via `.superRefine` —\n * keeping `defaultLocale` as a top-level field is product-explicit\n * (\"which locale URL is the canonical one\") and decouples it from\n * list ordering. Locale-path uniqueness AND locale-value\n * uniqueness are also enforced — duplicate paths would silently\n * overwrite each other in the Astro build and corrupt the sitemap;\n * duplicate locales would produce duplicate hreflang entries.\n *\n * - Locale primitive is `AvailableLanguage`. The schema imports it\n * from `@wildo-ai/presets-components-models/public-runtime` (the\n * side-effect-free public-runtime seam), but **consumers authoring\n * `wildo.website.config.ts` must import it from\n * `@wildo-ai/saas-models/public-runtime`**. Both paths produce identical\n * values; saas-models simply re-exports the presets atom. Picking\n * one path consistently across consumer files prevents drift in\n * docs / examples / search results. Values authored in\n * `wildo.website.config.ts` are the lowercase BCP-47 strings\n * (`'en'`, `'fr'`), NOT the UPPERCASE TS enum member names\n * (`EN`, `FR`). `z.enum(AvailableLanguage)` parses against the\n * enum VALUES, which is what we want.\n */\n\nimport { WildoCspConfigSchema } from '@wildo-ai/platform-config-lib';\nimport { z } from 'zod';\nimport { AvailableLanguage } from '@wildo-ai/presets-components-models/public-runtime';\n\n/**\n * Per-locale configuration. Each authored locale gets one entry.\n *\n * `path` is the locale URL prefix segment (e.g. `'en'`, `'fr'`) — the\n * Astro builder maps `/en/...` and `/fr/...` paths via this value. We\n * do NOT auto-derive `path` from the locale string because some\n * deployments may want a flat root for the default locale (`/...`\n * instead of `/en/...`); keeping it explicit per locale leaves that\n * door open without a config-shape change.\n */\nexport const WebsiteLocaleConfigSchema = z.strictObject({\n locale: z.enum(AvailableLanguage),\n path: z\n .string()\n .min(1)\n .regex(\n /^[a-z0-9-]+$/,\n 'locale path must be lowercase alphanumeric with hyphens (becomes a URL segment)',\n ),\n});\nexport type WebsiteLocaleConfig = z.infer<typeof WebsiteLocaleConfigSchema>;\n\n/**\n * Per-service marketing-website configuration block.\n *\n * Authored as `export default defineWebsiteConfig({...})` in\n * `<website-service-root>/wildo.website.config.ts`. The Astro\n * build loads this file once at bootstrap via jiti and composes\n * a `WebsiteSiteContext`; helpers consume that composed context rather than\n * this raw value.\n *\n * @example\n * ```ts\n * import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n * import { defineWebsiteConfig } from '@wildo-ai/saas-website';\n *\n * export default defineWebsiteConfig({\n * publicOrigin: 'https://example.com',\n * appOrigin: 'https://app.example.com',\n * defaultLocale: AvailableLanguage.EN,\n * locales: [\n * { locale: AvailableLanguage.EN, path: 'en' },\n * { locale: AvailableLanguage.FR, path: 'fr' },\n * ],\n * csp: { enabled: true, reportOnly: true },\n * });\n * ```\n */\nexport const WildoMarketingWebsiteConfigSchema = z\n .strictObject({\n /**\n * Public origin of the marketing site (apex / www) — canonical\n * input the Astro head renderer emits as absolute URLs\n * (`<link rel=\"canonical\">`, `og:url`, hreflang `href`,\n * sitemap base, structured-data `mainEntityOfPage`).\n *\n * MUST NOT end with `/` — the head renderer composes\n * `${publicOrigin}${routePath}` and a trailing slash would emit\n * `https://example.com//`. Surface the constraint loudly here\n * rather than silently corrupting URLs at build time.\n */\n publicOrigin: z\n .url()\n .refine((value) => !value.endsWith('/'), {\n error:\n 'WildoMarketingWebsiteConfig.publicOrigin must NOT end with \"/\" (the head ' +\n 'renderer concatenates publicOrigin + routePath; trailing slash would emit \"//\")',\n }),\n\n /**\n * Origin of the SaaS application the marketing site links into\n * (signup / login / pricing → checkout).\n *\n * Read at runtime since #493: each `.astro` page forwards it to its\n * bridge island, and the CTA reads it off `WebsiteRuntimeContext`.\n *\n * Per environment: a deployment's app lives at a different host in each\n * environment, and a static site bakes the value in at build time, so the\n * generated deploy workflow passes the environment's app origin as\n * `PUBLIC_APP_ORIGIN` — derived from the infra config's `serviceHosts` and\n * `publicDomain`, not authored a second time. An application's\n * `wildo.website.config.ts` reads that first and keeps its authored value\n * as the default for a build that passes none.\n *\n * Distinct from the general `WildoSaasConfig.services.<name>`\n * URL surface because the marketing site is a separate Astro\n * project that does not load the full SaaS app graph at build\n * time.\n */\n appOrigin: z.url(),\n\n /**\n * Backend API origin the marketing site POSTs to for anonymous\n * captures (lead / draft-note forms) — e.g.\n * `https://api.example.com`. Threaded through\n * `WebsiteRuntimeContext.apiOrigin` into the React island that\n * constructs the `WebsiteAnonymousSessionClient`.\n *\n * OPTIONAL because a pure brochure site with no capture forms never\n * talks to an API — only sites that host an anonymous-capture\n * section need it. When such a section renders without a resolved\n * `apiOrigin`, the section surfaces a loud misconfiguration rather\n * than silently POSTing nowhere.\n *\n * MUST NOT end with `/` — the anon client composes\n * `${apiOrigin}${MAIN_API_BASE_PATH}${path}` and a trailing slash\n * would emit a doubled `//` in the request URL. (The client also\n * defensively strips trailing slashes, but the refine surfaces the\n * mistake at authoring time.)\n *\n * Distinct from `appOrigin` (the human-facing app the marketing CTAs\n * link into) and `publicOrigin` (the marketing site's own canonical\n * origin): this is the machine-to-machine API host.\n */\n apiOrigin: z\n .url()\n .refine((value) => !value.endsWith('/'), {\n error:\n 'WildoMarketingWebsiteConfig.apiOrigin must NOT end with \"/\" (the anonymous ' +\n 'client concatenates apiOrigin + MAIN_API_BASE_PATH + path; a trailing slash ' +\n 'would emit \"//\")',\n })\n .optional(),\n\n /**\n * Optional Content-Security-Policy override for the marketing site.\n * Consumed by the website-local `sync-nginx-csp` step, which emits\n * the generated artifact for Astro page injection and rewrites the\n * website nginx marker file from the same resolved output.\n */\n csp: WildoCspConfigSchema.optional(),\n\n /**\n * Default locale the marketing site serves for unmatched URLs\n * and as the canonical hreflang signal. Must appear in\n * `locales` below (cross-check enforced by the schema-level\n * `.superRefine`).\n *\n * Read by `buildAstroI18nConfig(...)` (drives Astro's own\n * `i18n.defaultLocale`) and by the head renderer (drives\n * `<html lang=\"...\">`, `og:locale`, hreflang `x-default`).\n */\n defaultLocale: z.enum(AvailableLanguage),\n\n /**\n * Ordered list of locales the marketing site builds. Order is\n * preserved (used by the language switcher rendering and by\n * the sitemap emitter — when the sitemap surface lands).\n *\n * Read by `buildAstroI18nConfig(...)` (Astro `i18n.locales`),\n * by `assertLabelPacksValid(...)` (one label pack per locale),\n * by `loadWebsitePageRuntime(...)` (per-page locale resolution),\n * and by the head renderer (hreflang alternates).\n */\n locales: z.array(WebsiteLocaleConfigSchema).min(1),\n })\n .superRefine((config, ctx) => {\n const declaredLocales = new Set(config.locales.map((entry) => entry.locale));\n if (!declaredLocales.has(config.defaultLocale)) {\n ctx.addIssue({\n code: 'custom',\n path: ['defaultLocale'],\n message:\n `defaultLocale '${config.defaultLocale}' is not declared in 'locales' ` +\n `(declared: [${[...declaredLocales].join(', ')}]). The default locale ` +\n `must be one of the locales the site actually builds.`,\n });\n }\n\n // Locale-path uniqueness: two entries collapsing to the same URL\n // segment would silently overwrite each other in the Astro build\n // and corrupt the sitemap. Fail closed at parse time.\n const pathOwners = new Map<string, AvailableLanguage>();\n for (const entry of config.locales) {\n const previous = pathOwners.get(entry.path);\n if (previous !== undefined) {\n ctx.addIssue({\n code: 'custom',\n path: ['locales'],\n message:\n `Locale path '${entry.path}' is declared by both '${previous}' and ` +\n `'${entry.locale}'. Each locale must have a unique URL path segment.`,\n });\n continue;\n }\n pathOwners.set(entry.path, entry.locale);\n }\n\n // Locale-value uniqueness: same locale appearing twice would\n // produce duplicate hreflang entries and ambiguous default-locale\n // resolution.\n const localeOwners = new Map<AvailableLanguage, string>();\n for (const entry of config.locales) {\n const previousPath = localeOwners.get(entry.locale);\n if (previousPath !== undefined) {\n ctx.addIssue({\n code: 'custom',\n path: ['locales'],\n message:\n `Locale '${entry.locale}' is declared twice (paths '${previousPath}' ` +\n `and '${entry.path}'). Each locale must appear at most once.`,\n });\n continue;\n }\n localeOwners.set(entry.locale, entry.path);\n }\n });\nexport type WildoMarketingWebsiteConfig = z.infer<typeof WildoMarketingWebsiteConfigSchema>;\nexport type WildoMarketingWebsiteConfigInput = z.input<typeof WildoMarketingWebsiteConfigSchema>;\n/** @wildo_source:part:end engine.saas-website.config */\n"]}
@@ -32,4 +32,6 @@
32
32
  * exists for a reason.
33
33
  */
34
34
  export * from './config/load-website-config';
35
+ export * from './config/load-website-site-context';
36
+ export * from './config/load-website-pricing-catalog';
35
37
  //# sourceMappingURL=config-loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config-loader.d.ts","sourceRoot":"","sources":["../../src/config-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,cAAc,8BAA8B,CAAC"}
1
+ {"version":3,"file":"config-loader.d.ts","sourceRoot":"","sources":["../../../src/config-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oCAAoC,CAAC;AACnD,cAAc,uCAAuC,CAAC"}
@@ -32,4 +32,6 @@
32
32
  * exists for a reason.
33
33
  */
34
34
  export * from './config/load-website-config.js';
35
+ export * from './config/load-website-site-context.js';
36
+ export * from './config/load-website-pricing-catalog.js';
35
37
  //# sourceMappingURL=config-loader.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config-loader.js","sourceRoot":"","sources":["../../../src/config-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,cAAc,8BAA8B,CAAC","sourcesContent":["/**\n * @wildo-package @wildo-ai/saas-website (Node-only config-loader entry)\n *\n * Dedicated subpath for the per-service `wildo.website.config.ts`\n * loader (`saas-website-slice.md` Step 7.5 — mirrors the\n * `saas-technical-doc.md` Step 7.5 ship). Consumers:\n *\n * - `examples/wonder-todos/website/astro.config.ts` — wires the\n * loader into the Astro build's `i18n` config.\n * - Future companion subprocesses that introspect the marketing\n * surface (locale coverage audits, SEO-link sanity checks).\n *\n * @wildo-boundary\n * This entry is on the **Node-only** side of the bundle boundary.\n * It re-exports `loadWebsiteConfig` (which transitively pulls\n * `loadConfig` from `@wildo-ai/platform-config-lib` — jiti-backed,\n * uses `node:fs` / `node:path` / `node:crypto`) and is therefore\n * carved into its own package subpath so it cannot leak into the\n * browser-side React-island bundle.\n *\n * Enforced by `engine/saas-website/src/__tests__/bundle-isolation.test.ts`:\n * - This file IS in `ENTRY_FILES` (so the closure walker covers it).\n * - `@wildo-ai/platform-config-lib` is in `ALLOWED_BARE_SPECIFIERS`\n * AND `SSR_ONLY_BARE_SPECIFIERS`, which together prove the\n * specifier reaches the closure ONLY through this entry — never\n * through `index.ts`, `companion-exports.ts`, or `astro-island.ts`.\n *\n * Re-exporting `loadWebsiteConfig` from `index.ts` would (a) drag\n * `@wildo-ai/platform-config-lib`'s heavy graph into the marketing-\n * site browser bundle and (b) trip the SSR-tier negative test\n * above — both fail-loud signals that this subpath separation\n * exists for a reason.\n */\n\nexport * from './config/load-website-config';\n"]}
1
+ {"version":3,"file":"config-loader.js","sourceRoot":"","sources":["../../../src/config-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oCAAoC,CAAC;AACnD,cAAc,uCAAuC,CAAC","sourcesContent":["/**\n * @wildo-package @wildo-ai/saas-website (Node-only config-loader entry)\n *\n * Dedicated subpath for the per-service `wildo.website.config.ts`\n * loader (`saas-website-slice.md` Step 7.5 — mirrors the\n * `saas-technical-doc.md` Step 7.5 ship). Consumers:\n *\n * - `examples/wonder-todos/website/astro.config.ts` — wires the\n * loader into the Astro build's `i18n` config.\n * - Future companion subprocesses that introspect the marketing\n * surface (locale coverage audits, SEO-link sanity checks).\n *\n * @wildo-boundary\n * This entry is on the **Node-only** side of the bundle boundary.\n * It re-exports `loadWebsiteConfig` (which transitively pulls\n * `loadConfig` from `@wildo-ai/platform-config-lib` — jiti-backed,\n * uses `node:fs` / `node:path` / `node:crypto`) and is therefore\n * carved into its own package subpath so it cannot leak into the\n * browser-side React-island bundle.\n *\n * Enforced by `engine/saas-website/src/__tests__/bundle-isolation.test.ts`:\n * - This file IS in `ENTRY_FILES` (so the closure walker covers it).\n * - `@wildo-ai/platform-config-lib` is in `ALLOWED_BARE_SPECIFIERS`\n * AND `SSR_ONLY_BARE_SPECIFIERS`, which together prove the\n * specifier reaches the closure ONLY through this entry — never\n * through `index.ts`, `companion-exports.ts`, or `astro-island.ts`.\n *\n * Re-exporting `loadWebsiteConfig` from `index.ts` would (a) drag\n * `@wildo-ai/platform-config-lib`'s heavy graph into the marketing-\n * site browser bundle and (b) trip the SSR-tier negative test\n * above — both fail-loud signals that this subpath separation\n * exists for a reason.\n */\n\nexport * from './config/load-website-config';\nexport * from './config/load-website-site-context';\nexport * from './config/load-website-pricing-catalog';\n"]}
@@ -8,12 +8,26 @@
8
8
  * the anonymous session (init header) and the backend stamps the lead's `anonymousUserId`
9
9
  * so it can later transpose to the visitor's account on signup/login.
10
10
  *
11
- * Labels are injected (optional props with English fallbacks) rather than read from the
12
- * website runtime context, so the component stays framework-light and unit-testable; the
13
- * section wrapper passes i18n-resolved strings.
11
+ * Labels are injected rather than read from the website runtime context, so the component stays framework-light and
12
+ * unit-testable; the section wrapper passes the strings it resolved through `useWebsiteLabel`. EVERY label is required
13
+ * and the form carries no words of its own (#1986): a fallback would be English in every locale and would hide the
14
+ * missing label, while a required prop makes an omission a type error in the section, and a key the pack does not
15
+ * carry reaches the page as the key itself — which is what `useWebsiteLabel` returns, so somebody authors it.
16
+ *
17
+ * Submission challenge (#1735): when the application protects the form with a captcha, each submission carries an
18
+ * answer — the consented vendor's token when its browser part runs on the page, otherwise the engine's proof of work,
19
+ * which needs no consent (see `website-submission-challenge.ts`). Nothing for the visitor to click or read: both are
20
+ * invisible, so the form stays operable by keyboard and screen reader exactly as before. A vendor token the backend
21
+ * refuses is retried once with the proof of work; a refusal that survives that shows `challengeRefusedMessage`.
14
22
  */
15
23
  import React from "react";
16
24
  import type { WebsiteAnonymousSessionClient } from "./website-anonymous-session-client";
25
+ import { type WebsiteSubmissionChallengeSource } from "./website-submission-challenge";
26
+ /**
27
+ * Every word the form shows. All required (#1986): the section resolves each through `useWebsiteLabel` and lists its
28
+ * key in `expectedLabelKeys`, so a missing label is a type error in the section or a visible key on the page, never a
29
+ * silent English substitute.
30
+ */
17
31
  export interface InboundContactFormLabels {
18
32
  emailLabel: string;
19
33
  firstNameLabel: string;
@@ -25,14 +39,20 @@ export interface InboundContactFormLabels {
25
39
  submittingLabel: string;
26
40
  successMessage: string;
27
41
  errorMessage: string;
42
+ /** The anti-spam check did not pass, even with the proof of work (#1735). */
43
+ challengeRefusedMessage: string;
44
+ /** Shown beside the email field when what was typed is not an email address (#1986). */
45
+ invalidEmailMessage: string;
28
46
  }
29
47
  export interface InboundContactFormProps {
30
48
  /** The anonymous-session client (constructed by the section wiring from site config). */
31
49
  client: WebsiteAnonymousSessionClient;
32
- /** Optional i18n-resolved labels; English fallbacks are used per-field when omitted. */
33
- labels?: Partial<InboundContactFormLabels>;
50
+ /** The section's i18n-resolved labels — every one of them (#1986). */
51
+ labels: InboundContactFormLabels;
34
52
  /** Optional callback fired after a successful submission (e.g. analytics). */
35
53
  onSubmitted?: () => void;
54
+ /** The page's captcha, for a form rendered outside a page layout (tests). Defaults to the page's own. */
55
+ challengeSource?: WebsiteSubmissionChallengeSource;
36
56
  }
37
- export declare function InboundContactForm({ client, labels, onSubmitted }: InboundContactFormProps): React.ReactElement;
57
+ export declare function InboundContactForm({ client, labels, onSubmitted, challengeSource }: InboundContactFormProps): React.ReactElement;
38
58
  //# sourceMappingURL=InboundContactForm.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"InboundContactForm.d.ts","sourceRoot":"","sources":["../../../../src/core/anonymous-session/InboundContactForm.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAyC,MAAM,OAAO,CAAC;AAE9D,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,oCAAoC,CAAC;AAExF,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,qBAAqB,EAAE,MAAM,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;CACtB;AAgBD,MAAM,WAAW,uBAAuB;IACtC,yFAAyF;IACzF,MAAM,EAAE,6BAA6B,CAAC;IACtC,wFAAwF;IACxF,MAAM,CAAC,EAAE,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAC3C,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B;AAUD,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,uBAAuB,GAAG,KAAK,CAAC,YAAY,CA6F/G"}
1
+ {"version":3,"file":"InboundContactForm.d.ts","sourceRoot":"","sources":["../../../../../src/core/anonymous-session/InboundContactForm.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAgC,MAAM,OAAO,CAAC;AASrD,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,oCAAoC,CAAC;AACxF,OAAO,EAIL,KAAK,gCAAgC,EACtC,MAAM,gCAAgC,CAAC;AAExC;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,qBAAqB,EAAE,MAAM,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,uBAAuB,EAAE,MAAM,CAAC;IAChC,wFAAwF;IACxF,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAID,MAAM,WAAW,uBAAuB;IACtC,yFAAyF;IACzF,MAAM,EAAE,6BAA6B,CAAC;IACtC,sEAAsE;IACtE,MAAM,EAAE,wBAAwB,CAAC;IACjC,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,yGAAyG;IACzG,eAAe,CAAC,EAAE,gCAAgC,CAAC;CACpD;AAUD,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,eAAe,EAAE,EAAE,uBAAuB,GAAG,KAAK,CAAC,YAAY,CAgIhI"}
@@ -9,23 +9,22 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  * the anonymous session (init header) and the backend stamps the lead's `anonymousUserId`
10
10
  * so it can later transpose to the visitor's account on signup/login.
11
11
  *
12
- * Labels are injected (optional props with English fallbacks) rather than read from the
13
- * website runtime context, so the component stays framework-light and unit-testable; the
14
- * section wrapper passes i18n-resolved strings.
12
+ * Labels are injected rather than read from the website runtime context, so the component stays framework-light and
13
+ * unit-testable; the section wrapper passes the strings it resolved through `useWebsiteLabel`. EVERY label is required
14
+ * and the form carries no words of its own (#1986): a fallback would be English in every locale and would hide the
15
+ * missing label, while a required prop makes an omission a type error in the section, and a key the pack does not
16
+ * carry reaches the page as the key itself — which is what `useWebsiteLabel` returns, so somebody authors it.
17
+ *
18
+ * Submission challenge (#1735): when the application protects the form with a captcha, each submission carries an
19
+ * answer — the consented vendor's token when its browser part runs on the page, otherwise the engine's proof of work,
20
+ * which needs no consent (see `website-submission-challenge.ts`). Nothing for the visitor to click or read: both are
21
+ * invisible, so the form stays operable by keyboard and screen reader exactly as before. A vendor token the backend
22
+ * refuses is retried once with the proof of work; a refusal that survives that shows `challengeRefusedMessage`.
15
23
  */
16
- import { useCallback, useMemo, useState } from "react";
17
- import { InboundContactFormSchema, buildInboundContactCreateBody } from "./inbound-contact-form.schema.js";
18
- const DEFAULT_LABELS = {
19
- emailLabel: "Email",
20
- firstNameLabel: "First name",
21
- lastNameLabel: "Last name",
22
- phoneLabel: "Phone",
23
- marketingConsentLabel: "Keep me updated about news and offers",
24
- submitLabel: "Get in touch",
25
- submittingLabel: "Sending…",
26
- successMessage: "Thanks — we'll be in touch shortly.",
27
- errorMessage: "Something went wrong. Please try again.",
28
- };
24
+ import { useCallback, useState } from "react";
25
+ import { SubmissionChallengeAction, WildoHeaderKeys } from "@wildo-ai/saas-models/public-runtime";
26
+ import { INBOUND_CONTACT_NAME_MAX_LENGTH, INBOUND_CONTACT_PHONE_MAX_LENGTH, InboundContactFormSchema, buildInboundContactCreateBody, } from "./inbound-contact-form.schema.js";
27
+ import { isSubmissionChallengeRefusal, obtainSubmissionChallengeAnswer, useWebsiteSubmissionChallengeSource, } from "./website-submission-challenge.js";
29
28
  const EMPTY_VALUES = {
30
29
  email: "",
31
30
  firstName: "",
@@ -33,45 +32,64 @@ const EMPTY_VALUES = {
33
32
  phone: "",
34
33
  marketingConsent: false,
35
34
  };
36
- export function InboundContactForm({ client, labels, onSubmitted }) {
37
- const resolvedLabels = useMemo(() => ({ ...DEFAULT_LABELS, ...labels }), [labels]);
35
+ export function InboundContactForm({ client, labels, onSubmitted, challengeSource }) {
36
+ const pageChallengeSource = useWebsiteSubmissionChallengeSource();
37
+ const source = challengeSource ?? pageChallengeSource;
38
38
  const [values, setValues] = useState(EMPTY_VALUES);
39
- const [fieldErrors, setFieldErrors] = useState({});
39
+ const [emailInvalid, setEmailInvalid] = useState(false);
40
40
  const [submit, setSubmit] = useState({ status: "idle" });
41
41
  const setField = useCallback((key, value) => {
42
42
  setValues((prev) => ({ ...prev, [key]: value }));
43
43
  }, []);
44
44
  const handleSubmit = useCallback(async (event) => {
45
45
  event.preventDefault();
46
- setFieldErrors({});
46
+ setEmailInvalid(false);
47
47
  const parsed = InboundContactFormSchema.safeParse(values);
48
48
  if (!parsed.success) {
49
- const nextErrors = {};
50
- for (const issue of parsed.error.issues) {
51
- const key = issue.path[0];
52
- if (key && !nextErrors[key])
53
- nextErrors[key] = issue.message;
54
- }
55
- setFieldErrors(nextErrors);
49
+ // The schema's own issue messages are never shown: they are English and nobody authors them (#1986). The email
50
+ // is the one field a visitor can get wrong, and it has its label. The other fields' only rule is a length the
51
+ // inputs already enforce with `maxLength`, so reaching a failure there means the values did not come from
52
+ // typing, and the form answers with its generic error rather than inventing a sentence.
53
+ const emailFailed = parsed.error.issues.some((issue) => issue.path[0] === "email");
54
+ setEmailInvalid(emailFailed);
55
+ if (!emailFailed)
56
+ setSubmit({ status: "error", message: labels.errorMessage });
56
57
  return;
57
58
  }
58
59
  setSubmit({ status: "submitting" });
60
+ const body = buildInboundContactCreateBody(parsed.data);
61
+ const send = async (proofOfWorkOnly) => {
62
+ const answer = await obtainSubmissionChallengeAnswer({ source, action: SubmissionChallengeAction.INBOUND_CONTACT, client, proofOfWorkOnly });
63
+ await client.postAnonymous("/inbound-contacts", body, answer === undefined ? undefined : { [WildoHeaderKeys.SUBMISSION_CHALLENGE_RESPONSE]: answer });
64
+ };
59
65
  try {
60
- await client.postAnonymous("/inbound-contacts", buildInboundContactCreateBody(parsed.data));
66
+ try {
67
+ await send(false);
68
+ }
69
+ catch (error) {
70
+ // A refused challenge is answered ONCE more, with the proof of work: a vendor that scored a real person low,
71
+ // could not be reached, or a site that did not know the backend protects the form must not lock anyone out.
72
+ if (!isSubmissionChallengeRefusal(error))
73
+ throw error;
74
+ await send(true);
75
+ }
61
76
  setSubmit({ status: "success" });
62
77
  setValues(EMPTY_VALUES);
63
78
  onSubmitted?.();
64
79
  }
65
- catch {
80
+ catch (error) {
66
81
  // The backend-only `emailUserMatch` triage signal is never returned here, so a
67
82
  // failure exposes no account-existence information — just a generic retry.
68
- setSubmit({ status: "error", message: resolvedLabels.errorMessage });
83
+ setSubmit({
84
+ status: "error",
85
+ message: isSubmissionChallengeRefusal(error) ? labels.challengeRefusedMessage : labels.errorMessage,
86
+ });
69
87
  }
70
- }, [client, values, onSubmitted, resolvedLabels.errorMessage]);
88
+ }, [client, values, onSubmitted, source, labels.errorMessage, labels.challengeRefusedMessage]);
71
89
  if (submit.status === "success") {
72
- return (_jsx("div", { role: "status", "aria-live": "polite", children: resolvedLabels.successMessage }));
90
+ return (_jsx("div", { role: "status", "aria-live": "polite", children: labels.successMessage }));
73
91
  }
74
92
  const isSubmitting = submit.status === "submitting";
75
- return (_jsxs("form", { onSubmit: handleSubmit, noValidate: true, children: [_jsxs("label", { children: [resolvedLabels.emailLabel, _jsx("input", { type: "email", name: "email", required: true, value: values.email, onChange: (e) => setField("email", e.target.value), "aria-invalid": fieldErrors.email ? true : undefined }), fieldErrors.email ? _jsx("span", { role: "alert", children: fieldErrors.email }) : null] }), _jsxs("label", { children: [resolvedLabels.firstNameLabel, _jsx("input", { type: "text", name: "firstName", value: values.firstName ?? "", onChange: (e) => setField("firstName", e.target.value) })] }), _jsxs("label", { children: [resolvedLabels.lastNameLabel, _jsx("input", { type: "text", name: "lastName", value: values.lastName ?? "", onChange: (e) => setField("lastName", e.target.value) })] }), _jsxs("label", { children: [resolvedLabels.phoneLabel, _jsx("input", { type: "tel", name: "phone", value: values.phone ?? "", onChange: (e) => setField("phone", e.target.value) })] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", name: "marketingConsent", checked: values.marketingConsent, onChange: (e) => setField("marketingConsent", e.target.checked) }), resolvedLabels.marketingConsentLabel] }), submit.status === "error" ? _jsx("div", { role: "alert", children: submit.message }) : null, _jsx("button", { type: "submit", disabled: isSubmitting, children: isSubmitting ? resolvedLabels.submittingLabel : resolvedLabels.submitLabel })] }));
93
+ return (_jsxs("form", { onSubmit: handleSubmit, noValidate: true, children: [_jsxs("label", { children: [labels.emailLabel, _jsx("input", { type: "email", name: "email", required: true, value: values.email, onChange: (e) => setField("email", e.target.value), "aria-invalid": emailInvalid ? true : undefined }), emailInvalid ? _jsx("span", { role: "alert", children: labels.invalidEmailMessage }) : null] }), _jsxs("label", { children: [labels.firstNameLabel, _jsx("input", { type: "text", name: "firstName", maxLength: INBOUND_CONTACT_NAME_MAX_LENGTH, value: values.firstName ?? "", onChange: (e) => setField("firstName", e.target.value) })] }), _jsxs("label", { children: [labels.lastNameLabel, _jsx("input", { type: "text", name: "lastName", maxLength: INBOUND_CONTACT_NAME_MAX_LENGTH, value: values.lastName ?? "", onChange: (e) => setField("lastName", e.target.value) })] }), _jsxs("label", { children: [labels.phoneLabel, _jsx("input", { type: "tel", name: "phone", maxLength: INBOUND_CONTACT_PHONE_MAX_LENGTH, value: values.phone ?? "", onChange: (e) => setField("phone", e.target.value) })] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", name: "marketingConsent", checked: values.marketingConsent, onChange: (e) => setField("marketingConsent", e.target.checked) }), labels.marketingConsentLabel] }), submit.status === "error" ? _jsx("div", { role: "alert", children: submit.message }) : null, _jsx("button", { type: "submit", disabled: isSubmitting, children: isSubmitting ? labels.submittingLabel : labels.submitLabel })] }));
76
94
  }
77
95
  //# sourceMappingURL=InboundContactForm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"InboundContactForm.js","sourceRoot":"","sources":["../../../../../src/core/anonymous-session/InboundContactForm.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * InboundContactForm — the marketing site's real lead-capture form (React island).\n *\n * Self-contained + bundle-safe: uses only `react`, the website-local input schema, and\n * a `WebsiteAnonymousSessionClient` instance (constructed by the section/bridge wiring\n * from the site's API-origin config). On submit it validates client-side, then POSTs\n * `/inbound-contacts` through the anonymous-session client — the FIRST submission mints\n * the anonymous session (init header) and the backend stamps the lead's `anonymousUserId`\n * so it can later transpose to the visitor's account on signup/login.\n *\n * Labels are injected (optional props with English fallbacks) rather than read from the\n * website runtime context, so the component stays framework-light and unit-testable; the\n * section wrapper passes i18n-resolved strings.\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { InboundContactFormSchema, buildInboundContactCreateBody, type InboundContactFormValues } from \"./inbound-contact-form.schema\";\nimport type { WebsiteAnonymousSessionClient } from \"./website-anonymous-session-client\";\n\nexport interface InboundContactFormLabels {\n emailLabel: string;\n firstNameLabel: string;\n lastNameLabel: string;\n phoneLabel: string;\n /** OPTIONAL marketing opt-in checkbox. */\n marketingConsentLabel: string;\n submitLabel: string;\n submittingLabel: string;\n successMessage: string;\n errorMessage: string;\n}\n\nconst DEFAULT_LABELS: InboundContactFormLabels = {\n emailLabel: \"Email\",\n firstNameLabel: \"First name\",\n lastNameLabel: \"Last name\",\n phoneLabel: \"Phone\",\n marketingConsentLabel: \"Keep me updated about news and offers\",\n submitLabel: \"Get in touch\",\n submittingLabel: \"Sending…\",\n successMessage: \"Thanks — we'll be in touch shortly.\",\n errorMessage: \"Something went wrong. Please try again.\",\n};\n\ntype SubmitState = { status: \"idle\" } | { status: \"submitting\" } | { status: \"success\" } | { status: \"error\"; message: string };\n\nexport interface InboundContactFormProps {\n /** The anonymous-session client (constructed by the section wiring from site config). */\n client: WebsiteAnonymousSessionClient;\n /** Optional i18n-resolved labels; English fallbacks are used per-field when omitted. */\n labels?: Partial<InboundContactFormLabels>;\n /** Optional callback fired after a successful submission (e.g. analytics). */\n onSubmitted?: () => void;\n}\n\nconst EMPTY_VALUES: InboundContactFormValues = {\n email: \"\",\n firstName: \"\",\n lastName: \"\",\n phone: \"\",\n marketingConsent: false,\n};\n\nexport function InboundContactForm({ client, labels, onSubmitted }: InboundContactFormProps): React.ReactElement {\n const resolvedLabels = useMemo(() => ({ ...DEFAULT_LABELS, ...labels }), [labels]);\n const [values, setValues] = useState<InboundContactFormValues>(EMPTY_VALUES);\n const [fieldErrors, setFieldErrors] = useState<Partial<Record<keyof InboundContactFormValues, string>>>({});\n const [submit, setSubmit] = useState<SubmitState>({ status: \"idle\" });\n\n const setField = useCallback(<K extends keyof InboundContactFormValues>(key: K, value: InboundContactFormValues[K]) => {\n setValues((prev) => ({ ...prev, [key]: value }));\n }, []);\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n setFieldErrors({});\n\n const parsed = InboundContactFormSchema.safeParse(values);\n if (!parsed.success) {\n const nextErrors: Partial<Record<keyof InboundContactFormValues, string>> = {};\n for (const issue of parsed.error.issues) {\n const key = issue.path[0] as keyof InboundContactFormValues | undefined;\n if (key && !nextErrors[key]) nextErrors[key] = issue.message;\n }\n setFieldErrors(nextErrors);\n return;\n }\n\n setSubmit({ status: \"submitting\" });\n try {\n await client.postAnonymous(\"/inbound-contacts\", buildInboundContactCreateBody(parsed.data));\n setSubmit({ status: \"success\" });\n setValues(EMPTY_VALUES);\n onSubmitted?.();\n } catch {\n // The backend-only `emailUserMatch` triage signal is never returned here, so a\n // failure exposes no account-existence information — just a generic retry.\n setSubmit({ status: \"error\", message: resolvedLabels.errorMessage });\n }\n },\n [client, values, onSubmitted, resolvedLabels.errorMessage],\n );\n\n if (submit.status === \"success\") {\n return (\n <div role=\"status\" aria-live=\"polite\">\n {resolvedLabels.successMessage}\n </div>\n );\n }\n\n const isSubmitting = submit.status === \"submitting\";\n\n return (\n <form onSubmit={handleSubmit} noValidate>\n <label>\n {resolvedLabels.emailLabel}\n <input\n type=\"email\"\n name=\"email\"\n required\n value={values.email}\n onChange={(e) => setField(\"email\", e.target.value)}\n aria-invalid={fieldErrors.email ? true : undefined}\n />\n {fieldErrors.email ? <span role=\"alert\">{fieldErrors.email}</span> : null}\n </label>\n\n <label>\n {resolvedLabels.firstNameLabel}\n <input type=\"text\" name=\"firstName\" value={values.firstName ?? \"\"} onChange={(e) => setField(\"firstName\", e.target.value)} />\n </label>\n\n <label>\n {resolvedLabels.lastNameLabel}\n <input type=\"text\" name=\"lastName\" value={values.lastName ?? \"\"} onChange={(e) => setField(\"lastName\", e.target.value)} />\n </label>\n\n <label>\n {resolvedLabels.phoneLabel}\n <input type=\"tel\" name=\"phone\" value={values.phone ?? \"\"} onChange={(e) => setField(\"phone\", e.target.value)} />\n </label>\n\n <label>\n <input type=\"checkbox\" name=\"marketingConsent\" checked={values.marketingConsent} onChange={(e) => setField(\"marketingConsent\", e.target.checked)} />\n {resolvedLabels.marketingConsentLabel}\n </label>\n\n {submit.status === \"error\" ? <div role=\"alert\">{submit.message}</div> : null}\n\n <button type=\"submit\" disabled={isSubmitting}>\n {isSubmitting ? resolvedLabels.submittingLabel : resolvedLabels.submitLabel}\n </button>\n </form>\n );\n}\n"]}
1
+ {"version":3,"file":"InboundContactForm.js","sourceRoot":"","sources":["../../../../../src/core/anonymous-session/InboundContactForm.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * InboundContactForm — the marketing site's real lead-capture form (React island).\n *\n * Self-contained + bundle-safe: uses only `react`, the website-local input schema, and\n * a `WebsiteAnonymousSessionClient` instance (constructed by the section/bridge wiring\n * from the site's API-origin config). On submit it validates client-side, then POSTs\n * `/inbound-contacts` through the anonymous-session client — the FIRST submission mints\n * the anonymous session (init header) and the backend stamps the lead's `anonymousUserId`\n * so it can later transpose to the visitor's account on signup/login.\n *\n * Labels are injected rather than read from the website runtime context, so the component stays framework-light and\n * unit-testable; the section wrapper passes the strings it resolved through `useWebsiteLabel`. EVERY label is required\n * and the form carries no words of its own (#1986): a fallback would be English in every locale and would hide the\n * missing label, while a required prop makes an omission a type error in the section, and a key the pack does not\n * carry reaches the page as the key itself — which is what `useWebsiteLabel` returns, so somebody authors it.\n *\n * Submission challenge (#1735): when the application protects the form with a captcha, each submission carries an\n * answer — the consented vendor's token when its browser part runs on the page, otherwise the engine's proof of work,\n * which needs no consent (see `website-submission-challenge.ts`). Nothing for the visitor to click or read: both are\n * invisible, so the form stays operable by keyboard and screen reader exactly as before. A vendor token the backend\n * refuses is retried once with the proof of work; a refusal that survives that shows `challengeRefusedMessage`.\n */\n\nimport React, { useCallback, useState } from \"react\";\nimport { SubmissionChallengeAction, WildoHeaderKeys } from \"@wildo-ai/saas-models/public-runtime\";\nimport {\n INBOUND_CONTACT_NAME_MAX_LENGTH,\n INBOUND_CONTACT_PHONE_MAX_LENGTH,\n InboundContactFormSchema,\n buildInboundContactCreateBody,\n type InboundContactFormValues,\n} from \"./inbound-contact-form.schema\";\nimport type { WebsiteAnonymousSessionClient } from \"./website-anonymous-session-client\";\nimport {\n isSubmissionChallengeRefusal,\n obtainSubmissionChallengeAnswer,\n useWebsiteSubmissionChallengeSource,\n type WebsiteSubmissionChallengeSource,\n} from \"./website-submission-challenge\";\n\n/**\n * Every word the form shows. All required (#1986): the section resolves each through `useWebsiteLabel` and lists its\n * key in `expectedLabelKeys`, so a missing label is a type error in the section or a visible key on the page, never a\n * silent English substitute.\n */\nexport interface InboundContactFormLabels {\n emailLabel: string;\n firstNameLabel: string;\n lastNameLabel: string;\n phoneLabel: string;\n /** OPTIONAL marketing opt-in checkbox. */\n marketingConsentLabel: string;\n submitLabel: string;\n submittingLabel: string;\n successMessage: string;\n errorMessage: string;\n /** The anti-spam check did not pass, even with the proof of work (#1735). */\n challengeRefusedMessage: string;\n /** Shown beside the email field when what was typed is not an email address (#1986). */\n invalidEmailMessage: string;\n}\n\ntype SubmitState = { status: \"idle\" } | { status: \"submitting\" } | { status: \"success\" } | { status: \"error\"; message: string };\n\nexport interface InboundContactFormProps {\n /** The anonymous-session client (constructed by the section wiring from site config). */\n client: WebsiteAnonymousSessionClient;\n /** The section's i18n-resolved labels — every one of them (#1986). */\n labels: InboundContactFormLabels;\n /** Optional callback fired after a successful submission (e.g. analytics). */\n onSubmitted?: () => void;\n /** The page's captcha, for a form rendered outside a page layout (tests). Defaults to the page's own. */\n challengeSource?: WebsiteSubmissionChallengeSource;\n}\n\nconst EMPTY_VALUES: InboundContactFormValues = {\n email: \"\",\n firstName: \"\",\n lastName: \"\",\n phone: \"\",\n marketingConsent: false,\n};\n\nexport function InboundContactForm({ client, labels, onSubmitted, challengeSource }: InboundContactFormProps): React.ReactElement {\n const pageChallengeSource = useWebsiteSubmissionChallengeSource();\n const source = challengeSource ?? pageChallengeSource;\n const [values, setValues] = useState<InboundContactFormValues>(EMPTY_VALUES);\n const [emailInvalid, setEmailInvalid] = useState(false);\n const [submit, setSubmit] = useState<SubmitState>({ status: \"idle\" });\n\n const setField = useCallback(<K extends keyof InboundContactFormValues>(key: K, value: InboundContactFormValues[K]) => {\n setValues((prev) => ({ ...prev, [key]: value }));\n }, []);\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n setEmailInvalid(false);\n\n const parsed = InboundContactFormSchema.safeParse(values);\n if (!parsed.success) {\n // The schema's own issue messages are never shown: they are English and nobody authors them (#1986). The email\n // is the one field a visitor can get wrong, and it has its label. The other fields' only rule is a length the\n // inputs already enforce with `maxLength`, so reaching a failure there means the values did not come from\n // typing, and the form answers with its generic error rather than inventing a sentence.\n const emailFailed = parsed.error.issues.some((issue) => issue.path[0] === \"email\");\n setEmailInvalid(emailFailed);\n if (!emailFailed) setSubmit({ status: \"error\", message: labels.errorMessage });\n return;\n }\n\n setSubmit({ status: \"submitting\" });\n const body = buildInboundContactCreateBody(parsed.data);\n const send = async (proofOfWorkOnly: boolean): Promise<void> => {\n const answer = await obtainSubmissionChallengeAnswer({ source, action: SubmissionChallengeAction.INBOUND_CONTACT, client, proofOfWorkOnly });\n await client.postAnonymous(\"/inbound-contacts\", body, answer === undefined ? undefined : { [WildoHeaderKeys.SUBMISSION_CHALLENGE_RESPONSE]: answer });\n };\n try {\n try {\n await send(false);\n } catch (error) {\n // A refused challenge is answered ONCE more, with the proof of work: a vendor that scored a real person low,\n // could not be reached, or a site that did not know the backend protects the form must not lock anyone out.\n if (!isSubmissionChallengeRefusal(error)) throw error;\n await send(true);\n }\n setSubmit({ status: \"success\" });\n setValues(EMPTY_VALUES);\n onSubmitted?.();\n } catch (error) {\n // The backend-only `emailUserMatch` triage signal is never returned here, so a\n // failure exposes no account-existence information — just a generic retry.\n setSubmit({\n status: \"error\",\n message: isSubmissionChallengeRefusal(error) ? labels.challengeRefusedMessage : labels.errorMessage,\n });\n }\n },\n [client, values, onSubmitted, source, labels.errorMessage, labels.challengeRefusedMessage],\n );\n\n if (submit.status === \"success\") {\n return (\n <div role=\"status\" aria-live=\"polite\">\n {labels.successMessage}\n </div>\n );\n }\n\n const isSubmitting = submit.status === \"submitting\";\n\n return (\n <form onSubmit={handleSubmit} noValidate>\n <label>\n {labels.emailLabel}\n <input\n type=\"email\"\n name=\"email\"\n required\n value={values.email}\n onChange={(e) => setField(\"email\", e.target.value)}\n aria-invalid={emailInvalid ? true : undefined}\n />\n {emailInvalid ? <span role=\"alert\">{labels.invalidEmailMessage}</span> : null}\n </label>\n\n <label>\n {labels.firstNameLabel}\n <input\n type=\"text\"\n name=\"firstName\"\n maxLength={INBOUND_CONTACT_NAME_MAX_LENGTH}\n value={values.firstName ?? \"\"}\n onChange={(e) => setField(\"firstName\", e.target.value)}\n />\n </label>\n\n <label>\n {labels.lastNameLabel}\n <input\n type=\"text\"\n name=\"lastName\"\n maxLength={INBOUND_CONTACT_NAME_MAX_LENGTH}\n value={values.lastName ?? \"\"}\n onChange={(e) => setField(\"lastName\", e.target.value)}\n />\n </label>\n\n <label>\n {labels.phoneLabel}\n <input\n type=\"tel\"\n name=\"phone\"\n maxLength={INBOUND_CONTACT_PHONE_MAX_LENGTH}\n value={values.phone ?? \"\"}\n onChange={(e) => setField(\"phone\", e.target.value)}\n />\n </label>\n\n <label>\n <input type=\"checkbox\" name=\"marketingConsent\" checked={values.marketingConsent} onChange={(e) => setField(\"marketingConsent\", e.target.checked)} />\n {labels.marketingConsentLabel}\n </label>\n\n {submit.status === \"error\" ? <div role=\"alert\">{submit.message}</div> : null}\n\n <button type=\"submit\" disabled={isSubmitting}>\n {isSubmitting ? labels.submittingLabel : labels.submitLabel}\n </button>\n </form>\n );\n}\n"]}
@@ -11,18 +11,29 @@
11
11
  *
12
12
  * Mirrors the non-excluded, non-backend-only CREATE inputs of `InboundContact`
13
13
  * (`email` required; `firstName` / `lastName` / `phone` optional), plus ONE privacy
14
- * signal: an OPTIONAL marketing opt-in (mapped to the `privacy.dataProcessingConsent`
15
- * tier). There is no second, required agreement field: a governed notice, contract or
14
+ * signal: an OPTIONAL marketing opt-in (mapped to the `privacy.consentedPurposes` set).
15
+ * There is no second, required agreement field: a governed notice, contract or
16
16
  * consent interaction must use the separate exact-release receipt contract, because this
17
17
  * DTO cannot manufacture legal evidence from a checkbox and a client timestamp.
18
18
  * `relatedUserId` is intentionally absent — it is `.excludeFromCreate()` on the
19
19
  * resource (sales/admin-only, never client-authored).
20
20
  */
21
21
  import { z } from "zod";
22
+ /**
23
+ * Length ceilings of the optional text fields. Exported so the form puts the SAME numbers on its inputs as
24
+ * `maxLength`: the browser then stops a visitor at the ceiling, and a length failure is not something the form ever
25
+ * has to word (#1986).
26
+ */
27
+ export declare const INBOUND_CONTACT_NAME_MAX_LENGTH = 200;
28
+ export declare const INBOUND_CONTACT_PHONE_MAX_LENGTH = 64;
22
29
  /**
23
30
  * The wire payload POSTed to `/api/v1/inbound-contacts`. `privacy` matches the
24
31
  * resource's `InboundContactPrivacySchema` shape (a nested consent object) so the
25
32
  * backend accepts it verbatim.
33
+ *
34
+ * No issue carries a message (#1986). A visitor reads the application's own words, which the form takes from its
35
+ * section's label pack (`InboundContactFormLabels.invalidEmailMessage`); a message written here would be English in
36
+ * every locale, and nobody authoring the pack could find it.
26
37
  */
27
38
  export declare const InboundContactFormSchema: z.ZodObject<{
28
39
  email: z.ZodEmail;
@@ -34,7 +45,7 @@ export declare const InboundContactFormSchema: z.ZodObject<{
34
45
  export type InboundContactFormValues = z.infer<typeof InboundContactFormSchema>;
35
46
  /**
36
47
  * Build the exact JSON body the backend CREATE expects from validated form values.
37
- * Keeps the `privacy` nesting + the consent enum mapping in ONE place so the form
48
+ * Keeps the `privacy` nesting + the consent purpose mapping in ONE place so the form
38
49
  * component stays declarative.
39
50
  */
40
51
  export declare function buildInboundContactCreateBody(values: InboundContactFormValues): Record<string, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"inbound-contact-form.schema.d.ts","sourceRoot":"","sources":["../../../../src/core/anonymous-session/inbound-contact-form.schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;;;;;;iBAanC,CAAC;AAEH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYvG"}
1
+ {"version":3,"file":"inbound-contact-form.schema.d.ts","sourceRoot":"","sources":["../../../../../src/core/anonymous-session/inbound-contact-form.schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,MAAM,CAAC;AACnD,eAAO,MAAM,gCAAgC,KAAK,CAAC;AAEnD;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB;;;;;;iBAcnC,CAAC;AAEH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAcvG"}
@@ -11,36 +11,49 @@
11
11
  *
12
12
  * Mirrors the non-excluded, non-backend-only CREATE inputs of `InboundContact`
13
13
  * (`email` required; `firstName` / `lastName` / `phone` optional), plus ONE privacy
14
- * signal: an OPTIONAL marketing opt-in (mapped to the `privacy.dataProcessingConsent`
15
- * tier). There is no second, required agreement field: a governed notice, contract or
14
+ * signal: an OPTIONAL marketing opt-in (mapped to the `privacy.consentedPurposes` set).
15
+ * There is no second, required agreement field: a governed notice, contract or
16
16
  * consent interaction must use the separate exact-release receipt contract, because this
17
17
  * DTO cannot manufacture legal evidence from a checkbox and a client timestamp.
18
18
  * `relatedUserId` is intentionally absent — it is `.excludeFromCreate()` on the
19
19
  * resource (sales/admin-only, never client-authored).
20
20
  */
21
21
  import { z } from "zod";
22
+ import { ConsentPurpose } from "@wildo-ai/saas-models/public-runtime";
23
+ /**
24
+ * Length ceilings of the optional text fields. Exported so the form puts the SAME numbers on its inputs as
25
+ * `maxLength`: the browser then stops a visitor at the ceiling, and a length failure is not something the form ever
26
+ * has to word (#1986).
27
+ */
28
+ export const INBOUND_CONTACT_NAME_MAX_LENGTH = 200;
29
+ export const INBOUND_CONTACT_PHONE_MAX_LENGTH = 64;
22
30
  /**
23
31
  * The wire payload POSTed to `/api/v1/inbound-contacts`. `privacy` matches the
24
32
  * resource's `InboundContactPrivacySchema` shape (a nested consent object) so the
25
33
  * backend accepts it verbatim.
34
+ *
35
+ * No issue carries a message (#1986). A visitor reads the application's own words, which the form takes from its
36
+ * section's label pack (`InboundContactFormLabels.invalidEmailMessage`); a message written here would be English in
37
+ * every locale, and nobody authoring the pack could find it.
26
38
  */
27
39
  export const InboundContactFormSchema = z.object({
28
40
  // Zod 4: top-level `z.email()` (not the deprecated `z.string().email()`), matching
29
41
  // the resource schema. An empty string fails it, so it also covers "required".
30
- email: z.email("Enter a valid email address"),
31
- firstName: z.string().trim().max(200).optional(),
32
- lastName: z.string().trim().max(200).optional(),
33
- phone: z.string().trim().max(64).optional(),
42
+ email: z.email(),
43
+ firstName: z.string().trim().max(INBOUND_CONTACT_NAME_MAX_LENGTH).optional(),
44
+ lastName: z.string().trim().max(INBOUND_CONTACT_NAME_MAX_LENGTH).optional(),
45
+ phone: z.string().trim().max(INBOUND_CONTACT_PHONE_MAX_LENGTH).optional(),
34
46
  /**
35
- * OPTIONAL marketing opt-in — maps into the resource's `privacy.dataProcessingConsent`
36
- * tier ('marketing' vs the BASIC default). It is the only privacy field on this form,
37
- * and it gates nothing: an unchecked box still submits, at the BASIC tier.
47
+ * OPTIONAL marketing opt-in — maps into the resource's `privacy.consentedPurposes` set.
48
+ * It is the only privacy field on this form, and it gates nothing: sending the form is the
49
+ * essential act and never depends on optional consent, so an unchecked box still submits,
50
+ * granting nothing.
38
51
  */
39
52
  marketingConsent: z.boolean().default(false),
40
53
  });
41
54
  /**
42
55
  * Build the exact JSON body the backend CREATE expects from validated form values.
43
- * Keeps the `privacy` nesting + the consent enum mapping in ONE place so the form
56
+ * Keeps the `privacy` nesting + the consent purpose mapping in ONE place so the form
44
57
  * component stays declarative.
45
58
  */
46
59
  export function buildInboundContactCreateBody(values) {
@@ -50,9 +63,11 @@ export function buildInboundContactCreateBody(values) {
50
63
  ...(values.lastName ? { lastName: values.lastName } : {}),
51
64
  ...(values.phone ? { phone: values.phone } : {}),
52
65
  privacy: {
53
- // OPTIONAL marketing tier: 'marketing' when the visitor opted in, else the
54
- // resource's BASIC default.
55
- dataProcessingConsent: values.marketingConsent ? "marketing" : "basic",
66
+ // The visitor SAW the opt-in and submitted, so this is a decision either way (#534): the set
67
+ // they granted — MARKETING or nothing — plus a CLAIM that they decided now. The backend's
68
+ // CREATE prefix replaces the instant with its own clock; a browser clock is never recorded.
69
+ consentedPurposes: values.marketingConsent ? [ConsentPurpose.MARKETING] : [],
70
+ consentDecidedAt: new Date().toISOString(),
56
71
  },
57
72
  };
58
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"inbound-contact-form.schema.js","sourceRoot":"","sources":["../../../../src/core/anonymous-session/inbound-contact-form.schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C,mFAAmF;IACnF,+EAA+E;IAC/E,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,6BAA6B,CAAC;IAC7C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;IAChD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;IAC/C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;IAC3C;;;;OAIG;IACH,gBAAgB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;CAC7C,CAAC,CAAC;AAIH;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,MAAgC;IAC5E,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE;QACxC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzD,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChD,OAAO,EAAE;YACP,2EAA2E;YAC3E,4BAA4B;YAC5B,qBAAqB,EAAE,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO;SACvE;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Inbound-contact form input schema (website-local, bundle-safe).\n *\n * Deliberately a SEPARATE plain-Zod schema, NOT `InboundContactSchema` from\n * `@wildo-ai/saas-models`: that schema is decorator-bound (`.isPrimaryKey()`,\n * `.excludeFromCreate()`, `.isBackendOnly()`, …), so importing it would boot the\n * decorator framework into the browser bundle — forbidden by bundle discipline. The\n * website only needs to validate the handful of client-authored CREATE fields; the\n * backend is the authority on the full contract (it re-validates + injects the\n * server-set fields: `_id`, `anonymousUserId`, `userId`, `emailUserMatch`, timestamps).\n *\n * Mirrors the non-excluded, non-backend-only CREATE inputs of `InboundContact`\n * (`email` required; `firstName` / `lastName` / `phone` optional), plus ONE privacy\n * signal: an OPTIONAL marketing opt-in (mapped to the `privacy.dataProcessingConsent`\n * tier). There is no second, required agreement field: a governed notice, contract or\n * consent interaction must use the separate exact-release receipt contract, because this\n * DTO cannot manufacture legal evidence from a checkbox and a client timestamp.\n * `relatedUserId` is intentionally absent — it is `.excludeFromCreate()` on the\n * resource (sales/admin-only, never client-authored).\n */\n\nimport { z } from \"zod\";\n\n/**\n * The wire payload POSTed to `/api/v1/inbound-contacts`. `privacy` matches the\n * resource's `InboundContactPrivacySchema` shape (a nested consent object) so the\n * backend accepts it verbatim.\n */\nexport const InboundContactFormSchema = z.object({\n // Zod 4: top-level `z.email()` (not the deprecated `z.string().email()`), matching\n // the resource schema. An empty string fails it, so it also covers \"required\".\n email: z.email(\"Enter a valid email address\"),\n firstName: z.string().trim().max(200).optional(),\n lastName: z.string().trim().max(200).optional(),\n phone: z.string().trim().max(64).optional(),\n /**\n * OPTIONAL marketing opt-in — maps into the resource's `privacy.dataProcessingConsent`\n * tier ('marketing' vs the BASIC default). It is the only privacy field on this form,\n * and it gates nothing: an unchecked box still submits, at the BASIC tier.\n */\n marketingConsent: z.boolean().default(false),\n});\n\nexport type InboundContactFormValues = z.infer<typeof InboundContactFormSchema>;\n\n/**\n * Build the exact JSON body the backend CREATE expects from validated form values.\n * Keeps the `privacy` nesting + the consent enum mapping in ONE place so the form\n * component stays declarative.\n */\nexport function buildInboundContactCreateBody(values: InboundContactFormValues): Record<string, unknown> {\n return {\n email: values.email.trim().toLowerCase(),\n ...(values.firstName ? { firstName: values.firstName } : {}),\n ...(values.lastName ? { lastName: values.lastName } : {}),\n ...(values.phone ? { phone: values.phone } : {}),\n privacy: {\n // OPTIONAL marketing tier: 'marketing' when the visitor opted in, else the\n // resource's BASIC default.\n dataProcessingConsent: values.marketingConsent ? \"marketing\" : \"basic\",\n },\n };\n}\n"]}
1
+ {"version":3,"file":"inbound-contact-form.schema.js","sourceRoot":"","sources":["../../../../../src/core/anonymous-session/inbound-contact-form.schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAEtE;;;;GAIG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,GAAG,CAAC;AACnD,MAAM,CAAC,MAAM,gCAAgC,GAAG,EAAE,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C,mFAAmF;IACnF,+EAA+E;IAC/E,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE;IAChB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC,QAAQ,EAAE;IAC5E,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC,QAAQ,EAAE;IAC3E,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC,QAAQ,EAAE;IACzE;;;;;OAKG;IACH,gBAAgB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;CAC7C,CAAC,CAAC;AAIH;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,MAAgC;IAC5E,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE;QACxC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzD,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChD,OAAO,EAAE;YACP,6FAA6F;YAC7F,0FAA0F;YAC1F,4FAA4F;YAC5F,iBAAiB,EAAE,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE;YAC5E,gBAAgB,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SAC3C;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Inbound-contact form input schema (website-local, bundle-safe).\n *\n * Deliberately a SEPARATE plain-Zod schema, NOT `InboundContactSchema` from\n * `@wildo-ai/saas-models`: that schema is decorator-bound (`.isPrimaryKey()`,\n * `.excludeFromCreate()`, `.isBackendOnly()`, …), so importing it would boot the\n * decorator framework into the browser bundle — forbidden by bundle discipline. The\n * website only needs to validate the handful of client-authored CREATE fields; the\n * backend is the authority on the full contract (it re-validates + injects the\n * server-set fields: `_id`, `anonymousUserId`, `userId`, `emailUserMatch`, timestamps).\n *\n * Mirrors the non-excluded, non-backend-only CREATE inputs of `InboundContact`\n * (`email` required; `firstName` / `lastName` / `phone` optional), plus ONE privacy\n * signal: an OPTIONAL marketing opt-in (mapped to the `privacy.consentedPurposes` set).\n * There is no second, required agreement field: a governed notice, contract or\n * consent interaction must use the separate exact-release receipt contract, because this\n * DTO cannot manufacture legal evidence from a checkbox and a client timestamp.\n * `relatedUserId` is intentionally absent — it is `.excludeFromCreate()` on the\n * resource (sales/admin-only, never client-authored).\n */\n\nimport { z } from \"zod\";\n\nimport { ConsentPurpose } from \"@wildo-ai/saas-models/public-runtime\";\n\n/**\n * Length ceilings of the optional text fields. Exported so the form puts the SAME numbers on its inputs as\n * `maxLength`: the browser then stops a visitor at the ceiling, and a length failure is not something the form ever\n * has to word (#1986).\n */\nexport const INBOUND_CONTACT_NAME_MAX_LENGTH = 200;\nexport const INBOUND_CONTACT_PHONE_MAX_LENGTH = 64;\n\n/**\n * The wire payload POSTed to `/api/v1/inbound-contacts`. `privacy` matches the\n * resource's `InboundContactPrivacySchema` shape (a nested consent object) so the\n * backend accepts it verbatim.\n *\n * No issue carries a message (#1986). A visitor reads the application's own words, which the form takes from its\n * section's label pack (`InboundContactFormLabels.invalidEmailMessage`); a message written here would be English in\n * every locale, and nobody authoring the pack could find it.\n */\nexport const InboundContactFormSchema = z.object({\n // Zod 4: top-level `z.email()` (not the deprecated `z.string().email()`), matching\n // the resource schema. An empty string fails it, so it also covers \"required\".\n email: z.email(),\n firstName: z.string().trim().max(INBOUND_CONTACT_NAME_MAX_LENGTH).optional(),\n lastName: z.string().trim().max(INBOUND_CONTACT_NAME_MAX_LENGTH).optional(),\n phone: z.string().trim().max(INBOUND_CONTACT_PHONE_MAX_LENGTH).optional(),\n /**\n * OPTIONAL marketing opt-in — maps into the resource's `privacy.consentedPurposes` set.\n * It is the only privacy field on this form, and it gates nothing: sending the form is the\n * essential act and never depends on optional consent, so an unchecked box still submits,\n * granting nothing.\n */\n marketingConsent: z.boolean().default(false),\n});\n\nexport type InboundContactFormValues = z.infer<typeof InboundContactFormSchema>;\n\n/**\n * Build the exact JSON body the backend CREATE expects from validated form values.\n * Keeps the `privacy` nesting + the consent purpose mapping in ONE place so the form\n * component stays declarative.\n */\nexport function buildInboundContactCreateBody(values: InboundContactFormValues): Record<string, unknown> {\n return {\n email: values.email.trim().toLowerCase(),\n ...(values.firstName ? { firstName: values.firstName } : {}),\n ...(values.lastName ? { lastName: values.lastName } : {}),\n ...(values.phone ? { phone: values.phone } : {}),\n privacy: {\n // The visitor SAW the opt-in and submitted, so this is a decision either way (#534): the set\n // they granted — MARKETING or nothing — plus a CLAIM that they decided now. The backend's\n // CREATE prefix replaces the instant with its own clock; a browser clock is never recorded.\n consentedPurposes: values.marketingConsent ? [ConsentPurpose.MARKETING] : [],\n consentDecidedAt: new Date().toISOString(),\n },\n };\n}\n"]}
@@ -39,6 +39,11 @@ export declare class WebsiteAnonymousRequestError extends Error {
39
39
  readonly status: number;
40
40
  readonly bodyText: string;
41
41
  constructor(status: number, bodyText: string);
42
+ /**
43
+ * The refusal's stable `error.code` — what a client branches on (the engine hoists an error's `context.code` there) —
44
+ * or `undefined` when the body is not the engine's error envelope or carries no code.
45
+ */
46
+ get errorCode(): string | undefined;
42
47
  }
43
48
  export declare class WebsiteAnonymousSessionClient {
44
49
  private anonymousUserId;
@@ -71,7 +76,25 @@ export declare class WebsiteAnonymousSessionClient {
71
76
  * service-name headers, captures any returned session token, and returns the parsed
72
77
  * JSON response. Throws `WebsiteAnonymousRequestError` on a non-2xx status.
73
78
  */
74
- postAnonymous<TResponse = unknown>(path: string, body: unknown): Promise<TResponse>;
79
+ postAnonymous<TResponse = unknown>(path: string, body: unknown, extraHeaders?: Readonly<Record<string, string>>): Promise<TResponse>;
80
+ /**
81
+ * GET a PUBLIC endpoint — one that needs no session (a proof-of-work challenge, #1735). Starts no anonymous session
82
+ * and sends no session header or cookie: asking for a challenge must not mint an identity for the visitor.
83
+ */
84
+ getPublic<TResponse = unknown>(path: string): Promise<TResponse>;
85
+ /**
86
+ * Send `body` to an anonymous endpoint with an explicit METHOD.
87
+ *
88
+ * `postAnonymous` above is the POST case and delegates here. This exists because not every anonymous
89
+ * door is a POST and the method is not guessable from the path: the visitor consent door declares
90
+ * `resourceOperationLike: UPDATE_MANY`, which the engine maps to PUT, so a POST to it answers 404 —
91
+ * and this client's one silent caller swallowed exactly that. A caller reads the door's method from
92
+ * the same place it reads the path (`ANONYMOUS_CONSENT_RECORD_METHOD` beside
93
+ * `ANONYMOUS_CONSENT_RECORD_PATH`) rather than inferring it from a method name.
94
+ */
95
+ requestAnonymous<TResponse = unknown>(method: string, path: string, body: unknown,
96
+ /** Request-specific headers, such as a public form's challenge answer (#1735). Never overrides the session headers. */
97
+ extraHeaders?: Readonly<Record<string, string>>): Promise<TResponse>;
75
98
  private ensureSessionStarted;
76
99
  private buildHeaders;
77
100
  private captureSessionToken;