@brandlab-digital/site-next 3.3.2 → 3.3.3

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.
@@ -17,7 +17,12 @@ export declare const GOOGLE_PLACE_REVIEWS_AVAILABLE_HEADERS: {
17
17
  export interface GooglePlaceReviewsRouteOptions extends FetchGooglePlaceReviewsOptions {
18
18
  responseInit?: ResponseInit;
19
19
  }
20
- /** Low-level route response for tests and approved custom adapters. */
20
+ /**
21
+ * Low-level route response for tests and approved custom adapters. Each request
22
+ * answered without a Places call (no key or no Place ID) writes one
23
+ * `brandlab_google_reviews_refused` line; each paid lookup writes one
24
+ * `brandlab_google_reviews_lookup` line.
25
+ */
21
26
  export declare function createGooglePlaceReviewsRouteResponse(options: GooglePlaceReviewsRouteOptions): Promise<Response>;
22
27
  export interface BrandlabGooglePlaceReviewsProductionRuntimeOptions {
23
28
  placeId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"google-reviews-server.d.ts","sourceRoot":"","sources":["../src/google-reviews-server.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,EAOL,KAAK,8BAA8B,EACnC,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAEjC,MAAM,4CAA4C,CAAA;AAEnD,eAAO,MAAM,0CAA0C,EACrD,uCAAgD,CAAA;AAElD,eAAO,MAAM,yCAAyC;;CAE5C,CAAA;AAEV,eAAO,MAAM,wCAAwC;;;;CAI3C,CAAA;AAEV,eAAO,MAAM,sCAAsC;;;;CACT,CAAA;AAE1C,MAAM,WAAW,8BAA+B,SAAQ,8BAA8B;IACpF,YAAY,CAAC,EAAE,YAAY,CAAA;CAC5B;AA2CD,uEAAuE;AACvE,wBAAsB,qCAAqC,CACzD,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,QAAQ,CAAC,CAYnB;AAED,MAAM,WAAW,kDAAkD;IACjE,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,sBAAsB,CAAA;IAC5B,qGAAqG;IACrG,SAAS,CAAC,EAAE,2BAA2B,CAAA;IACvC,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IACzD,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAA;CACvB;AAED,MAAM,WAAW,2CAA2C;IAC1D,WAAW,EAAE,OAAO,0CAA0C,CAAA;IAC9D,IAAI,EAAE,sBAAsB,CAAA;IAC5B;;;;;;OAMG;IACH,YAAY,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CACvD;AAoCD;;;;;;GAMG;AACH,wBAAgB,iDAAiD,CAC/D,OAAO,EAAE,kDAAkD,GAC1D,2CAA2C,CA4C7C"}
1
+ {"version":3,"file":"google-reviews-server.d.ts","sourceRoot":"","sources":["../src/google-reviews-server.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,EAOL,KAAK,8BAA8B,EACnC,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAGjC,MAAM,4CAA4C,CAAA;AAEnD,eAAO,MAAM,0CAA0C,EACrD,uCAAgD,CAAA;AAElD,eAAO,MAAM,yCAAyC;;CAE5C,CAAA;AAEV,eAAO,MAAM,wCAAwC;;;;CAI3C,CAAA;AAEV,eAAO,MAAM,sCAAsC;;;;CACT,CAAA;AAE1C,MAAM,WAAW,8BAA+B,SAAQ,8BAA8B;IACpF,YAAY,CAAC,EAAE,YAAY,CAAA;CAC5B;AAkGD;;;;;GAKG;AACH,wBAAsB,qCAAqC,CACzD,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,QAAQ,CAAC,CAcnB;AAED,MAAM,WAAW,kDAAkD;IACjE,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,sBAAsB,CAAA;IAC5B,qGAAqG;IACrG,SAAS,CAAC,EAAE,2BAA2B,CAAA;IACvC,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IACzD,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAA;CACvB;AAED,MAAM,WAAW,2CAA2C;IAC1D,WAAW,EAAE,OAAO,0CAA0C,CAAA;IAC9D,IAAI,EAAE,sBAAsB,CAAA;IAC5B;;;;;;OAMG;IACH,YAAY,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CACvD;AAoCD;;;;;;GAMG;AACH,wBAAgB,iDAAiD,CAC/D,OAAO,EAAE,kDAAkD,GAC1D,2CAA2C,CA6C7C"}
@@ -10,6 +10,42 @@ export const GOOGLE_PLACE_REVIEWS_UNAVAILABLE_HEADERS = {
10
10
  'X-Content-Type-Options': 'nosniff',
11
11
  };
12
12
  export const GOOGLE_PLACE_REVIEWS_AVAILABLE_HEADERS = GOOGLE_PLACE_REVIEWS_UNAVAILABLE_HEADERS;
13
+ /** One line per paid Places lookup: the route called Google. */
14
+ const LOOKUP_EVENT = 'brandlab_google_reviews_lookup';
15
+ /** One line per request answered without a Places call, with the reason. */
16
+ const REFUSED_EVENT = 'brandlab_google_reviews_refused';
17
+ /** The only reasons the reviews log writes: anything else is written as `unknown`. */
18
+ const LOGGED_REASONS = new Set([
19
+ 'not-same-origin',
20
+ 'missing-api-key',
21
+ 'missing-place-id',
22
+ 'api-error',
23
+ 'invalid-response',
24
+ ]);
25
+ /**
26
+ * One JSON line on the server's standard output for each paid Places lookup and
27
+ * each request answered without one, for the daily cost monitor (Vercel runtime logs):
28
+ * `{"event":"brandlab_google_reviews_lookup","outcome":"available","mode":"full"}` or
29
+ * `{"event":"brandlab_google_reviews_refused","reason":"not-same-origin","mode":"summary"}`.
30
+ * Only the outcome, a bounded reason and the mode are written: never an address, a user
31
+ * agent, a Place ID, a key or anything from a review.
32
+ */
33
+ function logReviewsLine(line) {
34
+ console.info(JSON.stringify({
35
+ event: line.event,
36
+ ...(line.outcome ? { outcome: line.outcome === 'available' ? 'available' : 'unavailable' } : {}),
37
+ ...(line.reason !== undefined ? { reason: LOGGED_REASONS.has(line.reason) ? line.reason : 'unknown' } : {}),
38
+ mode: line.mode === 'summary' ? 'summary' : 'full',
39
+ }));
40
+ }
41
+ /** The reason no Places call can run for these options (no key or no Place ID), or undefined when one will. */
42
+ function lookupRefusal(options) {
43
+ if (typeof options.apiKey !== 'string' || !options.apiKey.trim())
44
+ return 'missing-api-key';
45
+ if (!normalizeGooglePlaceId(options.placeId))
46
+ return 'missing-place-id';
47
+ return undefined;
48
+ }
13
49
  const inFlightByFetcher = new WeakMap();
14
50
  function coalescingKey(options) {
15
51
  return JSON.stringify([
@@ -19,8 +55,18 @@ function coalescingKey(options) {
19
55
  options.fallbackGoogleMapsUri ?? '',
20
56
  ]);
21
57
  }
58
+ /** Runs once per coalesced request: a paid Places lookup is logged once, however many requests shared it. */
22
59
  async function loadGooglePlaceReviewsSnapshot(options) {
23
- return { snapshot: await fetchGooglePlaceReviewsSnapshot(options) };
60
+ const snapshot = await fetchGooglePlaceReviewsSnapshot(options);
61
+ if (!lookupRefusal(options)) {
62
+ logReviewsLine({
63
+ event: LOOKUP_EVENT,
64
+ outcome: snapshot.status,
65
+ ...(snapshot.status === 'unavailable' ? { reason: snapshot.reason } : {}),
66
+ mode: normalizeGooglePlaceReviewsMode(options.mode),
67
+ });
68
+ }
69
+ return { snapshot };
24
70
  }
25
71
  function getGooglePlaceReviewsRequest(options) {
26
72
  const fetcher = options.fetcher ?? fetch;
@@ -38,8 +84,16 @@ function getGooglePlaceReviewsRequest(options) {
38
84
  requests.set(key, request);
39
85
  return request;
40
86
  }
41
- /** Low-level route response for tests and approved custom adapters. */
87
+ /**
88
+ * Low-level route response for tests and approved custom adapters. Each request
89
+ * answered without a Places call (no key or no Place ID) writes one
90
+ * `brandlab_google_reviews_refused` line; each paid lookup writes one
91
+ * `brandlab_google_reviews_lookup` line.
92
+ */
42
93
  export async function createGooglePlaceReviewsRouteResponse(options) {
94
+ const refusal = lookupRefusal(options);
95
+ if (refusal)
96
+ logReviewsLine({ event: REFUSED_EVENT, reason: refusal, mode: normalizeGooglePlaceReviewsMode(options.mode) });
43
97
  const result = await getGooglePlaceReviewsRequest(options);
44
98
  const headers = new Headers(options.responseInit?.headers);
45
99
  Object.entries(GOOGLE_PLACE_REVIEWS_AVAILABLE_HEADERS).forEach(([name, value]) => {
@@ -108,6 +162,7 @@ export function createBrandlabGooglePlaceReviewsProductionRuntime(options) {
108
162
  routeHandler: async (request) => {
109
163
  // Without a key no lookup can run, and the route answers as before (missing-api-key).
110
164
  if (apiKey && !(await requestedByOwnPage(request))) {
165
+ logReviewsLine({ event: REFUSED_EVENT, reason: 'not-same-origin', mode });
111
166
  return Response.json(createGooglePlaceReviewsUnavailableSnapshot('not-same-origin', options.fallbackGoogleMapsUri, mode), { headers: GOOGLE_PLACE_REVIEWS_UNAVAILABLE_HEADERS });
112
167
  }
113
168
  return createGooglePlaceReviewsRouteResponse({
@@ -1 +1 @@
1
- {"version":3,"file":"google-reviews-server.js","sourceRoot":"","sources":["../src/google-reviews-server.ts"],"names":[],"mappings":"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","sourcesContent":["import 'server-only'\n\nimport {\n GOOGLE_PLACE_REVIEWS_MAX_REVIEWS,\n createGooglePlaceReviewsUnavailableSnapshot,\n fetchGooglePlaceReviewsSnapshot,\n normalizeGooglePlaceId,\n normalizeGooglePlaceReviewsMode,\n normalizeGooglePlaceReviewsSelection,\n type FetchGooglePlaceReviewsOptions,\n type GooglePlaceReviewsMode,\n type GooglePlaceReviewsSelection,\n type GooglePlaceReviewsSnapshot,\n} from '@brandlab-digital/site-core/google-reviews'\n\nexport const BRANDLAB_GOOGLE_REVIEWS_PRODUCTION_RUNTIME =\n 'brandlab-google-reviews-production-v2' as const\n\nexport const BRANDLAB_GOOGLE_REVIEWS_ENVIRONMENT_NAMES = {\n apiKey: 'GOOGLE_MAPS_PLATFORM_API_KEY',\n} as const\n\nexport const GOOGLE_PLACE_REVIEWS_UNAVAILABLE_HEADERS = {\n 'Cache-Control': 'private, no-store, max-age=0',\n Pragma: 'no-cache',\n 'X-Content-Type-Options': 'nosniff',\n} as const\n\nexport const GOOGLE_PLACE_REVIEWS_AVAILABLE_HEADERS =\n GOOGLE_PLACE_REVIEWS_UNAVAILABLE_HEADERS\n\nexport interface GooglePlaceReviewsRouteOptions extends FetchGooglePlaceReviewsOptions {\n responseInit?: ResponseInit\n}\n\ninterface GooglePlaceReviewsRequestResult {\n snapshot: GooglePlaceReviewsSnapshot\n}\n\nconst inFlightByFetcher = new WeakMap<\n typeof fetch,\n Map<string, Promise<GooglePlaceReviewsRequestResult>>\n>()\n\nfunction coalescingKey(options: GooglePlaceReviewsRouteOptions) {\n return JSON.stringify([\n normalizeGooglePlaceId(options.placeId) ?? '',\n normalizeGooglePlaceReviewsMode(options.mode),\n options.maxReviews ?? null,\n options.fallbackGoogleMapsUri ?? '',\n ])\n}\n\nasync function loadGooglePlaceReviewsSnapshot(\n options: GooglePlaceReviewsRouteOptions,\n): Promise<GooglePlaceReviewsRequestResult> {\n return { snapshot: await fetchGooglePlaceReviewsSnapshot(options) }\n}\n\nfunction getGooglePlaceReviewsRequest(\n options: GooglePlaceReviewsRouteOptions,\n): Promise<GooglePlaceReviewsRequestResult> {\n const fetcher = options.fetcher ?? fetch\n const requests = inFlightByFetcher.get(fetcher) ?? new Map()\n inFlightByFetcher.set(fetcher, requests)\n const key = coalescingKey(options)\n const existing = requests.get(key)\n if (existing) return existing\n const request = loadGooglePlaceReviewsSnapshot(options).finally(() => {\n requests.delete(key)\n if (requests.size === 0) inFlightByFetcher.delete(fetcher)\n })\n requests.set(key, request)\n return request\n}\n\n/** Low-level route response for tests and approved custom adapters. */\nexport async function createGooglePlaceReviewsRouteResponse(\n options: GooglePlaceReviewsRouteOptions,\n): Promise<Response> {\n const result = await getGooglePlaceReviewsRequest(options)\n const headers = new Headers(options.responseInit?.headers)\n Object.entries(GOOGLE_PLACE_REVIEWS_AVAILABLE_HEADERS).forEach(([name, value]) => {\n headers.set(name, value)\n })\n\n return Response.json(result.snapshot, {\n ...options.responseInit,\n status: options.responseInit?.status,\n headers,\n })\n}\n\nexport interface BrandlabGooglePlaceReviewsProductionRuntimeOptions {\n placeId: string\n mode: GooglePlaceReviewsMode\n /** Full-mode presentation policy. Featured mode always requests Google's full five-review window. */\n selection?: GooglePlaceReviewsSelection\n /** Full mode only. Google currently returns at most five reviews. */\n maxReviews?: number\n fallbackGoogleMapsUri?: string\n environment: Readonly<Record<string, string | undefined>>\n /** Test-only transport override. */\n fetcher?: typeof fetch\n}\n\nexport interface BrandlabGooglePlaceReviewsProductionRuntime {\n runtimeKind: typeof BRANDLAB_GOOGLE_REVIEWS_PRODUCTION_RUNTIME\n mode: GooglePlaceReviewsMode\n /**\n * The route's GET. A paid Places lookup runs only for a request from the site's\n * own page (`Sec-Fetch-Site: same-origin`); anything else gets the unavailable\n * snapshot. It reads the request it is given (`export const GET =\n * runtime.routeHandler`, which Next calls with the request), and otherwise Next's\n * request scope (`export function GET() { return runtime.routeHandler() }`).\n */\n routeHandler: (request?: Request) => Promise<Response>\n}\n\n/** The errors Next throws to control rendering (dynamic usage, redirects, not-found, postpone) carry a string digest. */\nfunction isNextControlError(error: unknown) {\n return typeof error === 'object' && error !== null && typeof (error as { digest?: unknown }).digest === 'string'\n}\n\n/**\n * Whether the request comes from one of the site's own pages: every current\n * browser sends `Sec-Fetch-Site: same-origin` on the page's own fetch, while\n * crawlers, curl and scripted direct hits normally send nothing. Anything that\n * cannot be read counts as no.\n */\nasync function requestedByOwnPage(request: Request | undefined): Promise<boolean> {\n const sameOrigin = (value: string | null | undefined) => value?.trim().toLowerCase() === 'same-origin'\n if (request instanceof Request) return sameOrigin(request.headers.get('sec-fetch-site'))\n try {\n const { headers } = await import('next/headers')\n return sameOrigin((await headers()).get('sec-fetch-site'))\n } catch (error) {\n // Next's own control flow (for example a dynamic-usage signal) must reach Next.\n if (isNextControlError(error)) throw error\n return false\n }\n}\n\nfunction optionalEnvironment(\n environment: Readonly<Record<string, string | undefined>>,\n name: string,\n) {\n const value = environment[name]\n if (typeof value !== 'string' || !value.trim()) return undefined\n if (value.length > 8_192 || /[\\u0000-\\u001f\\u007f]/.test(value)) return undefined\n return value.trim()\n}\n\n/**\n * Creates the approved production route. Neither the public response nor the\n * server-only Places response is stored. Concurrent identical requests are\n * coalesced only until the active request completes. With a Places key set, a\n * request that is not from the site's own page answers the unavailable snapshot\n * (`not-same-origin`) without calling Places.\n */\nexport function createBrandlabGooglePlaceReviewsProductionRuntime(\n options: BrandlabGooglePlaceReviewsProductionRuntimeOptions,\n): BrandlabGooglePlaceReviewsProductionRuntime {\n if (options.fetcher && options.environment.NODE_ENV !== 'test') {\n throw new Error('Google reviews transport overrides are available only in test mode.')\n }\n const apiKey = optionalEnvironment(\n options.environment,\n BRANDLAB_GOOGLE_REVIEWS_ENVIRONMENT_NAMES.apiKey,\n )\n const mode = normalizeGooglePlaceReviewsMode(options.mode)\n const selection = normalizeGooglePlaceReviewsSelection(options.selection)\n if (selection === 'five-star-featured' && mode !== 'full') {\n throw new Error('Five-star featured reviews require full mode.')\n }\n\n return {\n runtimeKind: BRANDLAB_GOOGLE_REVIEWS_PRODUCTION_RUNTIME,\n mode,\n // Next calls GET(request, context); a GET written as GET() passes nothing.\n routeHandler: async (request?: Request) => {\n // Without a key no lookup can run, and the route answers as before (missing-api-key).\n if (apiKey && !(await requestedByOwnPage(request))) {\n return Response.json(\n createGooglePlaceReviewsUnavailableSnapshot('not-same-origin', options.fallbackGoogleMapsUri, mode),\n { headers: GOOGLE_PLACE_REVIEWS_UNAVAILABLE_HEADERS },\n )\n }\n return createGooglePlaceReviewsRouteResponse({\n apiKey,\n placeId: options.placeId,\n mode,\n ...(mode === 'full' && (selection === 'five-star-featured' || options.maxReviews !== undefined)\n ? {\n maxReviews: selection === 'five-star-featured'\n ? GOOGLE_PLACE_REVIEWS_MAX_REVIEWS\n : options.maxReviews,\n }\n : {}),\n ...(options.fallbackGoogleMapsUri\n ? { fallbackGoogleMapsUri: options.fallbackGoogleMapsUri }\n : {}),\n fetcher: options.fetcher ?? fetch,\n })\n },\n }\n}\n"]}
1
+ {"version":3,"file":"google-reviews-server.js","sourceRoot":"","sources":["../src/google-reviews-server.ts"],"names":[],"mappings":"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","sourcesContent":["import 'server-only'\n\nimport {\n GOOGLE_PLACE_REVIEWS_MAX_REVIEWS,\n createGooglePlaceReviewsUnavailableSnapshot,\n fetchGooglePlaceReviewsSnapshot,\n normalizeGooglePlaceId,\n normalizeGooglePlaceReviewsMode,\n normalizeGooglePlaceReviewsSelection,\n type FetchGooglePlaceReviewsOptions,\n type GooglePlaceReviewsMode,\n type GooglePlaceReviewsSelection,\n type GooglePlaceReviewsSnapshot,\n type GooglePlaceReviewsUnavailableReason,\n} from '@brandlab-digital/site-core/google-reviews'\n\nexport const BRANDLAB_GOOGLE_REVIEWS_PRODUCTION_RUNTIME =\n 'brandlab-google-reviews-production-v2' as const\n\nexport const BRANDLAB_GOOGLE_REVIEWS_ENVIRONMENT_NAMES = {\n apiKey: 'GOOGLE_MAPS_PLATFORM_API_KEY',\n} as const\n\nexport const GOOGLE_PLACE_REVIEWS_UNAVAILABLE_HEADERS = {\n 'Cache-Control': 'private, no-store, max-age=0',\n Pragma: 'no-cache',\n 'X-Content-Type-Options': 'nosniff',\n} as const\n\nexport const GOOGLE_PLACE_REVIEWS_AVAILABLE_HEADERS =\n GOOGLE_PLACE_REVIEWS_UNAVAILABLE_HEADERS\n\nexport interface GooglePlaceReviewsRouteOptions extends FetchGooglePlaceReviewsOptions {\n responseInit?: ResponseInit\n}\n\ninterface GooglePlaceReviewsRequestResult {\n snapshot: GooglePlaceReviewsSnapshot\n}\n\n/** One line per paid Places lookup: the route called Google. */\nconst LOOKUP_EVENT = 'brandlab_google_reviews_lookup'\n/** One line per request answered without a Places call, with the reason. */\nconst REFUSED_EVENT = 'brandlab_google_reviews_refused'\n\n/** The only reasons the reviews log writes: anything else is written as `unknown`. */\nconst LOGGED_REASONS: ReadonlySet<string> = new Set<GooglePlaceReviewsUnavailableReason>([\n 'not-same-origin',\n 'missing-api-key',\n 'missing-place-id',\n 'api-error',\n 'invalid-response',\n])\n\n/**\n * One JSON line on the server's standard output for each paid Places lookup and\n * each request answered without one, for the daily cost monitor (Vercel runtime logs):\n * `{\"event\":\"brandlab_google_reviews_lookup\",\"outcome\":\"available\",\"mode\":\"full\"}` or\n * `{\"event\":\"brandlab_google_reviews_refused\",\"reason\":\"not-same-origin\",\"mode\":\"summary\"}`.\n * Only the outcome, a bounded reason and the mode are written: never an address, a user\n * agent, a Place ID, a key or anything from a review.\n */\nfunction logReviewsLine(line: {\n event: typeof LOOKUP_EVENT | typeof REFUSED_EVENT\n outcome?: GooglePlaceReviewsSnapshot['status']\n reason?: string\n mode: GooglePlaceReviewsMode\n}) {\n console.info(\n JSON.stringify({\n event: line.event,\n ...(line.outcome ? { outcome: line.outcome === 'available' ? 'available' : 'unavailable' } : {}),\n ...(line.reason !== undefined ? { reason: LOGGED_REASONS.has(line.reason) ? line.reason : 'unknown' } : {}),\n mode: line.mode === 'summary' ? 'summary' : 'full',\n }),\n )\n}\n\n/** The reason no Places call can run for these options (no key or no Place ID), or undefined when one will. */\nfunction lookupRefusal(options: FetchGooglePlaceReviewsOptions): GooglePlaceReviewsUnavailableReason | undefined {\n if (typeof options.apiKey !== 'string' || !options.apiKey.trim()) return 'missing-api-key'\n if (!normalizeGooglePlaceId(options.placeId)) return 'missing-place-id'\n return undefined\n}\n\nconst inFlightByFetcher = new WeakMap<\n typeof fetch,\n Map<string, Promise<GooglePlaceReviewsRequestResult>>\n>()\n\nfunction coalescingKey(options: GooglePlaceReviewsRouteOptions) {\n return JSON.stringify([\n normalizeGooglePlaceId(options.placeId) ?? '',\n normalizeGooglePlaceReviewsMode(options.mode),\n options.maxReviews ?? null,\n options.fallbackGoogleMapsUri ?? '',\n ])\n}\n\n/** Runs once per coalesced request: a paid Places lookup is logged once, however many requests shared it. */\nasync function loadGooglePlaceReviewsSnapshot(\n options: GooglePlaceReviewsRouteOptions,\n): Promise<GooglePlaceReviewsRequestResult> {\n const snapshot = await fetchGooglePlaceReviewsSnapshot(options)\n if (!lookupRefusal(options)) {\n logReviewsLine({\n event: LOOKUP_EVENT,\n outcome: snapshot.status,\n ...(snapshot.status === 'unavailable' ? { reason: snapshot.reason } : {}),\n mode: normalizeGooglePlaceReviewsMode(options.mode),\n })\n }\n return { snapshot }\n}\n\nfunction getGooglePlaceReviewsRequest(\n options: GooglePlaceReviewsRouteOptions,\n): Promise<GooglePlaceReviewsRequestResult> {\n const fetcher = options.fetcher ?? fetch\n const requests = inFlightByFetcher.get(fetcher) ?? new Map()\n inFlightByFetcher.set(fetcher, requests)\n const key = coalescingKey(options)\n const existing = requests.get(key)\n if (existing) return existing\n const request = loadGooglePlaceReviewsSnapshot(options).finally(() => {\n requests.delete(key)\n if (requests.size === 0) inFlightByFetcher.delete(fetcher)\n })\n requests.set(key, request)\n return request\n}\n\n/**\n * Low-level route response for tests and approved custom adapters. Each request\n * answered without a Places call (no key or no Place ID) writes one\n * `brandlab_google_reviews_refused` line; each paid lookup writes one\n * `brandlab_google_reviews_lookup` line.\n */\nexport async function createGooglePlaceReviewsRouteResponse(\n options: GooglePlaceReviewsRouteOptions,\n): Promise<Response> {\n const refusal = lookupRefusal(options)\n if (refusal) logReviewsLine({ event: REFUSED_EVENT, reason: refusal, mode: normalizeGooglePlaceReviewsMode(options.mode) })\n const result = await getGooglePlaceReviewsRequest(options)\n const headers = new Headers(options.responseInit?.headers)\n Object.entries(GOOGLE_PLACE_REVIEWS_AVAILABLE_HEADERS).forEach(([name, value]) => {\n headers.set(name, value)\n })\n\n return Response.json(result.snapshot, {\n ...options.responseInit,\n status: options.responseInit?.status,\n headers,\n })\n}\n\nexport interface BrandlabGooglePlaceReviewsProductionRuntimeOptions {\n placeId: string\n mode: GooglePlaceReviewsMode\n /** Full-mode presentation policy. Featured mode always requests Google's full five-review window. */\n selection?: GooglePlaceReviewsSelection\n /** Full mode only. Google currently returns at most five reviews. */\n maxReviews?: number\n fallbackGoogleMapsUri?: string\n environment: Readonly<Record<string, string | undefined>>\n /** Test-only transport override. */\n fetcher?: typeof fetch\n}\n\nexport interface BrandlabGooglePlaceReviewsProductionRuntime {\n runtimeKind: typeof BRANDLAB_GOOGLE_REVIEWS_PRODUCTION_RUNTIME\n mode: GooglePlaceReviewsMode\n /**\n * The route's GET. A paid Places lookup runs only for a request from the site's\n * own page (`Sec-Fetch-Site: same-origin`); anything else gets the unavailable\n * snapshot. It reads the request it is given (`export const GET =\n * runtime.routeHandler`, which Next calls with the request), and otherwise Next's\n * request scope (`export function GET() { return runtime.routeHandler() }`).\n */\n routeHandler: (request?: Request) => Promise<Response>\n}\n\n/** The errors Next throws to control rendering (dynamic usage, redirects, not-found, postpone) carry a string digest. */\nfunction isNextControlError(error: unknown) {\n return typeof error === 'object' && error !== null && typeof (error as { digest?: unknown }).digest === 'string'\n}\n\n/**\n * Whether the request comes from one of the site's own pages: every current\n * browser sends `Sec-Fetch-Site: same-origin` on the page's own fetch, while\n * crawlers, curl and scripted direct hits normally send nothing. Anything that\n * cannot be read counts as no.\n */\nasync function requestedByOwnPage(request: Request | undefined): Promise<boolean> {\n const sameOrigin = (value: string | null | undefined) => value?.trim().toLowerCase() === 'same-origin'\n if (request instanceof Request) return sameOrigin(request.headers.get('sec-fetch-site'))\n try {\n const { headers } = await import('next/headers')\n return sameOrigin((await headers()).get('sec-fetch-site'))\n } catch (error) {\n // Next's own control flow (for example a dynamic-usage signal) must reach Next.\n if (isNextControlError(error)) throw error\n return false\n }\n}\n\nfunction optionalEnvironment(\n environment: Readonly<Record<string, string | undefined>>,\n name: string,\n) {\n const value = environment[name]\n if (typeof value !== 'string' || !value.trim()) return undefined\n if (value.length > 8_192 || /[\\u0000-\\u001f\\u007f]/.test(value)) return undefined\n return value.trim()\n}\n\n/**\n * Creates the approved production route. Neither the public response nor the\n * server-only Places response is stored. Concurrent identical requests are\n * coalesced only until the active request completes. With a Places key set, a\n * request that is not from the site's own page answers the unavailable snapshot\n * (`not-same-origin`) without calling Places.\n */\nexport function createBrandlabGooglePlaceReviewsProductionRuntime(\n options: BrandlabGooglePlaceReviewsProductionRuntimeOptions,\n): BrandlabGooglePlaceReviewsProductionRuntime {\n if (options.fetcher && options.environment.NODE_ENV !== 'test') {\n throw new Error('Google reviews transport overrides are available only in test mode.')\n }\n const apiKey = optionalEnvironment(\n options.environment,\n BRANDLAB_GOOGLE_REVIEWS_ENVIRONMENT_NAMES.apiKey,\n )\n const mode = normalizeGooglePlaceReviewsMode(options.mode)\n const selection = normalizeGooglePlaceReviewsSelection(options.selection)\n if (selection === 'five-star-featured' && mode !== 'full') {\n throw new Error('Five-star featured reviews require full mode.')\n }\n\n return {\n runtimeKind: BRANDLAB_GOOGLE_REVIEWS_PRODUCTION_RUNTIME,\n mode,\n // Next calls GET(request, context); a GET written as GET() passes nothing.\n routeHandler: async (request?: Request) => {\n // Without a key no lookup can run, and the route answers as before (missing-api-key).\n if (apiKey && !(await requestedByOwnPage(request))) {\n logReviewsLine({ event: REFUSED_EVENT, reason: 'not-same-origin', mode })\n return Response.json(\n createGooglePlaceReviewsUnavailableSnapshot('not-same-origin', options.fallbackGoogleMapsUri, mode),\n { headers: GOOGLE_PLACE_REVIEWS_UNAVAILABLE_HEADERS },\n )\n }\n return createGooglePlaceReviewsRouteResponse({\n apiKey,\n placeId: options.placeId,\n mode,\n ...(mode === 'full' && (selection === 'five-star-featured' || options.maxReviews !== undefined)\n ? {\n maxReviews: selection === 'five-star-featured'\n ? GOOGLE_PLACE_REVIEWS_MAX_REVIEWS\n : options.maxReviews,\n }\n : {}),\n ...(options.fallbackGoogleMapsUri\n ? { fallbackGoogleMapsUri: options.fallbackGoogleMapsUri }\n : {}),\n fetcher: options.fetcher ?? fetch,\n })\n },\n }\n}\n"]}
@@ -29,7 +29,18 @@ export interface BrandlabNextImagesOptions {
29
29
  imageSizes?: readonly number[];
30
30
  qualities?: readonly number[];
31
31
  minimumCacheTTL?: number;
32
+ /**
33
+ * Remote image sources the optimiser may fetch. From 3.3.3 a production build
34
+ * (`next build`, `next start`) refuses an `http` pattern, a pattern without an
35
+ * explicit `https` protocol and a local or private-network host such as `localhost`.
36
+ */
32
37
  remotePatterns?: readonly BrandlabNextRemotePattern[];
38
+ /**
39
+ * Sources for `next dev` only, such as `{ protocol: 'http', hostname: 'localhost' }`
40
+ * (3.3.3). They are added to remotePatterns when the build is not a production
41
+ * build and never reach a deployment.
42
+ */
43
+ developmentRemotePatterns?: readonly BrandlabNextRemotePattern[];
33
44
  }
34
45
  export interface BrandlabNextImagesConfig {
35
46
  formats: 'image/webp'[];
@@ -116,7 +127,8 @@ export declare function brandlabAliasHosts(site: BrandlabSite, aliasHosts: reado
116
127
  * /api/contact, NEXT_PUBLIC_VERCEL_ENV for browser gates, the deployment's
117
128
  * header rules (with immutable caching for the declared static public folders on
118
129
  * production builds) and the redirect table. Validation runs when the config
119
- * loads, so an invalid table, header exception or static folder fails the build.
130
+ * loads, so an invalid table, header exception or static folder fails the build,
131
+ * as does (3.3.3) a production build's http or local image remote pattern.
120
132
  */
121
133
  export declare function defineBrandlabNextConfig(options: BrandlabNextConfigOptions & {
122
134
  mdx?: undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"next-config.d.ts","sourceRoot":"","sources":["../src/next-config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"next-config.d.ts","sourceRoot":"","sources":["../src/next-config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,KAAK,qBAAqB,EAAE,MAAM,uCAAuC,CAAA;AACnG,OAAO,EAGL,KAAK,6BAA6B,EAClC,KAAK,mBAAmB,EACxB,KAAK,YAAY,EAClB,MAAM,kCAAkC,CAAA;AACzC,OAAO,EAGL,KAAK,uCAAuC,EAC5C,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAE3B,MAAM,cAAc,CAAA;AAErB,uFAAuF;AACvF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CACxC,GAAG,CAAC;IAAE,SAAS,EAAE,IAAI,CAAC;IAAC,UAAU,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,UAAU,EAAE,GAAG,CAAC;IAAC,SAAS,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC,CAAA;AAEtF,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,uEAAuE;AACvE,MAAM,WAAW,yBAAyB;IACxC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC/B,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;OAIG;IACH,cAAc,CAAC,EAAE,SAAS,yBAAyB,EAAE,CAAA;IACrD;;;;OAIG;IACH,yBAAyB,CAAC,EAAE,SAAS,yBAAyB,EAAE,CAAA;CACjE;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,cAAc,CAAC,EAAE,yBAAyB,EAAE,CAAA;CAC7C;AAED,MAAM,WAAW,kBAAkB;IACjC,eAAe,EAAE,KAAK,CAAA;IACtB,yEAAyE;IACzE,yBAAyB,EAAE,IAAI,CAAA;IAC/B,GAAG,EAAE;QAAE,sBAAsB,EAAE,6BAA6B,CAAA;KAAE,CAAA;IAC9D,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IACnD,MAAM,EAAE,wBAAwB,CAAA;IAChC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,OAAO,EAAE,MAAM,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAA;IAC5C,SAAS,EAAE,MAAM,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAA;CACjD;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,YAAY,CAAA;IAClB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,yDAAyD;IACzD,SAAS,EAAE,qBAAqB,CAAA;IAChC,MAAM,EAAE,yBAAyB,CAAA;IACjC,uFAAuF;IACvF,GAAG,CAAC,EAAE,uCAAuC,CAAA;IAC7C,IAAI,CAAC,EAAE,qBAAqB,CAAA;IAC5B,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,6FAA6F;IAC7F,YAAY,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;IACxC;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,iEAAiE;IACjE,WAAW,CAAC,EAAE,mBAAmB,CAAA;IACjC,mFAAmF;IACnF,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,mFAAmF;AACnF,eAAO,MAAM,oCAAoC;;EAM/C,CAAA;AA2OF;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE;IAC9C,IAAI,EAAE,YAAY,CAAA;IAClB,SAAS,EAAE,qBAAqB,CAAA;IAChC,wEAAwE;IACxE,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC/B,GAAG,oBAAoB,EAAE,CA4BzB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,SAAS,MAAM,EAAE,CAOnH;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,yBAAyB,GAAG;IAAE,GAAG,CAAC,EAAE,SAAS,CAAA;CAAE,GACvD,kBAAkB,CAAA;AACrB,wBAAgB,wBAAwB,CAAC,OAAO,EAC9C,OAAO,EAAE,yBAAyB,GAAG;IACnC,wFAAwF;IACxF,GAAG,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,OAAO,CAAA;CAC7C,GACA,OAAO,CAAA"}
@@ -26,7 +26,8 @@ const OPTION_KEYS = new Set([
26
26
  'environment',
27
27
  'projectRoot',
28
28
  ]);
29
- const IMAGE_KEYS = new Set(['deviceSizes', 'imageSizes', 'qualities', 'minimumCacheTTL', 'remotePatterns']);
29
+ const IMAGE_KEYS = new Set(['deviceSizes', 'imageSizes', 'qualities', 'minimumCacheTTL', 'remotePatterns', 'developmentRemotePatterns']);
30
+ const REMOTE_PATTERN_KEYS = new Set(['protocol', 'hostname', 'port', 'pathname', 'search']);
30
31
  function assertKnownKeys(value, allowed, field, hint = '') {
31
32
  const unsupported = Object.keys(value).filter((key) => !allowed.has(key));
32
33
  if (unsupported.length > 0) {
@@ -43,7 +44,107 @@ function positiveIntegers(values, field) {
43
44
  }
44
45
  return [...values];
45
46
  }
46
- function buildImagesConfig(images) {
47
+ /**
48
+ * A host no deployment should fetch images from: loopback (`localhost`, `*.localhost`,
49
+ * 127.0.0.0/8, ::1), unspecified (0.0.0.0, ::), private (10/8, 172.16/12, 192.168/16,
50
+ * fc00::/7), shared (100.64/10) or link-local (169.254/16, fe80::/10), in any spelling a URL
51
+ * reads as one of them, or a wildcard host that admits one (`**`, `10.*.*.*`).
52
+ */
53
+ function isLocalImageHost(hostname) {
54
+ const host = hostname.trim().toLowerCase().replace(/^\[(.*)\]$/, '$1').replace(/\.$/, '');
55
+ // Other picomatch syntax (?, braces, classes, extglobs) cannot be judged here, so it is refused.
56
+ if (/[?{}[\]()!@+]/.test(host))
57
+ return true;
58
+ // A wildcard host is local when it admits a local address: Next matches hostnames with picomatch, whose `*` stops
59
+ // only at a slash, so `*` and `**` both admit `localhost`, and `10.*.*.*` admits 10.0.0.1. Any wildcard address
60
+ // (`192.168.1.*`, `fd12:*`) counts as local.
61
+ if (host.includes('*')) {
62
+ if (/^[\d.*]+$/.test(host) || host.includes(':'))
63
+ return true;
64
+ const source = host.split(/(\*\*|\*)/).map((part) => (part === '**' ? '.*' : part === '*' ? '[^/]*' : part.replace(/[.+?^$()|{}[\]\\]/g, '\\$&'))).join('');
65
+ const glob = new RegExp('^' + source + '$');
66
+ return LOCAL_HOST_SAMPLES.some((sample) => glob.test(sample));
67
+ }
68
+ // The address a URL reads it as: 127.1, 0x7f.0.0.1 and [::ffff:127.0.0.1] all name 127.0.0.1.
69
+ let named = host;
70
+ try {
71
+ named = new URL('https://' + (host.includes(':') ? '[' + host + ']' : host)).hostname.replace(/^\[(.*)\]$/, '$1');
72
+ }
73
+ catch {
74
+ // not a host a URL can name: judged as written
75
+ }
76
+ if (named === 'localhost' || named.endsWith('.localhost'))
77
+ return true;
78
+ const hex = named.match(/^(?:0{0,4}:){0,5}:?ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
79
+ const dotted = hex ? [parseInt(hex[1], 16) >> 8, parseInt(hex[1], 16) & 255, parseInt(hex[2], 16) >> 8, parseInt(hex[2], 16) & 255].join('.') : undefined;
80
+ const mapped = named.match(/^(?:0{0,4}:){0,5}:?ffff:(\d{1,3}(?:\.\d{1,3}){3})$/);
81
+ const ipv4 = (dotted ?? mapped?.[1] ?? named).match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
82
+ if (ipv4) {
83
+ const [a, b] = [Number(ipv4[1]), Number(ipv4[2])];
84
+ return a === 0 || a === 10 || a === 127 || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) ||
85
+ (a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127);
86
+ }
87
+ if (!named.includes(':'))
88
+ return false;
89
+ const groups = named.split(':');
90
+ const allZero = (parts) => parts.every((part) => /^0*$/.test(part));
91
+ // :: and ::1 in any spelling (0:0:0:0:0:0:0:1), then the private and link-local prefixes.
92
+ if (allZero(groups) || (allZero(groups.slice(0, -1)) && /^0*1$/.test(groups.at(-1) ?? '')))
93
+ return true;
94
+ return /^f[cd][0-9a-f]{0,2}:/.test(named) || /^fe[89ab][0-9a-f]?:/.test(named);
95
+ }
96
+ /** One address in each local range, to judge a wildcard host by. */
97
+ const LOCAL_HOST_SAMPLES = ['localhost', 'a.localhost', '0.0.0.0', '10.0.0.1', '100.64.0.1', '127.0.0.1', '169.254.169.254', '172.16.0.1', '192.168.0.1', '::1', 'fc00::1', 'fe80::1'];
98
+ function describeRemotePattern(pattern) {
99
+ const path = pattern.pathname === undefined ? '/**' : pattern.pathname.startsWith('/') ? pattern.pathname : `/${pattern.pathname}`;
100
+ return `${pattern.protocol ?? 'any protocol'}://${pattern.hostname}${pattern.port ? `:${pattern.port}` : ''}${path}`;
101
+ }
102
+ /** A validated copy of a remote pattern list: the keys and value types Next reads. */
103
+ function remotePatternList(value, field) {
104
+ if (value === undefined)
105
+ return undefined;
106
+ if (!Array.isArray(value))
107
+ throw new Error(`images.${field} must be an array.`);
108
+ return value.map((pattern, index) => {
109
+ const at = `images.${field}[${index}]`;
110
+ if (!pattern || typeof pattern !== 'object' || Array.isArray(pattern)) {
111
+ throw new Error(`${at} must be a { protocol, hostname, port?, pathname?, search? } pattern.`);
112
+ }
113
+ assertKnownKeys(pattern, REMOTE_PATTERN_KEYS, at);
114
+ const record = pattern;
115
+ if (typeof record.hostname !== 'string' || !record.hostname.trim())
116
+ throw new Error(`${at}.hostname must name a host.`);
117
+ if (record.protocol !== undefined && record.protocol !== 'https' && record.protocol !== 'http') {
118
+ throw new Error(`${at}.protocol must be 'https' ('http' only in images.developmentRemotePatterns).`);
119
+ }
120
+ for (const key of ['port', 'pathname', 'search']) {
121
+ if (record[key] !== undefined && typeof record[key] !== 'string')
122
+ throw new Error(`${at}.${key} must be a string.`);
123
+ }
124
+ return { ...pattern };
125
+ });
126
+ }
127
+ /**
128
+ * A production build fetches remote images only over https from a public host:
129
+ * the optimiser fetches a remote pattern's images on any visitor's request
130
+ * (GHSA-cjq9-62q9-8jv4), and an http or local source is a development convenience.
131
+ */
132
+ function assertProductionRemotePatterns(patterns) {
133
+ patterns.forEach((pattern, index) => {
134
+ const problems = [
135
+ ...(pattern.protocol === 'https' ? [] : [pattern.protocol === 'http' ? 'uses http' : "does not name protocol 'https'"]),
136
+ ...(isLocalImageHost(pattern.hostname)
137
+ ? [/[*?{}[\]()!@+]/.test(pattern.hostname) ? `admits local addresses (${pattern.hostname} can match localhost or a private address)` : `names the local address ${pattern.hostname}`]
138
+ : []),
139
+ ];
140
+ if (problems.length === 0)
141
+ return;
142
+ throw new Error(`images.remotePatterns[${index}] (${describeRemotePattern(pattern)}) ${problems.join(' and ')}. ` +
143
+ 'A production build only fetches remote images over https from a public host. Remove the pattern, ' +
144
+ 'or move a development-only source to images.developmentRemotePatterns, which only next dev uses.');
145
+ });
146
+ }
147
+ function buildImagesConfig(images, productionBuild) {
47
148
  if (!images || typeof images !== 'object' || Array.isArray(images)) {
48
149
  throw new Error('images must declare the site image settings ({} for Next defaults).');
49
150
  }
@@ -58,18 +159,21 @@ function buildImagesConfig(images) {
58
159
  (!Number.isSafeInteger(images.minimumCacheTTL) || images.minimumCacheTTL < 0)) {
59
160
  throw new Error('images.minimumCacheTTL must be a non-negative integer of seconds.');
60
161
  }
61
- if (images.remotePatterns !== undefined && !Array.isArray(images.remotePatterns)) {
62
- throw new Error('images.remotePatterns must be an array.');
63
- }
162
+ const remotePatterns = remotePatternList(images.remotePatterns, 'remotePatterns');
163
+ const developmentRemotePatterns = remotePatternList(images.developmentRemotePatterns, 'developmentRemotePatterns');
164
+ if (productionBuild && remotePatterns)
165
+ assertProductionRemotePatterns(remotePatterns);
166
+ // next dev (and tests) also fetch the development-only sources; a production build never does.
167
+ const served = productionBuild || !developmentRemotePatterns?.length
168
+ ? remotePatterns
169
+ : [...(remotePatterns ?? []), ...developmentRemotePatterns];
64
170
  return {
65
171
  formats: ['image/webp'],
66
172
  ...(deviceSizes ? { deviceSizes } : {}),
67
173
  ...(imageSizes ? { imageSizes } : {}),
68
174
  ...(qualities ? { qualities } : {}),
69
175
  ...(images.minimumCacheTTL !== undefined ? { minimumCacheTTL: images.minimumCacheTTL } : {}),
70
- ...(images.remotePatterns
71
- ? { remotePatterns: images.remotePatterns.map((pattern) => ({ ...pattern })) }
72
- : {}),
176
+ ...(served ? { remotePatterns: served } : {}),
73
177
  };
74
178
  }
75
179
  const PAGE_OR_ROUTE_FILE = /^(?:page|route)\.[A-Za-z0-9]+$/;
@@ -221,7 +325,8 @@ export function defineBrandlabNextConfig(options) {
221
325
  skipTrailingSlashRedirect: true,
222
326
  env: { NEXT_PUBLIC_VERCEL_ENV: deployment.environment },
223
327
  outputFileTracingIncludes: Object.fromEntries(Object.entries(BRANDLAB_CONTACT_OUTPUT_FILE_TRACING).map(([route, globs]) => [route, [...globs]])),
224
- images: buildImagesConfig(options.images),
328
+ // A Vercel build (preview or production) is checked like any production build, whatever NODE_ENV says.
329
+ images: buildImagesConfig(options.images, deployment.productionBuild || deployment.environment !== 'development'),
225
330
  ...(options.mdx ? { pageExtensions: [...MDX_PAGE_EXTENSIONS] } : {}),
226
331
  headers: async () => headerRules.map((rule) => ({
227
332
  source: rule.source,
@@ -1 +1 @@
1
- {"version":3,"file":"next-config.js","sourceRoot":"","sources":["../src/next-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdirSync, statSync } from 'node:fs'\nimport { join, posix } from 'node:path'\nimport { defineRedirects, type BrandlabRedirectTable } from '@brandlab-digital/site-core/redirects'\nimport {\n assertBrandlabAliasHosts,\n resolveBrandlabDeployment,\n type BrandlabDeploymentEnvironment,\n type BrandlabEnvironment,\n type BrandlabSite,\n} from '@brandlab-digital/site-core/site'\nimport {\n buildBrandlabHeaderRules,\n parseBrandlabStaticAssetCache,\n type BrandlabContentSecurityPolicyExtensions,\n type BrandlabHeader,\n type BrandlabHeaderRule,\n type BrandlabHstsExpansion,\n type BrandlabStaticAssetFolder,\n} from './headers.js'\n\n/** Next redirect records: 308 stays `permanent: true`, 301 stays `statusCode: 301`. */\nexport type BrandlabNextRedirect = {\n source: string\n destination: string\n has?: { type: 'host'; value: string }[]\n} & ({ permanent: true; statusCode?: never } | { statusCode: 301; permanent?: never })\n\nexport interface BrandlabNextRemotePattern {\n protocol?: 'http' | 'https'\n hostname: string\n port?: string\n pathname?: string\n search?: string\n}\n\n/** The site's own image settings; the output format is always WebP. */\nexport interface BrandlabNextImagesOptions {\n deviceSizes?: readonly number[]\n imageSizes?: readonly number[]\n qualities?: readonly number[]\n minimumCacheTTL?: number\n remotePatterns?: readonly BrandlabNextRemotePattern[]\n}\n\nexport interface BrandlabNextImagesConfig {\n formats: 'image/webp'[]\n deviceSizes?: number[]\n imageSizes?: number[]\n qualities?: number[]\n minimumCacheTTL?: number\n remotePatterns?: BrandlabNextRemotePattern[]\n}\n\nexport interface BrandlabNextConfig {\n poweredByHeader: false\n /** Trailing slashes are handled by single-hop redirect rules instead. */\n skipTrailingSlashRedirect: true\n env: { NEXT_PUBLIC_VERCEL_ENV: BrandlabDeploymentEnvironment }\n outputFileTracingIncludes: Record<string, string[]>\n images: BrandlabNextImagesConfig\n pageExtensions?: string[]\n headers: () => Promise<BrandlabHeaderRule[]>\n redirects: () => Promise<BrandlabNextRedirect[]>\n}\n\nexport interface BrandlabNextConfigOptions {\n site: BrandlabSite\n /**\n * Hosts that permanently redirect every path to the canonical origin (the\n * `www` host, a Vercel project host, a retired domain). Declared here rather\n * than in the site identity, which client components import, so the list\n * never reaches a browser bundle. Their rules follow any `site.aliasHosts`\n * rules, in order, as the last redirects.\n */\n aliasHosts?: readonly string[]\n /** The src/data/redirects.ts table (defineRedirects). */\n redirects: BrandlabRedirectTable\n images: BrandlabNextImagesOptions\n /** Reviewed CSP origins for declared integrations such as Meta Pixel or map embeds. */\n csp?: BrandlabContentSecurityPolicyExtensions\n hsts?: BrandlabHstsExpansion\n /** Reviewed exception to the default Permissions-Policy that is already live. */\n permissionsPolicy?: string\n /** Reviewed response headers that are already live; protected headers cannot be replaced. */\n extraHeaders?: readonly BrandlabHeader[]\n /**\n * Public folders of static files that never change in place, written as Next\n * sources such as `['/images/:path*', '/fonts/:path*']`. Production builds serve\n * their files (names with an extension) with `public, max-age=31536000, immutable`;\n * Previews keep `private, no-store`. Each folder must exist under `public/`, and\n * no route under `src/app` or `app` may answer below it.\n */\n staticAssetCache?: readonly string[]\n /** Defaults to process.env; tests supply a fixed environment. */\n environment?: BrandlabEnvironment\n /** The project root for the staticAssetCache checks; defaults to process.cwd(). */\n projectRoot?: string\n}\n\n/** Sharp's Linux binaries must travel with the contact route's image sanitiser. */\nexport const BRANDLAB_CONTACT_OUTPUT_FILE_TRACING = Object.freeze({\n '/api/contact': Object.freeze([\n 'node_modules/sharp/**/*',\n 'node_modules/@img/sharp-linux-x64/**/*',\n 'node_modules/@img/sharp-libvips-linux-x64/**/*',\n ]),\n})\n\nconst MDX_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js', 'mdx', 'md']\nconst OPTION_KEYS = new Set([\n 'site',\n 'aliasHosts',\n 'redirects',\n 'images',\n 'csp',\n 'hsts',\n 'permissionsPolicy',\n 'extraHeaders',\n 'staticAssetCache',\n 'mdx',\n 'environment',\n 'projectRoot',\n])\nconst IMAGE_KEYS = new Set(['deviceSizes', 'imageSizes', 'qualities', 'minimumCacheTTL', 'remotePatterns'])\n\nfunction assertKnownKeys(value: object, allowed: ReadonlySet<string>, field: string, hint = '') {\n const unsupported = Object.keys(value).filter((key) => !allowed.has(key))\n if (unsupported.length > 0) {\n throw new Error(`${field} contains unsupported keys: ${unsupported.join(', ')}.${hint}`)\n }\n}\n\nfunction positiveIntegers(values: readonly number[] | undefined, field: string) {\n if (values === undefined) return undefined\n if (\n !Array.isArray(values) ||\n values.length === 0 ||\n values.some((value) => !Number.isSafeInteger(value) || value < 1)\n ) {\n throw new Error(`images.${field} must list positive integers.`)\n }\n return [...values]\n}\n\nfunction buildImagesConfig(images: BrandlabNextImagesOptions): BrandlabNextImagesConfig {\n if (!images || typeof images !== 'object' || Array.isArray(images)) {\n throw new Error('images must declare the site image settings ({} for Next defaults).')\n }\n assertKnownKeys(images, IMAGE_KEYS, 'images', ' The output format is always image/webp.')\n const deviceSizes = positiveIntegers(images.deviceSizes, 'deviceSizes')\n const imageSizes = positiveIntegers(images.imageSizes, 'imageSizes')\n const qualities = positiveIntegers(images.qualities, 'qualities')\n if (qualities?.some((quality) => quality > 100)) {\n throw new Error('images.qualities must be between 1 and 100.')\n }\n if (\n images.minimumCacheTTL !== undefined &&\n (!Number.isSafeInteger(images.minimumCacheTTL) || images.minimumCacheTTL < 0)\n ) {\n throw new Error('images.minimumCacheTTL must be a non-negative integer of seconds.')\n }\n if (images.remotePatterns !== undefined && !Array.isArray(images.remotePatterns)) {\n throw new Error('images.remotePatterns must be an array.')\n }\n return {\n formats: ['image/webp'],\n ...(deviceSizes ? { deviceSizes } : {}),\n ...(imageSizes ? { imageSizes } : {}),\n ...(qualities ? { qualities } : {}),\n ...(images.minimumCacheTTL !== undefined ? { minimumCacheTTL: images.minimumCacheTTL } : {}),\n ...(images.remotePatterns\n ? { remotePatterns: images.remotePatterns.map((pattern) => ({ ...pattern })) }\n : {}),\n }\n}\n\nconst PAGE_OR_ROUTE_FILE = /^(?:page|route)\\.[A-Za-z0-9]+$/\nconst METADATA_ROUTE_FILE = /^(?:opengraph-image|twitter-image|icon|apple-icon|sitemap|robots|manifest)(?:[\\w.-]*)\\.[A-Za-z0-9]+$/\n\nfunction isDirectory(path: string) {\n try {\n return statSync(path).isDirectory()\n } catch {\n return false\n }\n}\n\n/**\n * The first route file under an app directory that can answer a path strictly\n * below `/<folder>/`. `depth` counts the folder segments matched so far: route\n * groups, intercepts and parallel slots add none, a dynamic segment matches any\n * one, and a catch-all reaches every path below the folder.\n */\nfunction routeBelowFolder(\n directory: string,\n folder: readonly string[],\n depth: number,\n relative: string,\n): string | null {\n for (const entry of readdirSync(directory, { withFileTypes: true })) {\n const name = entry.name\n if (name.startsWith('_') || name.startsWith('.') || name === 'node_modules') continue\n const path = join(directory, name)\n const nextRelative = posix.join(relative, name)\n if (entry.isFile()) {\n // Page and route files serve their own directory; metadata files serve a path below it.\n const below = depth > folder.length && PAGE_OR_ROUTE_FILE.test(name)\n const metadata = depth >= folder.length && METADATA_ROUTE_FILE.test(name)\n if (below || metadata) return nextRelative\n continue\n }\n if (!entry.isDirectory()) continue\n let next: number | null = null\n if (name.startsWith('(') || name.startsWith('@')) next = depth\n else if (/^\\[\\[?\\.\\.\\./.test(name)) next = folder.length + 1\n else if (depth >= folder.length || name.startsWith('[') || name === folder[depth]) next = depth + 1\n if (next === null) continue\n const found = routeBelowFolder(path, folder, next, nextRelative)\n if (found) return found\n }\n return null\n}\n\n/** Each declared folder exists under public/ and no app route answers below it. */\nfunction assertStaticAssetFolders(folders: readonly BrandlabStaticAssetFolder[], root: string) {\n for (const { source, folder } of folders) {\n const publicFolder = posix.join('public', ...folder)\n if (!isDirectory(join(root, publicFolder))) {\n throw new Error(`staticAssetCache \"${source}\": ${publicFolder} is not a folder of static files in ${root}.`)\n }\n for (const appDirectory of ['src/app', 'app']) {\n if (!isDirectory(join(root, appDirectory))) continue\n const route = routeBelowFolder(join(root, appDirectory), folder, 0, appDirectory)\n if (route) {\n throw new Error(\n `staticAssetCache \"${source}\": ${route} answers below /${folder.join('/')}, so immutable caching could reach a page or route.`,\n )\n }\n }\n }\n}\n\n/**\n * Converts the redirect table to Next redirects: each entry keeps its 301 or\n * 308 status (Next forwards the query string), gains a single-hop\n * trailing-slash variant, then one trailing-slash rule and one permanent\n * canonical-host rule per alias host follow. There is no x-forwarded-proto\n * rule: Vercel's edge already enforces HTTPS.\n */\nexport function buildBrandlabRedirects(options: {\n site: BrandlabSite\n redirects: BrandlabRedirectTable\n /** next.config's alias hosts, after the site identity's own (3.2.1). */\n aliasHosts?: readonly string[]\n}): BrandlabNextRedirect[] {\n const aliasHosts = brandlabAliasHosts(options.site, options.aliasHosts)\n const table = defineRedirects(options.redirects)\n const entries = table.flatMap((redirect): BrandlabNextRedirect[] => {\n const status = redirect.status === 301\n ? { statusCode: 301 as const }\n : { permanent: true as const }\n const has = redirect.host ? { has: [{ type: 'host' as const, value: redirect.host }] } : {}\n const record = (source: string): BrandlabNextRedirect => ({\n source,\n destination: redirect.destination,\n ...status,\n ...has,\n })\n return redirect.source === '/'\n ? [record(redirect.source)]\n : [record(redirect.source), record(`${redirect.source}/`)]\n })\n return [\n ...entries,\n { source: '/:path+/', destination: '/:path+', permanent: true },\n ...aliasHosts.map((host): BrandlabNextRedirect => ({\n source: '/:path*',\n has: [{ type: 'host', value: host }],\n destination: `${options.site.canonicalUrl}/:path*`,\n permanent: true,\n })),\n ]\n}\n\n/**\n * Every alias host: the site identity's (the 3.0–3.2.0 place) then next.config's\n * (3.2.1), validated together so a host is never declared twice.\n */\nexport function brandlabAliasHosts(site: BrandlabSite, aliasHosts: readonly string[] | undefined): readonly string[] {\n if (aliasHosts === undefined) return site.aliasHosts\n assertBrandlabAliasHosts(aliasHosts, site.canonicalHost ?? new URL(site.canonicalUrl).hostname, 'next.config aliasHosts')\n const all = [...site.aliasHosts, ...aliasHosts]\n const duplicate = all.find((host, index) => all.indexOf(host) !== index)\n if (duplicate) throw new Error(`Alias host \"${duplicate}\" is declared in both the site identity and next.config aliasHosts.`)\n return all\n}\n\n/**\n * The whole next.config.ts after its first line\n * `import '@brandlab-digital/site-next/image-runtime'`:\n * `export default defineBrandlabNextConfig({ site, redirects, images })`.\n *\n * Sets poweredByHeader: false, WebP image output, Sharp output tracing for\n * /api/contact, NEXT_PUBLIC_VERCEL_ENV for browser gates, the deployment's\n * header rules (with immutable caching for the declared static public folders on\n * production builds) and the redirect table. Validation runs when the config\n * loads, so an invalid table, header exception or static folder fails the build.\n */\nexport function defineBrandlabNextConfig(\n options: BrandlabNextConfigOptions & { mdx?: undefined },\n): BrandlabNextConfig\nexport function defineBrandlabNextConfig<Wrapped>(\n options: BrandlabNextConfigOptions & {\n /** The @next/mdx wrapper: createMDX({ options: { remarkPlugins: ['remark-gfm'] } }). */\n mdx: (config: BrandlabNextConfig) => Wrapped\n },\n): Wrapped\nexport function defineBrandlabNextConfig(\n options: BrandlabNextConfigOptions & { mdx?: (config: BrandlabNextConfig) => unknown },\n): unknown {\n if (!options || typeof options !== 'object') {\n throw new TypeError('defineBrandlabNextConfig requires the site, redirects and images.')\n }\n assertKnownKeys(options, OPTION_KEYS, 'next.config options')\n if (!options.site?.canonicalUrl || !Array.isArray(options.site.aliasHosts)) {\n throw new Error('site must come from defineBrandlabSite.')\n }\n if (options.mdx !== undefined && typeof options.mdx !== 'function') {\n throw new TypeError('mdx must be the @next/mdx config wrapper.')\n }\n const deployment = resolveBrandlabDeployment(options.environment ?? process.env)\n if (options.staticAssetCache !== undefined) {\n assertStaticAssetFolders(\n parseBrandlabStaticAssetCache(options.staticAssetCache),\n options.projectRoot ?? process.cwd(),\n )\n }\n const headerRules = buildBrandlabHeaderRules({\n deployment,\n canonicalHost: options.site.canonicalHost ?? new URL(options.site.canonicalUrl).hostname,\n ...(options.csp ? { contentSecurityPolicyExtensions: options.csp } : {}),\n ...(options.hsts ? { hsts: options.hsts } : {}),\n ...(options.permissionsPolicy ? { permissionsPolicy: options.permissionsPolicy } : {}),\n ...(options.extraHeaders ? { extraHeaders: options.extraHeaders } : {}),\n ...(options.staticAssetCache ? { staticAssetCache: options.staticAssetCache } : {}),\n })\n const redirects = buildBrandlabRedirects({\n site: options.site,\n redirects: options.redirects,\n ...(options.aliasHosts !== undefined ? { aliasHosts: options.aliasHosts } : {}),\n })\n\n const config: BrandlabNextConfig = {\n poweredByHeader: false,\n skipTrailingSlashRedirect: true,\n env: { NEXT_PUBLIC_VERCEL_ENV: deployment.environment },\n outputFileTracingIncludes: Object.fromEntries(\n Object.entries(BRANDLAB_CONTACT_OUTPUT_FILE_TRACING).map(([route, globs]) => [route, [...globs]]),\n ),\n images: buildImagesConfig(options.images),\n ...(options.mdx ? { pageExtensions: [...MDX_PAGE_EXTENSIONS] } : {}),\n headers: async () =>\n headerRules.map((rule) => ({\n source: rule.source,\n ...(rule.missing ? { missing: rule.missing.map((condition) => ({ ...condition })) } : {}),\n headers: rule.headers.map((header) => ({ ...header })),\n })),\n redirects: async () =>\n redirects.map((redirect) => ({\n ...redirect,\n ...(redirect.has ? { has: redirect.has.map((condition) => ({ ...condition })) } : {}),\n })),\n }\n return options.mdx ? options.mdx(config) : config\n}\n"]}
1
+ {"version":3,"file":"next-config.js","sourceRoot":"","sources":["../src/next-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdirSync, statSync } from 'node:fs'\nimport { join, posix } from 'node:path'\nimport { defineRedirects, type BrandlabRedirectTable } from '@brandlab-digital/site-core/redirects'\nimport {\n assertBrandlabAliasHosts,\n resolveBrandlabDeployment,\n type BrandlabDeploymentEnvironment,\n type BrandlabEnvironment,\n type BrandlabSite,\n} from '@brandlab-digital/site-core/site'\nimport {\n buildBrandlabHeaderRules,\n parseBrandlabStaticAssetCache,\n type BrandlabContentSecurityPolicyExtensions,\n type BrandlabHeader,\n type BrandlabHeaderRule,\n type BrandlabHstsExpansion,\n type BrandlabStaticAssetFolder,\n} from './headers.js'\n\n/** Next redirect records: 308 stays `permanent: true`, 301 stays `statusCode: 301`. */\nexport type BrandlabNextRedirect = {\n source: string\n destination: string\n has?: { type: 'host'; value: string }[]\n} & ({ permanent: true; statusCode?: never } | { statusCode: 301; permanent?: never })\n\nexport interface BrandlabNextRemotePattern {\n protocol?: 'http' | 'https'\n hostname: string\n port?: string\n pathname?: string\n search?: string\n}\n\n/** The site's own image settings; the output format is always WebP. */\nexport interface BrandlabNextImagesOptions {\n deviceSizes?: readonly number[]\n imageSizes?: readonly number[]\n qualities?: readonly number[]\n minimumCacheTTL?: number\n /**\n * Remote image sources the optimiser may fetch. From 3.3.3 a production build\n * (`next build`, `next start`) refuses an `http` pattern, a pattern without an\n * explicit `https` protocol and a local or private-network host such as `localhost`.\n */\n remotePatterns?: readonly BrandlabNextRemotePattern[]\n /**\n * Sources for `next dev` only, such as `{ protocol: 'http', hostname: 'localhost' }`\n * (3.3.3). They are added to remotePatterns when the build is not a production\n * build and never reach a deployment.\n */\n developmentRemotePatterns?: readonly BrandlabNextRemotePattern[]\n}\n\nexport interface BrandlabNextImagesConfig {\n formats: 'image/webp'[]\n deviceSizes?: number[]\n imageSizes?: number[]\n qualities?: number[]\n minimumCacheTTL?: number\n remotePatterns?: BrandlabNextRemotePattern[]\n}\n\nexport interface BrandlabNextConfig {\n poweredByHeader: false\n /** Trailing slashes are handled by single-hop redirect rules instead. */\n skipTrailingSlashRedirect: true\n env: { NEXT_PUBLIC_VERCEL_ENV: BrandlabDeploymentEnvironment }\n outputFileTracingIncludes: Record<string, string[]>\n images: BrandlabNextImagesConfig\n pageExtensions?: string[]\n headers: () => Promise<BrandlabHeaderRule[]>\n redirects: () => Promise<BrandlabNextRedirect[]>\n}\n\nexport interface BrandlabNextConfigOptions {\n site: BrandlabSite\n /**\n * Hosts that permanently redirect every path to the canonical origin (the\n * `www` host, a Vercel project host, a retired domain). Declared here rather\n * than in the site identity, which client components import, so the list\n * never reaches a browser bundle. Their rules follow any `site.aliasHosts`\n * rules, in order, as the last redirects.\n */\n aliasHosts?: readonly string[]\n /** The src/data/redirects.ts table (defineRedirects). */\n redirects: BrandlabRedirectTable\n images: BrandlabNextImagesOptions\n /** Reviewed CSP origins for declared integrations such as Meta Pixel or map embeds. */\n csp?: BrandlabContentSecurityPolicyExtensions\n hsts?: BrandlabHstsExpansion\n /** Reviewed exception to the default Permissions-Policy that is already live. */\n permissionsPolicy?: string\n /** Reviewed response headers that are already live; protected headers cannot be replaced. */\n extraHeaders?: readonly BrandlabHeader[]\n /**\n * Public folders of static files that never change in place, written as Next\n * sources such as `['/images/:path*', '/fonts/:path*']`. Production builds serve\n * their files (names with an extension) with `public, max-age=31536000, immutable`;\n * Previews keep `private, no-store`. Each folder must exist under `public/`, and\n * no route under `src/app` or `app` may answer below it.\n */\n staticAssetCache?: readonly string[]\n /** Defaults to process.env; tests supply a fixed environment. */\n environment?: BrandlabEnvironment\n /** The project root for the staticAssetCache checks; defaults to process.cwd(). */\n projectRoot?: string\n}\n\n/** Sharp's Linux binaries must travel with the contact route's image sanitiser. */\nexport const BRANDLAB_CONTACT_OUTPUT_FILE_TRACING = Object.freeze({\n '/api/contact': Object.freeze([\n 'node_modules/sharp/**/*',\n 'node_modules/@img/sharp-linux-x64/**/*',\n 'node_modules/@img/sharp-libvips-linux-x64/**/*',\n ]),\n})\n\nconst MDX_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js', 'mdx', 'md']\nconst OPTION_KEYS = new Set([\n 'site',\n 'aliasHosts',\n 'redirects',\n 'images',\n 'csp',\n 'hsts',\n 'permissionsPolicy',\n 'extraHeaders',\n 'staticAssetCache',\n 'mdx',\n 'environment',\n 'projectRoot',\n])\nconst IMAGE_KEYS = new Set(['deviceSizes', 'imageSizes', 'qualities', 'minimumCacheTTL', 'remotePatterns', 'developmentRemotePatterns'])\nconst REMOTE_PATTERN_KEYS = new Set(['protocol', 'hostname', 'port', 'pathname', 'search'])\n\nfunction assertKnownKeys(value: object, allowed: ReadonlySet<string>, field: string, hint = '') {\n const unsupported = Object.keys(value).filter((key) => !allowed.has(key))\n if (unsupported.length > 0) {\n throw new Error(`${field} contains unsupported keys: ${unsupported.join(', ')}.${hint}`)\n }\n}\n\nfunction positiveIntegers(values: readonly number[] | undefined, field: string) {\n if (values === undefined) return undefined\n if (\n !Array.isArray(values) ||\n values.length === 0 ||\n values.some((value) => !Number.isSafeInteger(value) || value < 1)\n ) {\n throw new Error(`images.${field} must list positive integers.`)\n }\n return [...values]\n}\n\n/**\n * A host no deployment should fetch images from: loopback (`localhost`, `*.localhost`,\n * 127.0.0.0/8, ::1), unspecified (0.0.0.0, ::), private (10/8, 172.16/12, 192.168/16,\n * fc00::/7), shared (100.64/10) or link-local (169.254/16, fe80::/10), in any spelling a URL\n * reads as one of them, or a wildcard host that admits one (`**`, `10.*.*.*`).\n */\nfunction isLocalImageHost(hostname: string): boolean {\n const host = hostname.trim().toLowerCase().replace(/^\\[(.*)\\]$/, '$1').replace(/\\.$/, '')\n // Other picomatch syntax (?, braces, classes, extglobs) cannot be judged here, so it is refused.\n if (/[?{}[\\]()!@+]/.test(host)) return true\n // A wildcard host is local when it admits a local address: Next matches hostnames with picomatch, whose `*` stops\n // only at a slash, so `*` and `**` both admit `localhost`, and `10.*.*.*` admits 10.0.0.1. Any wildcard address\n // (`192.168.1.*`, `fd12:*`) counts as local.\n if (host.includes('*')) {\n if (/^[\\d.*]+$/.test(host) || host.includes(':')) return true\n const source = host.split(/(\\*\\*|\\*)/).map((part) => (part === '**' ? '.*' : part === '*' ? '[^/]*' : part.replace(/[.+?^$()|{}[\\]\\\\]/g, '\\\\$&'))).join('')\n const glob = new RegExp('^' + source + '$')\n return LOCAL_HOST_SAMPLES.some((sample) => glob.test(sample))\n }\n // The address a URL reads it as: 127.1, 0x7f.0.0.1 and [::ffff:127.0.0.1] all name 127.0.0.1.\n let named = host\n try {\n named = new URL('https://' + (host.includes(':') ? '[' + host + ']' : host)).hostname.replace(/^\\[(.*)\\]$/, '$1')\n } catch {\n // not a host a URL can name: judged as written\n }\n if (named === 'localhost' || named.endsWith('.localhost')) return true\n const hex = named.match(/^(?:0{0,4}:){0,5}:?ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/)\n const dotted = hex ? [parseInt(hex[1]!, 16) >> 8, parseInt(hex[1]!, 16) & 255, parseInt(hex[2]!, 16) >> 8, parseInt(hex[2]!, 16) & 255].join('.') : undefined\n const mapped = named.match(/^(?:0{0,4}:){0,5}:?ffff:(\\d{1,3}(?:\\.\\d{1,3}){3})$/)\n const ipv4 = (dotted ?? mapped?.[1] ?? named).match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/)\n if (ipv4) {\n const [a, b] = [Number(ipv4[1]), Number(ipv4[2])]\n return a === 0 || a === 10 || a === 127 || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) ||\n (a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127)\n }\n if (!named.includes(':')) return false\n const groups = named.split(':')\n const allZero = (parts: string[]) => parts.every((part) => /^0*$/.test(part))\n // :: and ::1 in any spelling (0:0:0:0:0:0:0:1), then the private and link-local prefixes.\n if (allZero(groups) || (allZero(groups.slice(0, -1)) && /^0*1$/.test(groups.at(-1) ?? ''))) return true\n return /^f[cd][0-9a-f]{0,2}:/.test(named) || /^fe[89ab][0-9a-f]?:/.test(named)\n}\n\n/** One address in each local range, to judge a wildcard host by. */\nconst LOCAL_HOST_SAMPLES = ['localhost', 'a.localhost', '0.0.0.0', '10.0.0.1', '100.64.0.1', '127.0.0.1', '169.254.169.254', '172.16.0.1', '192.168.0.1', '::1', 'fc00::1', 'fe80::1']\n\nfunction describeRemotePattern(pattern: BrandlabNextRemotePattern): string {\n const path = pattern.pathname === undefined ? '/**' : pattern.pathname.startsWith('/') ? pattern.pathname : `/${pattern.pathname}`\n return `${pattern.protocol ?? 'any protocol'}://${pattern.hostname}${pattern.port ? `:${pattern.port}` : ''}${path}`\n}\n\n/** A validated copy of a remote pattern list: the keys and value types Next reads. */\nfunction remotePatternList(value: unknown, field: string): BrandlabNextRemotePattern[] | undefined {\n if (value === undefined) return undefined\n if (!Array.isArray(value)) throw new Error(`images.${field} must be an array.`)\n return value.map((pattern: unknown, index) => {\n const at = `images.${field}[${index}]`\n if (!pattern || typeof pattern !== 'object' || Array.isArray(pattern)) {\n throw new Error(`${at} must be a { protocol, hostname, port?, pathname?, search? } pattern.`)\n }\n assertKnownKeys(pattern, REMOTE_PATTERN_KEYS, at)\n const record = pattern as Record<string, unknown>\n if (typeof record.hostname !== 'string' || !record.hostname.trim()) throw new Error(`${at}.hostname must name a host.`)\n if (record.protocol !== undefined && record.protocol !== 'https' && record.protocol !== 'http') {\n throw new Error(`${at}.protocol must be 'https' ('http' only in images.developmentRemotePatterns).`)\n }\n for (const key of ['port', 'pathname', 'search'] as const) {\n if (record[key] !== undefined && typeof record[key] !== 'string') throw new Error(`${at}.${key} must be a string.`)\n }\n return { ...(pattern as BrandlabNextRemotePattern) }\n })\n}\n\n/**\n * A production build fetches remote images only over https from a public host:\n * the optimiser fetches a remote pattern's images on any visitor's request\n * (GHSA-cjq9-62q9-8jv4), and an http or local source is a development convenience.\n */\nfunction assertProductionRemotePatterns(patterns: readonly BrandlabNextRemotePattern[]) {\n patterns.forEach((pattern, index) => {\n const problems = [\n ...(pattern.protocol === 'https' ? [] : [pattern.protocol === 'http' ? 'uses http' : \"does not name protocol 'https'\"]),\n ...(isLocalImageHost(pattern.hostname)\n ? [/[*?{}[\\]()!@+]/.test(pattern.hostname) ? `admits local addresses (${pattern.hostname} can match localhost or a private address)` : `names the local address ${pattern.hostname}`]\n : []),\n ]\n if (problems.length === 0) return\n throw new Error(\n `images.remotePatterns[${index}] (${describeRemotePattern(pattern)}) ${problems.join(' and ')}. ` +\n 'A production build only fetches remote images over https from a public host. Remove the pattern, ' +\n 'or move a development-only source to images.developmentRemotePatterns, which only next dev uses.',\n )\n })\n}\n\nfunction buildImagesConfig(images: BrandlabNextImagesOptions, productionBuild: boolean): BrandlabNextImagesConfig {\n if (!images || typeof images !== 'object' || Array.isArray(images)) {\n throw new Error('images must declare the site image settings ({} for Next defaults).')\n }\n assertKnownKeys(images, IMAGE_KEYS, 'images', ' The output format is always image/webp.')\n const deviceSizes = positiveIntegers(images.deviceSizes, 'deviceSizes')\n const imageSizes = positiveIntegers(images.imageSizes, 'imageSizes')\n const qualities = positiveIntegers(images.qualities, 'qualities')\n if (qualities?.some((quality) => quality > 100)) {\n throw new Error('images.qualities must be between 1 and 100.')\n }\n if (\n images.minimumCacheTTL !== undefined &&\n (!Number.isSafeInteger(images.minimumCacheTTL) || images.minimumCacheTTL < 0)\n ) {\n throw new Error('images.minimumCacheTTL must be a non-negative integer of seconds.')\n }\n const remotePatterns = remotePatternList(images.remotePatterns, 'remotePatterns')\n const developmentRemotePatterns = remotePatternList(images.developmentRemotePatterns, 'developmentRemotePatterns')\n if (productionBuild && remotePatterns) assertProductionRemotePatterns(remotePatterns)\n // next dev (and tests) also fetch the development-only sources; a production build never does.\n const served = productionBuild || !developmentRemotePatterns?.length\n ? remotePatterns\n : [...(remotePatterns ?? []), ...developmentRemotePatterns]\n return {\n formats: ['image/webp'],\n ...(deviceSizes ? { deviceSizes } : {}),\n ...(imageSizes ? { imageSizes } : {}),\n ...(qualities ? { qualities } : {}),\n ...(images.minimumCacheTTL !== undefined ? { minimumCacheTTL: images.minimumCacheTTL } : {}),\n ...(served ? { remotePatterns: served } : {}),\n }\n}\n\nconst PAGE_OR_ROUTE_FILE = /^(?:page|route)\\.[A-Za-z0-9]+$/\nconst METADATA_ROUTE_FILE = /^(?:opengraph-image|twitter-image|icon|apple-icon|sitemap|robots|manifest)(?:[\\w.-]*)\\.[A-Za-z0-9]+$/\n\nfunction isDirectory(path: string) {\n try {\n return statSync(path).isDirectory()\n } catch {\n return false\n }\n}\n\n/**\n * The first route file under an app directory that can answer a path strictly\n * below `/<folder>/`. `depth` counts the folder segments matched so far: route\n * groups, intercepts and parallel slots add none, a dynamic segment matches any\n * one, and a catch-all reaches every path below the folder.\n */\nfunction routeBelowFolder(\n directory: string,\n folder: readonly string[],\n depth: number,\n relative: string,\n): string | null {\n for (const entry of readdirSync(directory, { withFileTypes: true })) {\n const name = entry.name\n if (name.startsWith('_') || name.startsWith('.') || name === 'node_modules') continue\n const path = join(directory, name)\n const nextRelative = posix.join(relative, name)\n if (entry.isFile()) {\n // Page and route files serve their own directory; metadata files serve a path below it.\n const below = depth > folder.length && PAGE_OR_ROUTE_FILE.test(name)\n const metadata = depth >= folder.length && METADATA_ROUTE_FILE.test(name)\n if (below || metadata) return nextRelative\n continue\n }\n if (!entry.isDirectory()) continue\n let next: number | null = null\n if (name.startsWith('(') || name.startsWith('@')) next = depth\n else if (/^\\[\\[?\\.\\.\\./.test(name)) next = folder.length + 1\n else if (depth >= folder.length || name.startsWith('[') || name === folder[depth]) next = depth + 1\n if (next === null) continue\n const found = routeBelowFolder(path, folder, next, nextRelative)\n if (found) return found\n }\n return null\n}\n\n/** Each declared folder exists under public/ and no app route answers below it. */\nfunction assertStaticAssetFolders(folders: readonly BrandlabStaticAssetFolder[], root: string) {\n for (const { source, folder } of folders) {\n const publicFolder = posix.join('public', ...folder)\n if (!isDirectory(join(root, publicFolder))) {\n throw new Error(`staticAssetCache \"${source}\": ${publicFolder} is not a folder of static files in ${root}.`)\n }\n for (const appDirectory of ['src/app', 'app']) {\n if (!isDirectory(join(root, appDirectory))) continue\n const route = routeBelowFolder(join(root, appDirectory), folder, 0, appDirectory)\n if (route) {\n throw new Error(\n `staticAssetCache \"${source}\": ${route} answers below /${folder.join('/')}, so immutable caching could reach a page or route.`,\n )\n }\n }\n }\n}\n\n/**\n * Converts the redirect table to Next redirects: each entry keeps its 301 or\n * 308 status (Next forwards the query string), gains a single-hop\n * trailing-slash variant, then one trailing-slash rule and one permanent\n * canonical-host rule per alias host follow. There is no x-forwarded-proto\n * rule: Vercel's edge already enforces HTTPS.\n */\nexport function buildBrandlabRedirects(options: {\n site: BrandlabSite\n redirects: BrandlabRedirectTable\n /** next.config's alias hosts, after the site identity's own (3.2.1). */\n aliasHosts?: readonly string[]\n}): BrandlabNextRedirect[] {\n const aliasHosts = brandlabAliasHosts(options.site, options.aliasHosts)\n const table = defineRedirects(options.redirects)\n const entries = table.flatMap((redirect): BrandlabNextRedirect[] => {\n const status = redirect.status === 301\n ? { statusCode: 301 as const }\n : { permanent: true as const }\n const has = redirect.host ? { has: [{ type: 'host' as const, value: redirect.host }] } : {}\n const record = (source: string): BrandlabNextRedirect => ({\n source,\n destination: redirect.destination,\n ...status,\n ...has,\n })\n return redirect.source === '/'\n ? [record(redirect.source)]\n : [record(redirect.source), record(`${redirect.source}/`)]\n })\n return [\n ...entries,\n { source: '/:path+/', destination: '/:path+', permanent: true },\n ...aliasHosts.map((host): BrandlabNextRedirect => ({\n source: '/:path*',\n has: [{ type: 'host', value: host }],\n destination: `${options.site.canonicalUrl}/:path*`,\n permanent: true,\n })),\n ]\n}\n\n/**\n * Every alias host: the site identity's (the 3.0–3.2.0 place) then next.config's\n * (3.2.1), validated together so a host is never declared twice.\n */\nexport function brandlabAliasHosts(site: BrandlabSite, aliasHosts: readonly string[] | undefined): readonly string[] {\n if (aliasHosts === undefined) return site.aliasHosts\n assertBrandlabAliasHosts(aliasHosts, site.canonicalHost ?? new URL(site.canonicalUrl).hostname, 'next.config aliasHosts')\n const all = [...site.aliasHosts, ...aliasHosts]\n const duplicate = all.find((host, index) => all.indexOf(host) !== index)\n if (duplicate) throw new Error(`Alias host \"${duplicate}\" is declared in both the site identity and next.config aliasHosts.`)\n return all\n}\n\n/**\n * The whole next.config.ts after its first line\n * `import '@brandlab-digital/site-next/image-runtime'`:\n * `export default defineBrandlabNextConfig({ site, redirects, images })`.\n *\n * Sets poweredByHeader: false, WebP image output, Sharp output tracing for\n * /api/contact, NEXT_PUBLIC_VERCEL_ENV for browser gates, the deployment's\n * header rules (with immutable caching for the declared static public folders on\n * production builds) and the redirect table. Validation runs when the config\n * loads, so an invalid table, header exception or static folder fails the build,\n * as does (3.3.3) a production build's http or local image remote pattern.\n */\nexport function defineBrandlabNextConfig(\n options: BrandlabNextConfigOptions & { mdx?: undefined },\n): BrandlabNextConfig\nexport function defineBrandlabNextConfig<Wrapped>(\n options: BrandlabNextConfigOptions & {\n /** The @next/mdx wrapper: createMDX({ options: { remarkPlugins: ['remark-gfm'] } }). */\n mdx: (config: BrandlabNextConfig) => Wrapped\n },\n): Wrapped\nexport function defineBrandlabNextConfig(\n options: BrandlabNextConfigOptions & { mdx?: (config: BrandlabNextConfig) => unknown },\n): unknown {\n if (!options || typeof options !== 'object') {\n throw new TypeError('defineBrandlabNextConfig requires the site, redirects and images.')\n }\n assertKnownKeys(options, OPTION_KEYS, 'next.config options')\n if (!options.site?.canonicalUrl || !Array.isArray(options.site.aliasHosts)) {\n throw new Error('site must come from defineBrandlabSite.')\n }\n if (options.mdx !== undefined && typeof options.mdx !== 'function') {\n throw new TypeError('mdx must be the @next/mdx config wrapper.')\n }\n const deployment = resolveBrandlabDeployment(options.environment ?? process.env)\n if (options.staticAssetCache !== undefined) {\n assertStaticAssetFolders(\n parseBrandlabStaticAssetCache(options.staticAssetCache),\n options.projectRoot ?? process.cwd(),\n )\n }\n const headerRules = buildBrandlabHeaderRules({\n deployment,\n canonicalHost: options.site.canonicalHost ?? new URL(options.site.canonicalUrl).hostname,\n ...(options.csp ? { contentSecurityPolicyExtensions: options.csp } : {}),\n ...(options.hsts ? { hsts: options.hsts } : {}),\n ...(options.permissionsPolicy ? { permissionsPolicy: options.permissionsPolicy } : {}),\n ...(options.extraHeaders ? { extraHeaders: options.extraHeaders } : {}),\n ...(options.staticAssetCache ? { staticAssetCache: options.staticAssetCache } : {}),\n })\n const redirects = buildBrandlabRedirects({\n site: options.site,\n redirects: options.redirects,\n ...(options.aliasHosts !== undefined ? { aliasHosts: options.aliasHosts } : {}),\n })\n\n const config: BrandlabNextConfig = {\n poweredByHeader: false,\n skipTrailingSlashRedirect: true,\n env: { NEXT_PUBLIC_VERCEL_ENV: deployment.environment },\n outputFileTracingIncludes: Object.fromEntries(\n Object.entries(BRANDLAB_CONTACT_OUTPUT_FILE_TRACING).map(([route, globs]) => [route, [...globs]]),\n ),\n // A Vercel build (preview or production) is checked like any production build, whatever NODE_ENV says.\n images: buildImagesConfig(options.images, deployment.productionBuild || deployment.environment !== 'development'),\n ...(options.mdx ? { pageExtensions: [...MDX_PAGE_EXTENSIONS] } : {}),\n headers: async () =>\n headerRules.map((rule) => ({\n source: rule.source,\n ...(rule.missing ? { missing: rule.missing.map((condition) => ({ ...condition })) } : {}),\n headers: rule.headers.map((header) => ({ ...header })),\n })),\n redirects: async () =>\n redirects.map((redirect) => ({\n ...redirect,\n ...(redirect.has ? { has: redirect.has.map((condition) => ({ ...condition })) } : {}),\n })),\n }\n return options.mdx ? options.mdx(config) : config\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandlab-digital/site-next",
3
- "version": "3.3.2",
3
+ "version": "3.3.3",
4
4
  "license": "UNLICENSED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,7 +24,7 @@
24
24
  "typecheck": "tsc --noEmit -p tsconfig.json"
25
25
  },
26
26
  "dependencies": {
27
- "@brandlab-digital/site-core": "3.3.2",
27
+ "@brandlab-digital/site-core": "3.3.3",
28
28
  "browser-image-compression": "2.0.2",
29
29
  "pdf-lib": "1.17.1",
30
30
  "server-only": "0.0.1",
@@ -65,6 +65,7 @@
65
65
  },
66
66
  "./contact/server": {
67
67
  "types": "./dist/contact/server-entry.d.ts",
68
+ "browser": null,
68
69
  "import": "./dist/contact/server-entry.js"
69
70
  },
70
71
  "./contact/testing": {
@@ -127,6 +128,7 @@
127
128
  },
128
129
  "./google-reviews/server": {
129
130
  "types": "./dist/google-reviews-server.d.ts",
131
+ "browser": null,
130
132
  "import": "./dist/google-reviews-server.js"
131
133
  },
132
134
  "./google-reviews/client": {