@wildo-ai/saas-website 1.1.4 → 1.1.5
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 +14 -20
- package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -1
- package/dist/esm/astro/blog-post-bridge.js +4 -2
- package/dist/esm/astro/blog-post-bridge.js.map +1 -1
- package/dist/esm/astro/blog-post-head.renderer.d.ts.map +1 -1
- package/dist/esm/astro/bridge-runtime.d.ts +30 -20
- package/dist/esm/astro/bridge-runtime.d.ts.map +1 -1
- package/dist/esm/astro/bridge-runtime.js +13 -3
- package/dist/esm/astro/bridge-runtime.js.map +1 -1
- package/dist/esm/astro/collect-expected-label-keys.d.ts.map +1 -1
- package/dist/esm/astro/headers-renderer.d.ts.map +1 -1
- package/dist/esm/astro/i18n-routing.helper.d.ts.map +1 -1
- package/dist/esm/astro/label-pack-loader.d.ts.map +1 -1
- package/dist/esm/astro/label-pack-loader.js +5 -0
- package/dist/esm/astro/label-pack-loader.js.map +1 -1
- package/dist/esm/astro/llms-txt-server.d.ts.map +1 -1
- package/dist/esm/astro/pick-labels-for-page.d.ts.map +1 -1
- package/dist/esm/astro/robots-renderer.d.ts.map +1 -1
- package/dist/esm/astro/robots-renderer.js.map +1 -1
- package/dist/esm/astro/sitemap-coverage.d.ts.map +1 -1
- package/dist/esm/astro/structured-data.renderer.d.ts.map +1 -1
- package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -1
- package/dist/esm/astro/website-page-head.renderer.js.map +1 -1
- package/dist/esm/astro/website-page-runtime.helper.d.ts.map +1 -1
- package/dist/esm/astro/website-site-context.d.ts.map +1 -1
- package/dist/esm/astro-island.d.ts.map +1 -1
- package/dist/esm/astro.d.ts.map +1 -1
- package/dist/esm/companion-exports.d.ts.map +1 -1
- package/dist/esm/components/low-level/WebsiteInternalButton.d.ts.map +1 -1
- package/dist/esm/components/low-level/WebsiteInternalLink.d.ts.map +1 -1
- package/dist/esm/config/define-website-config.d.ts.map +1 -1
- package/dist/esm/config/index.d.ts.map +1 -1
- package/dist/esm/config/load-website-config.d.ts.map +1 -1
- package/dist/esm/config/load-website-config.js +2 -1
- package/dist/esm/config/load-website-config.js.map +1 -1
- package/dist/esm/config/wildo-website-config.schemas.d.ts +10 -5
- package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -1
- package/dist/esm/config/wildo-website-config.schemas.js +10 -5
- package/dist/esm/config/wildo-website-config.schemas.js.map +1 -1
- package/dist/esm/config-loader.d.ts.map +1 -1
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -1
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +3 -3
- 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 +13 -9
- 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 +11 -0
- 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 +14 -1
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -1
- package/dist/esm/core/consent/WebsiteConsentBanner.d.ts +6 -0
- package/dist/esm/core/consent/WebsiteConsentBanner.d.ts.map +1 -0
- package/dist/esm/core/consent/WebsiteConsentBanner.js +128 -0
- package/dist/esm/core/consent/WebsiteConsentBanner.js.map +1 -0
- package/dist/esm/core/consent/WebsiteConsentContext.d.ts +85 -0
- package/dist/esm/core/consent/WebsiteConsentContext.d.ts.map +1 -0
- package/dist/esm/core/consent/WebsiteConsentContext.js +89 -0
- package/dist/esm/core/consent/WebsiteConsentContext.js.map +1 -0
- package/dist/esm/core/consent/WebsiteConsentGate.d.ts +27 -0
- package/dist/esm/core/consent/WebsiteConsentGate.d.ts.map +1 -0
- package/dist/esm/core/consent/WebsiteConsentGate.js +9 -0
- package/dist/esm/core/consent/WebsiteConsentGate.js.map +1 -0
- package/dist/esm/core/consent/useWebsiteConsent.d.ts +10 -0
- package/dist/esm/core/consent/useWebsiteConsent.d.ts.map +1 -0
- package/dist/esm/core/consent/useWebsiteConsent.js +17 -0
- package/dist/esm/core/consent/useWebsiteConsent.js.map +1 -0
- package/dist/esm/core/consent/website-consent-storage.d.ts +22 -0
- package/dist/esm/core/consent/website-consent-storage.d.ts.map +1 -0
- package/dist/esm/core/consent/website-consent-storage.js +102 -0
- package/dist/esm/core/consent/website-consent-storage.js.map +1 -0
- package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -1
- package/dist/esm/core/contexts/WebsitePageContext.js.map +1 -1
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +8 -1
- 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/WebsiteSectionContext.d.ts.map +1 -1
- package/dist/esm/core/contexts/WebsiteSectionContext.js.map +1 -1
- package/dist/esm/core/contexts/useWebsitePage.d.ts.map +1 -1
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -1
- package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -1
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts +1 -2
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -1
- package/dist/esm/core/external-providers/frontend-provider-registry.website.js.map +1 -1
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts +33 -10
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts.map +1 -1
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.js +64 -16
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.js.map +1 -1
- package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -1
- package/dist/esm/core/factories/define-website-section.d.ts.map +1 -1
- package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts.map +1 -1
- package/dist/esm/core/hooks/useWebsiteLabel.d.ts.map +1 -1
- package/dist/esm/core/layouts/WebsitePageLayout.d.ts +21 -1
- package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -1
- package/dist/esm/core/layouts/WebsitePageLayout.js +32 -4
- package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -1
- package/dist/esm/core/layouts/WebsiteSection.d.ts.map +1 -1
- package/dist/esm/core/routing/internal-routing.utils.d.ts.map +1 -1
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/mdx/BlogPost.d.ts.map +1 -1
- package/dist/esm/mdx/blog-post-collection.config.d.ts.map +1 -1
- package/dist/esm/mdx/blog-post-frontmatter-source.d.ts.map +1 -1
- package/dist/esm/mdx/index.d.ts.map +1 -1
- package/dist/esm/mdx.d.ts.map +1 -1
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts +61 -0
- package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js +73 -0
- package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js.map +1 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/refs/page-ref.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/refs/section-ref.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -1
- package/dist/esm/schemas/sections/website-section-category.shared.js.map +1 -1
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/validators/label-pack.validator.d.ts.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -29
- package/src/astro/__tests__/blog-post-bridge.test.tsx +41 -0
- package/src/astro/__tests__/bridge-runtime.test.tsx +25 -0
- package/src/astro/__tests__/label-pack-loader.test.ts +26 -1
- package/src/astro/blog-post-bridge.tsx +18 -22
- package/src/astro/bridge-runtime.tsx +45 -23
- package/src/astro/label-pack-loader.ts +4 -0
- package/src/config/load-website-config.ts +2 -1
- package/src/config/wildo-website-config.schemas.ts +10 -5
- package/src/core/__tests__/WebsitePageLayout.test.tsx +1 -1
- package/src/core/__tests__/website-consent.test.tsx +311 -0
- package/src/core/anonymous-session/__tests__/inbound-contact-form.schema.test.ts +11 -4
- package/src/core/anonymous-session/inbound-contact-form.schema.ts +14 -9
- package/src/core/anonymous-session/website-anonymous-session-client.ts +15 -1
- package/src/core/consent/WebsiteConsentBanner.tsx +222 -0
- package/src/core/consent/WebsiteConsentContext.tsx +164 -0
- package/src/core/consent/WebsiteConsentGate.tsx +34 -0
- package/src/core/consent/useWebsiteConsent.ts +18 -0
- package/src/core/consent/website-consent-storage.ts +116 -0
- package/src/core/contexts/WebsiteRuntimeContext.tsx +8 -1
- package/src/core/external-providers/frontend-provider-registry.website.ts +1 -2
- package/src/core/external-providers/useWebsiteProviderScripts.ts +72 -17
- package/src/core/layouts/WebsitePageLayout.tsx +66 -3
- package/src/index.ts +7 -0
- package/src/schemas/label-keys/__tests__/scaffolded-consent-label-pack.parity.test.ts +71 -0
- package/src/schemas/label-keys/website-consent-label-keys.schemas.ts +78 -0
- package/dist/esm/core/external-providers/provider-scripts.website.d.ts +0 -68
- package/dist/esm/core/external-providers/provider-scripts.website.d.ts.map +0 -1
- package/dist/esm/core/external-providers/provider-scripts.website.js +0 -99
- package/dist/esm/core/external-providers/provider-scripts.website.js.map +0 -1
- package/src/core/external-providers/__tests__/provider-scripts.website.test.ts +0 -146
- package/src/core/external-providers/provider-scripts.website.ts +0 -126
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { WebsiteConsentContext } from './WebsiteConsentContext.js';
|
|
3
|
+
/**
|
|
4
|
+
* The visitor's consent state (#534). Read this — never storage, never a cookie — to decide whether
|
|
5
|
+
* optional behaviour may run.
|
|
6
|
+
*
|
|
7
|
+
* Throws outside `WebsitePageLayout`, which provides it: a consumer that silently read "nothing granted"
|
|
8
|
+
* would look correct and hide a page mounted outside the layout.
|
|
9
|
+
*/
|
|
10
|
+
export function useWebsiteConsent() {
|
|
11
|
+
const value = useContext(WebsiteConsentContext);
|
|
12
|
+
if (value === null) {
|
|
13
|
+
throw new Error('useWebsiteConsent must be used inside <WebsitePageLayout>, which provides the visitor consent state.');
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=useWebsiteConsent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useWebsiteConsent.js","sourceRoot":"","sources":["../../../../../src/core/consent/useWebsiteConsent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAEnC,OAAO,EAAE,qBAAqB,EAAmC,MAAM,yBAAyB,CAAC;AAEjG;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,KAAK,GAAG,UAAU,CAAC,qBAAqB,CAAC,CAAC;IAChD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,sGAAsG,CAAC,CAAC;IAC1H,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["import { useContext } from 'react';\n\nimport { WebsiteConsentContext, type WebsiteConsentContextValue } from './WebsiteConsentContext';\n\n/**\n * The visitor's consent state (#534). Read this — never storage, never a cookie — to decide whether\n * optional behaviour may run.\n *\n * Throws outside `WebsitePageLayout`, which provides it: a consumer that silently read \"nothing granted\"\n * would look correct and hide a page mounted outside the layout.\n */\nexport function useWebsiteConsent(): WebsiteConsentContextValue {\n const value = useContext(WebsiteConsentContext);\n if (value === null) {\n throw new Error('useWebsiteConsent must be used inside <WebsitePageLayout>, which provides the visitor consent state.');\n }\n return value;\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ConsentPurpose } from '@wildo-ai/saas-models/public-runtime';
|
|
2
|
+
/** A visitor's decision, as the website runtime uses it. */
|
|
3
|
+
export interface WebsiteVisitorConsentRecord {
|
|
4
|
+
readonly grantedPurposes: readonly ConsentPurpose[];
|
|
5
|
+
/** When the visitor decided, ISO-8601. Its presence is what separates "declined everything" from "never asked". */
|
|
6
|
+
readonly decidedAt: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Dispatched on `window` after this document writes a decision, so every island on the page updates. The
|
|
10
|
+
* browser's own `storage` event covers OTHER documents only.
|
|
11
|
+
*/
|
|
12
|
+
export declare const WEBSITE_CONSENT_CHANGED_EVENT = "wildo:website-consent-changed";
|
|
13
|
+
/** The stored decision, or `null` when there is none or it cannot be read. */
|
|
14
|
+
export declare function readWebsiteVisitorConsent(storage?: Storage | null): WebsiteVisitorConsentRecord | null;
|
|
15
|
+
/**
|
|
16
|
+
* Store a decision and tell the page. Returns whether it was stored: when storage is unavailable the
|
|
17
|
+
* decision still applies for this page, and the visitor is asked again on the next one.
|
|
18
|
+
*/
|
|
19
|
+
export declare function writeWebsiteVisitorConsent(grantedPurposes: readonly ConsentPurpose[], now?: Date, storage?: Storage | null): WebsiteVisitorConsentRecord;
|
|
20
|
+
/** Calls `listener` whenever a decision changes in this document or in another tab. Returns the unsubscribe. */
|
|
21
|
+
export declare function subscribeWebsiteVisitorConsent(listener: () => void): () => void;
|
|
22
|
+
//# sourceMappingURL=website-consent-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"website-consent-storage.d.ts","sourceRoot":"","sources":["../../../../../src/core/consent/website-consent-storage.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EAGf,MAAM,sCAAsC,CAAC;AA+B9C,4DAA4D;AAC5D,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,eAAe,EAAE,SAAS,cAAc,EAAE,CAAC;IACpD,mHAAmH;IACnH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;GAGG;AACH,eAAO,MAAM,6BAA6B,kCAAkC,CAAC;AAW7E,8EAA8E;AAC9E,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,OAAO,GAAG,IAAuB,GAAG,2BAA2B,GAAG,IAAI,CAkBxH;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,GAAG,GAAE,IAAiB,EACtB,OAAO,GAAE,OAAO,GAAG,IAAuB,GACzC,2BAA2B,CAc7B;AAED,gHAAgH;AAChH,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAW/E"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { normalizeConsentPurposes, WEBSITE_VISITOR_CONSENT_STORAGE_KEY, } from '@wildo-ai/saas-models/public-runtime';
|
|
3
|
+
/**
|
|
4
|
+
* The ONE place a Wildo website reads or writes a visitor's consent decision (#534).
|
|
5
|
+
*
|
|
6
|
+
* Page and section components never touch storage for consent: they read `useWebsiteConsent()`. A second
|
|
7
|
+
* reader would be a second interpretation of the record — the defect the old `CONSENT_ORDER` was, one
|
|
8
|
+
* layer down.
|
|
9
|
+
*
|
|
10
|
+
* ## Why web storage and not a cookie
|
|
11
|
+
*
|
|
12
|
+
* Nothing on the server reads a visitor's consent — the marketing site is static — so a cookie would be
|
|
13
|
+
* sent on every request for no reader. Web storage stays in the browser. The key is declared in the
|
|
14
|
+
* engine's cookie table (`ENGINE_CLIENT_STORAGE_ENTRIES`) as strictly necessary: a refusal that is not
|
|
15
|
+
* remembered does not hold.
|
|
16
|
+
*
|
|
17
|
+
* ## Fail closed, never loud
|
|
18
|
+
*
|
|
19
|
+
* Storage can be absent (a server render), disabled (a privacy mode) or corrupt (a hand edit). Every one
|
|
20
|
+
* of those reads as "no decision": the visitor is asked, and nothing optional starts. A corrupt record is
|
|
21
|
+
* not repaired silently — it is ignored, and the next decision overwrites it.
|
|
22
|
+
*/
|
|
23
|
+
/** Stored shape. `schemaVersion` so a future shape change can refuse an old record instead of misreading it. */
|
|
24
|
+
const StoredWebsiteVisitorConsentSchema = z.object({
|
|
25
|
+
schemaVersion: z.literal(1),
|
|
26
|
+
/** Strings, not the enum: a purpose later removed from the vocabulary must not invalidate the rest. */
|
|
27
|
+
grantedPurposes: z.array(z.string()),
|
|
28
|
+
decidedAt: z.iso.datetime(),
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* Dispatched on `window` after this document writes a decision, so every island on the page updates. The
|
|
32
|
+
* browser's own `storage` event covers OTHER documents only.
|
|
33
|
+
*/
|
|
34
|
+
export const WEBSITE_CONSENT_CHANGED_EVENT = 'wildo:website-consent-changed';
|
|
35
|
+
function browserStorage() {
|
|
36
|
+
try {
|
|
37
|
+
return typeof window === 'undefined' ? null : window.localStorage;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Accessing `localStorage` throws when the browser blocks site data.
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** The stored decision, or `null` when there is none or it cannot be read. */
|
|
45
|
+
export function readWebsiteVisitorConsent(storage = browserStorage()) {
|
|
46
|
+
if (storage === null)
|
|
47
|
+
return null;
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = storage.getItem(WEBSITE_VISITOR_CONSENT_STORAGE_KEY);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (raw === null)
|
|
56
|
+
return null;
|
|
57
|
+
let json;
|
|
58
|
+
try {
|
|
59
|
+
json = JSON.parse(raw);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const parsed = StoredWebsiteVisitorConsentSchema.safeParse(json);
|
|
65
|
+
if (!parsed.success)
|
|
66
|
+
return null;
|
|
67
|
+
return { grantedPurposes: normalizeConsentPurposes(parsed.data.grantedPurposes), decidedAt: parsed.data.decidedAt };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Store a decision and tell the page. Returns whether it was stored: when storage is unavailable the
|
|
71
|
+
* decision still applies for this page, and the visitor is asked again on the next one.
|
|
72
|
+
*/
|
|
73
|
+
export function writeWebsiteVisitorConsent(grantedPurposes, now = new Date(), storage = browserStorage()) {
|
|
74
|
+
const record = { grantedPurposes: normalizeConsentPurposes(grantedPurposes), decidedAt: now.toISOString() };
|
|
75
|
+
if (storage !== null) {
|
|
76
|
+
try {
|
|
77
|
+
storage.setItem(WEBSITE_VISITOR_CONSENT_STORAGE_KEY, JSON.stringify({ schemaVersion: 1, grantedPurposes: record.grantedPurposes, decidedAt: record.decidedAt }));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Quota or blocked storage: the decision holds for this page only.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (typeof window !== 'undefined')
|
|
84
|
+
window.dispatchEvent(new CustomEvent(WEBSITE_CONSENT_CHANGED_EVENT));
|
|
85
|
+
return record;
|
|
86
|
+
}
|
|
87
|
+
/** Calls `listener` whenever a decision changes in this document or in another tab. Returns the unsubscribe. */
|
|
88
|
+
export function subscribeWebsiteVisitorConsent(listener) {
|
|
89
|
+
if (typeof window === 'undefined')
|
|
90
|
+
return () => { };
|
|
91
|
+
const onStorage = (event) => {
|
|
92
|
+
if (event.key === null || event.key === WEBSITE_VISITOR_CONSENT_STORAGE_KEY)
|
|
93
|
+
listener();
|
|
94
|
+
};
|
|
95
|
+
window.addEventListener(WEBSITE_CONSENT_CHANGED_EVENT, listener);
|
|
96
|
+
window.addEventListener('storage', onStorage);
|
|
97
|
+
return () => {
|
|
98
|
+
window.removeEventListener(WEBSITE_CONSENT_CHANGED_EVENT, listener);
|
|
99
|
+
window.removeEventListener('storage', onStorage);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=website-consent-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"website-consent-storage.js","sourceRoot":"","sources":["../../../../../src/core/consent/website-consent-storage.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\nimport {\n ConsentPurpose,\n normalizeConsentPurposes,\n WEBSITE_VISITOR_CONSENT_STORAGE_KEY,\n} from '@wildo-ai/saas-models/public-runtime';\n\n/**\n * The ONE place a Wildo website reads or writes a visitor's consent decision (#534).\n *\n * Page and section components never touch storage for consent: they read `useWebsiteConsent()`. A second\n * reader would be a second interpretation of the record — the defect the old `CONSENT_ORDER` was, one\n * layer down.\n *\n * ## Why web storage and not a cookie\n *\n * Nothing on the server reads a visitor's consent — the marketing site is static — so a cookie would be\n * sent on every request for no reader. Web storage stays in the browser. The key is declared in the\n * engine's cookie table (`ENGINE_CLIENT_STORAGE_ENTRIES`) as strictly necessary: a refusal that is not\n * remembered does not hold.\n *\n * ## Fail closed, never loud\n *\n * Storage can be absent (a server render), disabled (a privacy mode) or corrupt (a hand edit). Every one\n * of those reads as \"no decision\": the visitor is asked, and nothing optional starts. A corrupt record is\n * not repaired silently — it is ignored, and the next decision overwrites it.\n */\n\n/** Stored shape. `schemaVersion` so a future shape change can refuse an old record instead of misreading it. */\nconst StoredWebsiteVisitorConsentSchema = z.object({\n schemaVersion: z.literal(1),\n /** Strings, not the enum: a purpose later removed from the vocabulary must not invalidate the rest. */\n grantedPurposes: z.array(z.string()),\n decidedAt: z.iso.datetime(),\n});\n\n/** A visitor's decision, as the website runtime uses it. */\nexport interface WebsiteVisitorConsentRecord {\n readonly grantedPurposes: readonly ConsentPurpose[];\n /** When the visitor decided, ISO-8601. Its presence is what separates \"declined everything\" from \"never asked\". */\n readonly decidedAt: string;\n}\n\n/**\n * Dispatched on `window` after this document writes a decision, so every island on the page updates. The\n * browser's own `storage` event covers OTHER documents only.\n */\nexport const WEBSITE_CONSENT_CHANGED_EVENT = 'wildo:website-consent-changed';\n\nfunction browserStorage(): Storage | null {\n try {\n return typeof window === 'undefined' ? null : window.localStorage;\n } catch {\n // Accessing `localStorage` throws when the browser blocks site data.\n return null;\n }\n}\n\n/** The stored decision, or `null` when there is none or it cannot be read. */\nexport function readWebsiteVisitorConsent(storage: Storage | null = browserStorage()): WebsiteVisitorConsentRecord | null {\n if (storage === null) return null;\n let raw: string | null;\n try {\n raw = storage.getItem(WEBSITE_VISITOR_CONSENT_STORAGE_KEY);\n } catch {\n return null;\n }\n if (raw === null) return null;\n let json: unknown;\n try {\n json = JSON.parse(raw);\n } catch {\n return null;\n }\n const parsed = StoredWebsiteVisitorConsentSchema.safeParse(json);\n if (!parsed.success) return null;\n return { grantedPurposes: normalizeConsentPurposes(parsed.data.grantedPurposes), decidedAt: parsed.data.decidedAt };\n}\n\n/**\n * Store a decision and tell the page. Returns whether it was stored: when storage is unavailable the\n * decision still applies for this page, and the visitor is asked again on the next one.\n */\nexport function writeWebsiteVisitorConsent(\n grantedPurposes: readonly ConsentPurpose[],\n now: Date = new Date(),\n storage: Storage | null = browserStorage(),\n): WebsiteVisitorConsentRecord {\n const record: WebsiteVisitorConsentRecord = { grantedPurposes: normalizeConsentPurposes(grantedPurposes), decidedAt: now.toISOString() };\n if (storage !== null) {\n try {\n storage.setItem(\n WEBSITE_VISITOR_CONSENT_STORAGE_KEY,\n JSON.stringify({ schemaVersion: 1, grantedPurposes: record.grantedPurposes, decidedAt: record.decidedAt }),\n );\n } catch {\n // Quota or blocked storage: the decision holds for this page only.\n }\n }\n if (typeof window !== 'undefined') window.dispatchEvent(new CustomEvent(WEBSITE_CONSENT_CHANGED_EVENT));\n return record;\n}\n\n/** Calls `listener` whenever a decision changes in this document or in another tab. Returns the unsubscribe. */\nexport function subscribeWebsiteVisitorConsent(listener: () => void): () => void {\n if (typeof window === 'undefined') return () => {};\n const onStorage = (event: StorageEvent): void => {\n if (event.key === null || event.key === WEBSITE_VISITOR_CONSENT_STORAGE_KEY) listener();\n };\n window.addEventListener(WEBSITE_CONSENT_CHANGED_EVENT, listener);\n window.addEventListener('storage', onStorage);\n return () => {\n window.removeEventListener(WEBSITE_CONSENT_CHANGED_EVENT, listener);\n window.removeEventListener('storage', onStorage);\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebsitePageContext.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"WebsitePageContext.d.ts","sourceRoot":"","sources":["../../../../../src/core/contexts/WebsitePageContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAE1E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,OAAO,EAAE,cAAc,CAAC;CACzB;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,OAAO,CAAC,uBAAuB,GAAG,IAAI,CACxB,CAAC;AAEtD,MAAM,WAAW,+BAA+B;IAC9C,KAAK,EAAE,uBAAuB,CAAC;IAC/B,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,0BAA0B,CAAC,EACzC,KAAK,EACL,QAAQ,GACT,EAAE,+BAA+B,GAAG,SAAS,CAM7C;yBATe,0BAA0B;;;AAW1C,uDAAuD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebsitePageContext.js","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"WebsitePageContext.js","sourceRoot":"","sources":["../../../../../src/core/contexts/WebsitePageContext.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,aAAa,EAAkB,MAAM,OAAO,CAAC;AAiC7D,MAAM,CAAC,MAAM,kBAAkB,GAC7B,aAAa,CAAiC,IAAI,CAAC,CAAC;AAOtD,MAAM,UAAU,0BAA0B,CAAC,EACzC,KAAK,EACL,QAAQ,GACwB;IAChC,OAAO,CACL,KAAC,kBAAkB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YACtC,QAAQ,GACmB,CAC/B,CAAC;AACJ,CAAC;AACD,0BAA0B,CAAC,WAAW,GAAG,4BAA4B,CAAC","sourcesContent":["import React, { createContext, type ReactNode } from 'react';\n\nimport type { WebsitePageRef } from '../../schemas/refs/page-ref.schemas';\n\n/**\n * @wildo_source:part:start saas.website.page.context facet:layer:core facet:family:website\n *\n * `WebsitePageContext` — identity of the page currently rendering.\n * Provided by `<WebsitePageLayout pageRef=\"…\">` exactly once per\n * page render and consumed by `useWebsiteLabel(leaf)` to compose the\n * full label key (`website.{pageRef}.{sectionRef}.{leaf}`).\n *\n * **Why an isolated context (not merged into runtime)**: the runtime\n * context is set at the host-adapter boundary and is constant per\n * build; the page context is set per page route. Splitting them\n * lets a single host adapter render multiple pages (preview tooling,\n * MDX-driven blog index aggregating post excerpts, etc.) without\n * lying about which page each subtree belongs to.\n *\n * **Why the hook lives in a separate file**: same Fast-Refresh\n * rationale as `WebsiteRuntimeContext` — the context and its provider\n * component stay together here (`useContext` matches on object\n * identity), and only the hook is split out, into `useWebsitePage.ts`.\n */\nexport interface WebsitePageContextValue {\n /**\n * The validated `WebsitePageRef` of the page currently rendering.\n * MUST match a `WebsitePageManifest.pageRef` registered in the\n * site's `WebsiteRootConfig`.\n */\n pageRef: WebsitePageRef;\n}\n\nexport const WebsitePageContext: React.Context<WebsitePageContextValue | null> =\n createContext<WebsitePageContextValue | null>(null);\n\nexport interface WebsitePageContextProviderProps {\n value: WebsitePageContextValue;\n children: ReactNode;\n}\n\nexport function WebsitePageContextProvider({\n value,\n children,\n}: WebsitePageContextProviderProps): ReactNode {\n return (\n <WebsitePageContext.Provider value={value}>\n {children}\n </WebsitePageContext.Provider>\n );\n}\nWebsitePageContextProvider.displayName = 'WebsitePageContextProvider';\n/** @wildo_source:part:end saas.website.page.context */\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { type ReactNode } from 'react';
|
|
1
|
+
import React, { type ComponentType, type ReactNode } from 'react';
|
|
2
2
|
import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
|
|
3
3
|
import type { WebsiteDesignTokens } from '../../schemas/design-tokens/website-design-tokens.shared.schemas';
|
|
4
4
|
import type { FrontendWebsiteProviderRegistry } from '../external-providers/frontend-provider-registry.website';
|
|
@@ -149,6 +149,13 @@ export interface WebsiteRuntimeContextValue {
|
|
|
149
149
|
* catalog for analytics / captcha / browser-monitoring slices.
|
|
150
150
|
*/
|
|
151
151
|
frontendProviderRegistry?: FrontendWebsiteProviderRegistry;
|
|
152
|
+
/**
|
|
153
|
+
* Replaces the framework's visitor consent panel (#534). Reads `useWebsiteConsent()`; see
|
|
154
|
+
* `WebsiteConsentBanner` for the three properties a replacement must keep. Absent uses the default.
|
|
155
|
+
* Closed over by the bridge rather than passed as an island prop, like `navigation`, because a
|
|
156
|
+
* component cannot cross Astro's serialization boundary.
|
|
157
|
+
*/
|
|
158
|
+
consentBannerComponent?: ComponentType;
|
|
152
159
|
/**
|
|
153
160
|
* Backend API origin (WITHOUT a trailing slash) that anonymous-capture
|
|
154
161
|
* sections POST to when constructing a `WebsiteAnonymousSessionClient`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebsiteRuntimeContext.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"WebsiteRuntimeContext.d.ts","sourceRoot":"","sources":["../../../../../src/core/contexts/WebsiteRuntimeContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAiB,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEjF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAE9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kEAAkE,CAAC;AAC5G,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,0DAA0D,CAAC;AAChH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oDAAoD,CAAC;AAC1F,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,4DAA4D,CAAC;AAC1G,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAE/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;;;;;;OASG;IACH,aAAa,EAAE,iBAAiB,CAAC;IACjC;;;;;;;;;;;;;;;;;;OAkBG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,SAAS,EAAE,SAAS,CAAC;IACrB;;;;;OAKG;IACH,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;;;OAMG;IACH,UAAU,EAAE,uBAAuB,CAAC;IACpC;;;;;;;;;;;;;;;OAeG;IACH,yBAAyB,EAAE,eAAe,CAAC;IAC3C;;;;;OAKG;IACH,wBAAwB,CAAC,EAAE,+BAA+B,CAAC;IAC3D;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,aAAa,CAAC;IACvC;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,OAAO,CAAC,0BAA0B,GAAG,IAAI,CAC3B,CAAC;AAEzD,MAAM,WAAW,kCAAkC;IACjD,KAAK,EAAE,0BAA0B,CAAC;IAClC,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,EAC5C,KAAK,EACL,QAAQ,GACT,EAAE,kCAAkC,GAAG,SAAS,CAMhD;yBATe,6BAA6B;;;AAW7C,0DAA0D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebsiteRuntimeContext.js","sourceRoot":"","sources":["../../../../src/core/contexts/WebsiteRuntimeContext.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,aAAa,EAAkB,MAAM,OAAO,CAAC;AA0K7D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAChC,aAAa,CAAoC,IAAI,CAAC,CAAC;AAOzD;;;;;GAKG;AACH,MAAM,UAAU,6BAA6B,CAAC,EAC5C,KAAK,EACL,QAAQ,GAC2B;IACnC,OAAO,CACL,KAAC,qBAAqB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YACzC,QAAQ,GACsB,CAClC,CAAC;AACJ,CAAC;AACD,6BAA6B,CAAC,WAAW,GAAG,+BAA+B,CAAC","sourcesContent":["import React, { createContext, type ReactNode } from 'react';\n\nimport type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport type { WebsiteDesignTokens } from '../../schemas/design-tokens/website-design-tokens.shared.schemas';\nimport type { FrontendWebsiteProviderRegistry } from '../external-providers/frontend-provider-registry.website';\nimport type { WebsiteLabelKey } from '../../schemas/label-keys/website-label-key.schemas';\nimport type { WebsiteNavigationConfig } from '../../schemas/manifests/website-root-config.shared.schemas';\nimport type { LabelPack } from '../../schemas/validators/label-pack.validator';\n\n/**\n * @wildo_source:part:start saas.website.runtime.context facet:layer:core facet:family:website\n *\n * `WebsiteRuntimeContext` — site-wide runtime state for the marketing\n * surface. Created **once per page render** by the host adapter\n * (Phase 5 `WebsitePageBridge.astro` reads `WebsiteRootConfig` and the\n * resolved label pack from disk and calls\n * `<WebsiteRuntimeContextProvider value={…}>` around the page tree).\n *\n * **Why \"runtime\" and not \"config\"**: the values here are the\n * locale-resolved subset of `WebsiteRootConfig` — only the active\n * locale's label pack travels with the React tree (the other locales\n * live in their own per-locale static builds). A `*Config` name would\n * suggest the full multi-locale surface is in scope.\n *\n * **Three pieces of state**:\n *\n * - `locale` — the currently-rendering locale. Sections that need\n * locale-aware formatting (number/date in marketing copy is rare\n * but legitimate) read it via `useWebsiteRuntime()`.\n * - `labelPack` — the locale-resolved flat key→string map, validated\n * against the manifest by `validateLabelPackAgainstManifest` at\n * build start. Consumed by `useWebsiteLabel` and\n * `useWebsiteLabelByKey`.\n * - `designTokens` — the `WebsiteDesignTokens` from\n * `WebsiteRootConfig.designTokens`. Consumed by\n * `useWebsiteDesignTokens` and by the Phase 5 CSS emitter that\n * materializes them as `--website-*` custom properties on the\n * `<WebsitePageLayout>` root.\n * - `navigation` — the `WebsiteNavigationConfig` from\n * `WebsiteRootConfig.navigation`. Carried in the runtime context\n * (rather than as a separate `WebsiteNavigationContext`) because\n * `<WebsitePageLayout>` already reads runtime state and\n * header/footer slots are inseparable from \"the site is rendering\n * right now\". A second context for two component refs would split\n * the host-adapter's wiring surface without buying isolation.\n * - `frontendProviderRegistry` — optional website-facing public provider\n * registry hydrated from the materialized provider artifact for the\n * concrete `STATIC_WEBSITE` service. This keeps analytics / captcha /\n * browser-monitoring integrations on the same frontend-provider\n * contract the SaaS app now uses, instead of inventing a website-only\n * metadata seam.\n *\n * **Why the hook lives in a separate file**: React Fast Refresh only\n * preserves state for a module whose every export is a component, so\n * a module mixing components with plain functions loses its HMR\n * boundary. This file holds the two things that must be identical\n * objects at runtime — the context and the provider component that\n * writes it — while `useWebsiteRuntime` sits alone in\n * `useWebsiteRuntime.ts`. A section author editing a component that\n * merely READS the runtime therefore never remounts the provider.\n *\n * Until 2026-09-09 this paragraph described a different arrangement:\n * that only the bare context was kept here and the provider lived in\n * a file of its own. No such file has ever existed — the provider is\n * exported below — and splitting it out would be the wrong shape\n * anyway, since `useContext` matches on object identity.\n * prose-referent-waiver: the retired file name is quoted only to retire it.\n * The claim named `WebsiteRuntimeContextProvider.tsx`.\n */\nexport interface WebsiteRuntimeContextValue {\n /**\n * The currently-rendering locale. Marketing-site routing emits one\n * static build per locale (D8), so this value is constant per build\n * — never re-renders mid-session.\n */\n locale: AvailableLanguage;\n /**\n * The site's default locale (mirrors `WebsiteRootConfig.defaultLocale`).\n * Carried in the runtime context so client-tier components — namely\n * `WebsiteInternalLink` (Phase 6 fix H-β) — can compute the correct\n * locale-prefixed pathname (`/pricing` on the EN site,\n * `/fr/pricing` on the FR site) without re-importing the root\n * config. Plumbing it through the runtime keeps the routing rule\n * a framework-internal concern: section authors NEVER hand-prefix\n * paths.\n */\n defaultLocale: AvailableLanguage;\n /**\n * Origin of the application this marketing site sends visitors INTO — the\n * marketing → product handoff target, authored once as\n * `WildoMarketingWebsiteConfig.appOrigin` (#493).\n *\n * Carried here for exactly the reason `defaultLocale` above is: a\n * `wildo.website.config.ts` is Node-loaded and therefore invisible to a\n * client island, so before this a site resolved the app origin a SECOND\n * time from a public build-time environment variable it could read in the\n * browser — one host authored twice, in two files, with no mechanical link\n * between the copies. That is how a marketing site comes to point at the\n * wrong environment: a defect nobody notices until a customer follows the\n * link.\n *\n * Threading it through the runtime keeps the handoff a framework-internal\n * concern, the same way threading `defaultLocale` kept locale-prefixing one:\n * section authors never hand-assemble the product URL, and there is one\n * place to change it.\n */\n appOrigin?: string;\n /**\n * Flat `Record<string, string>` label pack for the active locale.\n * Lookup keys MUST be fully-composed `website.<page>.<section>.<slot>`\n * strings; consumers that hold leaf paths use `useWebsiteLabel(leaf)`\n * which composes the full key against the page+section context.\n */\n labelPack: LabelPack;\n /**\n * Design-token bundle for the active site. Same object referenced\n * by `WebsiteRootConfig.designTokens`; passed by reference (not\n * cloned) so token-aware components can rely on referential\n * stability across renders.\n */\n designTokens: WebsiteDesignTokens;\n /**\n * Site-wide navigation slot bindings (header / footer components).\n * Same object referenced by `WebsiteRootConfig.navigation`. Read\n * by `<WebsitePageLayout>` to render the chrome around each page's\n * `<main>` content; per-page `header` / `footer` props on\n * `<WebsitePageLayout>` override these defaults.\n */\n navigation: WebsiteNavigationConfig;\n /**\n * Fully-composed label key the framework reads to render the\n * \"Skip to main content\" affordance as the FIRST focusable element\n * on every page (Phase 6 third audit fix M-α). Mirrors\n * `WebsiteRootConfig.skipToMainContentLabelKey`. Carried in the\n * runtime context (NOT re-derived from the root config inside\n * `WebsitePageLayout`) for two reasons:\n * - The layout is a client-tier React component; it cannot import\n * the consumer's `WEBSITE_ROOT_CONFIG` symbol without dragging\n * server-only code (manifest registry, page components) into\n * the client bundle.\n * - Threading the key through the runtime keeps the layout's\n * contract symmetric with how every other localized text on the\n * page is sourced (label pack + key), so the skip link is\n * translated automatically without any per-locale branching.\n */\n skipToMainContentLabelKey: WebsiteLabelKey;\n /**\n * Website-facing public providers for the active static website\n * service. Optional so older tests and provider-free sites can omit\n * it, but when present it is the canonical frontend-safe provider\n * catalog for analytics / captcha / browser-monitoring slices.\n */\n frontendProviderRegistry?: FrontendWebsiteProviderRegistry;\n /**\n * Backend API origin (WITHOUT a trailing slash) that anonymous-capture\n * sections POST to when constructing a `WebsiteAnonymousSessionClient`\n * (lead / draft-note forms). Sourced from\n * `WildoMarketingWebsiteConfig.apiOrigin` and passed as a\n * JSON-serializable island prop (the `.astro` page resolves the\n * per-environment value and forwards it).\n *\n * Optional because only pages that host an anonymous-capture section\n * need it — brochure pages omit it. A capture section that reads\n * `undefined` here surfaces a loud misconfiguration rather than\n * silently failing to submit.\n */\n apiOrigin?: string;\n}\n\n/**\n * Bare React context. Default `null` — `useWebsiteRuntime()` throws\n * with a clear error message when invoked outside of\n * `<WebsiteRuntimeContextProvider>`. The defensive throw is preferred\n * over a silent default value so a missing provider is detected at\n * the first hook call rather than at render time when a missing\n * label silently renders as the key string.\n */\nexport const WebsiteRuntimeContext: React.Context<WebsiteRuntimeContextValue | null> =\n createContext<WebsiteRuntimeContextValue | null>(null);\n\nexport interface WebsiteRuntimeContextProviderProps {\n value: WebsiteRuntimeContextValue;\n children: ReactNode;\n}\n\n/**\n * Provider component. Authored as a thin wrapper so consumers do NOT\n * have to import `WebsiteRuntimeContext.Provider` directly — the\n * provider component carries the documented authoring API and is the\n * Fast-Refresh-stable surface section authors compose against.\n */\nexport function WebsiteRuntimeContextProvider({\n value,\n children,\n}: WebsiteRuntimeContextProviderProps): ReactNode {\n return (\n <WebsiteRuntimeContext.Provider value={value}>\n {children}\n </WebsiteRuntimeContext.Provider>\n );\n}\nWebsiteRuntimeContextProvider.displayName = 'WebsiteRuntimeContextProvider';\n/** @wildo_source:part:end saas.website.runtime.context */\n"]}
|
|
1
|
+
{"version":3,"file":"WebsiteRuntimeContext.js","sourceRoot":"","sources":["../../../../../src/core/contexts/WebsiteRuntimeContext.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,aAAa,EAAsC,MAAM,OAAO,CAAC;AAiLjF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAChC,aAAa,CAAoC,IAAI,CAAC,CAAC;AAOzD;;;;;GAKG;AACH,MAAM,UAAU,6BAA6B,CAAC,EAC5C,KAAK,EACL,QAAQ,GAC2B;IACnC,OAAO,CACL,KAAC,qBAAqB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YACzC,QAAQ,GACsB,CAClC,CAAC;AACJ,CAAC;AACD,6BAA6B,CAAC,WAAW,GAAG,+BAA+B,CAAC","sourcesContent":["import React, { createContext, type ComponentType, type ReactNode } from 'react';\n\nimport type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport type { WebsiteDesignTokens } from '../../schemas/design-tokens/website-design-tokens.shared.schemas';\nimport type { FrontendWebsiteProviderRegistry } from '../external-providers/frontend-provider-registry.website';\nimport type { WebsiteLabelKey } from '../../schemas/label-keys/website-label-key.schemas';\nimport type { WebsiteNavigationConfig } from '../../schemas/manifests/website-root-config.shared.schemas';\nimport type { LabelPack } from '../../schemas/validators/label-pack.validator';\n\n/**\n * @wildo_source:part:start saas.website.runtime.context facet:layer:core facet:family:website\n *\n * `WebsiteRuntimeContext` — site-wide runtime state for the marketing\n * surface. Created **once per page render** by the host adapter\n * (Phase 5 `WebsitePageBridge.astro` reads `WebsiteRootConfig` and the\n * resolved label pack from disk and calls\n * `<WebsiteRuntimeContextProvider value={…}>` around the page tree).\n *\n * **Why \"runtime\" and not \"config\"**: the values here are the\n * locale-resolved subset of `WebsiteRootConfig` — only the active\n * locale's label pack travels with the React tree (the other locales\n * live in their own per-locale static builds). A `*Config` name would\n * suggest the full multi-locale surface is in scope.\n *\n * **Three pieces of state**:\n *\n * - `locale` — the currently-rendering locale. Sections that need\n * locale-aware formatting (number/date in marketing copy is rare\n * but legitimate) read it via `useWebsiteRuntime()`.\n * - `labelPack` — the locale-resolved flat key→string map, validated\n * against the manifest by `validateLabelPackAgainstManifest` at\n * build start. Consumed by `useWebsiteLabel` and\n * `useWebsiteLabelByKey`.\n * - `designTokens` — the `WebsiteDesignTokens` from\n * `WebsiteRootConfig.designTokens`. Consumed by\n * `useWebsiteDesignTokens` and by the Phase 5 CSS emitter that\n * materializes them as `--website-*` custom properties on the\n * `<WebsitePageLayout>` root.\n * - `navigation` — the `WebsiteNavigationConfig` from\n * `WebsiteRootConfig.navigation`. Carried in the runtime context\n * (rather than as a separate `WebsiteNavigationContext`) because\n * `<WebsitePageLayout>` already reads runtime state and\n * header/footer slots are inseparable from \"the site is rendering\n * right now\". A second context for two component refs would split\n * the host-adapter's wiring surface without buying isolation.\n * - `frontendProviderRegistry` — optional website-facing public provider\n * registry hydrated from the materialized provider artifact for the\n * concrete `STATIC_WEBSITE` service. This keeps analytics / captcha /\n * browser-monitoring integrations on the same frontend-provider\n * contract the SaaS app now uses, instead of inventing a website-only\n * metadata seam.\n *\n * **Why the hook lives in a separate file**: React Fast Refresh only\n * preserves state for a module whose every export is a component, so\n * a module mixing components with plain functions loses its HMR\n * boundary. This file holds the two things that must be identical\n * objects at runtime — the context and the provider component that\n * writes it — while `useWebsiteRuntime` sits alone in\n * `useWebsiteRuntime.ts`. A section author editing a component that\n * merely READS the runtime therefore never remounts the provider.\n *\n * Until 2026-09-09 this paragraph described a different arrangement:\n * that only the bare context was kept here and the provider lived in\n * a file of its own. No such file has ever existed — the provider is\n * exported below — and splitting it out would be the wrong shape\n * anyway, since `useContext` matches on object identity.\n * prose-referent-waiver: the retired file name is quoted only to retire it.\n * The claim named `WebsiteRuntimeContextProvider.tsx`.\n */\nexport interface WebsiteRuntimeContextValue {\n /**\n * The currently-rendering locale. Marketing-site routing emits one\n * static build per locale (D8), so this value is constant per build\n * — never re-renders mid-session.\n */\n locale: AvailableLanguage;\n /**\n * The site's default locale (mirrors `WebsiteRootConfig.defaultLocale`).\n * Carried in the runtime context so client-tier components — namely\n * `WebsiteInternalLink` (Phase 6 fix H-β) — can compute the correct\n * locale-prefixed pathname (`/pricing` on the EN site,\n * `/fr/pricing` on the FR site) without re-importing the root\n * config. Plumbing it through the runtime keeps the routing rule\n * a framework-internal concern: section authors NEVER hand-prefix\n * paths.\n */\n defaultLocale: AvailableLanguage;\n /**\n * Origin of the application this marketing site sends visitors INTO — the\n * marketing → product handoff target, authored once as\n * `WildoMarketingWebsiteConfig.appOrigin` (#493).\n *\n * Carried here for exactly the reason `defaultLocale` above is: a\n * `wildo.website.config.ts` is Node-loaded and therefore invisible to a\n * client island, so before this a site resolved the app origin a SECOND\n * time from a public build-time environment variable it could read in the\n * browser — one host authored twice, in two files, with no mechanical link\n * between the copies. That is how a marketing site comes to point at the\n * wrong environment: a defect nobody notices until a customer follows the\n * link.\n *\n * Threading it through the runtime keeps the handoff a framework-internal\n * concern, the same way threading `defaultLocale` kept locale-prefixing one:\n * section authors never hand-assemble the product URL, and there is one\n * place to change it.\n */\n appOrigin?: string;\n /**\n * Flat `Record<string, string>` label pack for the active locale.\n * Lookup keys MUST be fully-composed `website.<page>.<section>.<slot>`\n * strings; consumers that hold leaf paths use `useWebsiteLabel(leaf)`\n * which composes the full key against the page+section context.\n */\n labelPack: LabelPack;\n /**\n * Design-token bundle for the active site. Same object referenced\n * by `WebsiteRootConfig.designTokens`; passed by reference (not\n * cloned) so token-aware components can rely on referential\n * stability across renders.\n */\n designTokens: WebsiteDesignTokens;\n /**\n * Site-wide navigation slot bindings (header / footer components).\n * Same object referenced by `WebsiteRootConfig.navigation`. Read\n * by `<WebsitePageLayout>` to render the chrome around each page's\n * `<main>` content; per-page `header` / `footer` props on\n * `<WebsitePageLayout>` override these defaults.\n */\n navigation: WebsiteNavigationConfig;\n /**\n * Fully-composed label key the framework reads to render the\n * \"Skip to main content\" affordance as the FIRST focusable element\n * on every page (Phase 6 third audit fix M-α). Mirrors\n * `WebsiteRootConfig.skipToMainContentLabelKey`. Carried in the\n * runtime context (NOT re-derived from the root config inside\n * `WebsitePageLayout`) for two reasons:\n * - The layout is a client-tier React component; it cannot import\n * the consumer's `WEBSITE_ROOT_CONFIG` symbol without dragging\n * server-only code (manifest registry, page components) into\n * the client bundle.\n * - Threading the key through the runtime keeps the layout's\n * contract symmetric with how every other localized text on the\n * page is sourced (label pack + key), so the skip link is\n * translated automatically without any per-locale branching.\n */\n skipToMainContentLabelKey: WebsiteLabelKey;\n /**\n * Website-facing public providers for the active static website\n * service. Optional so older tests and provider-free sites can omit\n * it, but when present it is the canonical frontend-safe provider\n * catalog for analytics / captcha / browser-monitoring slices.\n */\n frontendProviderRegistry?: FrontendWebsiteProviderRegistry;\n /**\n * Replaces the framework's visitor consent panel (#534). Reads `useWebsiteConsent()`; see\n * `WebsiteConsentBanner` for the three properties a replacement must keep. Absent uses the default.\n * Closed over by the bridge rather than passed as an island prop, like `navigation`, because a\n * component cannot cross Astro's serialization boundary.\n */\n consentBannerComponent?: ComponentType;\n /**\n * Backend API origin (WITHOUT a trailing slash) that anonymous-capture\n * sections POST to when constructing a `WebsiteAnonymousSessionClient`\n * (lead / draft-note forms). Sourced from\n * `WildoMarketingWebsiteConfig.apiOrigin` and passed as a\n * JSON-serializable island prop (the `.astro` page resolves the\n * per-environment value and forwards it).\n *\n * Optional because only pages that host an anonymous-capture section\n * need it — brochure pages omit it. A capture section that reads\n * `undefined` here surfaces a loud misconfiguration rather than\n * silently failing to submit.\n */\n apiOrigin?: string;\n}\n\n/**\n * Bare React context. Default `null` — `useWebsiteRuntime()` throws\n * with a clear error message when invoked outside of\n * `<WebsiteRuntimeContextProvider>`. The defensive throw is preferred\n * over a silent default value so a missing provider is detected at\n * the first hook call rather than at render time when a missing\n * label silently renders as the key string.\n */\nexport const WebsiteRuntimeContext: React.Context<WebsiteRuntimeContextValue | null> =\n createContext<WebsiteRuntimeContextValue | null>(null);\n\nexport interface WebsiteRuntimeContextProviderProps {\n value: WebsiteRuntimeContextValue;\n children: ReactNode;\n}\n\n/**\n * Provider component. Authored as a thin wrapper so consumers do NOT\n * have to import `WebsiteRuntimeContext.Provider` directly — the\n * provider component carries the documented authoring API and is the\n * Fast-Refresh-stable surface section authors compose against.\n */\nexport function WebsiteRuntimeContextProvider({\n value,\n children,\n}: WebsiteRuntimeContextProviderProps): ReactNode {\n return (\n <WebsiteRuntimeContext.Provider value={value}>\n {children}\n </WebsiteRuntimeContext.Provider>\n );\n}\nWebsiteRuntimeContextProvider.displayName = 'WebsiteRuntimeContextProvider';\n/** @wildo_source:part:end saas.website.runtime.context */\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebsiteSectionContext.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"WebsiteSectionContext.d.ts","sourceRoot":"","sources":["../../../../../src/core/contexts/WebsiteSectionContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wDAAwD,CAAC;AACrG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAEhF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,UAAU,EAAE,iBAAiB,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,EAAE,sBAAsB,CAAC;CAClC;AAED,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,OAAO,CAAC,0BAA0B,GAAG,IAAI,CAC3B,CAAC;AAEzD,MAAM,WAAW,kCAAkC;IACjD,KAAK,EAAE,0BAA0B,CAAC;IAClC,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,6BAA6B,CAAC,EAC5C,KAAK,EACL,QAAQ,GACT,EAAE,kCAAkC,GAAG,SAAS,CAMhD;yBATe,6BAA6B;;;AAW7C,0DAA0D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebsiteSectionContext.js","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"WebsiteSectionContext.js","sourceRoot":"","sources":["../../../../../src/core/contexts/WebsiteSectionContext.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,aAAa,EAAkB,MAAM,OAAO,CAAC;AAwC7D,MAAM,CAAC,MAAM,qBAAqB,GAChC,aAAa,CAAoC,IAAI,CAAC,CAAC;AAOzD,MAAM,UAAU,6BAA6B,CAAC,EAC5C,KAAK,EACL,QAAQ,GAC2B;IACnC,OAAO,CACL,KAAC,qBAAqB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YACzC,QAAQ,GACsB,CAClC,CAAC;AACJ,CAAC;AACD,6BAA6B,CAAC,WAAW,GAAG,+BAA+B,CAAC","sourcesContent":["import React, { createContext, type ReactNode } from 'react';\n\nimport type { WebsiteSectionCategory } from '../../schemas/sections/website-section-category.shared';\nimport type { WebsiteSectionRef } from '../../schemas/refs/section-ref.schemas';\n\n/**\n * @wildo_source:part:start saas.website.section.context facet:layer:core facet:family:website\n *\n * `WebsiteSectionContext` — identity + metadata of the section\n * currently rendering. Provided by `<WebsiteSection>` and consumed\n * by `useWebsiteLabel(leaf)` to compose the full label key, by\n * analytics overlays that need the category, and by SEO emitters\n * that decorate sections with `data-*` attributes.\n *\n * **Why expose `category` here (not just `sectionRef`)**: the\n * category is the section's semantic role (`hero`, `cta`, `social`,\n * etc.) and is needed by overlay tooling and analytics consumers\n * that don't know the per-site `sectionRef` vocabulary. Bundling\n * both saves consumers from having to thread the manifest just to\n * map a ref back to its category.\n *\n * **Why the hook lives in a separate file**: same Fast-Refresh\n * rationale as the runtime + page contexts — context and provider\n * together here, the hook alone in `useWebsiteSection.ts`.\n */\nexport interface WebsiteSectionContextValue {\n /**\n * The validated `WebsiteSectionRef` of the section currently\n * rendering. Composed by `useWebsiteLabel(leaf)` into the full key.\n */\n sectionRef: WebsiteSectionRef;\n /**\n * The semantic category of the section (`hero` / `cta` / etc.).\n * Sourced from the matching `WebsiteSectionDefinition.category`\n * by the host adapter and surfaced here so analytics + SEO\n * tooling does not need to re-resolve it.\n */\n category: WebsiteSectionCategory;\n}\n\nexport const WebsiteSectionContext: React.Context<WebsiteSectionContextValue | null> =\n createContext<WebsiteSectionContextValue | null>(null);\n\nexport interface WebsiteSectionContextProviderProps {\n value: WebsiteSectionContextValue;\n children: ReactNode;\n}\n\nexport function WebsiteSectionContextProvider({\n value,\n children,\n}: WebsiteSectionContextProviderProps): ReactNode {\n return (\n <WebsiteSectionContext.Provider value={value}>\n {children}\n </WebsiteSectionContext.Provider>\n );\n}\nWebsiteSectionContextProvider.displayName = 'WebsiteSectionContextProvider';\n/** @wildo_source:part:end saas.website.section.context */\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useWebsitePage.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"useWebsitePage.d.ts","sourceRoot":"","sources":["../../../../../src/core/contexts/useWebsitePage.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,sBAAsB,CAAC;AAE9B;;;;;;;;GAQG;AACH,wBAAgB,cAAc,IAAI,uBAAuB,CAUxD;AACD,wDAAwD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useWebsiteRuntime.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"useWebsiteRuntime.d.ts","sourceRoot":"","sources":["../../../../../src/core/contexts/useWebsiteRuntime.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,IAAI,0BAA0B,CAU9D;AACD,2DAA2D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useWebsiteSection.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"useWebsiteSection.d.ts","sourceRoot":"","sources":["../../../../../src/core/contexts/useWebsiteSection.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,IAAI,0BAA0B,CAW9D;AACD,2DAA2D"}
|
|
@@ -28,8 +28,7 @@ export interface CreateFrontendWebsiteProviderRegistryInput {
|
|
|
28
28
|
readonly entries?: FrontendProvidersBlock;
|
|
29
29
|
/**
|
|
30
30
|
* The CODE channel. Absent is the ordinary case for a website that declares
|
|
31
|
-
* no
|
|
32
|
-
* so there is nothing to import and nothing to pass.
|
|
31
|
+
* no providers. Config sync still writes an empty map so startup imports remain stable.
|
|
33
32
|
*/
|
|
34
33
|
readonly modules?: GeneratedWebsiteProviderModules;
|
|
35
34
|
readonly onDiagnostic?: (diagnostic: FrontendProviderHydrationDiagnostic) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frontend-provider-registry.website.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"frontend-provider-registry.website.d.ts","sourceRoot":"","sources":["../../../../../src/core/external-providers/frontend-provider-registry.website.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAEL,KAAK,+BAA+B,EACrC,MAAM,8CAA8C,CAAC;AACtD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,KAAK,EAAE,mCAAmC,EAAE,MAAM,8CAA8C,CAAC;AAExG,YAAY,EACV,6BAA6B,EAC7B,0BAA0B,EAC1B,+BAA+B,EAC/B,+BAA+B,GAChC,MAAM,8CAA8C,CAAC;AAEtD,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,8CAA8C,CAAC;AAEpG,MAAM,WAAW,0CAA0C;IACzD,yEAAyE;IACzE,QAAQ,CAAC,OAAO,CAAC,EAAE,sBAAsB,CAAC;IAC1C;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,+BAA+B,CAAC;IACnD,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,mCAAmC,KAAK,IAAI,CAAC;CACnF;AAED;;GAEG;AACH,wBAAgB,qCAAqC,CACnD,KAAK,CAAC,EAAE,0CAA0C,GACjD,+BAA+B,CAMjC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frontend-provider-registry.website.js","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"frontend-provider-registry.website.js","sourceRoot":"","sources":["../../../../../src/core/external-providers/frontend-provider-registry.website.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EACL,uCAAuC,GAExC,MAAM,8CAA8C,CAAC;AAwBtD;;GAEG;AACH,MAAM,UAAU,qCAAqC,CACnD,KAAkD;IAElD,OAAO,uCAAuC,CAAC;QAC7C,OAAO,EAAE,KAAK,EAAE,OAAO;QACvB,OAAO,EAAE,KAAK,EAAE,OAAO;QACvB,YAAY,EAAE,KAAK,EAAE,YAAY;KAClC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * The website's provider registry — now composed from BOTH channels.\n *\n * It used to build itself by casting each served `FrontendProviderEntry` to a\n * module type that was an alias of that entry, which made the cast true and\n * useless: the registry could never hold anything a JSON payload could not\n * carry, so a provider's SDK loader had nowhere to arrive.\n *\n * - **DATA** — the materialized frontend-services block for THIS website\n * service. Authoritative for which providers are on, in this environment.\n * - **CODE** — `generatedFrontendProviderModules` from the website's own\n * `src/generated/frontend-providers.generated.ts`, written by\n * `wildo config sync` and handed in by the Astro bridge.\n *\n * The types are now the engine's own website-facet types rather than local\n * aliases, so a provider module authored against\n * `@wildo-ai/external-connectors-public/website` is the same type this registry\n * holds. Re-exported under the same names so every consumer's import is\n * unchanged.\n */\n\nimport {\n composeHydratedFrontendWebsiteProviders,\n type GeneratedWebsiteProviderModules,\n} from '@wildo-ai/external-connectors-public/website';\nimport type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';\nimport type { FrontendProviderHydrationDiagnostic } from '@wildo-ai/external-connectors-public/website';\n\nexport type {\n FrontendWebsiteProviderModule,\n FrontendWebsiteProviderRef,\n FrontendWebsiteProviderRegistry,\n GeneratedWebsiteProviderModules,\n} from '@wildo-ai/external-connectors-public/website';\n\nimport type { FrontendWebsiteProviderRegistry } from '@wildo-ai/external-connectors-public/website';\n\nexport interface CreateFrontendWebsiteProviderRegistryInput {\n /** The DATA channel: the materialized block for this website service. */\n readonly entries?: FrontendProvidersBlock;\n /**\n * The CODE channel. Absent is the ordinary case for a website that declares\n * no providers. Config sync still writes an empty map so startup imports remain stable.\n */\n readonly modules?: GeneratedWebsiteProviderModules;\n readonly onDiagnostic?: (diagnostic: FrontendProviderHydrationDiagnostic) => void;\n}\n\n/**\n * Website-side public-provider registry, hydrated from both channels.\n */\nexport function createFrontendWebsiteProviderRegistry(\n input?: CreateFrontendWebsiteProviderRegistryInput,\n): FrontendWebsiteProviderRegistry {\n return composeHydratedFrontendWebsiteProviders({\n entries: input?.entries,\n modules: input?.modules,\n onDiagnostic: input?.onDiagnostic,\n });\n}\n"]}
|
|
@@ -12,17 +12,33 @@
|
|
|
12
12
|
* does not exist would throw, and starting a vendor SDK there would run a live
|
|
13
13
|
* analytics client inside a build.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
15
|
+
* Both come from `@wildo-ai/external-connectors-public/website`. The script
|
|
16
|
+
* injector lives there rather than here so the SaaS application, the technical
|
|
17
|
+
* doc and this site run ONE copy of it: a second copy of a DOM mutation
|
|
18
|
+
* eventually disagrees about idempotence, which is how a vendor tag gets loaded
|
|
19
|
+
* twice. It first shipped here alone, and that is how the application came to
|
|
20
|
+
* declare its Drive Picker's Google API script and never inject it.
|
|
21
|
+
*
|
|
22
|
+
* ## Client-side injection, and the alternative that was rejected
|
|
23
|
+
*
|
|
24
|
+
* This is an Astro static site, so the "obvious" home for a third-party tag is
|
|
25
|
+
* the built HTML `<head>`. That was rejected for now: the set of live providers
|
|
26
|
+
* is per SERVICE and per ENVIRONMENT, resolved from the materialized artifact,
|
|
27
|
+
* while the built HTML is one artifact shared across deploys of the same build.
|
|
28
|
+
* Baking the tags in would make the build environment-specific — the same class
|
|
29
|
+
* of coupling the DATA channel exists to avoid. Injecting from the client keeps
|
|
30
|
+
* one build correct everywhere, at the cost of the tag arriving after hydration.
|
|
31
|
+
* Revisit if a provider ever needs to run before first paint.
|
|
23
32
|
*/
|
|
33
|
+
import { type ConsentPurpose } from '@wildo-ai/saas-models/public-runtime';
|
|
24
34
|
import type { FrontendWebsiteProviderRegistry } from './frontend-provider-registry.website';
|
|
25
|
-
export declare function useWebsiteProviderScripts(registry: FrontendWebsiteProviderRegistry | undefined
|
|
35
|
+
export declare function useWebsiteProviderScripts(registry: FrontendWebsiteProviderRegistry | undefined,
|
|
36
|
+
/**
|
|
37
|
+
* The purposes the visitor granted (#534). A provider resting on an ungranted purpose injects nothing;
|
|
38
|
+
* granting it later injects its script then. A withdrawal cannot remove an executed script, so it takes
|
|
39
|
+
* effect on the next page load.
|
|
40
|
+
*/
|
|
41
|
+
grantedConsentPurposes: readonly ConsentPurpose[]): void;
|
|
26
42
|
/**
|
|
27
43
|
* Loads and starts the npm SDK of every live provider that declares one.
|
|
28
44
|
*
|
|
@@ -30,5 +46,12 @@ export declare function useWebsiteProviderScripts(registry: FrontendWebsiteProvi
|
|
|
30
46
|
* offers a teardown — an analytics client that cannot be stopped cannot honour
|
|
31
47
|
* a consent withdrawal, and accumulates one instance per view transition.
|
|
32
48
|
*/
|
|
33
|
-
export declare function useWebsiteProviderSdks(registry: FrontendWebsiteProviderRegistry | undefined
|
|
49
|
+
export declare function useWebsiteProviderSdks(registry: FrontendWebsiteProviderRegistry | undefined,
|
|
50
|
+
/**
|
|
51
|
+
* The purposes the visitor granted (#534). Providers are activated in TWO groups on purpose: those that
|
|
52
|
+
* rest on no purpose (error monitoring, captcha) are started once and never restarted by a consent
|
|
53
|
+
* change, while the consent-based ones start when their purpose is granted and are DEACTIVATED when it
|
|
54
|
+
* is withdrawn — which is the only way a withdrawal stops a running analytics client.
|
|
55
|
+
*/
|
|
56
|
+
grantedConsentPurposes: readonly ConsentPurpose[]): void;
|
|
34
57
|
//# sourceMappingURL=useWebsiteProviderScripts.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useWebsiteProviderScripts.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"useWebsiteProviderScripts.d.ts","sourceRoot":"","sources":["../../../../../src/core/external-providers/useWebsiteProviderScripts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAUH,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAC5F,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,sCAAsC,CAAC;AAU5F,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,+BAA+B,GAAG,SAAS;AACrD;;;;GAIG;AACH,sBAAsB,EAAE,SAAS,cAAc,EAAE,GAChD,IAAI,CAeN;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,+BAA+B,GAAG,SAAS;AACrD;;;;;GAKG;AACH,sBAAsB,EAAE,SAAS,cAAc,EAAE,GAChD,IAAI,CAGN"}
|
|
@@ -12,24 +12,46 @@
|
|
|
12
12
|
* does not exist would throw, and starting a vendor SDK there would run a live
|
|
13
13
|
* analytics client inside a build.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
15
|
+
* Both come from `@wildo-ai/external-connectors-public/website`. The script
|
|
16
|
+
* injector lives there rather than here so the SaaS application, the technical
|
|
17
|
+
* doc and this site run ONE copy of it: a second copy of a DOM mutation
|
|
18
|
+
* eventually disagrees about idempotence, which is how a vendor tag gets loaded
|
|
19
|
+
* twice. It first shipped here alone, and that is how the application came to
|
|
20
|
+
* declare its Drive Picker's Google API script and never inject it.
|
|
21
|
+
*
|
|
22
|
+
* ## Client-side injection, and the alternative that was rejected
|
|
23
|
+
*
|
|
24
|
+
* This is an Astro static site, so the "obvious" home for a third-party tag is
|
|
25
|
+
* the built HTML `<head>`. That was rejected for now: the set of live providers
|
|
26
|
+
* is per SERVICE and per ENVIRONMENT, resolved from the materialized artifact,
|
|
27
|
+
* while the built HTML is one artifact shared across deploys of the same build.
|
|
28
|
+
* Baking the tags in would make the build environment-specific — the same class
|
|
29
|
+
* of coupling the DATA channel exists to avoid. Injecting from the client keeps
|
|
30
|
+
* one build correct everywhere, at the cost of the tag arriving after hydration.
|
|
31
|
+
* Revisit if a provider ever needs to run before first paint.
|
|
23
32
|
*/
|
|
24
33
|
import { useEffect } from 'react';
|
|
25
|
-
import { activateFrontendProviderSdks, } from '@wildo-ai/external-connectors-public/website';
|
|
26
|
-
import { collectDeclaredProviderScripts, injectProviderScripts, } from './provider-scripts.website.js';
|
|
34
|
+
import { activateFrontendProviderSdks, collectDeclaredProviderScripts, consentPurposesRequiredByProvider, injectProviderScripts, } from '@wildo-ai/external-connectors-public/website';
|
|
27
35
|
import { AppFrontendType } from '@wildo-ai/saas-models/public-runtime';
|
|
28
|
-
|
|
36
|
+
/**
|
|
37
|
+
* A stable key for a granted-purpose set, so an effect re-runs when the SET changes and not when a new
|
|
38
|
+
* array with the same members is handed in.
|
|
39
|
+
*/
|
|
40
|
+
function grantedKey(grantedConsentPurposes) {
|
|
41
|
+
return [...grantedConsentPurposes].sort().join(',');
|
|
42
|
+
}
|
|
43
|
+
export function useWebsiteProviderScripts(registry,
|
|
44
|
+
/**
|
|
45
|
+
* The purposes the visitor granted (#534). A provider resting on an ungranted purpose injects nothing;
|
|
46
|
+
* granting it later injects its script then. A withdrawal cannot remove an executed script, so it takes
|
|
47
|
+
* effect on the next page load.
|
|
48
|
+
*/
|
|
49
|
+
grantedConsentPurposes) {
|
|
50
|
+
const key = grantedKey(grantedConsentPurposes);
|
|
29
51
|
useEffect(() => {
|
|
30
52
|
if (typeof document === 'undefined')
|
|
31
53
|
return;
|
|
32
|
-
const scripts = collectDeclaredProviderScripts(registry);
|
|
54
|
+
const scripts = collectDeclaredProviderScripts(registry?.modules.values(), grantedConsentPurposes);
|
|
33
55
|
if (scripts.length === 0)
|
|
34
56
|
return;
|
|
35
57
|
injectProviderScripts(scripts, document);
|
|
@@ -37,7 +59,8 @@ export function useWebsiteProviderScripts(registry) {
|
|
|
37
59
|
// work immediately; removing the element undoes neither, so a cleanup that
|
|
38
60
|
// removed it would only make the next mount inject a SECOND copy of an
|
|
39
61
|
// already-running script. Idempotence by `src` is the real guard.
|
|
40
|
-
|
|
62
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- keyed by the granted SET, see grantedKey.
|
|
63
|
+
}, [registry, key]);
|
|
41
64
|
}
|
|
42
65
|
/**
|
|
43
66
|
* Loads and starts the npm SDK of every live provider that declares one.
|
|
@@ -46,15 +69,39 @@ export function useWebsiteProviderScripts(registry) {
|
|
|
46
69
|
* offers a teardown — an analytics client that cannot be stopped cannot honour
|
|
47
70
|
* a consent withdrawal, and accumulates one instance per view transition.
|
|
48
71
|
*/
|
|
49
|
-
export function useWebsiteProviderSdks(registry
|
|
72
|
+
export function useWebsiteProviderSdks(registry,
|
|
73
|
+
/**
|
|
74
|
+
* The purposes the visitor granted (#534). Providers are activated in TWO groups on purpose: those that
|
|
75
|
+
* rest on no purpose (error monitoring, captcha) are started once and never restarted by a consent
|
|
76
|
+
* change, while the consent-based ones start when their purpose is granted and are DEACTIVATED when it
|
|
77
|
+
* is withdrawn — which is the only way a withdrawal stops a running analytics client.
|
|
78
|
+
*/
|
|
79
|
+
grantedConsentPurposes) {
|
|
80
|
+
useProviderSdkGroup(registry, false, []);
|
|
81
|
+
useProviderSdkGroup(registry, true, grantedConsentPurposes);
|
|
82
|
+
}
|
|
83
|
+
function useProviderSdkGroup(registry, consentBased, grantedConsentPurposes) {
|
|
84
|
+
// Keyed by the grants this group's providers actually rest on, so changing an unrelated purpose does
|
|
85
|
+
// not stop and restart a running analytics client.
|
|
86
|
+
const relevant = registry === undefined
|
|
87
|
+
? []
|
|
88
|
+
: [...registry.modules.values()]
|
|
89
|
+
.filter((module) => (consentPurposesRequiredByProvider(module).length > 0) === consentBased)
|
|
90
|
+
.flatMap((module) => consentPurposesRequiredByProvider(module))
|
|
91
|
+
.filter((purpose) => grantedConsentPurposes.includes(purpose));
|
|
92
|
+
const key = grantedKey(relevant);
|
|
50
93
|
useEffect(() => {
|
|
51
94
|
if (registry === undefined)
|
|
52
95
|
return;
|
|
96
|
+
const modules = [...registry.modules.values()].filter((module) => (consentPurposesRequiredByProvider(module).length > 0) === consentBased);
|
|
97
|
+
if (modules.length === 0)
|
|
98
|
+
return;
|
|
53
99
|
let unmounted = false;
|
|
54
100
|
let activated;
|
|
55
101
|
void activateFrontendProviderSdks({
|
|
56
102
|
surface: AppFrontendType.STATIC_WEBSITE,
|
|
57
|
-
modules
|
|
103
|
+
modules,
|
|
104
|
+
grantedConsentPurposes,
|
|
58
105
|
onDiagnostic: (diagnostic) => {
|
|
59
106
|
// The website runtime has no logger service, and a provider that cannot
|
|
60
107
|
// start is a developer-facing fact that must not be silent.
|
|
@@ -73,6 +120,7 @@ export function useWebsiteProviderSdks(registry) {
|
|
|
73
120
|
unmounted = true;
|
|
74
121
|
void activated?.deactivateAll();
|
|
75
122
|
};
|
|
76
|
-
|
|
123
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- keyed by the granted SET, see grantedKey.
|
|
124
|
+
}, [registry, consentBased, key]);
|
|
77
125
|
}
|
|
78
126
|
//# sourceMappingURL=useWebsiteProviderScripts.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useWebsiteProviderScripts.js","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"useWebsiteProviderScripts.js","sourceRoot":"","sources":["../../../../../src/core/external-providers/useWebsiteProviderScripts.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The website's executable-surface hooks: load what the live providers declared.\n *\n * Two mechanisms, one per kind of vendor. `useWebsiteProviderScripts` adds the\n * `<script>` tags a provider declared — the case a dynamic `import()` cannot\n * serve, and the dominant one on a marketing site. `useWebsiteProviderSdks`\n * activates the npm SDK of any provider that ships one, which reaches this\n * bundle through the CODE channel.\n *\n * Both are effect-only, and that is not incidental: this is an Astro site, so\n * page modules execute in Node at build time. Injecting into a document that\n * does not exist would throw, and starting a vendor SDK there would run a live\n * analytics client inside a build.\n *\n * Both come from `@wildo-ai/external-connectors-public/website`. The script\n * injector lives there rather than here so the SaaS application, the technical\n * doc and this site run ONE copy of it: a second copy of a DOM mutation\n * eventually disagrees about idempotence, which is how a vendor tag gets loaded\n * twice. It first shipped here alone, and that is how the application came to\n * declare its Drive Picker's Google API script and never inject it.\n *\n * ## Client-side injection, and the alternative that was rejected\n *\n * This is an Astro static site, so the \"obvious\" home for a third-party tag is\n * the built HTML `<head>`. That was rejected for now: the set of live providers\n * is per SERVICE and per ENVIRONMENT, resolved from the materialized artifact,\n * while the built HTML is one artifact shared across deploys of the same build.\n * Baking the tags in would make the build environment-specific — the same class\n * of coupling the DATA channel exists to avoid. Injecting from the client keeps\n * one build correct everywhere, at the cost of the tag arriving after hydration.\n * Revisit if a provider ever needs to run before first paint.\n */\n\nimport { useEffect } from 'react';\nimport {\n activateFrontendProviderSdks,\n collectDeclaredProviderScripts,\n consentPurposesRequiredByProvider,\n injectProviderScripts,\n type ActivatedFrontendProviderSdks,\n} from '@wildo-ai/external-connectors-public/website';\nimport { AppFrontendType, type ConsentPurpose } from '@wildo-ai/saas-models/public-runtime';\nimport type { FrontendWebsiteProviderRegistry } from './frontend-provider-registry.website';\n\n/**\n * A stable key for a granted-purpose set, so an effect re-runs when the SET changes and not when a new\n * array with the same members is handed in.\n */\nfunction grantedKey(grantedConsentPurposes: readonly ConsentPurpose[]): string {\n return [...grantedConsentPurposes].sort().join(',');\n}\n\nexport function useWebsiteProviderScripts(\n registry: FrontendWebsiteProviderRegistry | undefined,\n /**\n * The purposes the visitor granted (#534). A provider resting on an ungranted purpose injects nothing;\n * granting it later injects its script then. A withdrawal cannot remove an executed script, so it takes\n * effect on the next page load.\n */\n grantedConsentPurposes: readonly ConsentPurpose[],\n): void {\n const key = grantedKey(grantedConsentPurposes);\n useEffect(() => {\n if (typeof document === 'undefined') return;\n\n const scripts = collectDeclaredProviderScripts(registry?.modules.values(), grantedConsentPurposes);\n if (scripts.length === 0) return;\n\n injectProviderScripts(scripts, document);\n // Deliberately no teardown. A vendor tag installs a global and often starts\n // work immediately; removing the element undoes neither, so a cleanup that\n // removed it would only make the next mount inject a SECOND copy of an\n // already-running script. Idempotence by `src` is the real guard.\n // eslint-disable-next-line react-hooks/exhaustive-deps -- keyed by the granted SET, see grantedKey.\n }, [registry, key]);\n}\n\n/**\n * Loads and starts the npm SDK of every live provider that declares one.\n *\n * Unlike a script tag, an SDK activation IS undone on unmount where the vendor\n * offers a teardown — an analytics client that cannot be stopped cannot honour\n * a consent withdrawal, and accumulates one instance per view transition.\n */\nexport function useWebsiteProviderSdks(\n registry: FrontendWebsiteProviderRegistry | undefined,\n /**\n * The purposes the visitor granted (#534). Providers are activated in TWO groups on purpose: those that\n * rest on no purpose (error monitoring, captcha) are started once and never restarted by a consent\n * change, while the consent-based ones start when their purpose is granted and are DEACTIVATED when it\n * is withdrawn — which is the only way a withdrawal stops a running analytics client.\n */\n grantedConsentPurposes: readonly ConsentPurpose[],\n): void {\n useProviderSdkGroup(registry, false, []);\n useProviderSdkGroup(registry, true, grantedConsentPurposes);\n}\n\nfunction useProviderSdkGroup(\n registry: FrontendWebsiteProviderRegistry | undefined,\n consentBased: boolean,\n grantedConsentPurposes: readonly ConsentPurpose[],\n): void {\n // Keyed by the grants this group's providers actually rest on, so changing an unrelated purpose does\n // not stop and restart a running analytics client.\n const relevant = registry === undefined\n ? []\n : [...registry.modules.values()]\n .filter((module) => (consentPurposesRequiredByProvider(module).length > 0) === consentBased)\n .flatMap((module) => consentPurposesRequiredByProvider(module))\n .filter((purpose) => grantedConsentPurposes.includes(purpose));\n const key = grantedKey(relevant);\n useEffect(() => {\n if (registry === undefined) return;\n const modules = [...registry.modules.values()].filter(\n (module) => (consentPurposesRequiredByProvider(module).length > 0) === consentBased,\n );\n if (modules.length === 0) return;\n\n let unmounted = false;\n let activated: ActivatedFrontendProviderSdks | undefined;\n\n void activateFrontendProviderSdks({\n surface: AppFrontendType.STATIC_WEBSITE,\n modules,\n grantedConsentPurposes,\n onDiagnostic: (diagnostic) => {\n // The website runtime has no logger service, and a provider that cannot\n // start is a developer-facing fact that must not be silent.\n // eslint-disable-next-line no-console\n console.warn(`[wildo-provider-sdk] ${diagnostic.kind}: ${diagnostic.message}`);\n },\n }).then((result) => {\n activated = result;\n // Activation is asynchronous and unmount is not: if the surface went away\n // while a vendor bundle was still downloading, stop what we just started\n // rather than leaving a client nobody can reach.\n if (unmounted) void result.deactivateAll();\n });\n\n return () => {\n unmounted = true;\n void activated?.deactivateAll();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- keyed by the granted SET, see grantedKey.\n }, [registry, consentBased, key]);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-website-page-manifest.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"define-website-page-manifest.d.ts","sourceRoot":"","sources":["../../../../../src/core/factories/define-website-page-manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,4BAA4B,EAC7B,MAAM,8DAA8D,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAEhF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,8BAA8B,CAAC,MAAM;IACpD,GAAG,EAAE,cAAc,GAAG,MAAM,CAAC;IAC7B,SAAS,EAAE,oBAAoB,GAAG,MAAM,CAAC;IACzC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACrC,WAAW,EAAE,SAAS,CAAC,iBAAiB,GAAG,MAAM,CAAC,EAAE,CAAC;IACrD,aAAa,EAAE,wBAAwB,CAAC;IACxC,SAAS,CAAC,EAAE,4BAA4B,CAAC;IACzC,GAAG,CAAC,EAAE,oBAAoB,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB,CAAC,MAAM,CAC9C,SAAQ,IAAI,CAAC,mBAAmB,EAAE,eAAe,CAAC;IAClD,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACtC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EACtE,KAAK,EAAE,8BAA8B,CAAC,MAAM,CAAC,GAC5C,wBAAwB,CAAC,MAAM,CAAC,CAmBlC;AACD,yEAAyE"}
|