@brandlab-digital/site-next 2.1.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics.d.ts +111 -52
- package/dist/analytics.d.ts.map +1 -1
- package/dist/analytics.js +281 -153
- package/dist/analytics.js.map +1 -1
- package/dist/blog.d.ts +4 -2
- package/dist/blog.d.ts.map +1 -1
- package/dist/blog.js +7 -7
- package/dist/blog.js.map +1 -1
- package/dist/contact/client-entry.d.ts +2 -0
- package/dist/contact/client-entry.d.ts.map +1 -1
- package/dist/contact/client-entry.js +1 -0
- package/dist/contact/client-entry.js.map +1 -1
- package/dist/contact/client.d.ts +12 -22
- package/dist/contact/client.d.ts.map +1 -1
- package/dist/contact/client.js +28 -21
- package/dist/contact/client.js.map +1 -1
- package/dist/contact/form-client.d.ts +159 -0
- package/dist/contact/form-client.d.ts.map +1 -0
- package/dist/contact/form-client.js +257 -0
- package/dist/contact/form-client.js.map +1 -0
- package/dist/contact/index.d.ts +1 -1
- package/dist/contact/index.d.ts.map +1 -1
- package/dist/contact/index.js +1 -1
- package/dist/contact/index.js.map +1 -1
- package/dist/contact/route-handler.d.ts +32 -0
- package/dist/contact/route-handler.d.ts.map +1 -0
- package/dist/contact/route-handler.js +44 -0
- package/dist/contact/route-handler.js.map +1 -0
- package/dist/contact/server-entry.d.ts +1 -0
- package/dist/contact/server-entry.d.ts.map +1 -1
- package/dist/contact/server-entry.js +1 -0
- package/dist/contact/server-entry.js.map +1 -1
- package/dist/contact/server.d.ts +5 -0
- package/dist/contact/server.d.ts.map +1 -1
- package/dist/contact/server.js +13 -7
- package/dist/contact/server.js.map +1 -1
- package/dist/contact/shared.d.ts +4 -0
- package/dist/contact/shared.d.ts.map +1 -1
- package/dist/contact/shared.js.map +1 -1
- package/dist/contact/testing.d.ts +29 -0
- package/dist/contact/testing.d.ts.map +1 -1
- package/dist/contact/testing.js +27 -0
- package/dist/contact/testing.js.map +1 -1
- package/dist/google-tag-plan.d.ts +1 -4
- package/dist/google-tag-plan.d.ts.map +1 -1
- package/dist/google-tag-plan.js +1 -14
- package/dist/google-tag-plan.js.map +1 -1
- package/dist/google-tag.d.ts +5 -4
- package/dist/google-tag.d.ts.map +1 -1
- package/dist/google-tag.js +5 -4
- package/dist/google-tag.js.map +1 -1
- package/dist/headers.d.ts +54 -27
- package/dist/headers.d.ts.map +1 -1
- package/dist/headers.js +122 -51
- package/dist/headers.js.map +1 -1
- package/dist/instrumentation-client.d.ts +21 -1
- package/dist/instrumentation-client.d.ts.map +1 -1
- package/dist/instrumentation-client.js +62 -7
- package/dist/instrumentation-client.js.map +1 -1
- package/dist/lead-registry.d.ts +34 -0
- package/dist/lead-registry.d.ts.map +1 -0
- package/dist/lead-registry.js +30 -0
- package/dist/lead-registry.js.map +1 -0
- package/dist/lead-reporting.d.ts +37 -0
- package/dist/lead-reporting.d.ts.map +1 -0
- package/dist/lead-reporting.js +74 -0
- package/dist/lead-reporting.js.map +1 -0
- package/dist/metadata.d.ts +10 -11
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +17 -19
- package/dist/metadata.js.map +1 -1
- package/dist/next-config.d.ts +113 -0
- package/dist/next-config.d.ts.map +1 -0
- package/dist/next-config.js +213 -0
- package/dist/next-config.js.map +1 -0
- package/dist/robots.d.ts +14 -6
- package/dist/robots.d.ts.map +1 -1
- package/dist/robots.js +26 -17
- package/dist/robots.js.map +1 -1
- package/dist/sitemaps.d.ts +34 -0
- package/dist/sitemaps.d.ts.map +1 -1
- package/dist/sitemaps.js +66 -1
- package/dist/sitemaps.js.map +1 -1
- package/dist/testing/server-only.d.ts +8 -0
- package/dist/testing/server-only.d.ts.map +1 -0
- package/dist/testing/server-only.js +8 -0
- package/dist/testing/server-only.js.map +1 -0
- package/dist/ui-client.d.ts +1 -12
- package/dist/ui-client.d.ts.map +1 -1
- package/dist/ui-client.js +0 -17
- package/dist/ui-client.js.map +1 -1
- package/dist/ui.d.ts +22 -2
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +67 -6
- package/dist/ui.js.map +1 -1
- package/package.json +16 -3
package/dist/contact/testing.js
CHANGED
|
@@ -1,2 +1,29 @@
|
|
|
1
|
+
import { defineContactContract } from '@brandlab-digital/site-core/contact';
|
|
2
|
+
import { leadBindingFor } from '../lead-registry.js';
|
|
3
|
+
import { reportAcceptedLead } from '../lead-reporting.js';
|
|
4
|
+
import { CONTACT_CONFIRMATION_ID_PATTERN } from './shared.js';
|
|
1
5
|
export { buildContactEmailSubject, escapeContactEmailHtml, renderContactEmail, sanitizeContactEmailSubject, } from './email.js';
|
|
6
|
+
/**
|
|
7
|
+
* Records exactly what useBrandlabContactForm sends for one accepted enquiry
|
|
8
|
+
* on the production deployment, without a browser: the GA4 contact_form event
|
|
9
|
+
* and, when the contract declares Google Ads, the form conversion. Use it to
|
|
10
|
+
* pin a site's declared lead reporting in its own tests.
|
|
11
|
+
*/
|
|
12
|
+
export function recordBrandlabContactLead(input) {
|
|
13
|
+
const lead = leadBindingFor(input.analytics);
|
|
14
|
+
if (!lead)
|
|
15
|
+
throw new TypeError('analytics must be the binding returned by defineBrandlabAnalytics.');
|
|
16
|
+
if (!CONTACT_CONFIRMATION_ID_PATTERN.test(input.confirmationId)) {
|
|
17
|
+
throw new TypeError('confirmationId must be 64 lowercase hexadecimal characters.');
|
|
18
|
+
}
|
|
19
|
+
const contract = defineContactContract(input.contract);
|
|
20
|
+
const calls = [];
|
|
21
|
+
const outcome = reportAcceptedLead(lead, { ok: true, accepted: true, trackConversion: true, confirmationId: input.confirmationId }, {
|
|
22
|
+
pagePath: input.pagePath,
|
|
23
|
+
formData: input.formData,
|
|
24
|
+
fieldRoles: { fields: contract.fields.map((field) => ({ name: field.key, role: field.role })) },
|
|
25
|
+
adUserDataConsent: input.adUserDataConsent === true,
|
|
26
|
+
}, { enabled: true, gtag: (...args) => void calls.push(args) });
|
|
27
|
+
return { calls, ...outcome };
|
|
28
|
+
}
|
|
2
29
|
//# sourceMappingURL=testing.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/contact/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,kBAAkB,EAClB,2BAA2B,GAG5B,MAAM,YAAY,CAAA","sourcesContent":["
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/contact/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAgC,MAAM,qCAAqC,CAAA;AAEzG,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,+BAA+B,EAAE,MAAM,aAAa,CAAA;AAE7D,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,kBAAkB,EAClB,2BAA2B,GAG5B,MAAM,YAAY,CAAA;AAwBnB;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAqC;IAC7E,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;IAC5C,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,SAAS,CAAC,oEAAoE,CAAC,CAAA;IACpG,IAAI,CAAC,+BAA+B,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,SAAS,CAAC,6DAA6D,CAAC,CAAA;IACpF,CAAC;IACD,MAAM,QAAQ,GAAG,qBAAqB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IACtD,MAAM,KAAK,GAAgB,EAAE,CAAA;IAC7B,MAAM,OAAO,GAAG,kBAAkB,CAChC,IAAI,EACJ,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,EAAE,EACzF;QACE,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,UAAU,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE;QAC/F,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,KAAK,IAAI;KACpD,EACD,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACvE,CAAA;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,CAAA;AAC9B,CAAC","sourcesContent":["import { defineContactContract, type BrandlabContactContract } from '@brandlab-digital/site-core/contact'\nimport type { BrandlabAnalytics } from '../analytics.js'\nimport { leadBindingFor } from '../lead-registry.js'\nimport { reportAcceptedLead } from '../lead-reporting.js'\nimport { CONTACT_CONFIRMATION_ID_PATTERN } from './shared.js'\n\nexport {\n buildContactEmailSubject,\n escapeContactEmailHtml,\n renderContactEmail,\n sanitizeContactEmailSubject,\n type RenderContactEmailInput,\n type RenderedContactEmail,\n} from './email.js'\n\nexport interface RecordBrandlabContactLeadInput {\n /** The site's defineBrandlabAnalytics binding. */\n analytics: BrandlabAnalytics\n /** The site's contact contract (its email and phone roles feed Enhanced Conversions). */\n contract: BrandlabContactContract\n /** The form's pagePath, as useBrandlabContactForm receives it. */\n pagePath: string\n /** The accepted enquiry's fields, including any that onSubmitStart adds. */\n formData: FormData\n /** The provider confirmation ID (64 lowercase hex characters). */\n confirmationId: string\n adUserDataConsent?: boolean\n}\n\nexport interface RecordedBrandlabContactLead {\n /** Every gtag call, in order, that the accepted enquiry sends on the production deployment. */\n calls: unknown[][]\n ga4Sent: boolean\n enhancedConversionSent: boolean\n adsConversionSent: boolean\n}\n\n/**\n * Records exactly what useBrandlabContactForm sends for one accepted enquiry\n * on the production deployment, without a browser: the GA4 contact_form event\n * and, when the contract declares Google Ads, the form conversion. Use it to\n * pin a site's declared lead reporting in its own tests.\n */\nexport function recordBrandlabContactLead(input: RecordBrandlabContactLeadInput): RecordedBrandlabContactLead {\n const lead = leadBindingFor(input.analytics)\n if (!lead) throw new TypeError('analytics must be the binding returned by defineBrandlabAnalytics.')\n if (!CONTACT_CONFIRMATION_ID_PATTERN.test(input.confirmationId)) {\n throw new TypeError('confirmationId must be 64 lowercase hexadecimal characters.')\n }\n const contract = defineContactContract(input.contract)\n const calls: unknown[][] = []\n const outcome = reportAcceptedLead(\n lead,\n { ok: true, accepted: true, trackConversion: true, confirmationId: input.confirmationId },\n {\n pagePath: input.pagePath,\n formData: input.formData,\n fieldRoles: { fields: contract.fields.map((field) => ({ name: field.key, role: field.role })) },\n adUserDataConsent: input.adUserDataConsent === true,\n },\n { enabled: true, gtag: (...args: unknown[]) => void calls.push(args) },\n )\n return { calls, ...outcome }\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/** Supplied by `defineBrandlabAnalytics(...).googleTag`. */
|
|
2
2
|
export interface BrandlabGoogleTagConfig {
|
|
3
3
|
enabled: boolean;
|
|
4
4
|
measurementId?: string | null;
|
|
@@ -12,8 +12,5 @@ export interface BrandlabGoogleTagPlan {
|
|
|
12
12
|
destinations: readonly string[];
|
|
13
13
|
websiteCallDestination: string | null;
|
|
14
14
|
}
|
|
15
|
-
export declare function resolveBrandlabGoogleTagConfig(contract: BrandlabAnalyticsContract, environment: Readonly<Record<string, string | undefined>>, options: {
|
|
16
|
-
enabled: boolean;
|
|
17
|
-
}): BrandlabGoogleTagConfig;
|
|
18
15
|
export declare function createBrandlabGoogleTagPlan(config: BrandlabGoogleTagConfig, phoneNumber?: string | null): BrandlabGoogleTagPlan | null;
|
|
19
16
|
//# sourceMappingURL=google-tag-plan.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-tag-plan.d.ts","sourceRoot":"","sources":["../src/google-tag-plan.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"google-tag-plan.d.ts","sourceRoot":"","sources":["../src/google-tag-plan.ts"],"names":[],"mappings":"AAEA,4DAA4D;AAC5D,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,mCAAmC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACpD;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;IAC/B,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAA;CACtC;AAKD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,uBAAuB,EAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAC1B,qBAAqB,GAAG,IAAI,CA2C9B"}
|
package/dist/google-tag-plan.js
CHANGED
|
@@ -1,19 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getGoogleAdsWebsiteCallSendTo } from '@brandlab-digital/site-core/analytics';
|
|
2
2
|
const GA4_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]{4,20}$/i;
|
|
3
3
|
const GOOGLE_ADS_CONVERSION_ID_PATTERN = /^(?:AW-)?\d{6,20}$/;
|
|
4
|
-
export function resolveBrandlabGoogleTagConfig(contract, environment, options) {
|
|
5
|
-
defineAnalyticsContract(contract);
|
|
6
|
-
return {
|
|
7
|
-
enabled: options.enabled,
|
|
8
|
-
measurementId: environment[contract.measurementIdEnv],
|
|
9
|
-
googleAdsConversionId: contract.googleAds
|
|
10
|
-
? environment[contract.googleAds.conversionIdEnv]
|
|
11
|
-
: undefined,
|
|
12
|
-
googleAdsWebsiteCallConversionLabel: contract.googleAds?.websiteCalls
|
|
13
|
-
? environment[contract.googleAds.websiteCalls.conversionLabelEnv]
|
|
14
|
-
: undefined,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
4
|
export function createBrandlabGoogleTagPlan(config, phoneNumber) {
|
|
18
5
|
if (!config.enabled)
|
|
19
6
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-tag-plan.js","sourceRoot":"","sources":["../src/google-tag-plan.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"google-tag-plan.js","sourceRoot":"","sources":["../src/google-tag-plan.ts"],"names":[],"mappings":"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","sourcesContent":["import { getGoogleAdsWebsiteCallSendTo } from '@brandlab-digital/site-core/analytics'\n\n/** Supplied by `defineBrandlabAnalytics(...).googleTag`. */\nexport interface BrandlabGoogleTagConfig {\n enabled: boolean\n measurementId?: string | null\n googleAdsConversionId?: string | null\n googleAdsWebsiteCallConversionLabel?: string | null\n}\n\nexport interface BrandlabGoogleTagPlan {\n loaderId: string\n source: string\n inlineScript: string\n destinations: readonly string[]\n websiteCallDestination: string | null\n}\n\nconst GA4_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]{4,20}$/i\nconst GOOGLE_ADS_CONVERSION_ID_PATTERN = /^(?:AW-)?\\d{6,20}$/\n\nexport function createBrandlabGoogleTagPlan(\n config: BrandlabGoogleTagConfig,\n phoneNumber?: string | null,\n): BrandlabGoogleTagPlan | null {\n if (!config.enabled) return null\n const measurementId = validMeasurementId(config.measurementId)\n const googleAdsConversionId = validGoogleAdsConversionId(config.googleAdsConversionId)\n const loaderId = measurementId ?? googleAdsConversionId\n if (!loaderId) return null\n\n const destinations = [...new Set([\n ...(measurementId ? [measurementId] : []),\n ...(googleAdsConversionId ? [googleAdsConversionId] : []),\n ])]\n const displayPhone = safeDisplayedBusinessPhone(phoneNumber)\n const websiteCallDestination = displayPhone\n ? getGoogleAdsWebsiteCallSendTo({\n conversionId: googleAdsConversionId,\n conversionLabel: config.googleAdsWebsiteCallConversionLabel,\n }) ?? null\n : null\n const configCommands = destinations.map(\n (destination) => `window.gtag('config', ${safeJson(destination)});`,\n )\n if (websiteCallDestination && displayPhone) {\n configCommands.push(\n `window.gtag('config', ${safeJson(websiteCallDestination)}, ${safeJson({\n phone_conversion_number: displayPhone,\n })});`,\n )\n }\n\n return {\n loaderId,\n source: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(loaderId)}`,\n inlineScript: [\n 'window.dataLayer = window.dataLayer || [];',\n 'window.gtag = window.gtag || function(){window.dataLayer.push(arguments);};',\n \"window.gtag('consent', 'default', {ad_user_data:'denied',ad_personalization:'denied'});\",\n \"window.gtag('set', 'user_data', {});\",\n \"window.gtag('js', new Date());\",\n ...configCommands,\n ].join('\\n'),\n destinations,\n websiteCallDestination,\n }\n}\n\nfunction validMeasurementId(value: string | null | undefined) {\n const normalized = value?.trim()\n return normalized && GA4_MEASUREMENT_ID_PATTERN.test(normalized)\n ? normalized.toUpperCase()\n : undefined\n}\n\nfunction validGoogleAdsConversionId(value: string | null | undefined) {\n const normalized = value?.trim()\n if (!normalized || !GOOGLE_ADS_CONVERSION_ID_PATTERN.test(normalized)) return undefined\n return normalized.startsWith('AW-') ? normalized : `AW-${normalized}`\n}\n\nfunction safeDisplayedBusinessPhone(value: string | null | undefined) {\n const normalized = value?.trim()\n const digits = normalized?.replace(/\\D/g, '') ?? ''\n return normalized &&\n normalized.length <= 32 &&\n digits.length >= 6 &&\n /^[0-9+().\\s-]+$/.test(normalized)\n ? normalized\n : undefined\n}\n\nfunction safeJson(value: unknown) {\n return JSON.stringify(value)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/&/g, '\\\\u0026')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029')\n}\n"]}
|
package/dist/google-tag.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type BrandlabGoogleTagConfig } from './google-tag-plan.js';
|
|
2
|
-
export { createBrandlabGoogleTagPlan
|
|
2
|
+
export { createBrandlabGoogleTagPlan } from './google-tag-plan.js';
|
|
3
3
|
export type { BrandlabGoogleTagConfig, BrandlabGoogleTagPlan, } from './google-tag-plan.js';
|
|
4
4
|
export interface BrandlabGoogleTagProps extends BrandlabGoogleTagConfig {
|
|
5
5
|
/** Must match the verified public number exactly as it is rendered on the page. */
|
|
@@ -9,9 +9,10 @@ export interface BrandlabGoogleTagProps extends BrandlabGoogleTagConfig {
|
|
|
9
9
|
/**
|
|
10
10
|
* Shared Google tag and website-call forwarding-number snippet.
|
|
11
11
|
*
|
|
12
|
-
* Render this exactly once from the root layout
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Render this exactly once from the root layout:
|
|
13
|
+
* `<BrandlabGoogleTag {...analytics.googleTag} phoneNumber={displayedPhone} />`.
|
|
14
|
+
* `analytics.googleTag.enabled` is true only on the production deployment;
|
|
15
|
+
* account-side conversion settings remain outside website runtime.
|
|
15
16
|
*/
|
|
16
17
|
export declare function BrandlabGoogleTag(props: BrandlabGoogleTagProps): import("react/jsx-runtime").JSX.Element | null;
|
|
17
18
|
//# sourceMappingURL=google-tag.d.ts.map
|
package/dist/google-tag.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-tag.d.ts","sourceRoot":"","sources":["../src/google-tag.tsx"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,sBAAsB,CAAA;AAE7B,OAAO,
|
|
1
|
+
{"version":3,"file":"google-tag.d.ts","sourceRoot":"","sources":["../src/google-tag.tsx"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAA;AAClE,YAAY,EACV,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,sBAAsB,CAAA;AAE7B,MAAM,WAAW,sBAAuB,SAAQ,uBAAuB;IACrE,mFAAmF;IACnF,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,kDAqB9D"}
|
package/dist/google-tag.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import Script from 'next/script';
|
|
3
3
|
import { createBrandlabGoogleTagPlan, } from './google-tag-plan.js';
|
|
4
|
-
export { createBrandlabGoogleTagPlan
|
|
4
|
+
export { createBrandlabGoogleTagPlan } from './google-tag-plan.js';
|
|
5
5
|
/**
|
|
6
6
|
* Shared Google tag and website-call forwarding-number snippet.
|
|
7
7
|
*
|
|
8
|
-
* Render this exactly once from the root layout
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Render this exactly once from the root layout:
|
|
9
|
+
* `<BrandlabGoogleTag {...analytics.googleTag} phoneNumber={displayedPhone} />`.
|
|
10
|
+
* `analytics.googleTag.enabled` is true only on the production deployment;
|
|
11
|
+
* account-side conversion settings remain outside website runtime.
|
|
11
12
|
*/
|
|
12
13
|
export function BrandlabGoogleTag(props) {
|
|
13
14
|
const plan = createBrandlabGoogleTagPlan(props, props.phoneNumber);
|
package/dist/google-tag.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-tag.js","sourceRoot":"","sources":["../src/google-tag.tsx"],"names":[],"mappings":";AAAA,OAAO,MAAM,MAAM,aAAa,CAAA;AAChC,OAAO,EACL,2BAA2B,GAE5B,MAAM,sBAAsB,CAAA;AAE7B,OAAO,
|
|
1
|
+
{"version":3,"file":"google-tag.js","sourceRoot":"","sources":["../src/google-tag.tsx"],"names":[],"mappings":";AAAA,OAAO,MAAM,MAAM,aAAa,CAAA;AAChC,OAAO,EACL,2BAA2B,GAE5B,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAA;AAYlE;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAA6B;IAC7D,MAAM,IAAI,GAAG,2BAA2B,CAAC,KAAK,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;IAClE,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACtB,OAAO,CACL,8BACE,KAAC,MAAM,IACL,EAAE,EAAC,4BAA4B,8BACN,QAAQ,EACjC,GAAG,EAAE,IAAI,CAAC,MAAM,EAChB,QAAQ,EAAC,kBAAkB,EAC3B,KAAK,EAAE,KAAK,CAAC,KAAK,GAClB,EACF,KAAC,MAAM,IACL,EAAE,EAAC,4BAA4B,8BACN,QAAQ,EACjC,QAAQ,EAAC,kBAAkB,EAC3B,KAAK,EAAE,KAAK,CAAC,KAAK,EAClB,uBAAuB,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,GACtD,IACD,CACJ,CAAA;AACH,CAAC","sourcesContent":["import Script from 'next/script'\nimport {\n createBrandlabGoogleTagPlan,\n type BrandlabGoogleTagConfig,\n} from './google-tag-plan.js'\n\nexport { createBrandlabGoogleTagPlan } from './google-tag-plan.js'\nexport type {\n BrandlabGoogleTagConfig,\n BrandlabGoogleTagPlan,\n} from './google-tag-plan.js'\n\nexport interface BrandlabGoogleTagProps extends BrandlabGoogleTagConfig {\n /** Must match the verified public number exactly as it is rendered on the page. */\n phoneNumber?: string | null\n nonce?: string\n}\n\n/**\n * Shared Google tag and website-call forwarding-number snippet.\n *\n * Render this exactly once from the root layout:\n * `<BrandlabGoogleTag {...analytics.googleTag} phoneNumber={displayedPhone} />`.\n * `analytics.googleTag.enabled` is true only on the production deployment;\n * account-side conversion settings remain outside website runtime.\n */\nexport function BrandlabGoogleTag(props: BrandlabGoogleTagProps) {\n const plan = createBrandlabGoogleTagPlan(props, props.phoneNumber)\n if (!plan) return null\n return (\n <>\n <Script\n id=\"brandlab-google-tag-loader\"\n data-brandlab-google-tag=\"loader\"\n src={plan.source}\n strategy=\"afterInteractive\"\n nonce={props.nonce}\n />\n <Script\n id=\"brandlab-google-tag-config\"\n data-brandlab-google-tag=\"config\"\n strategy=\"afterInteractive\"\n nonce={props.nonce}\n dangerouslySetInnerHTML={{ __html: plan.inlineScript }}\n />\n </>\n )\n}\n"]}
|
package/dist/headers.d.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Brandlab security/response headers
|
|
2
|
+
* Brandlab security/response headers: one canonical header set for every
|
|
3
|
+
* client site, emitted by `defineBrandlabNextConfig`.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* `next
|
|
5
|
+
* The deployment decides everything environment-specific:
|
|
6
|
+
* - a production build (`next build`, on Vercel or locally) enforces the
|
|
7
|
+
* production Content Security Policy and sends HSTS;
|
|
8
|
+
* - `next dev` sends a report-only development policy without HSTS;
|
|
9
|
+
* - a Vercel Preview additionally sends `noindex, nofollow, noarchive` and
|
|
10
|
+
* `private, no-store`, so previews never reach search results or caches.
|
|
6
11
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* isProduction,
|
|
10
|
-
* profile: 'next-local-service',
|
|
11
|
-
* })
|
|
12
|
-
* }
|
|
12
|
+
* The production X-Robots-Tag carries snippet-display controls only; per-page
|
|
13
|
+
* indexation stays owned by Metadata.robots.
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Declared public folders of static files (`staticAssetCache`) are served with
|
|
16
|
+
* `public, max-age=31536000, immutable` by production builds; Previews and
|
|
17
|
+
* `next dev` never cache them.
|
|
17
18
|
*/
|
|
19
|
+
import type { BrandlabDeployment } from '@brandlab-digital/site-core/site';
|
|
18
20
|
export interface BrandlabHeader {
|
|
19
21
|
key: string;
|
|
20
22
|
value: string;
|
|
@@ -23,11 +25,13 @@ export interface BrandlabHeaderRule {
|
|
|
23
25
|
source: string;
|
|
24
26
|
headers: BrandlabHeader[];
|
|
25
27
|
}
|
|
26
|
-
export declare const BRANDLAB_NEXT_HEADER_PROFILES: readonly ["next-local-service", "next-root-level"];
|
|
27
|
-
export type BrandlabNextHeaderProfile = (typeof BRANDLAB_NEXT_HEADER_PROFILES)[number];
|
|
28
28
|
export declare const BRANDLAB_DEFAULT_PERMISSIONS_POLICY = "camera=(), microphone=(), geolocation=()";
|
|
29
29
|
export declare const BRANDLAB_DEFAULT_X_ROBOTS_TAG = "max-snippet:-1, max-image-preview:large, max-video-preview:-1";
|
|
30
|
+
export declare const BRANDLAB_PREVIEW_X_ROBOTS_TAG = "noindex, nofollow, noarchive";
|
|
31
|
+
export declare const BRANDLAB_PREVIEW_CACHE_CONTROL = "private, no-store, max-age=0";
|
|
30
32
|
export declare const BRANDLAB_HSTS_VALUE = "max-age=31536000";
|
|
33
|
+
/** Cache-Control for the declared static public folders on production builds. */
|
|
34
|
+
export declare const BRANDLAB_STATIC_ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
31
35
|
export interface BrandlabHstsExpansion {
|
|
32
36
|
includeSubDomains: true;
|
|
33
37
|
preload?: true;
|
|
@@ -39,26 +43,49 @@ export interface BrandlabContentSecurityPolicyExtensions {
|
|
|
39
43
|
connectSrc?: readonly string[];
|
|
40
44
|
frameSrc?: readonly string[];
|
|
41
45
|
}
|
|
42
|
-
export type BrandlabContentSecurityPolicyMode = 'enforce' | 'report-only';
|
|
43
46
|
export interface BrandlabSecurityHeaderOptions {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
permissionsPolicy?: string;
|
|
47
|
-
/** Pass null to omit the snippet-control header entirely. */
|
|
48
|
-
xRobotsTag?: string | null;
|
|
49
|
-
extraHeaders?: readonly BrandlabHeader[];
|
|
47
|
+
/** From resolveBrandlabDeployment(process.env). */
|
|
48
|
+
deployment: BrandlabDeployment;
|
|
50
49
|
hsts?: BrandlabHstsExpansion;
|
|
51
|
-
contentSecurityPolicyMode?: BrandlabContentSecurityPolicyMode;
|
|
52
50
|
/** Reviewed HTTPS origins needed by an approved project integration. */
|
|
53
51
|
contentSecurityPolicyExtensions?: BrandlabContentSecurityPolicyExtensions;
|
|
52
|
+
/** Reviewed project exception, for example `geolocation=(self)` for a location feature. */
|
|
53
|
+
permissionsPolicy?: string;
|
|
54
|
+
/** Reviewed project headers; they cannot replace a protected header. */
|
|
55
|
+
extraHeaders?: readonly BrandlabHeader[];
|
|
54
56
|
}
|
|
55
57
|
export declare function buildBrandlabContentSecurityPolicy(options: {
|
|
56
|
-
|
|
57
|
-
profile: BrandlabNextHeaderProfile;
|
|
58
|
+
deployment: BrandlabDeployment;
|
|
58
59
|
contentSecurityPolicyExtensions?: BrandlabContentSecurityPolicyExtensions;
|
|
59
60
|
}): string;
|
|
60
61
|
export declare function buildBrandlabSecurityHeaders(options: BrandlabSecurityHeaderOptions): BrandlabHeader[];
|
|
61
|
-
export
|
|
62
|
-
source
|
|
63
|
-
|
|
62
|
+
export interface BrandlabStaticAssetFolder {
|
|
63
|
+
/** The declared source, for example `/images/:path*`. */
|
|
64
|
+
source: string;
|
|
65
|
+
/** The public folder's path segments, for example `['images']`. */
|
|
66
|
+
folder: readonly string[];
|
|
67
|
+
/**
|
|
68
|
+
* The emitted rule's source: file names with an extension below the folder,
|
|
69
|
+
* such as `/images/:path(.+\.[A-Za-z0-9]+)`. The folder itself and
|
|
70
|
+
* extensionless paths, where a page would answer, never match.
|
|
71
|
+
*/
|
|
72
|
+
fileSource: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Validates `staticAssetCache`: at most 10 public folders, each written as Next's
|
|
76
|
+
* source `/<folder>/:path*` (one to four literal segments, then one catch-all
|
|
77
|
+
* parameter, at most 100 characters). `/`, `/api`, `/_next` and `/_vercel` are
|
|
78
|
+
* rejected.
|
|
79
|
+
*/
|
|
80
|
+
export declare function parseBrandlabStaticAssetCache(sources: unknown): BrandlabStaticAssetFolder[];
|
|
81
|
+
export interface BrandlabHeaderRuleOptions extends BrandlabSecurityHeaderOptions {
|
|
82
|
+
/**
|
|
83
|
+
* Public folders of static files that never change in place, for example
|
|
84
|
+
* `['/images/:path*', '/fonts/:path*']`: production builds serve their files with
|
|
85
|
+
* `public, max-age=31536000, immutable`. A file replaced under the same name stays
|
|
86
|
+
* cached in browsers for up to a year, so give a changed file a new name.
|
|
87
|
+
*/
|
|
88
|
+
staticAssetCache?: readonly string[];
|
|
89
|
+
}
|
|
90
|
+
export declare function buildBrandlabHeaderRules(options: BrandlabHeaderRuleOptions): BrandlabHeaderRule[];
|
|
64
91
|
//# sourceMappingURL=headers.d.ts.map
|
package/dist/headers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAEnB,MAAM,kCAAkC,CAAA;AAEzC,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,cAAc,EAAE,CAAA;CAC1B;AAED,eAAO,MAAM,mCAAmC,6CACJ,CAAA;AAE5C,eAAO,MAAM,6BAA6B,kEACuB,CAAA;AAEjE,eAAO,MAAM,6BAA6B,iCAAiC,CAAA;AAE3E,eAAO,MAAM,8BAA8B,iCAAiC,CAAA;AAE5E,eAAO,MAAM,mBAAmB,qBACZ,CAAA;AAEpB,iFAAiF;AACjF,eAAO,MAAM,mCAAmC,wCAAwC,CAAA;AAExF,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,EAAE,IAAI,CAAA;IACvB,OAAO,CAAC,EAAE,IAAI,CAAA;IACd,mEAAmE;IACnE,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAoBD,MAAM,WAAW,uCAAuC;IACtD,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7B,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,6BAA6B;IAC5C,mDAAmD;IACnD,UAAU,EAAE,kBAAkB,CAAA;IAC9B,IAAI,CAAC,EAAE,qBAAqB,CAAA;IAC5B,wEAAwE;IACxE,+BAA+B,CAAC,EAAE,uCAAuC,CAAA;IACzE,2FAA2F;IAC3F,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,wEAAwE;IACxE,YAAY,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;CACzC;AAcD,wBAAgB,kCAAkC,CAAC,OAAO,EAAE;IAC1D,UAAU,EAAE,kBAAkB,CAAA;IAC9B,+BAA+B,CAAC,EAAE,uCAAuC,CAAA;CAC1E,UAsDA;AAiDD,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,6BAA6B,GACrC,cAAc,EAAE,CAmDlB;AAOD,MAAM,WAAW,yBAAyB;IACxC,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IACzB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,yBAAyB,EAAE,CA4B3F;AAED,MAAM,WAAW,yBAA0B,SAAQ,6BAA6B;IAC9E;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACrC;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,kBAAkB,EAAE,CAuBjG"}
|
package/dist/headers.js
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Brandlab security/response headers
|
|
2
|
+
* Brandlab security/response headers: one canonical header set for every
|
|
3
|
+
* client site, emitted by `defineBrandlabNextConfig`.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* `next
|
|
5
|
+
* The deployment decides everything environment-specific:
|
|
6
|
+
* - a production build (`next build`, on Vercel or locally) enforces the
|
|
7
|
+
* production Content Security Policy and sends HSTS;
|
|
8
|
+
* - `next dev` sends a report-only development policy without HSTS;
|
|
9
|
+
* - a Vercel Preview additionally sends `noindex, nofollow, noarchive` and
|
|
10
|
+
* `private, no-store`, so previews never reach search results or caches.
|
|
6
11
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* isProduction,
|
|
10
|
-
* profile: 'next-local-service',
|
|
11
|
-
* })
|
|
12
|
-
* }
|
|
12
|
+
* The production X-Robots-Tag carries snippet-display controls only; per-page
|
|
13
|
+
* indexation stays owned by Metadata.robots.
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Declared public folders of static files (`staticAssetCache`) are served with
|
|
16
|
+
* `public, max-age=31536000, immutable` by production builds; Previews and
|
|
17
|
+
* `next dev` never cache them.
|
|
17
18
|
*/
|
|
18
|
-
export const BRANDLAB_NEXT_HEADER_PROFILES = [
|
|
19
|
-
'next-local-service',
|
|
20
|
-
'next-root-level',
|
|
21
|
-
];
|
|
22
19
|
export const BRANDLAB_DEFAULT_PERMISSIONS_POLICY = 'camera=(), microphone=(), geolocation=()';
|
|
23
20
|
export const BRANDLAB_DEFAULT_X_ROBOTS_TAG = 'max-snippet:-1, max-image-preview:large, max-video-preview:-1';
|
|
21
|
+
export const BRANDLAB_PREVIEW_X_ROBOTS_TAG = 'noindex, nofollow, noarchive';
|
|
22
|
+
export const BRANDLAB_PREVIEW_CACHE_CONTROL = 'private, no-store, max-age=0';
|
|
24
23
|
export const BRANDLAB_HSTS_VALUE = 'max-age=31536000';
|
|
24
|
+
/** Cache-Control for the declared static public folders on production builds. */
|
|
25
|
+
export const BRANDLAB_STATIC_ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable';
|
|
25
26
|
const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
26
27
|
const PROTECTED_HEADER_NAMES = new Set([
|
|
28
|
+
'cache-control',
|
|
27
29
|
'content-security-policy',
|
|
28
30
|
'content-security-policy-report-only',
|
|
29
31
|
'x-frame-options',
|
|
@@ -33,11 +35,23 @@ const PROTECTED_HEADER_NAMES = new Set([
|
|
|
33
35
|
'x-robots-tag',
|
|
34
36
|
'strict-transport-security',
|
|
35
37
|
]);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
const DEPLOYMENT_ENVIRONMENTS = new Set([
|
|
39
|
+
'production',
|
|
40
|
+
'preview',
|
|
41
|
+
'development',
|
|
42
|
+
]);
|
|
43
|
+
function assertDeployment(deployment) {
|
|
44
|
+
const candidate = deployment;
|
|
45
|
+
if (!candidate ||
|
|
46
|
+
typeof candidate !== 'object' ||
|
|
47
|
+
!DEPLOYMENT_ENVIRONMENTS.has(candidate.environment) ||
|
|
48
|
+
typeof candidate.productionBuild !== 'boolean') {
|
|
49
|
+
throw new Error('Header rules require the deployment from resolveBrandlabDeployment(process.env).');
|
|
40
50
|
}
|
|
51
|
+
}
|
|
52
|
+
export function buildBrandlabContentSecurityPolicy(options) {
|
|
53
|
+
assertDeployment(options.deployment);
|
|
54
|
+
const productionBuild = options.deployment.productionBuild;
|
|
41
55
|
const scriptSources = [
|
|
42
56
|
"'self'",
|
|
43
57
|
"'unsafe-inline'",
|
|
@@ -49,7 +63,7 @@ export function buildBrandlabContentSecurityPolicy(options) {
|
|
|
49
63
|
'https://googleads.g.doubleclick.net',
|
|
50
64
|
'https://challenges.cloudflare.com',
|
|
51
65
|
...normalizeCspExtensions(options.contentSecurityPolicyExtensions?.scriptSrc, 'scriptSrc'),
|
|
52
|
-
...(
|
|
66
|
+
...(productionBuild ? [] : ["'unsafe-eval'", 'http://localhost:*']),
|
|
53
67
|
];
|
|
54
68
|
const connectSources = [
|
|
55
69
|
"'self'",
|
|
@@ -66,7 +80,7 @@ export function buildBrandlabContentSecurityPolicy(options) {
|
|
|
66
80
|
'https://*.analytics.google.com',
|
|
67
81
|
'https://challenges.cloudflare.com',
|
|
68
82
|
...normalizeCspExtensions(options.contentSecurityPolicyExtensions?.connectSrc, 'connectSrc'),
|
|
69
|
-
...(
|
|
83
|
+
...(productionBuild ? [] : ['http://localhost:*', 'ws://localhost:*']),
|
|
70
84
|
];
|
|
71
85
|
const directives = [
|
|
72
86
|
"default-src 'self'",
|
|
@@ -82,7 +96,7 @@ export function buildBrandlabContentSecurityPolicy(options) {
|
|
|
82
96
|
`frame-src 'self' https://challenges.cloudflare.com https://www.googletagmanager.com https://www.google.com ${normalizeCspExtensions(options.contentSecurityPolicyExtensions?.frameSrc, 'frameSrc').join(' ')}`.trim(),
|
|
83
97
|
"manifest-src 'self'",
|
|
84
98
|
"worker-src 'self' blob:",
|
|
85
|
-
...(
|
|
99
|
+
...(productionBuild ? ['upgrade-insecure-requests'] : []),
|
|
86
100
|
];
|
|
87
101
|
return directives.join('; ');
|
|
88
102
|
}
|
|
@@ -120,15 +134,25 @@ function validateExtraHeaders(headers) {
|
|
|
120
134
|
assertHeaderValue(header.value, `extraHeaders[${index}].value`);
|
|
121
135
|
});
|
|
122
136
|
}
|
|
137
|
+
const HEADER_OPTION_KEYS = new Set([
|
|
138
|
+
'deployment',
|
|
139
|
+
'hsts',
|
|
140
|
+
'contentSecurityPolicyExtensions',
|
|
141
|
+
'permissionsPolicy',
|
|
142
|
+
'extraHeaders',
|
|
143
|
+
]);
|
|
123
144
|
export function buildBrandlabSecurityHeaders(options) {
|
|
124
|
-
if (typeof options
|
|
125
|
-
throw new
|
|
145
|
+
if (!options || typeof options !== 'object') {
|
|
146
|
+
throw new TypeError('Header options are required.');
|
|
147
|
+
}
|
|
148
|
+
const unsupported = Object.keys(options).filter((key) => !HEADER_OPTION_KEYS.has(key));
|
|
149
|
+
if (unsupported.length > 0) {
|
|
150
|
+
throw new Error(`Header options contain unsupported keys: ${unsupported.join(', ')}. The deployment decides CSP, HSTS and X-Robots-Tag.`);
|
|
126
151
|
}
|
|
127
|
-
|
|
152
|
+
assertDeployment(options.deployment);
|
|
153
|
+
const { environment, productionBuild } = options.deployment;
|
|
128
154
|
const permissionsPolicy = options.permissionsPolicy ?? BRANDLAB_DEFAULT_PERMISSIONS_POLICY;
|
|
129
155
|
assertHeaderValue(permissionsPolicy, 'permissionsPolicy');
|
|
130
|
-
if (xRobotsTag !== null)
|
|
131
|
-
assertHeaderValue(xRobotsTag, 'xRobotsTag');
|
|
132
156
|
validateExtraHeaders(options.extraHeaders ?? []);
|
|
133
157
|
if (options.hsts) {
|
|
134
158
|
assertHeaderValue(options.hsts.readinessEvidence, 'hsts.readinessEvidence');
|
|
@@ -139,19 +163,7 @@ export function buildBrandlabSecurityHeaders(options) {
|
|
|
139
163
|
const hstsValue = options.hsts
|
|
140
164
|
? `${BRANDLAB_HSTS_VALUE}; includeSubDomains${options.hsts.preload ? '; preload' : ''}`
|
|
141
165
|
: BRANDLAB_HSTS_VALUE;
|
|
142
|
-
const
|
|
143
|
-
if (!new Set(['enforce', 'report-only']).has(cspMode)) {
|
|
144
|
-
throw new Error(`Unsupported contentSecurityPolicyMode "${cspMode}".`);
|
|
145
|
-
}
|
|
146
|
-
if (options.isProduction && cspMode !== 'enforce') {
|
|
147
|
-
throw new Error('Production Content Security Policy must use enforce mode.');
|
|
148
|
-
}
|
|
149
|
-
const cspHeader = [{
|
|
150
|
-
key: cspMode === 'enforce'
|
|
151
|
-
? 'Content-Security-Policy'
|
|
152
|
-
: 'Content-Security-Policy-Report-Only',
|
|
153
|
-
value: buildBrandlabContentSecurityPolicy(options),
|
|
154
|
-
}];
|
|
166
|
+
const preview = environment === 'preview';
|
|
155
167
|
return [
|
|
156
168
|
{ key: 'X-Frame-Options', value: 'DENY' },
|
|
157
169
|
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
|
@@ -160,24 +172,83 @@ export function buildBrandlabSecurityHeaders(options) {
|
|
|
160
172
|
key: 'Permissions-Policy',
|
|
161
173
|
value: permissionsPolicy,
|
|
162
174
|
},
|
|
163
|
-
|
|
164
|
-
|
|
175
|
+
{
|
|
176
|
+
key: 'X-Robots-Tag',
|
|
177
|
+
value: preview ? BRANDLAB_PREVIEW_X_ROBOTS_TAG : BRANDLAB_DEFAULT_X_ROBOTS_TAG,
|
|
178
|
+
},
|
|
179
|
+
...(productionBuild
|
|
165
180
|
? [{ key: 'Strict-Transport-Security', value: hstsValue }]
|
|
166
181
|
: []),
|
|
167
|
-
|
|
168
|
-
|
|
182
|
+
{
|
|
183
|
+
key: productionBuild
|
|
184
|
+
? 'Content-Security-Policy'
|
|
185
|
+
: 'Content-Security-Policy-Report-Only',
|
|
186
|
+
value: buildBrandlabContentSecurityPolicy(options),
|
|
187
|
+
},
|
|
188
|
+
...(options.extraHeaders ?? []).map((header) => ({ ...header })),
|
|
189
|
+
...(preview ? [{ key: 'Cache-Control', value: BRANDLAB_PREVIEW_CACHE_CONTROL }] : []),
|
|
169
190
|
];
|
|
170
191
|
}
|
|
192
|
+
const STATIC_ASSET_SOURCE = /^\/((?:[A-Za-z0-9][A-Za-z0-9._-]*\/){1,4}):([A-Za-z_][A-Za-z0-9_]*)\*$/;
|
|
193
|
+
const STATIC_ASSET_RESERVED = new Set(['api', '_next', '_vercel']);
|
|
194
|
+
const STATIC_ASSET_MAX_SOURCES = 10;
|
|
195
|
+
const STATIC_ASSET_MAX_LENGTH = 100;
|
|
196
|
+
/**
|
|
197
|
+
* Validates `staticAssetCache`: at most 10 public folders, each written as Next's
|
|
198
|
+
* source `/<folder>/:path*` (one to four literal segments, then one catch-all
|
|
199
|
+
* parameter, at most 100 characters). `/`, `/api`, `/_next` and `/_vercel` are
|
|
200
|
+
* rejected.
|
|
201
|
+
*/
|
|
202
|
+
export function parseBrandlabStaticAssetCache(sources) {
|
|
203
|
+
if (!Array.isArray(sources)) {
|
|
204
|
+
throw new Error('staticAssetCache must list public folders such as /images/:path*.');
|
|
205
|
+
}
|
|
206
|
+
if (sources.length > STATIC_ASSET_MAX_SOURCES) {
|
|
207
|
+
throw new Error(`staticAssetCache lists at most ${STATIC_ASSET_MAX_SOURCES} public folders.`);
|
|
208
|
+
}
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
return sources.map((source, index) => {
|
|
211
|
+
const field = `staticAssetCache[${index}]`;
|
|
212
|
+
if (typeof source !== 'string' || source.length > STATIC_ASSET_MAX_LENGTH) {
|
|
213
|
+
throw new Error(`${field} must be a source of at most ${STATIC_ASSET_MAX_LENGTH} characters.`);
|
|
214
|
+
}
|
|
215
|
+
if (STATIC_ASSET_RESERVED.has(source.split('/')[1]?.toLowerCase() ?? '')) {
|
|
216
|
+
throw new Error(`${field} "${source}" is a Next or API path, not a public folder.`);
|
|
217
|
+
}
|
|
218
|
+
const match = STATIC_ASSET_SOURCE.exec(source);
|
|
219
|
+
if (!match) {
|
|
220
|
+
throw new Error(`${field} "${source}" must name a public folder as /<folder>/:path* (literal folder segments, then one catch-all parameter).`);
|
|
221
|
+
}
|
|
222
|
+
const folder = match[1].slice(0, -1).split('/');
|
|
223
|
+
const key = folder.join('/').toLowerCase();
|
|
224
|
+
if (seen.has(key))
|
|
225
|
+
throw new Error(`${field} repeats the /${folder.join('/')} folder.`);
|
|
226
|
+
seen.add(key);
|
|
227
|
+
return { source, folder, fileSource: `/${folder.join('/')}/:${match[2]}(.+\\.[A-Za-z0-9]+)` };
|
|
228
|
+
});
|
|
229
|
+
}
|
|
171
230
|
export function buildBrandlabHeaderRules(options) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
throw new Error('Header-rule source must be a root-relative single-line pattern.');
|
|
231
|
+
if (!options || typeof options !== 'object') {
|
|
232
|
+
throw new TypeError('Header options are required.');
|
|
175
233
|
}
|
|
176
|
-
|
|
234
|
+
const { staticAssetCache, ...security } = options;
|
|
235
|
+
const folders = staticAssetCache === undefined ? [] : parseBrandlabStaticAssetCache(staticAssetCache);
|
|
236
|
+
const rules = [
|
|
177
237
|
{
|
|
178
|
-
source,
|
|
179
|
-
headers: buildBrandlabSecurityHeaders(
|
|
238
|
+
source: '/:path*',
|
|
239
|
+
headers: buildBrandlabSecurityHeaders(security),
|
|
180
240
|
},
|
|
181
241
|
];
|
|
242
|
+
// A production build outside a Preview; Previews keep private, no-store everywhere.
|
|
243
|
+
const { environment, productionBuild } = options.deployment;
|
|
244
|
+
if (productionBuild && environment !== 'preview') {
|
|
245
|
+
for (const folder of folders) {
|
|
246
|
+
rules.push({
|
|
247
|
+
source: folder.fileSource,
|
|
248
|
+
headers: [{ key: 'Cache-Control', value: BRANDLAB_STATIC_ASSET_CACHE_CONTROL }],
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return rules;
|
|
182
253
|
}
|
|
183
254
|
//# sourceMappingURL=headers.js.map
|