@wildo-ai/saas-website 1.1.5 → 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.
- package/dist/esm/astro/blog-post-bridge.d.ts +9 -0
- package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -1
- package/dist/esm/astro/blog-post-bridge.js +10 -5
- package/dist/esm/astro/blog-post-bridge.js.map +1 -1
- package/dist/esm/astro/bridge-runtime.d.ts +25 -6
- package/dist/esm/astro/bridge-runtime.d.ts.map +1 -1
- package/dist/esm/astro/bridge-runtime.js +10 -6
- package/dist/esm/astro/bridge-runtime.js.map +1 -1
- package/dist/esm/astro/label-pack-loader.d.ts +4 -4
- package/dist/esm/astro/label-pack-loader.d.ts.map +1 -1
- package/dist/esm/astro/label-pack-loader.js +9 -4
- package/dist/esm/astro/label-pack-loader.js.map +1 -1
- package/dist/esm/astro/robots-renderer.d.ts +9 -8
- package/dist/esm/astro/robots-renderer.d.ts.map +1 -1
- package/dist/esm/astro/robots-renderer.js +15 -15
- package/dist/esm/astro/robots-renderer.js.map +1 -1
- package/dist/esm/astro/website-page-head.renderer.d.ts +1 -1
- package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -1
- package/dist/esm/astro/website-page-head.renderer.js +2 -1
- package/dist/esm/astro/website-page-head.renderer.js.map +1 -1
- package/dist/esm/astro/website-site-context.d.ts +5 -2
- package/dist/esm/astro/website-site-context.d.ts.map +1 -1
- package/dist/esm/astro/website-site-context.js.map +1 -1
- package/dist/esm/companion-exports.d.ts +8 -15
- package/dist/esm/companion-exports.d.ts.map +1 -1
- package/dist/esm/companion-exports.js +8 -15
- package/dist/esm/companion-exports.js.map +1 -1
- package/dist/esm/config/define-website-config.d.ts +3 -2
- package/dist/esm/config/define-website-config.d.ts.map +1 -1
- package/dist/esm/config/define-website-config.js +10 -10
- package/dist/esm/config/define-website-config.js.map +1 -1
- package/dist/esm/config/load-website-config.d.ts +6 -0
- package/dist/esm/config/load-website-config.d.ts.map +1 -1
- package/dist/esm/config/load-website-config.js +18 -9
- package/dist/esm/config/load-website-config.js.map +1 -1
- package/dist/esm/config/load-website-pricing-catalog.d.ts +13 -0
- package/dist/esm/config/load-website-pricing-catalog.d.ts.map +1 -0
- package/dist/esm/config/load-website-pricing-catalog.js +34 -0
- package/dist/esm/config/load-website-pricing-catalog.js.map +1 -0
- package/dist/esm/config/load-website-site-context.d.ts +53 -0
- package/dist/esm/config/load-website-site-context.d.ts.map +1 -0
- package/dist/esm/config/load-website-site-context.js +98 -0
- package/dist/esm/config/load-website-site-context.js.map +1 -0
- package/dist/esm/config-loader.d.ts +2 -0
- package/dist/esm/config-loader.d.ts.map +1 -1
- package/dist/esm/config-loader.js +2 -0
- package/dist/esm/config-loader.js.map +1 -1
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts +26 -6
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -1
- package/dist/esm/core/anonymous-session/InboundContactForm.js +51 -33
- package/dist/esm/core/anonymous-session/InboundContactForm.js.map +1 -1
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +11 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -1
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +15 -4
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -1
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +14 -2
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -1
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +34 -4
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -1
- package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts +36 -0
- package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts.map +1 -0
- package/dist/esm/core/anonymous-session/website-submission-challenge.js +69 -0
- package/dist/esm/core/anonymous-session/website-submission-challenge.js.map +1 -0
- package/dist/esm/core/consent/WebsiteConsentBanner.d.ts.map +1 -1
- package/dist/esm/core/consent/WebsiteConsentBanner.js +11 -10
- package/dist/esm/core/consent/WebsiteConsentBanner.js.map +1 -1
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +25 -5
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -1
- package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -1
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts +18 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -1
- package/dist/esm/core/contexts/useWebsiteRuntime.js +28 -2
- package/dist/esm/core/contexts/useWebsiteRuntime.js.map +1 -1
- package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts +57 -0
- package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts.map +1 -0
- package/dist/esm/core/external-providers/WebsiteProviderComponent.js +56 -0
- package/dist/esm/core/external-providers/WebsiteProviderComponent.js.map +1 -0
- package/dist/esm/core/external-providers/provider-component-registry.website.d.ts +63 -0
- package/dist/esm/core/external-providers/provider-component-registry.website.d.ts.map +1 -0
- package/dist/esm/core/external-providers/provider-component-registry.website.js +68 -0
- package/dist/esm/core/external-providers/provider-component-registry.website.js.map +1 -0
- package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts +36 -0
- package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts.map +1 -0
- package/dist/esm/core/external-providers/useWebsiteProviderComponent.js +59 -0
- package/dist/esm/core/external-providers/useWebsiteProviderComponent.js.map +1 -0
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts +15 -5
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts.map +1 -1
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.js +39 -24
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.js.map +1 -1
- package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts +11 -0
- package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts.map +1 -0
- package/dist/esm/core/external-providers/website-provider-sdk-activations.js +14 -0
- package/dist/esm/core/external-providers/website-provider-sdk-activations.js.map +1 -0
- package/dist/esm/core/factories/define-website-section.d.ts +3 -3
- package/dist/esm/core/factories/define-website-section.js +3 -3
- package/dist/esm/core/factories/define-website-section.js.map +1 -1
- package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -1
- package/dist/esm/core/layouts/WebsitePageLayout.js +10 -7
- package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -1
- package/dist/esm/index.d.ts +30 -65
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +30 -65
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts +35 -6
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js +39 -5
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js.map +1 -1
- package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts +32 -0
- package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js +36 -0
- package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js.map +1 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts +6 -5
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js +8 -7
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js.map +1 -1
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts +14 -7
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js +14 -7
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js.map +1 -1
- package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts +57 -0
- package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts.map +1 -0
- package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js +76 -0
- package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js.map +1 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts +23 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts.map +1 -1
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js +20 -2
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js.map +1 -1
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts +7 -5
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/__tests__/bundle-isolation.test.ts +19 -22
- package/src/astro/__tests__/blog-post-bridge.test.tsx +18 -2
- package/src/astro/__tests__/bridge-runtime.test.tsx +21 -3
- package/src/astro/__tests__/label-pack-loader.test.ts +7 -4
- package/src/astro/blog-post-bridge.tsx +19 -5
- package/src/astro/bridge-runtime.tsx +35 -11
- package/src/astro/label-pack-loader.ts +8 -4
- package/src/astro/robots-renderer.ts +15 -15
- package/src/astro/website-page-head.renderer.ts +2 -2
- package/src/astro/website-site-context.ts +5 -2
- package/src/companion-exports.ts +8 -15
- package/src/config/__tests__/website-build-origins.test.ts +52 -0
- package/src/config/define-website-config.ts +10 -10
- package/src/config/load-website-config.ts +25 -14
- package/src/config/load-website-pricing-catalog.ts +52 -0
- package/src/config/load-website-site-context.ts +145 -0
- package/src/config-loader.ts +2 -0
- package/src/core/__tests__/contexts.test.tsx +20 -1
- package/src/core/__tests__/website-consent.test.tsx +2 -1
- package/src/core/anonymous-session/InboundContactForm.tsx +100 -45
- package/src/core/anonymous-session/__tests__/InboundContactForm.labels.test.tsx +108 -0
- package/src/core/anonymous-session/__tests__/InboundContactForm.submission-challenge.test.tsx +139 -0
- package/src/core/anonymous-session/inbound-contact-form.schema.ts +16 -4
- package/src/core/anonymous-session/website-anonymous-session-client.ts +39 -4
- package/src/core/anonymous-session/website-submission-challenge.ts +99 -0
- package/src/core/consent/WebsiteConsentBanner.tsx +11 -10
- package/src/core/contexts/WebsiteRuntimeContext.tsx +25 -5
- package/src/core/contexts/useWebsiteRuntime.ts +33 -2
- package/src/core/external-providers/WebsiteProviderComponent.tsx +153 -0
- package/src/core/external-providers/__tests__/frontend-provider-registry.website.test.ts +4 -0
- package/src/core/external-providers/__tests__/website-provider-component.test.tsx +238 -0
- package/src/core/external-providers/__tests__/website-provider-sdks.test.tsx +72 -0
- package/src/core/external-providers/provider-component-registry.website.ts +115 -0
- package/src/core/external-providers/useWebsiteProviderComponent.ts +76 -0
- package/src/core/external-providers/useWebsiteProviderScripts.ts +56 -37
- package/src/core/external-providers/website-provider-sdk-activations.ts +23 -0
- package/src/core/factories/define-website-section.ts +3 -3
- package/src/core/layouts/WebsitePageLayout.tsx +11 -6
- package/src/index.ts +31 -65
- package/src/schemas/__tests__/design-token-css-property.test.ts +72 -0
- package/src/schemas/__tests__/pricing-offer-structured-data.test.ts +122 -0
- package/src/schemas/__tests__/structured-data-reconciliation.test.ts +35 -0
- package/src/schemas/design-tokens/website-design-tokens.shared.schemas.ts +45 -5
- package/src/schemas/label-keys/__tests__/scaffolded-consent-label-pack.parity.test.ts +12 -0
- package/src/schemas/label-keys/website-provider-component-label-keys.schemas.ts +40 -0
- package/src/schemas/manifests/website-root-config.shared.schemas.ts +10 -9
- package/src/schemas/sections/website-section-definition.shared.schemas.ts +14 -7
- package/src/schemas/structured-data/pricing-offer-structured-data.shared.ts +144 -0
- package/src/schemas/structured-data/structured-data-reconciliation.shared.ts +46 -1
- package/src/schemas/structured-data/website-structured-data.shared.schemas.ts +7 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-website-config.js","sourceRoot":"","sources":["../../../../src/config/load-website-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAI5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAC3E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,oCAAoC,GAAG,yBAAyB,CAAC;AAE9E;;;;;;;;;;;;;GAaG;AACH,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAAuC,CAAC;AAE5E;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CACrC,kBAA0B,EAC1B,MAAmC;IAEnC,oBAAoB,CAAC,GAAG,CAAC,WAAW,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB;IACrC,oBAAoB,CAAC,KAAK,EAAE,CAAC;AAC/B,CAAC;AAyBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAA2B;IAE3B,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;IAEtC,MAAM,mBAAmB,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,IAAI,CAClE,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,KAAK,eAAe,CAAC,cAAc,CACzE,CAAC;IACF,IAAI,CAAC,mBAAmB,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,gGAAgG;cAC5F,wFAAwF;cACxF,0EAA0E;cAC1E,+EAA+E;cAC/E,gFAAgF;cAChF,uBAAuB,CAC5B,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,iBAAiB,EAAE,cAAc,CAAC,GAAG,mBAAmB,CAAC;IAChE,MAAM,iBAAiB,GAAG,WAAW,CACnC,QAAQ,EACR,cAAc,CAAC,IAAI,EACnB,oCAAoC,CACrC,CAAC;IAEF,MAAM,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAC3D,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,CAAC,UAAU,CAAC,iBAAiB,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,kDAAkD,iBAAiB,mBAAmB;cAClF,gEAAgE;cAChE,IAAI,oCAAoC,qBAAqB;cAC7D,IAAI,cAAc,CAAC,IAAI,yCAAyC;cAChE,uEAAuE;cACvE,2EAA2E;cAC3E,8BAA8B;cAC9B,IAAI,iBAAiB,SAAS;cAC9B,8EAA8E,CACnF,CAAC;IACJ,CAAC;IAED,IAAI,MAAmC,CAAC;IACxC,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,UAAU,CAA8B,iBAAiB,CAAC,CAAC;IAC5E,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,KAAK,GAAG,mBAAmB,CAAC,CAAC,CAAC,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,gFAAgF;cAC5E,GAAG,iBAAiB,qBAAqB;cACzC,uDAAuD;cACvD,6EAA6E;cAC7E,6CAA6C;cAC7C,UAAU,KAAK,EAAE,CACtB,CAAC;IACJ,CAAC;IAED,oBAAoB,CAAC,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;IACpD,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * @fileoverview Per-service `wildo.website.config.ts` loader.\n *\n * Mirrors the `loadTechnicalDocConfig()` shape used by\n * `platform/factory/wildo-dev-companion/src/technical-doc/services/technical-doc.companion.service.ts`\n * (see `saas-technical-doc.md` Step 7.5). Discovers the unique\n * `frontendType: STATIC_WEBSITE` service in the SaaS config, joins\n * `<saasRoot>/<service.path>/wildo.website.config.ts`, and loads it\n * via the jiti-backed `loadConfig` from `@wildo-ai/platform-config-lib`.\n *\n * The actual Zod validation runs INSIDE `defineWebsiteConfig` (which\n * the per-service config file invokes via\n * `export default defineWebsiteConfig({...})`), so by the time\n * `loadConfig` returns we already hold a parsed\n * `WildoMarketingWebsiteConfig`.\n *\n * @wildo-boundary\n * This module is **Node-only**. It imports `loadConfig` and\n * `AppFrontendType` from `@wildo-ai/platform-config-lib`, which\n * transitively pulls `jiti`, `node:fs`, `node:path`, `node:crypto`,\n * and other Node-only API surface. It MUST NOT be imported from\n * any browser-bundled entry of this engine package — specifically\n * NOT from `index.ts`, `companion-exports.ts`, or `astro-island.ts`.\n * The dedicated `./config-loader` package subpath exists precisely\n * to keep this surface OUT of the React-island bundle (enforced by\n * `__tests__/bundle-isolation.test.ts`).\n *\n * The Astro build's `astro.config.ts` (a Node-side file by\n * construction) is the canonical caller. A future companion\n * subprocess that audits the marketing surface would also call\n * from here. Browser bundles MUST NOT.\n */\n\nimport { existsSync } from 'node:fs';\nimport { resolve as resolvePath } from 'node:path';\nimport { AppFrontendType, loadConfig } from '@wildo-ai/platform-config-lib';\nimport type { WildoSaasConfig } from '@wildo-ai/platform-config-lib';\nimport type { WildoMarketingWebsiteConfig } from './wildo-website-config.schemas';\n\nimport { describeThrownValue } from '@wildo-ai/saas-models/public-runtime';\n/**\n * Conventional file name for the per-service marketing-website\n * configuration (Step 7.5 refactor — `saas-website-slice.md` /\n * `saas-technical-doc.md` Step 7.5 mirror). Authored at the website\n * service root next to `package.json` / `astro.config.ts`.\n *\n * Discovery is \"by convention, not by configuration\":\n *\n * - The loader locates this file by walking the `services` entry\n * whose `frontendType === STATIC_WEBSITE` and joining\n * `<saasRoot>/<service.path>/`. The saas-config schema's\n * `superRefine` already enforces \"at most one STATIC_WEBSITE\n * service per app\", so the lookup is deterministic — apps can\n * name the website service anything (`'website'`, `'marketing'`,\n * `'site'`).\n *\n * - File presence is the enable signal — there is NO `enabled`\n * field on `WildoMarketingWebsiteConfigSchema`. A STATIC_WEBSITE\n * service without this file is treated as a misconfiguration\n * and the loader throws (rather than silently shipping an Astro\n * build with no `i18n` routing / no canonical origin).\n */\nexport const PER_SERVICE_WEBSITE_CONFIG_FILE_NAME = 'wildo.website.config.ts';\n\n/**\n * Memoization slot scoped to the loader module. Keyed by absolute\n * config file path so multiple SaaS configs evaluated in the same\n * process (rare in production — possible in tests) do not collide.\n *\n * Lifetime: the loader is invoked from the Astro build / dev-server\n * pipeline, both of which restart the Node process on every dev-stack\n * reload (the watcher that picks up changes to `wildo.website.config.ts`\n * is the same watcher that reboots the build). A stale entry here\n * therefore cannot outlive a config edit.\n *\n * Tests can pre-populate a known path via {@link primeWebsiteConfigCache}\n * to skip the jiti round-trip when exercising downstream consumers.\n */\nconst WEBSITE_CONFIG_CACHE = new Map<string, WildoMarketingWebsiteConfig>();\n\n/**\n * Test helper — pre-populate the loader's memoization slot for a\n * given absolute config path. Mirrors the\n * `cachedTechnicalDocConfig` pre-population trick used by\n * `technical-doc.publish-openapi.service.test.ts` so the loader's\n * downstream consumers can be exercised in tests without writing a\n * `.ts` file to disk + invoking jiti.\n *\n * Production callers MUST NOT use this helper.\n */\nexport function primeWebsiteConfigCache(\n absoluteConfigPath: string,\n config: WildoMarketingWebsiteConfig,\n): void {\n WEBSITE_CONFIG_CACHE.set(resolvePath(absoluteConfigPath), config);\n}\n\n/**\n * Test helper — clear every cached entry. Use in `beforeEach`/`afterEach`\n * to prevent test cross-contamination.\n */\nexport function clearWebsiteConfigCache(): void {\n WEBSITE_CONFIG_CACHE.clear();\n}\n\n/**\n * Arguments for {@link loadWebsiteConfig}. Kept as a structured shape\n * (rather than positional `loadWebsiteConfig(saasRoot, saasConfig)`)\n * so that future per-call options (e.g. an explicit `serviceKey`\n * override for multi-website apps the day the singleton invariant\n * relaxes) slot in without breaking callers.\n */\nexport interface LoadWebsiteConfigArgs {\n /**\n * Absolute path to the SaaS app root — the directory holding\n * `wildo.saas.config.ts`. Used to resolve `<saasRoot>/<service.path>/`\n * for the website service folder.\n */\n saasRoot: string;\n /**\n * The already-parsed SaaS config (load once via\n * `loadConfig<WildoSaasConfig>(...)` upstream and pass it down here\n * — re-parsing per call would invoke jiti twice for one logical\n * load).\n */\n saasConfig: WildoSaasConfig;\n}\n\n/**\n * Discover and load the per-service marketing-website config.\n *\n * Resolution algorithm (mirrors\n * `TechnicalDocCompanionService.loadTechnicalDocConfig`):\n *\n * 1. Find the unique service in `saasConfig.services` whose\n * `frontendType === STATIC_WEBSITE`. The saas-config\n * `superRefine` guarantees there is at most one — if there is\n * none, throw with the misconfiguration message.\n * 2. Resolve `<saasRoot>/<service.path>/wildo.website.config.ts`.\n * 3. If the file does not exist, throw (file presence IS the\n * enable signal — a STATIC_WEBSITE service without this file\n * is broken).\n * 4. Check the memoization slot keyed on the absolute path; return\n * the cached value if present.\n * 5. Call `loadConfig<WildoMarketingWebsiteConfig>(absolutePath)`.\n * jiti evaluates the file, which invokes `defineWebsiteConfig`,\n * which invokes `WildoMarketingWebsiteConfigSchema.parse` and\n * emits any `@unimplemented` warnings. The returned object is\n * already-validated — we trust the in-file `defineWebsiteConfig`\n * contract here (mirrors the technical-doc loader pattern in\n * `TechnicalDocCompanionService.loadTechnicalDocConfig`, which\n * also does NOT re-parse). A file that hand-rolls the default\n * export without going through `defineWebsiteConfig` would\n * surface as a downstream type mismatch — acceptable given the\n * authoring contract is explicit and grep-discoverable.\n * 6. Memoize and return.\n *\n * @throws Error with a `[wildo-website-config]` prefix if no\n * STATIC_WEBSITE service is declared, the per-service config\n * file is missing, or the file's default export does not\n * pass `WildoMarketingWebsiteConfigSchema`. The loader\n * intentionally throws plain `Error`s rather than building a\n * `WildoBackendError` so it stays usable from the Astro\n * build pipeline (which has no DI container) — the companion\n * can wrap with `errorBuilder` at the call site if needed\n * (mirrors how `loadConfig` itself throws plain `Error`).\n */\nexport async function loadWebsiteConfig(\n args: LoadWebsiteConfigArgs,\n): Promise<WildoMarketingWebsiteConfig> {\n const { saasRoot, saasConfig } = args;\n\n const websiteServiceEntry = Object.entries(saasConfig.services).find(\n ([, service]) => service.frontendType === AppFrontendType.STATIC_WEBSITE,\n );\n if (!websiteServiceEntry) {\n throw new Error(\n '[wildo-website-config] No frontend service with `frontendType: STATIC_WEBSITE` is declared in '\n + 'wildo.saas.config.ts. The marketing-website Astro build / loader requires an authored '\n + 'STATIC_WEBSITE service because that service folder owns the per-service '\n + '`wildo.website.config.ts` (publicOrigin, appOrigin, defaultLocale, locales). '\n + 'Add a STATIC_WEBSITE service in wildo.saas.config.ts and ship the per-service '\n + 'config file under it.',\n );\n }\n\n const [websiteServiceKey, websiteService] = websiteServiceEntry;\n const websiteConfigPath = resolvePath(\n saasRoot,\n websiteService.path,\n PER_SERVICE_WEBSITE_CONFIG_FILE_NAME,\n );\n\n const cached = WEBSITE_CONFIG_CACHE.get(websiteConfigPath);\n if (cached !== undefined) {\n return cached;\n }\n\n if (!existsSync(websiteConfigPath)) {\n throw new Error(\n `[wildo-website-config] STATIC_WEBSITE service '${websiteServiceKey}' is declared in `\n + `wildo.saas.config.ts but the required per-service config file `\n + `'${PER_SERVICE_WEBSITE_CONFIG_FILE_NAME}' is missing under `\n + `'${websiteService.path}/'. File presence IS the enable signal `\n + '(saas-website-slice.md / saas-technical-doc.md Step 7.5 — the legacy '\n + '`marketingWebsite` block on wildo.saas.config.ts was dropped in favor of '\n + 'a per-service file). Create '\n + `'${websiteConfigPath}' with `\n + '`export default defineWebsiteConfig({ ... })` from `@wildo-ai/saas-website`.',\n );\n }\n\n let loaded: WildoMarketingWebsiteConfig;\n try {\n loaded = await loadConfig<WildoMarketingWebsiteConfig>(websiteConfigPath);\n } catch (e) {\n const cause = describeThrownValue(e);\n throw new Error(\n `[wildo-website-config] Failed to load per-service marketing-website config at `\n + `${websiteConfigPath}. The file MUST do `\n + '`export default defineWebsiteConfig({...})` and pass '\n + 'WildoMarketingWebsiteConfigSchema (`z.strictObject` — unknown keys reject; '\n + '`defaultLocale` must appear in `locales`). '\n + `Cause: ${cause}`,\n );\n }\n\n WEBSITE_CONFIG_CACHE.set(websiteConfigPath, loaded);\n return loaded;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"load-website-config.js","sourceRoot":"","sources":["../../../../src/config/load-website-config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Per-service `wildo.website.config.ts` loader.\n *\n * Mirrors the `loadTechnicalDocConfig()` shape used by\n * `platform/factory/wildo-dev-companion/src/technical-doc/services/technical-doc.companion.service.ts`\n * (see `saas-technical-doc.md` Step 7.5). Discovers the unique\n * `frontendType: STATIC_WEBSITE` service in the SaaS config, joins\n * `<saasRoot>/<service.path>/wildo.website.config.ts`, and loads it\n * via the jiti-backed `loadConfig` from `@wildo-ai/platform-config-lib`.\n *\n * The actual Zod validation runs INSIDE `defineWebsiteConfig` (which\n * the per-service config file invokes via\n * `export default defineWebsiteConfig({...})`), so by the time\n * `loadConfig` returns we already hold a parsed\n * `WildoMarketingWebsiteConfig`.\n *\n * @wildo-boundary\n * This module is **Node-only**. It imports `loadConfig` and\n * `AppFrontendType` from `@wildo-ai/platform-config-lib`, which\n * transitively pulls `jiti`, `node:fs`, `node:path`, `node:crypto`,\n * and other Node-only API surface. It MUST NOT be imported from\n * any browser-bundled entry of this engine package — specifically\n * NOT from `index.ts`, `companion-exports.ts`, or `astro-island.ts`.\n * The dedicated `./config-loader` package subpath exists precisely\n * to keep this surface OUT of the React-island bundle (enforced by\n * `__tests__/bundle-isolation.test.ts`).\n *\n * The Astro build's `astro.config.ts` (a Node-side file by\n * construction) is the canonical caller. A future companion\n * subprocess that audits the marketing surface would also call\n * from here. Browser bundles MUST NOT.\n */\n\nimport { existsSync } from 'node:fs';\nimport { resolve as resolvePath } from 'node:path';\nimport { AppFrontendType, loadConfig } from '@wildo-ai/platform-config-lib';\nimport type { WildoSaasConfig } from '@wildo-ai/platform-config-lib';\nimport type { WildoMarketingWebsiteConfig } from './wildo-website-config.schemas';\n\nimport { describeThrownValue } from '@wildo-ai/saas-models/public-runtime';\n/**\n * Conventional file name for the per-service marketing-website\n * configuration (Step 7.5 refactor — `saas-website-slice.md` /\n * `saas-technical-doc.md` Step 7.5 mirror). Authored at the website\n * service root next to `package.json` / `astro.config.ts`.\n *\n * Discovery is \"by convention, not by configuration\":\n *\n * - The loader locates this file by walking the `services` entry\n * whose `frontendType === STATIC_WEBSITE` and joining\n * `<saasRoot>/<service.path>/`. The saas-config schema's\n * `superRefine` already enforces \"at most one STATIC_WEBSITE\n * service per app\", so the lookup is deterministic — apps can\n * name the website service anything (`'website'`, `'marketing'`,\n * `'site'`).\n *\n * - File presence is the enable signal — there is NO `enabled`\n * field on `WildoMarketingWebsiteConfigSchema`. A STATIC_WEBSITE\n * service without this file is treated as a misconfiguration\n * and the loader throws (rather than silently shipping an Astro\n * build with no `i18n` routing / no canonical origin).\n */\nexport const PER_SERVICE_WEBSITE_CONFIG_FILE_NAME = 'wildo.website.config.ts';\n\n/**\n * Memoization slot scoped to the loader module. Keyed by absolute\n * config file path so multiple SaaS configs evaluated in the same\n * process (rare in production — possible in tests) do not collide.\n *\n * Lifetime: the loader is invoked from the Astro build / dev-server\n * pipeline, both of which restart the Node process on every dev-stack\n * reload (the watcher that picks up changes to `wildo.website.config.ts`\n * is the same watcher that reboots the build). A stale entry here\n * therefore cannot outlive a config edit.\n *\n * Tests can pre-populate a known path via {@link primeWebsiteConfigCache}\n * to skip the jiti round-trip when exercising downstream consumers.\n */\nconst WEBSITE_CONFIG_CACHE = new Map<string, WildoMarketingWebsiteConfig>();\n\n/**\n * Test helper — pre-populate the loader's memoization slot for a\n * given absolute config path. Mirrors the\n * `cachedTechnicalDocConfig` pre-population trick used by\n * `technical-doc.publish-openapi.service.test.ts` so the loader's\n * downstream consumers can be exercised in tests without writing a\n * `.ts` file to disk + invoking jiti.\n *\n * Production callers MUST NOT use this helper.\n */\nexport function primeWebsiteConfigCache(\n absoluteConfigPath: string,\n config: WildoMarketingWebsiteConfig,\n): void {\n WEBSITE_CONFIG_CACHE.set(resolvePath(absoluteConfigPath), config);\n}\n\n/**\n * Test helper — clear every cached entry. Use in `beforeEach`/`afterEach`\n * to prevent test cross-contamination.\n */\nexport function clearWebsiteConfigCache(): void {\n WEBSITE_CONFIG_CACHE.clear();\n}\n\n/**\n * Arguments for {@link loadWebsiteConfig}. Kept as a structured shape\n * (rather than positional `loadWebsiteConfig(saasRoot, saasConfig)`)\n * so that future per-call options (e.g. an explicit `serviceKey`\n * override for multi-website apps the day the singleton invariant\n * relaxes) slot in without breaking callers.\n */\nexport interface LoadWebsiteConfigArgs {\n /**\n * Absolute path to the SaaS app root — the directory holding\n * `wildo.saas.config.ts`. Used to resolve `<saasRoot>/<service.path>/`\n * for the website service folder.\n */\n saasRoot: string;\n /**\n * The already-parsed SaaS config (load once via\n * `loadConfig<WildoSaasConfig>(...)` upstream and pass it down here\n * — re-parsing per call would invoke jiti twice for one logical\n * load).\n */\n saasConfig: WildoSaasConfig;\n}\n\n/**\n * The application's one `STATIC_WEBSITE` service, as `[serviceKey, service]`.\n * Throws when none is declared: the service folder is what owns\n * `wildo.website.config.ts`, so a website build without one has nothing to load.\n */\nexport function resolveStaticWebsiteServiceEntry(\n saasConfig: WildoSaasConfig,\n): [string, WildoSaasConfig['services'][string]] {\n const websiteServiceEntry = Object.entries(saasConfig.services).find(\n ([, service]) => service.frontendType === AppFrontendType.STATIC_WEBSITE,\n );\n if (!websiteServiceEntry) {\n throw new Error(\n '[wildo-website-config] No frontend service with `frontendType: STATIC_WEBSITE` is declared in '\n + 'wildo.saas.config.ts. The marketing-website Astro build / loader requires an authored '\n + 'STATIC_WEBSITE service because that service folder owns the per-service '\n + '`wildo.website.config.ts` (publicOrigin, appOrigin, defaultLocale, locales). '\n + 'Add a STATIC_WEBSITE service in wildo.saas.config.ts and ship the per-service '\n + 'config file under it.',\n );\n }\n return websiteServiceEntry;\n}\n\n/**\n * Discover and load the per-service marketing-website config.\n *\n * Resolution algorithm (mirrors\n * `TechnicalDocCompanionService.loadTechnicalDocConfig`):\n *\n * 1. Find the unique service in `saasConfig.services` whose\n * `frontendType === STATIC_WEBSITE`. The saas-config\n * `superRefine` guarantees there is at most one — if there is\n * none, throw with the misconfiguration message.\n * 2. Resolve `<saasRoot>/<service.path>/wildo.website.config.ts`.\n * 3. If the file does not exist, throw (file presence IS the\n * enable signal — a STATIC_WEBSITE service without this file\n * is broken).\n * 4. Check the memoization slot keyed on the absolute path; return\n * the cached value if present.\n * 5. Call `loadConfig<WildoMarketingWebsiteConfig>(absolutePath)`.\n * jiti evaluates the file, which invokes `defineWebsiteConfig`,\n * which invokes `WildoMarketingWebsiteConfigSchema.parse` and\n * emits any `@unimplemented` warnings. The returned object is\n * already-validated — we trust the in-file `defineWebsiteConfig`\n * contract here (mirrors the technical-doc loader pattern in\n * `TechnicalDocCompanionService.loadTechnicalDocConfig`, which\n * also does NOT re-parse). A file that hand-rolls the default\n * export without going through `defineWebsiteConfig` would\n * surface as a downstream type mismatch — acceptable given the\n * authoring contract is explicit and grep-discoverable.\n * 6. Memoize and return.\n *\n * @throws Error with a `[wildo-website-config]` prefix if no\n * STATIC_WEBSITE service is declared, the per-service config\n * file is missing, or the file's default export does not\n * pass `WildoMarketingWebsiteConfigSchema`. The loader\n * intentionally throws plain `Error`s rather than building a\n * `WildoBackendError` so it stays usable from the Astro\n * build pipeline (which has no DI container) — the companion\n * can wrap with `errorBuilder` at the call site if needed\n * (mirrors how `loadConfig` itself throws plain `Error`).\n */\nexport async function loadWebsiteConfig(\n args: LoadWebsiteConfigArgs,\n): Promise<WildoMarketingWebsiteConfig> {\n const { saasRoot, saasConfig } = args;\n const websiteServiceEntry = resolveStaticWebsiteServiceEntry(saasConfig);\n\n const [websiteServiceKey, websiteService] = websiteServiceEntry;\n const websiteConfigPath = resolvePath(\n saasRoot,\n websiteService.path,\n PER_SERVICE_WEBSITE_CONFIG_FILE_NAME,\n );\n\n const cached = WEBSITE_CONFIG_CACHE.get(websiteConfigPath);\n if (cached !== undefined) {\n return cached;\n }\n\n if (!existsSync(websiteConfigPath)) {\n throw new Error(\n `[wildo-website-config] STATIC_WEBSITE service '${websiteServiceKey}' is declared in `\n + `wildo.saas.config.ts but the required per-service config file `\n + `'${PER_SERVICE_WEBSITE_CONFIG_FILE_NAME}' is missing under `\n + `'${websiteService.path}/'. File presence IS the enable signal `\n + '(saas-website-slice.md / saas-technical-doc.md Step 7.5 — the legacy '\n + '`marketingWebsite` block on wildo.saas.config.ts was dropped in favor of '\n + 'a per-service file). Create '\n + `'${websiteConfigPath}' with `\n + '`export default defineWebsiteConfig({ ... })` from `@wildo-ai/saas-website`.',\n );\n }\n\n let loaded: WildoMarketingWebsiteConfig;\n try {\n loaded = await loadConfig<WildoMarketingWebsiteConfig>(websiteConfigPath);\n } catch (e) {\n const cause = describeThrownValue(e);\n throw new Error(\n `[wildo-website-config] Failed to load per-service marketing-website config at `\n + `${websiteConfigPath}. The file MUST do `\n + '`export default defineWebsiteConfig({...})` and pass '\n + 'WildoMarketingWebsiteConfigSchema (`z.strictObject` — unknown keys reject; '\n + '`defaultLocale` must appear in `locales`). '\n + `Cause: ${cause}`,\n );\n }\n\n WEBSITE_CONFIG_CACHE.set(websiteConfigPath, loaded);\n return loaded;\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PublicPricingCatalog } from '@wildo-ai/saas-models/public-runtime';
|
|
2
|
+
export interface LoadWebsitePricingCatalogInput {
|
|
3
|
+
/** Where to start the walk up to `wildo.saas.config.ts`. Defaults to the working directory. */
|
|
4
|
+
readonly cwd?: string | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Keep only plans this scope may subscribe to (a `ResourcePrimaryScope` wire value, e.g.
|
|
7
|
+
* `'organizations'`). Omit when the catalogue sells to one audience.
|
|
8
|
+
*/
|
|
9
|
+
readonly targetScope?: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
export declare function loadWebsitePricingCatalog(input?: LoadWebsitePricingCatalogInput): Promise<PublicPricingCatalog>;
|
|
12
|
+
/** @wildo_source:part:end saas.website.config.pricing-catalog-loader */
|
|
13
|
+
//# sourceMappingURL=load-website-pricing-catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-website-pricing-catalog.d.ts","sourceRoot":"","sources":["../../../../src/config/load-website-pricing-catalog.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAEjF,MAAM,WAAW,8BAA8B;IAC7C,+FAA+F;IAC/F,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3C;AAED,wBAAsB,yBAAyB,CAAC,KAAK,GAAE,8BAAmC,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAczH;AACD,wEAAwE"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wildo_source:part:start saas.website.config.pricing-catalog-loader facet:layer:astro facet:family:website facet:audience:app-developer
|
|
3
|
+
*
|
|
4
|
+
* `loadWebsitePricingCatalog` — the application's public pricing catalogue, for a website's pricing
|
|
5
|
+
* page (#1835). Call it in the page's `.astro` frontmatter, pass the result to the bridge island as
|
|
6
|
+
* `pricingCatalog`, and read it in the section with `useWebsitePricingCatalog()`; build the page's
|
|
7
|
+
* `AggregateOffer` from the same value with `buildWebsitePricingOfferStructuredData`.
|
|
8
|
+
*
|
|
9
|
+
* It walks up to `wildo.saas.config.ts` the way `loadWebsiteSiteContext` does, and reads the billing
|
|
10
|
+
* catalogue the backend sells from — the `productDefinitions` of the application's shared modules —
|
|
11
|
+
* through `loadApplicationPublicPricingCatalog`, from SOURCE, so a website image that builds only the
|
|
12
|
+
* website still sees the current catalogue.
|
|
13
|
+
*
|
|
14
|
+
* Separate from `loadWebsiteSiteContext` on purpose: loading the shared graph costs seconds, and only a
|
|
15
|
+
* pricing page needs it. Every other page's build stays as fast as it was.
|
|
16
|
+
*
|
|
17
|
+
* **Node-only.** Lives on the `/config-loader` subpath; it must never reach a React-island bundle.
|
|
18
|
+
*/
|
|
19
|
+
import { loadApplicationPublicPricingCatalog, loadConfig, resolveWorkspaceScope, } from '@wildo-ai/platform-config-lib';
|
|
20
|
+
export async function loadWebsitePricingCatalog(input = {}) {
|
|
21
|
+
const workspaceScope = resolveWorkspaceScope(input.cwd);
|
|
22
|
+
if (workspaceScope.saasRoot === null || workspaceScope.saasConfigPath === null) {
|
|
23
|
+
throw new Error(`[saas-website] No wildo.saas.config.ts found walking up from ${input.cwd ?? process.cwd()}. `
|
|
24
|
+
+ 'Run astro from inside the application workspace.');
|
|
25
|
+
}
|
|
26
|
+
const saasConfig = await loadConfig(workspaceScope.saasConfigPath);
|
|
27
|
+
return loadApplicationPublicPricingCatalog({
|
|
28
|
+
saasRoot: workspaceScope.saasRoot,
|
|
29
|
+
saasConfig,
|
|
30
|
+
...(input.targetScope === undefined ? {} : { targetScope: input.targetScope }),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** @wildo_source:part:end saas.website.config.pricing-catalog-loader */
|
|
34
|
+
//# sourceMappingURL=load-website-pricing-catalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-website-pricing-catalog.js","sourceRoot":"","sources":["../../../../src/config/load-website-pricing-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EACL,mCAAmC,EACnC,UAAU,EACV,qBAAqB,GAEtB,MAAM,+BAA+B,CAAC;AAavC,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,QAAwC,EAAE;IACxF,MAAM,cAAc,GAAG,qBAAqB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACxD,IAAI,cAAc,CAAC,QAAQ,KAAK,IAAI,IAAI,cAAc,CAAC,cAAc,KAAK,IAAI,EAAE,CAAC;QAC/E,MAAM,IAAI,KAAK,CACb,gEAAgE,KAAK,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,IAAI;cAC1F,kDAAkD,CACvD,CAAC;IACJ,CAAC;IACD,MAAM,UAAU,GAAG,MAAM,UAAU,CAAkB,cAAc,CAAC,cAAc,CAAC,CAAC;IACpF,OAAO,mCAAmC,CAAC;QACzC,QAAQ,EAAE,cAAc,CAAC,QAAQ;QACjC,UAAU;QACV,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC;KAC/E,CAAC,CAAC;AACL,CAAC;AACD,wEAAwE","sourcesContent":["/**\n * @wildo_source:part:start saas.website.config.pricing-catalog-loader facet:layer:astro facet:family:website facet:audience:app-developer\n *\n * `loadWebsitePricingCatalog` — the application's public pricing catalogue, for a website's pricing\n * page (#1835). Call it in the page's `.astro` frontmatter, pass the result to the bridge island as\n * `pricingCatalog`, and read it in the section with `useWebsitePricingCatalog()`; build the page's\n * `AggregateOffer` from the same value with `buildWebsitePricingOfferStructuredData`.\n *\n * It walks up to `wildo.saas.config.ts` the way `loadWebsiteSiteContext` does, and reads the billing\n * catalogue the backend sells from — the `productDefinitions` of the application's shared modules —\n * through `loadApplicationPublicPricingCatalog`, from SOURCE, so a website image that builds only the\n * website still sees the current catalogue.\n *\n * Separate from `loadWebsiteSiteContext` on purpose: loading the shared graph costs seconds, and only a\n * pricing page needs it. Every other page's build stays as fast as it was.\n *\n * **Node-only.** Lives on the `/config-loader` subpath; it must never reach a React-island bundle.\n */\nimport {\n loadApplicationPublicPricingCatalog,\n loadConfig,\n resolveWorkspaceScope,\n type WildoSaasConfig,\n} from '@wildo-ai/platform-config-lib';\nimport type { PublicPricingCatalog } from '@wildo-ai/saas-models/public-runtime';\n\nexport interface LoadWebsitePricingCatalogInput {\n /** Where to start the walk up to `wildo.saas.config.ts`. Defaults to the working directory. */\n readonly cwd?: string | undefined;\n /**\n * Keep only plans this scope may subscribe to (a `ResourcePrimaryScope` wire value, e.g.\n * `'organizations'`). Omit when the catalogue sells to one audience.\n */\n readonly targetScope?: string | undefined;\n}\n\nexport async function loadWebsitePricingCatalog(input: LoadWebsitePricingCatalogInput = {}): Promise<PublicPricingCatalog> {\n const workspaceScope = resolveWorkspaceScope(input.cwd);\n if (workspaceScope.saasRoot === null || workspaceScope.saasConfigPath === null) {\n throw new Error(\n `[saas-website] No wildo.saas.config.ts found walking up from ${input.cwd ?? process.cwd()}. `\n + 'Run astro from inside the application workspace.',\n );\n }\n const saasConfig = await loadConfig<WildoSaasConfig>(workspaceScope.saasConfigPath);\n return loadApplicationPublicPricingCatalog({\n saasRoot: workspaceScope.saasRoot,\n saasConfig,\n ...(input.targetScope === undefined ? {} : { targetScope: input.targetScope }),\n });\n}\n/** @wildo_source:part:end saas.website.config.pricing-catalog-loader */\n"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type LocalhostApplicationOrigins } from '@wildo-ai/platform-config-lib';
|
|
2
|
+
import { type WebsiteSiteContext } from '../astro/website-site-context';
|
|
3
|
+
import type { WebsiteRootConfig } from '../schemas/manifests/website-root-config.shared.schemas';
|
|
4
|
+
import type { WildoMarketingWebsiteConfig } from './wildo-website-config.schemas';
|
|
5
|
+
/** Explicit per-build origin overrides — what a deploy build passes as build arguments. */
|
|
6
|
+
export interface WebsiteOriginOverrides {
|
|
7
|
+
/** `PUBLIC_APP_ORIGIN`: this environment's product origin, derived by the deploy workflow. */
|
|
8
|
+
readonly appOrigin?: string | undefined;
|
|
9
|
+
/** `PUBLIC_API_ORIGIN`: this environment's API origin. */
|
|
10
|
+
readonly apiOrigin?: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
export interface ResolveWebsiteBuildOriginsInput {
|
|
13
|
+
/** The authored `wildo.website.config.ts` — the production default. */
|
|
14
|
+
readonly websiteConfig: WildoMarketingWebsiteConfig;
|
|
15
|
+
/** `true` under `astro dev`. */
|
|
16
|
+
readonly isDevelopment: boolean;
|
|
17
|
+
/** Where the product and API run locally, from the app's own declared ports. */
|
|
18
|
+
readonly localOrigins: LocalhostApplicationOrigins;
|
|
19
|
+
/** Explicit overrides, which win over everything. */
|
|
20
|
+
readonly overrides: WebsiteOriginOverrides;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The `websiteConfig` this BUILD serves, with `appOrigin` and `apiOrigin` settled.
|
|
24
|
+
*
|
|
25
|
+
* Precedence, per origin:
|
|
26
|
+
*
|
|
27
|
+
* 1. an explicit override (`PUBLIC_APP_ORIGIN` / `PUBLIC_API_ORIGIN`) — the deploy
|
|
28
|
+
* workflow derives these per environment, so a staging build links into staging;
|
|
29
|
+
* 2. under `astro dev`, the locally running product / API, from the ports the
|
|
30
|
+
* application declares — so a CTA reaches the process `wildo local dev` started;
|
|
31
|
+
* 3. the value authored in `wildo.website.config.ts`.
|
|
32
|
+
*
|
|
33
|
+
* The result replaces the two fields in `websiteConfig` rather than living beside
|
|
34
|
+
* it, because every page already forwards `websiteConfig.appOrigin` /
|
|
35
|
+
* `websiteConfig.apiOrigin` into its island: a second field would be a second
|
|
36
|
+
* source that pages could read instead. `websiteConfig` in a site context is
|
|
37
|
+
* therefore "the config as it applies to this build", which the config file was
|
|
38
|
+
* already half-way to being — it reads `PUBLIC_APP_ORIGIN` itself.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveWebsiteBuildOrigins(input: ResolveWebsiteBuildOriginsInput): WildoMarketingWebsiteConfig;
|
|
41
|
+
export interface LoadWebsiteSiteContextInput {
|
|
42
|
+
/** The workspace's `WEBSITE_ROOT_CONFIG`. */
|
|
43
|
+
readonly rootConfig: WebsiteRootConfig;
|
|
44
|
+
/** Pass `import.meta.env?.DEV === true` (optional chaining: `astro.config.ts` evaluates the caller before Vite's env exists). */
|
|
45
|
+
readonly isDevelopment: boolean;
|
|
46
|
+
/** Where to start the walk up to `wildo.saas.config.ts`. Defaults to the working directory. */
|
|
47
|
+
readonly cwd?: string | undefined;
|
|
48
|
+
/** Defaults to `PUBLIC_APP_ORIGIN` / `PUBLIC_API_ORIGIN` from the process environment. */
|
|
49
|
+
readonly overrides?: WebsiteOriginOverrides | undefined;
|
|
50
|
+
}
|
|
51
|
+
export declare function loadWebsiteSiteContext(input: LoadWebsiteSiteContextInput): Promise<WebsiteSiteContext>;
|
|
52
|
+
/** @wildo_source:part:end saas.website.config.site-context-loader */
|
|
53
|
+
//# sourceMappingURL=load-website-site-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-website-site-context.d.ts","sourceRoot":"","sources":["../../../../src/config/load-website-site-context.ts"],"names":[],"mappings":"AA0CA,OAAO,EAML,KAAK,2BAA2B,EAEjC,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAA6B,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yDAAyD,CAAC;AAEjG,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,gCAAgC,CAAC;AAElF,2FAA2F;AAC3F,MAAM,WAAW,sBAAsB;IACrC,8FAA8F;IAC9F,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC;AAED,MAAM,WAAW,+BAA+B;IAC9C,uEAAuE;IACvE,QAAQ,CAAC,aAAa,EAAE,2BAA2B,CAAC;IACpD,gCAAgC;IAChC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,gFAAgF;IAChF,QAAQ,CAAC,YAAY,EAAE,2BAA2B,CAAC;IACnD,qDAAqD;IACrD,QAAQ,CAAC,SAAS,EAAE,sBAAsB,CAAC;CAC5C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,+BAA+B,GAAG,2BAA2B,CAO9G;AAED,MAAM,WAAW,2BAA2B;IAC1C,6CAA6C;IAC7C,QAAQ,CAAC,UAAU,EAAE,iBAAiB,CAAC;IACvC,iIAAiI;IACjI,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,+FAA+F;IAC/F,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,0FAA0F;IAC1F,QAAQ,CAAC,SAAS,CAAC,EAAE,sBAAsB,GAAG,SAAS,CAAC;CACzD;AAED,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,2BAA2B,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA6B5G;AACD,qEAAqE"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wildo_source:part:start saas.website.config.site-context-loader facet:layer:astro facet:family:website facet:audience:app-developer
|
|
3
|
+
*
|
|
4
|
+
* `loadWebsiteSiteContext` — the ONE way a website workspace builds its
|
|
5
|
+
* `WebsiteSiteContext`. Called once, from the workspace's
|
|
6
|
+
* `src/website-site-context.ts`:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { loadWebsiteSiteContext } from '@wildo-ai/saas-website/config-loader';
|
|
10
|
+
* import { WEBSITE_ROOT_CONFIG } from './website-root.config.js';
|
|
11
|
+
*
|
|
12
|
+
* export const WEBSITE_SITE_CONTEXT = await loadWebsiteSiteContext({
|
|
13
|
+
* rootConfig: WEBSITE_ROOT_CONFIG,
|
|
14
|
+
* isDevelopment: import.meta.env?.DEV === true,
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* It walks up from the working directory to `wildo.saas.config.ts`, finds the
|
|
19
|
+
* application's single `STATIC_WEBSITE` service, loads that service's
|
|
20
|
+
* `wildo.website.config.ts` and its materialized provider block, and settles the
|
|
21
|
+
* two origins the site reaches the product through (see
|
|
22
|
+
* {@link resolveWebsiteBuildOrigins}).
|
|
23
|
+
*
|
|
24
|
+
* **Why the engine owns this.** Every website workspace used to carry its own
|
|
25
|
+
* copy of this lookup — the scaffolded ones and the hand-written ones alike —
|
|
26
|
+
* and each copy hardcoded its local API port while the app origin had no local
|
|
27
|
+
* form at all. The result: under `astro dev`, every "Start free" CTA pointed at a
|
|
28
|
+
* production-shaped host that resolves to nothing. One loader means one rule.
|
|
29
|
+
*
|
|
30
|
+
* **Why it walks up from the working directory, never from `import.meta.url`.**
|
|
31
|
+
* Astro's prerender pass bundles the calling module into `dist/.prerender/chunks/…`,
|
|
32
|
+
* where a `new URL('../..', import.meta.url)` anchor resolves to `dist/` rather than
|
|
33
|
+
* the application root — every prerendered route then failed with "Config file not
|
|
34
|
+
* found: …/website/dist/wildo.saas.config.ts", while `astro dev` hid it by serving
|
|
35
|
+
* from `src/`. Every `astro dev` / `astro build` runs with the website package (or
|
|
36
|
+
* an ancestor) as its working directory, which is inside the application either way.
|
|
37
|
+
*
|
|
38
|
+
* **Node-only.** Lives on the `/config-loader` subpath with the other loaders; it
|
|
39
|
+
* must never reach a React-island bundle.
|
|
40
|
+
*/
|
|
41
|
+
import { resolve as resolvePath } from 'node:path';
|
|
42
|
+
import { loadConfig, loadMaterializedFrontendProviders, resolveFrontendServicesArtifactPath, resolveLocalhostApplicationOrigins, resolveWorkspaceScope, } from '@wildo-ai/platform-config-lib';
|
|
43
|
+
import { composeWebsiteSiteContext } from '../astro/website-site-context.js';
|
|
44
|
+
import { loadWebsiteConfig, resolveStaticWebsiteServiceEntry } from './load-website-config.js';
|
|
45
|
+
/**
|
|
46
|
+
* The `websiteConfig` this BUILD serves, with `appOrigin` and `apiOrigin` settled.
|
|
47
|
+
*
|
|
48
|
+
* Precedence, per origin:
|
|
49
|
+
*
|
|
50
|
+
* 1. an explicit override (`PUBLIC_APP_ORIGIN` / `PUBLIC_API_ORIGIN`) — the deploy
|
|
51
|
+
* workflow derives these per environment, so a staging build links into staging;
|
|
52
|
+
* 2. under `astro dev`, the locally running product / API, from the ports the
|
|
53
|
+
* application declares — so a CTA reaches the process `wildo local dev` started;
|
|
54
|
+
* 3. the value authored in `wildo.website.config.ts`.
|
|
55
|
+
*
|
|
56
|
+
* The result replaces the two fields in `websiteConfig` rather than living beside
|
|
57
|
+
* it, because every page already forwards `websiteConfig.appOrigin` /
|
|
58
|
+
* `websiteConfig.apiOrigin` into its island: a second field would be a second
|
|
59
|
+
* source that pages could read instead. `websiteConfig` in a site context is
|
|
60
|
+
* therefore "the config as it applies to this build", which the config file was
|
|
61
|
+
* already half-way to being — it reads `PUBLIC_APP_ORIGIN` itself.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveWebsiteBuildOrigins(input) {
|
|
64
|
+
const { websiteConfig, isDevelopment, localOrigins, overrides } = input;
|
|
65
|
+
const settle = (override, local, authored) => override || (isDevelopment ? local : undefined) || authored;
|
|
66
|
+
const appOrigin = settle(overrides.appOrigin, localOrigins.app, websiteConfig.appOrigin) ?? websiteConfig.appOrigin;
|
|
67
|
+
const apiOrigin = settle(overrides.apiOrigin, localOrigins.api, websiteConfig.apiOrigin);
|
|
68
|
+
return { ...websiteConfig, appOrigin, ...(apiOrigin === undefined ? {} : { apiOrigin }) };
|
|
69
|
+
}
|
|
70
|
+
export async function loadWebsiteSiteContext(input) {
|
|
71
|
+
const workspaceScope = resolveWorkspaceScope(input.cwd);
|
|
72
|
+
if (workspaceScope.saasRoot === null || workspaceScope.saasConfigPath === null) {
|
|
73
|
+
throw new Error(`[saas-website] No wildo.saas.config.ts found walking up from ${input.cwd ?? process.cwd()}. `
|
|
74
|
+
+ 'Run astro from inside the application workspace.');
|
|
75
|
+
}
|
|
76
|
+
const saasRoot = workspaceScope.saasRoot;
|
|
77
|
+
const saasConfig = await loadConfig(workspaceScope.saasConfigPath);
|
|
78
|
+
const [websiteServiceKey] = resolveStaticWebsiteServiceEntry(saasConfig);
|
|
79
|
+
const websiteConfig = await loadWebsiteConfig({ saasRoot, saasConfig });
|
|
80
|
+
return composeWebsiteSiteContext({
|
|
81
|
+
rootConfig: input.rootConfig,
|
|
82
|
+
websiteConfig: resolveWebsiteBuildOrigins({
|
|
83
|
+
websiteConfig,
|
|
84
|
+
isDevelopment: input.isDevelopment,
|
|
85
|
+
localOrigins: resolveLocalhostApplicationOrigins(saasConfig.services),
|
|
86
|
+
overrides: input.overrides ?? {
|
|
87
|
+
appOrigin: process.env.PUBLIC_APP_ORIGIN,
|
|
88
|
+
apiOrigin: process.env.PUBLIC_API_ORIGIN,
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
frontendProviders: loadMaterializedFrontendProviders({
|
|
92
|
+
artifactPath: resolveFrontendServicesArtifactPath(resolvePath(saasRoot)),
|
|
93
|
+
serviceKey: websiteServiceKey,
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** @wildo_source:part:end saas.website.config.site-context-loader */
|
|
98
|
+
//# sourceMappingURL=load-website-site-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-website-site-context.js","sourceRoot":"","sources":["../../../../src/config/load-website-site-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AAEnD,OAAO,EACL,UAAU,EACV,iCAAiC,EACjC,mCAAmC,EACnC,kCAAkC,EAClC,qBAAqB,GAGtB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAAE,yBAAyB,EAA2B,MAAM,+BAA+B,CAAC;AAEnG,OAAO,EAAE,iBAAiB,EAAE,gCAAgC,EAAE,MAAM,uBAAuB,CAAC;AAsB5F;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,0BAA0B,CAAC,KAAsC;IAC/E,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IACxE,MAAM,MAAM,GAAG,CAAC,QAA4B,EAAE,KAAyB,EAAE,QAA4B,EAAsB,EAAE,CAC3H,QAAQ,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,QAAQ,CAAC;IAC9D,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,EAAE,aAAa,CAAC,SAAS,CAAC,IAAI,aAAa,CAAC,SAAS,CAAC;IACpH,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IACzF,OAAO,EAAE,GAAG,aAAa,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;AAC5F,CAAC;AAaD,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,KAAkC;IAC7E,MAAM,cAAc,GAAG,qBAAqB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACxD,IAAI,cAAc,CAAC,QAAQ,KAAK,IAAI,IAAI,cAAc,CAAC,cAAc,KAAK,IAAI,EAAE,CAAC;QAC/E,MAAM,IAAI,KAAK,CACb,gEAAgE,KAAK,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,IAAI;cAC1F,kDAAkD,CACvD,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAC;IACzC,MAAM,UAAU,GAAG,MAAM,UAAU,CAAkB,cAAc,CAAC,cAAc,CAAC,CAAC;IACpF,MAAM,CAAC,iBAAiB,CAAC,GAAG,gCAAgC,CAAC,UAAU,CAAC,CAAC;IACzE,MAAM,aAAa,GAAG,MAAM,iBAAiB,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC;IAExE,OAAO,yBAAyB,CAAC;QAC/B,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,aAAa,EAAE,0BAA0B,CAAC;YACxC,aAAa;YACb,aAAa,EAAE,KAAK,CAAC,aAAa;YAClC,YAAY,EAAE,kCAAkC,CAAC,UAAU,CAAC,QAAQ,CAAC;YACrE,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI;gBAC5B,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB;gBACxC,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB;aACzC;SACF,CAAC;QACF,iBAAiB,EAAE,iCAAiC,CAAC;YACnD,YAAY,EAAE,mCAAmC,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;YACxE,UAAU,EAAE,iBAAiB;SAC9B,CAAC;KACH,CAAC,CAAC;AACL,CAAC;AACD,qEAAqE","sourcesContent":["/**\n * @wildo_source:part:start saas.website.config.site-context-loader facet:layer:astro facet:family:website facet:audience:app-developer\n *\n * `loadWebsiteSiteContext` — the ONE way a website workspace builds its\n * `WebsiteSiteContext`. Called once, from the workspace's\n * `src/website-site-context.ts`:\n *\n * ```ts\n * import { loadWebsiteSiteContext } from '@wildo-ai/saas-website/config-loader';\n * import { WEBSITE_ROOT_CONFIG } from './website-root.config';\n *\n * export const WEBSITE_SITE_CONTEXT = await loadWebsiteSiteContext({\n * rootConfig: WEBSITE_ROOT_CONFIG,\n * isDevelopment: import.meta.env?.DEV === true,\n * });\n * ```\n *\n * It walks up from the working directory to `wildo.saas.config.ts`, finds the\n * application's single `STATIC_WEBSITE` service, loads that service's\n * `wildo.website.config.ts` and its materialized provider block, and settles the\n * two origins the site reaches the product through (see\n * {@link resolveWebsiteBuildOrigins}).\n *\n * **Why the engine owns this.** Every website workspace used to carry its own\n * copy of this lookup — the scaffolded ones and the hand-written ones alike —\n * and each copy hardcoded its local API port while the app origin had no local\n * form at all. The result: under `astro dev`, every \"Start free\" CTA pointed at a\n * production-shaped host that resolves to nothing. One loader means one rule.\n *\n * **Why it walks up from the working directory, never from `import.meta.url`.**\n * Astro's prerender pass bundles the calling module into `dist/.prerender/chunks/…`,\n * where a `new URL('../..', import.meta.url)` anchor resolves to `dist/` rather than\n * the application root — every prerendered route then failed with \"Config file not\n * found: …/website/dist/wildo.saas.config.ts\", while `astro dev` hid it by serving\n * from `src/`. Every `astro dev` / `astro build` runs with the website package (or\n * an ancestor) as its working directory, which is inside the application either way.\n *\n * **Node-only.** Lives on the `/config-loader` subpath with the other loaders; it\n * must never reach a React-island bundle.\n */\nimport { resolve as resolvePath } from 'node:path';\n\nimport {\n loadConfig,\n loadMaterializedFrontendProviders,\n resolveFrontendServicesArtifactPath,\n resolveLocalhostApplicationOrigins,\n resolveWorkspaceScope,\n type LocalhostApplicationOrigins,\n type WildoSaasConfig,\n} from '@wildo-ai/platform-config-lib';\n\nimport { composeWebsiteSiteContext, type WebsiteSiteContext } from '../astro/website-site-context';\nimport type { WebsiteRootConfig } from '../schemas/manifests/website-root-config.shared.schemas';\nimport { loadWebsiteConfig, resolveStaticWebsiteServiceEntry } from './load-website-config';\nimport type { WildoMarketingWebsiteConfig } from './wildo-website-config.schemas';\n\n/** Explicit per-build origin overrides — what a deploy build passes as build arguments. */\nexport interface WebsiteOriginOverrides {\n /** `PUBLIC_APP_ORIGIN`: this environment's product origin, derived by the deploy workflow. */\n readonly appOrigin?: string | undefined;\n /** `PUBLIC_API_ORIGIN`: this environment's API origin. */\n readonly apiOrigin?: string | undefined;\n}\n\nexport interface ResolveWebsiteBuildOriginsInput {\n /** The authored `wildo.website.config.ts` — the production default. */\n readonly websiteConfig: WildoMarketingWebsiteConfig;\n /** `true` under `astro dev`. */\n readonly isDevelopment: boolean;\n /** Where the product and API run locally, from the app's own declared ports. */\n readonly localOrigins: LocalhostApplicationOrigins;\n /** Explicit overrides, which win over everything. */\n readonly overrides: WebsiteOriginOverrides;\n}\n\n/**\n * The `websiteConfig` this BUILD serves, with `appOrigin` and `apiOrigin` settled.\n *\n * Precedence, per origin:\n *\n * 1. an explicit override (`PUBLIC_APP_ORIGIN` / `PUBLIC_API_ORIGIN`) — the deploy\n * workflow derives these per environment, so a staging build links into staging;\n * 2. under `astro dev`, the locally running product / API, from the ports the\n * application declares — so a CTA reaches the process `wildo local dev` started;\n * 3. the value authored in `wildo.website.config.ts`.\n *\n * The result replaces the two fields in `websiteConfig` rather than living beside\n * it, because every page already forwards `websiteConfig.appOrigin` /\n * `websiteConfig.apiOrigin` into its island: a second field would be a second\n * source that pages could read instead. `websiteConfig` in a site context is\n * therefore \"the config as it applies to this build\", which the config file was\n * already half-way to being — it reads `PUBLIC_APP_ORIGIN` itself.\n */\nexport function resolveWebsiteBuildOrigins(input: ResolveWebsiteBuildOriginsInput): WildoMarketingWebsiteConfig {\n const { websiteConfig, isDevelopment, localOrigins, overrides } = input;\n const settle = (override: string | undefined, local: string | undefined, authored: string | undefined): string | undefined =>\n override || (isDevelopment ? local : undefined) || authored;\n const appOrigin = settle(overrides.appOrigin, localOrigins.app, websiteConfig.appOrigin) ?? websiteConfig.appOrigin;\n const apiOrigin = settle(overrides.apiOrigin, localOrigins.api, websiteConfig.apiOrigin);\n return { ...websiteConfig, appOrigin, ...(apiOrigin === undefined ? {} : { apiOrigin }) };\n}\n\nexport interface LoadWebsiteSiteContextInput {\n /** The workspace's `WEBSITE_ROOT_CONFIG`. */\n readonly rootConfig: WebsiteRootConfig;\n /** Pass `import.meta.env?.DEV === true` (optional chaining: `astro.config.ts` evaluates the caller before Vite's env exists). */\n readonly isDevelopment: boolean;\n /** Where to start the walk up to `wildo.saas.config.ts`. Defaults to the working directory. */\n readonly cwd?: string | undefined;\n /** Defaults to `PUBLIC_APP_ORIGIN` / `PUBLIC_API_ORIGIN` from the process environment. */\n readonly overrides?: WebsiteOriginOverrides | undefined;\n}\n\nexport async function loadWebsiteSiteContext(input: LoadWebsiteSiteContextInput): Promise<WebsiteSiteContext> {\n const workspaceScope = resolveWorkspaceScope(input.cwd);\n if (workspaceScope.saasRoot === null || workspaceScope.saasConfigPath === null) {\n throw new Error(\n `[saas-website] No wildo.saas.config.ts found walking up from ${input.cwd ?? process.cwd()}. `\n + 'Run astro from inside the application workspace.',\n );\n }\n const saasRoot = workspaceScope.saasRoot;\n const saasConfig = await loadConfig<WildoSaasConfig>(workspaceScope.saasConfigPath);\n const [websiteServiceKey] = resolveStaticWebsiteServiceEntry(saasConfig);\n const websiteConfig = await loadWebsiteConfig({ saasRoot, saasConfig });\n\n return composeWebsiteSiteContext({\n rootConfig: input.rootConfig,\n websiteConfig: resolveWebsiteBuildOrigins({\n websiteConfig,\n isDevelopment: input.isDevelopment,\n localOrigins: resolveLocalhostApplicationOrigins(saasConfig.services),\n overrides: input.overrides ?? {\n appOrigin: process.env.PUBLIC_APP_ORIGIN,\n apiOrigin: process.env.PUBLIC_API_ORIGIN,\n },\n }),\n frontendProviders: loadMaterializedFrontendProviders({\n artifactPath: resolveFrontendServicesArtifactPath(resolvePath(saasRoot)),\n serviceKey: websiteServiceKey,\n }),\n });\n}\n/** @wildo_source:part:end saas.website.config.site-context-loader */\n"]}
|
|
@@ -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"}
|
|
@@ -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
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
/**
|
|
33
|
-
labels
|
|
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
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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,
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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 [
|
|
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
|
-
|
|
46
|
+
setEmailInvalid(false);
|
|
47
47
|
const parsed = InboundContactFormSchema.safeParse(values);
|
|
48
48
|
if (!parsed.success) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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({
|
|
83
|
+
setSubmit({
|
|
84
|
+
status: "error",
|
|
85
|
+
message: isSubmissionChallengeRefusal(error) ? labels.challengeRefusedMessage : labels.errorMessage,
|
|
86
|
+
});
|
|
69
87
|
}
|
|
70
|
-
}, [client, values, onSubmitted,
|
|
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:
|
|
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: [
|
|
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"]}
|