@shware/http 3.1.0 → 3.2.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/index.cjs CHANGED
@@ -7,6 +7,7 @@ const require_response = require("./response.cjs");
7
7
  const require_snowflake = require("./snowflake.cjs");
8
8
  const require_max_length_index = require("./max-length/index.cjs");
9
9
  const require_utils_timing = require("./utils/timing.cjs");
10
+ const require_utils_geolocation = require("./utils/geolocation.cjs");
10
11
  const require_utils_google_tag_gateway = require("./utils/google-tag-gateway.cjs");
11
12
  const require_iso_iso_3601_1 = require("./iso/iso-3601-1.cjs");
12
13
  exports.CheckoutCreateError = require_error_index.CheckoutCreateError;
@@ -32,6 +33,7 @@ exports.forwardToGoogleTagGateway = require_utils_google_tag_gateway.forwardToGo
32
33
  exports.getErrorInfo = require_error_parse.getErrorInfo;
33
34
  exports.getErrorMessage = require_error_parse.getErrorMessage;
34
35
  exports.getFieldViolations = require_error_parse.getFieldViolations;
36
+ exports.getGeolocation = require_utils_geolocation.getGeolocation;
35
37
  exports.getNextPageParam = require_response.getNextPageParam;
36
38
  exports.getPreviousPageParam = require_response.getPreviousPageParam;
37
39
  exports.initialPageParam = require_response.initialPageParam;
package/dist/index.d.cts CHANGED
@@ -3,10 +3,11 @@ import { BadRequest, DebugInfo, Detail, DetailType, Details, ErrorInfo, Help, Lo
3
3
  import { CheckoutCreateError, LoginCanceledError, LoginTimeoutError, PurchaseError } from "./error/index.cjs";
4
4
  import { getErrorInfo, getErrorMessage, getFieldViolations } from "./error/parse.cjs";
5
5
  import { ErrorBody, Status, StatusError } from "./error/status.cjs";
6
+ import { getGeolocation } from "./utils/geolocation.cjs";
6
7
  import { Cursor, Empty, Entity, EntityId, InfinitePageData, InitParams, Items, NextParams, Page, PageParams, Pages, ParentPageParams, PrevParams, Response, getNextPageParam, getPreviousPageParam, initialPageParam, pageParamsSchema } from "./response.cjs";
7
8
  import { UidGenerator, uid } from "./snowflake.cjs";
8
9
  import { index_d_exports } from "./max-length/index.cjs";
9
10
  import { timing } from "./utils/timing.cjs";
10
11
  import { forwardToGoogleTagGateway } from "./utils/google-tag-gateway.cjs";
11
12
  import { ISO3166CountryCode, ISO_3601_1 } from "./iso/iso-3601-1.cjs";
12
- export { type BadRequest, CheckoutCreateError, Cursor, type DebugInfo, type Detail, DetailType, Details, type Empty, type Entity, type EntityId, type ErrorBody, type ErrorInfo, type ErrorReason, type Help, type ISO3166CountryCode, ISO_3601_1, type InfinitePageData, type InitParams, Items, type LocalizedMessage, LoginCanceledError, LoginTimeoutError, index_d_exports as MAX_LENGTH, type NextParams, type Page, type PageParams, Pages, type ParentPageParams, type PreconditionFailure, type PrevParams, PurchaseError, type QuotaFailure, type RequestInfo, type ResolvedErrorReason, type ResourceInfo, type Response, type RetryInfo, Status, StatusError, UidGenerator, forwardToGoogleTagGateway, getErrorInfo, getErrorMessage, getFieldViolations, getNextPageParam, getPreviousPageParam, initialPageParam, pageParamsSchema, timing, uid };
13
+ export { type BadRequest, CheckoutCreateError, Cursor, type DebugInfo, type Detail, DetailType, Details, type Empty, type Entity, type EntityId, type ErrorBody, type ErrorInfo, type ErrorReason, type Help, type ISO3166CountryCode, ISO_3601_1, type InfinitePageData, type InitParams, Items, type LocalizedMessage, LoginCanceledError, LoginTimeoutError, index_d_exports as MAX_LENGTH, type NextParams, type Page, type PageParams, Pages, type ParentPageParams, type PreconditionFailure, type PrevParams, PurchaseError, type QuotaFailure, type RequestInfo, type ResolvedErrorReason, type ResourceInfo, type Response, type RetryInfo, Status, StatusError, UidGenerator, forwardToGoogleTagGateway, getErrorInfo, getErrorMessage, getFieldViolations, getGeolocation, getNextPageParam, getPreviousPageParam, initialPageParam, pageParamsSchema, timing, uid };
package/dist/index.d.mts CHANGED
@@ -3,10 +3,11 @@ import { BadRequest, DebugInfo, Detail, DetailType, Details, ErrorInfo, Help, Lo
3
3
  import { CheckoutCreateError, LoginCanceledError, LoginTimeoutError, PurchaseError } from "./error/index.mjs";
4
4
  import { getErrorInfo, getErrorMessage, getFieldViolations } from "./error/parse.mjs";
5
5
  import { ErrorBody, Status, StatusError } from "./error/status.mjs";
6
+ import { getGeolocation } from "./utils/geolocation.mjs";
6
7
  import { Cursor, Empty, Entity, EntityId, InfinitePageData, InitParams, Items, NextParams, Page, PageParams, Pages, ParentPageParams, PrevParams, Response, getNextPageParam, getPreviousPageParam, initialPageParam, pageParamsSchema } from "./response.mjs";
7
8
  import { UidGenerator, uid } from "./snowflake.mjs";
8
9
  import { index_d_exports } from "./max-length/index.mjs";
9
10
  import { timing } from "./utils/timing.mjs";
10
11
  import { forwardToGoogleTagGateway } from "./utils/google-tag-gateway.mjs";
11
12
  import { ISO3166CountryCode, ISO_3601_1 } from "./iso/iso-3601-1.mjs";
12
- export { type BadRequest, CheckoutCreateError, Cursor, type DebugInfo, type Detail, DetailType, Details, type Empty, type Entity, type EntityId, type ErrorBody, type ErrorInfo, type ErrorReason, type Help, type ISO3166CountryCode, ISO_3601_1, type InfinitePageData, type InitParams, Items, type LocalizedMessage, LoginCanceledError, LoginTimeoutError, index_d_exports as MAX_LENGTH, type NextParams, type Page, type PageParams, Pages, type ParentPageParams, type PreconditionFailure, type PrevParams, PurchaseError, type QuotaFailure, type RequestInfo, type ResolvedErrorReason, type ResourceInfo, type Response, type RetryInfo, Status, StatusError, UidGenerator, forwardToGoogleTagGateway, getErrorInfo, getErrorMessage, getFieldViolations, getNextPageParam, getPreviousPageParam, initialPageParam, pageParamsSchema, timing, uid };
13
+ export { type BadRequest, CheckoutCreateError, Cursor, type DebugInfo, type Detail, DetailType, Details, type Empty, type Entity, type EntityId, type ErrorBody, type ErrorInfo, type ErrorReason, type Help, type ISO3166CountryCode, ISO_3601_1, type InfinitePageData, type InitParams, Items, type LocalizedMessage, LoginCanceledError, LoginTimeoutError, index_d_exports as MAX_LENGTH, type NextParams, type Page, type PageParams, Pages, type ParentPageParams, type PreconditionFailure, type PrevParams, PurchaseError, type QuotaFailure, type RequestInfo, type ResolvedErrorReason, type ResourceInfo, type Response, type RetryInfo, Status, StatusError, UidGenerator, forwardToGoogleTagGateway, getErrorInfo, getErrorMessage, getFieldViolations, getGeolocation, getNextPageParam, getPreviousPageParam, initialPageParam, pageParamsSchema, timing, uid };
package/dist/index.mjs CHANGED
@@ -6,6 +6,7 @@ import { Cursor, Items, Pages, getNextPageParam, getPreviousPageParam, initialPa
6
6
  import { UidGenerator, uid } from "./snowflake.mjs";
7
7
  import { max_length_exports } from "./max-length/index.mjs";
8
8
  import { timing } from "./utils/timing.mjs";
9
+ import { getGeolocation } from "./utils/geolocation.mjs";
9
10
  import { forwardToGoogleTagGateway } from "./utils/google-tag-gateway.mjs";
10
11
  import { ISO_3601_1 } from "./iso/iso-3601-1.mjs";
11
- export { CheckoutCreateError, Cursor, DetailType, Details, ISO_3601_1, Items, LoginCanceledError, LoginTimeoutError, max_length_exports as MAX_LENGTH, Pages, PurchaseError, Status, StatusError, UidGenerator, forwardToGoogleTagGateway, getErrorInfo, getErrorMessage, getFieldViolations, getNextPageParam, getPreviousPageParam, initialPageParam, pageParamsSchema, timing, uid };
12
+ export { CheckoutCreateError, Cursor, DetailType, Details, ISO_3601_1, Items, LoginCanceledError, LoginTimeoutError, max_length_exports as MAX_LENGTH, Pages, PurchaseError, Status, StatusError, UidGenerator, forwardToGoogleTagGateway, getErrorInfo, getErrorMessage, getFieldViolations, getGeolocation, getNextPageParam, getPreviousPageParam, initialPageParam, pageParamsSchema, timing, uid };
@@ -11,18 +11,21 @@ async function forwardToGoogleTagGateway(request, gaId) {
11
11
  const { pathname, search } = new URL(request.url);
12
12
  const target = `https://${GATEWAY_HOST}${pathname}${search}`;
13
13
  const headers = new Headers();
14
- headers.set("host", GATEWAY_HOST);
15
- const cookie = request.headers.get("cookie");
16
- if (cookie) headers.set("cookie", cookie);
17
- const { country, region } = require_utils_geolocation.getGeolocation(request);
14
+ for (const [name, value] of request.headers) if (name === "cookie" || name === "user-agent" || name === "accept-language" || name === "referer" || name === "x-forwarded-for" || name.startsWith("sec-ch-")) headers.set(name, value);
15
+ const { country, region, city, latitude, longitude, ip_address } = require_utils_geolocation.getGeolocation(request);
16
+ if (ip_address && !headers.has("x-forwarded-for")) headers.set("x-forwarded-for", ip_address);
18
17
  if (country && region) headers.set("x-forwarded-countryregion", `${country}-${region}`);
19
18
  else if (country) headers.set("x-forwarded-country", country);
20
- else if (region) headers.set("x-forwarded-region", region);
21
- const body = request.method !== "GET" && request.method !== "HEAD" ? await request.arrayBuffer() : void 0;
19
+ const geolocation = [latitude !== void 0 && longitude !== void 0 && `latlong=${latitude},${longitude}`, city && `city=${city}`].filter(Boolean);
20
+ if (geolocation.length > 0) headers.set("x-forwarded-geolocation", geolocation.join(";"));
21
+ const hasBody = request.method !== "GET" && request.method !== "HEAD";
22
22
  const response = await fetch(target, {
23
23
  method: request.method,
24
24
  headers,
25
- body
25
+ body: hasBody ? request.body : void 0,
26
+ redirect: "manual",
27
+ cache: "no-store",
28
+ ...hasBody && { duplex: "half" }
26
29
  });
27
30
  const responseHeaders = new Headers(response.headers);
28
31
  responseHeaders.delete("content-encoding");
@@ -1 +1 @@
1
- {"version":3,"file":"google-tag-gateway.cjs","names":["getGeolocation"],"sources":["../../src/utils/google-tag-gateway.ts"],"sourcesContent":["import { getGeolocation } from './geolocation';\n\n/**\n * In a browser, verify the setup by navigating to: https://example.com/metrics/healthy. The page\n * should read ok. Verify that geographical information is being included by navigating to:\n * https://example.com/metrics/?validate_geo=healthy. The page should read ok.\n */\nexport async function forwardToGoogleTagGateway(request: Request, gaId: string) {\n const GATEWAY_HOST = `${gaId}.fps.goog`;\n const { pathname, search } = new URL(request.url);\n\n const target = `https://${GATEWAY_HOST}${pathname}${search}`;\n\n const headers = new Headers();\n headers.set('host', GATEWAY_HOST);\n\n // Forward cookies\n const cookie = request.headers.get('cookie');\n if (cookie) headers.set('cookie', cookie);\n\n // Convert Vercel geo headers to Google Tag Gateway format\n // https://developers.google.com/tag-platform/tag-manager/gateway/setup-guide\n const { country, region } = getGeolocation(request);\n\n if (country && region) {\n headers.set('x-forwarded-countryregion', `${country}-${region}`);\n } else if (country) {\n headers.set('x-forwarded-country', country);\n } else if (region) {\n headers.set('x-forwarded-region', region);\n }\n\n const hasBody = request.method !== 'GET' && request.method !== 'HEAD';\n\n // Buffer the body instead of streaming it. The conversion endpoint\n // (g/measurement/conversion) answers POSTs with a 302 to www.google.com, and we want\n // to follow that hop on the server so the browser never sees a Google domain (the whole\n // point of the first-party gateway). Following a redirect requires re-issuing the\n // request, which undici cannot do with a one-shot `request.body` stream — it throws\n // \"fetch failed\", surfacing as a 500. A buffered body is replayable, so the redirect\n // is followed transparently here.\n const body = hasBody ? await request.arrayBuffer() : undefined;\n\n const response = await fetch(target, {\n method: request.method,\n headers,\n body,\n });\n\n // Strip content-encoding/content-length because fetch() auto-decompresses\n // but keeps the original headers, causing ERR_CONTENT_DECODING_FAILED\n const responseHeaders = new Headers(response.headers);\n responseHeaders.delete('content-encoding');\n responseHeaders.delete('content-length');\n\n return new Response(response.body, { status: response.status, headers: responseHeaders });\n}\n"],"mappings":";;;;;;;;AAOA,eAAsB,0BAA0B,SAAkB,MAAc;CAC9E,MAAM,eAAe,GAAG,KAAK;CAC7B,MAAM,EAAE,UAAU,WAAW,IAAI,IAAI,QAAQ,GAAG;CAEhD,MAAM,SAAS,WAAW,eAAe,WAAW;CAEpD,MAAM,UAAU,IAAI,QAAQ;CAC5B,QAAQ,IAAI,QAAQ,YAAY;CAGhC,MAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ;CAC3C,IAAI,QAAQ,QAAQ,IAAI,UAAU,MAAM;CAIxC,MAAM,EAAE,SAAS,WAAWA,0BAAAA,eAAe,OAAO;CAElD,IAAI,WAAW,QACb,QAAQ,IAAI,6BAA6B,GAAG,QAAQ,GAAG,QAAQ;MAC1D,IAAI,SACT,QAAQ,IAAI,uBAAuB,OAAO;MACrC,IAAI,QACT,QAAQ,IAAI,sBAAsB,MAAM;CAY1C,MAAM,OATU,QAAQ,WAAW,SAAS,QAAQ,WAAW,SASxC,MAAM,QAAQ,YAAY,IAAI,KAAA;CAErD,MAAM,WAAW,MAAM,MAAM,QAAQ;EACnC,QAAQ,QAAQ;EAChB;EACA;CACF,CAAC;CAID,MAAM,kBAAkB,IAAI,QAAQ,SAAS,OAAO;CACpD,gBAAgB,OAAO,kBAAkB;CACzC,gBAAgB,OAAO,gBAAgB;CAEvC,OAAO,IAAI,SAAS,SAAS,MAAM;EAAE,QAAQ,SAAS;EAAQ,SAAS;CAAgB,CAAC;AAC1F"}
1
+ {"version":3,"file":"google-tag-gateway.cjs","names":["getGeolocation"],"sources":["../../src/utils/google-tag-gateway.ts"],"sourcesContent":["import { getGeolocation } from './geolocation';\n\n/**\n * In a browser, verify the setup by navigating to: https://example.com/metrics/healthy. The page\n * should read ok. Verify that geographical information is being included by navigating to:\n * https://example.com/metrics/?validate_geo=healthy. The page should read ok.\n */\nexport async function forwardToGoogleTagGateway(request: Request, gaId: string) {\n const GATEWAY_HOST = `${gaId}.fps.goog`;\n const { pathname, search } = new URL(request.url);\n\n const target = `https://${GATEWAY_HOST}${pathname}${search}`;\n\n const headers = new Headers();\n\n // Forward the browser-identifying headers a load balancer would pass through:\n // cookies for first-party measurement, user-agent + client hints so GA doesn't\n // classify hits as bot traffic and the gateway can serve UA-appropriate scripts.\n for (const [name, value] of request.headers) {\n if (\n name === 'cookie' ||\n name === 'user-agent' ||\n name === 'accept-language' ||\n name === 'referer' ||\n name === 'x-forwarded-for' ||\n name.startsWith('sec-ch-')\n ) {\n headers.set(name, value);\n }\n }\n\n // Convert geo headers to Google Tag Gateway format\n // https://developers.google.com/tag-platform/tag-manager/gateway/setup-guide\n const { country, region, city, latitude, longitude, ip_address } = getGeolocation(request);\n\n // A CDN in front (Vercel/Cloudflare/CloudFront) normally sets x-forwarded-for already;\n // fall back to the platform-specific client-IP header if it was stripped.\n if (ip_address && !headers.has('x-forwarded-for')) {\n headers.set('x-forwarded-for', ip_address);\n }\n\n if (country && region) {\n headers.set('x-forwarded-countryregion', `${country}-${region}`);\n } else if (country) {\n headers.set('x-forwarded-country', country);\n }\n\n // City-level geolocation, format per the official Fastly/Google Cloud configs:\n // latlong=<lat>,<lng>;city=<city>\n const geolocation = [\n latitude !== undefined && longitude !== undefined && `latlong=${latitude},${longitude}`,\n city && `city=${city}`,\n ].filter(Boolean);\n\n if (geolocation.length > 0) {\n headers.set('x-forwarded-geolocation', geolocation.join(';'));\n }\n\n const hasBody = request.method !== 'GET' && request.method !== 'HEAD';\n\n // The conversion endpoint (g/measurement/conversion) answers POSTs with a 302 to\n // www.google.com. Pass that redirect through to the browser instead of following it\n // here: the hop only carries signal when the browser makes it with its own google.com\n // cookies (Google Signals / cross-domain conversion linking). Not following redirects\n // also means the one-shot body stream never needs to be replayed, so it can be\n // forwarded as-is.\n const response = await fetch(target, {\n method: request.method,\n headers,\n body: hasBody ? request.body : undefined,\n redirect: 'manual',\n // Opt out of Next.js fetch caching (the official CloudFront config likewise\n // requires CachingDisabled): measurement hits must never be served from cache.\n cache: 'no-store',\n ...(hasBody && { duplex: 'half' as const }),\n });\n\n // Strip content-encoding/content-length because fetch() auto-decompresses\n // but keeps the original headers, causing ERR_CONTENT_DECODING_FAILED\n const responseHeaders = new Headers(response.headers);\n responseHeaders.delete('content-encoding');\n responseHeaders.delete('content-length');\n\n return new Response(response.body, { status: response.status, headers: responseHeaders });\n}\n"],"mappings":";;;;;;;;AAOA,eAAsB,0BAA0B,SAAkB,MAAc;CAC9E,MAAM,eAAe,GAAG,KAAK;CAC7B,MAAM,EAAE,UAAU,WAAW,IAAI,IAAI,QAAQ,GAAG;CAEhD,MAAM,SAAS,WAAW,eAAe,WAAW;CAEpD,MAAM,UAAU,IAAI,QAAQ;CAK5B,KAAK,MAAM,CAAC,MAAM,UAAU,QAAQ,SAClC,IACE,SAAS,YACT,SAAS,gBACT,SAAS,qBACT,SAAS,aACT,SAAS,qBACT,KAAK,WAAW,SAAS,GAEzB,QAAQ,IAAI,MAAM,KAAK;CAM3B,MAAM,EAAE,SAAS,QAAQ,MAAM,UAAU,WAAW,eAAeA,0BAAAA,eAAe,OAAO;CAIzF,IAAI,cAAc,CAAC,QAAQ,IAAI,iBAAiB,GAC9C,QAAQ,IAAI,mBAAmB,UAAU;CAG3C,IAAI,WAAW,QACb,QAAQ,IAAI,6BAA6B,GAAG,QAAQ,GAAG,QAAQ;MAC1D,IAAI,SACT,QAAQ,IAAI,uBAAuB,OAAO;CAK5C,MAAM,cAAc,CAClB,aAAa,KAAA,KAAa,cAAc,KAAA,KAAa,WAAW,SAAS,GAAG,aAC5E,QAAQ,QAAQ,MAClB,CAAC,CAAC,OAAO,OAAO;CAEhB,IAAI,YAAY,SAAS,GACvB,QAAQ,IAAI,2BAA2B,YAAY,KAAK,GAAG,CAAC;CAG9D,MAAM,UAAU,QAAQ,WAAW,SAAS,QAAQ,WAAW;CAQ/D,MAAM,WAAW,MAAM,MAAM,QAAQ;EACnC,QAAQ,QAAQ;EAChB;EACA,MAAM,UAAU,QAAQ,OAAO,KAAA;EAC/B,UAAU;EAGV,OAAO;EACP,GAAI,WAAW,EAAE,QAAQ,OAAgB;CAC3C,CAAC;CAID,MAAM,kBAAkB,IAAI,QAAQ,SAAS,OAAO;CACpD,gBAAgB,OAAO,kBAAkB;CACzC,gBAAgB,OAAO,gBAAgB;CAEvC,OAAO,IAAI,SAAS,SAAS,MAAM;EAAE,QAAQ,SAAS;EAAQ,SAAS;CAAgB,CAAC;AAC1F"}
@@ -10,18 +10,21 @@ async function forwardToGoogleTagGateway(request, gaId) {
10
10
  const { pathname, search } = new URL(request.url);
11
11
  const target = `https://${GATEWAY_HOST}${pathname}${search}`;
12
12
  const headers = new Headers();
13
- headers.set("host", GATEWAY_HOST);
14
- const cookie = request.headers.get("cookie");
15
- if (cookie) headers.set("cookie", cookie);
16
- const { country, region } = getGeolocation(request);
13
+ for (const [name, value] of request.headers) if (name === "cookie" || name === "user-agent" || name === "accept-language" || name === "referer" || name === "x-forwarded-for" || name.startsWith("sec-ch-")) headers.set(name, value);
14
+ const { country, region, city, latitude, longitude, ip_address } = getGeolocation(request);
15
+ if (ip_address && !headers.has("x-forwarded-for")) headers.set("x-forwarded-for", ip_address);
17
16
  if (country && region) headers.set("x-forwarded-countryregion", `${country}-${region}`);
18
17
  else if (country) headers.set("x-forwarded-country", country);
19
- else if (region) headers.set("x-forwarded-region", region);
20
- const body = request.method !== "GET" && request.method !== "HEAD" ? await request.arrayBuffer() : void 0;
18
+ const geolocation = [latitude !== void 0 && longitude !== void 0 && `latlong=${latitude},${longitude}`, city && `city=${city}`].filter(Boolean);
19
+ if (geolocation.length > 0) headers.set("x-forwarded-geolocation", geolocation.join(";"));
20
+ const hasBody = request.method !== "GET" && request.method !== "HEAD";
21
21
  const response = await fetch(target, {
22
22
  method: request.method,
23
23
  headers,
24
- body
24
+ body: hasBody ? request.body : void 0,
25
+ redirect: "manual",
26
+ cache: "no-store",
27
+ ...hasBody && { duplex: "half" }
25
28
  });
26
29
  const responseHeaders = new Headers(response.headers);
27
30
  responseHeaders.delete("content-encoding");
@@ -1 +1 @@
1
- {"version":3,"file":"google-tag-gateway.mjs","names":[],"sources":["../../src/utils/google-tag-gateway.ts"],"sourcesContent":["import { getGeolocation } from './geolocation';\n\n/**\n * In a browser, verify the setup by navigating to: https://example.com/metrics/healthy. The page\n * should read ok. Verify that geographical information is being included by navigating to:\n * https://example.com/metrics/?validate_geo=healthy. The page should read ok.\n */\nexport async function forwardToGoogleTagGateway(request: Request, gaId: string) {\n const GATEWAY_HOST = `${gaId}.fps.goog`;\n const { pathname, search } = new URL(request.url);\n\n const target = `https://${GATEWAY_HOST}${pathname}${search}`;\n\n const headers = new Headers();\n headers.set('host', GATEWAY_HOST);\n\n // Forward cookies\n const cookie = request.headers.get('cookie');\n if (cookie) headers.set('cookie', cookie);\n\n // Convert Vercel geo headers to Google Tag Gateway format\n // https://developers.google.com/tag-platform/tag-manager/gateway/setup-guide\n const { country, region } = getGeolocation(request);\n\n if (country && region) {\n headers.set('x-forwarded-countryregion', `${country}-${region}`);\n } else if (country) {\n headers.set('x-forwarded-country', country);\n } else if (region) {\n headers.set('x-forwarded-region', region);\n }\n\n const hasBody = request.method !== 'GET' && request.method !== 'HEAD';\n\n // Buffer the body instead of streaming it. The conversion endpoint\n // (g/measurement/conversion) answers POSTs with a 302 to www.google.com, and we want\n // to follow that hop on the server so the browser never sees a Google domain (the whole\n // point of the first-party gateway). Following a redirect requires re-issuing the\n // request, which undici cannot do with a one-shot `request.body` stream — it throws\n // \"fetch failed\", surfacing as a 500. A buffered body is replayable, so the redirect\n // is followed transparently here.\n const body = hasBody ? await request.arrayBuffer() : undefined;\n\n const response = await fetch(target, {\n method: request.method,\n headers,\n body,\n });\n\n // Strip content-encoding/content-length because fetch() auto-decompresses\n // but keeps the original headers, causing ERR_CONTENT_DECODING_FAILED\n const responseHeaders = new Headers(response.headers);\n responseHeaders.delete('content-encoding');\n responseHeaders.delete('content-length');\n\n return new Response(response.body, { status: response.status, headers: responseHeaders });\n}\n"],"mappings":";;;;;;;AAOA,eAAsB,0BAA0B,SAAkB,MAAc;CAC9E,MAAM,eAAe,GAAG,KAAK;CAC7B,MAAM,EAAE,UAAU,WAAW,IAAI,IAAI,QAAQ,GAAG;CAEhD,MAAM,SAAS,WAAW,eAAe,WAAW;CAEpD,MAAM,UAAU,IAAI,QAAQ;CAC5B,QAAQ,IAAI,QAAQ,YAAY;CAGhC,MAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ;CAC3C,IAAI,QAAQ,QAAQ,IAAI,UAAU,MAAM;CAIxC,MAAM,EAAE,SAAS,WAAW,eAAe,OAAO;CAElD,IAAI,WAAW,QACb,QAAQ,IAAI,6BAA6B,GAAG,QAAQ,GAAG,QAAQ;MAC1D,IAAI,SACT,QAAQ,IAAI,uBAAuB,OAAO;MACrC,IAAI,QACT,QAAQ,IAAI,sBAAsB,MAAM;CAY1C,MAAM,OATU,QAAQ,WAAW,SAAS,QAAQ,WAAW,SASxC,MAAM,QAAQ,YAAY,IAAI,KAAA;CAErD,MAAM,WAAW,MAAM,MAAM,QAAQ;EACnC,QAAQ,QAAQ;EAChB;EACA;CACF,CAAC;CAID,MAAM,kBAAkB,IAAI,QAAQ,SAAS,OAAO;CACpD,gBAAgB,OAAO,kBAAkB;CACzC,gBAAgB,OAAO,gBAAgB;CAEvC,OAAO,IAAI,SAAS,SAAS,MAAM;EAAE,QAAQ,SAAS;EAAQ,SAAS;CAAgB,CAAC;AAC1F"}
1
+ {"version":3,"file":"google-tag-gateway.mjs","names":[],"sources":["../../src/utils/google-tag-gateway.ts"],"sourcesContent":["import { getGeolocation } from './geolocation';\n\n/**\n * In a browser, verify the setup by navigating to: https://example.com/metrics/healthy. The page\n * should read ok. Verify that geographical information is being included by navigating to:\n * https://example.com/metrics/?validate_geo=healthy. The page should read ok.\n */\nexport async function forwardToGoogleTagGateway(request: Request, gaId: string) {\n const GATEWAY_HOST = `${gaId}.fps.goog`;\n const { pathname, search } = new URL(request.url);\n\n const target = `https://${GATEWAY_HOST}${pathname}${search}`;\n\n const headers = new Headers();\n\n // Forward the browser-identifying headers a load balancer would pass through:\n // cookies for first-party measurement, user-agent + client hints so GA doesn't\n // classify hits as bot traffic and the gateway can serve UA-appropriate scripts.\n for (const [name, value] of request.headers) {\n if (\n name === 'cookie' ||\n name === 'user-agent' ||\n name === 'accept-language' ||\n name === 'referer' ||\n name === 'x-forwarded-for' ||\n name.startsWith('sec-ch-')\n ) {\n headers.set(name, value);\n }\n }\n\n // Convert geo headers to Google Tag Gateway format\n // https://developers.google.com/tag-platform/tag-manager/gateway/setup-guide\n const { country, region, city, latitude, longitude, ip_address } = getGeolocation(request);\n\n // A CDN in front (Vercel/Cloudflare/CloudFront) normally sets x-forwarded-for already;\n // fall back to the platform-specific client-IP header if it was stripped.\n if (ip_address && !headers.has('x-forwarded-for')) {\n headers.set('x-forwarded-for', ip_address);\n }\n\n if (country && region) {\n headers.set('x-forwarded-countryregion', `${country}-${region}`);\n } else if (country) {\n headers.set('x-forwarded-country', country);\n }\n\n // City-level geolocation, format per the official Fastly/Google Cloud configs:\n // latlong=<lat>,<lng>;city=<city>\n const geolocation = [\n latitude !== undefined && longitude !== undefined && `latlong=${latitude},${longitude}`,\n city && `city=${city}`,\n ].filter(Boolean);\n\n if (geolocation.length > 0) {\n headers.set('x-forwarded-geolocation', geolocation.join(';'));\n }\n\n const hasBody = request.method !== 'GET' && request.method !== 'HEAD';\n\n // The conversion endpoint (g/measurement/conversion) answers POSTs with a 302 to\n // www.google.com. Pass that redirect through to the browser instead of following it\n // here: the hop only carries signal when the browser makes it with its own google.com\n // cookies (Google Signals / cross-domain conversion linking). Not following redirects\n // also means the one-shot body stream never needs to be replayed, so it can be\n // forwarded as-is.\n const response = await fetch(target, {\n method: request.method,\n headers,\n body: hasBody ? request.body : undefined,\n redirect: 'manual',\n // Opt out of Next.js fetch caching (the official CloudFront config likewise\n // requires CachingDisabled): measurement hits must never be served from cache.\n cache: 'no-store',\n ...(hasBody && { duplex: 'half' as const }),\n });\n\n // Strip content-encoding/content-length because fetch() auto-decompresses\n // but keeps the original headers, causing ERR_CONTENT_DECODING_FAILED\n const responseHeaders = new Headers(response.headers);\n responseHeaders.delete('content-encoding');\n responseHeaders.delete('content-length');\n\n return new Response(response.body, { status: response.status, headers: responseHeaders });\n}\n"],"mappings":";;;;;;;AAOA,eAAsB,0BAA0B,SAAkB,MAAc;CAC9E,MAAM,eAAe,GAAG,KAAK;CAC7B,MAAM,EAAE,UAAU,WAAW,IAAI,IAAI,QAAQ,GAAG;CAEhD,MAAM,SAAS,WAAW,eAAe,WAAW;CAEpD,MAAM,UAAU,IAAI,QAAQ;CAK5B,KAAK,MAAM,CAAC,MAAM,UAAU,QAAQ,SAClC,IACE,SAAS,YACT,SAAS,gBACT,SAAS,qBACT,SAAS,aACT,SAAS,qBACT,KAAK,WAAW,SAAS,GAEzB,QAAQ,IAAI,MAAM,KAAK;CAM3B,MAAM,EAAE,SAAS,QAAQ,MAAM,UAAU,WAAW,eAAe,eAAe,OAAO;CAIzF,IAAI,cAAc,CAAC,QAAQ,IAAI,iBAAiB,GAC9C,QAAQ,IAAI,mBAAmB,UAAU;CAG3C,IAAI,WAAW,QACb,QAAQ,IAAI,6BAA6B,GAAG,QAAQ,GAAG,QAAQ;MAC1D,IAAI,SACT,QAAQ,IAAI,uBAAuB,OAAO;CAK5C,MAAM,cAAc,CAClB,aAAa,KAAA,KAAa,cAAc,KAAA,KAAa,WAAW,SAAS,GAAG,aAC5E,QAAQ,QAAQ,MAClB,CAAC,CAAC,OAAO,OAAO;CAEhB,IAAI,YAAY,SAAS,GACvB,QAAQ,IAAI,2BAA2B,YAAY,KAAK,GAAG,CAAC;CAG9D,MAAM,UAAU,QAAQ,WAAW,SAAS,QAAQ,WAAW;CAQ/D,MAAM,WAAW,MAAM,MAAM,QAAQ;EACnC,QAAQ,QAAQ;EAChB;EACA,MAAM,UAAU,QAAQ,OAAO,KAAA;EAC/B,UAAU;EAGV,OAAO;EACP,GAAI,WAAW,EAAE,QAAQ,OAAgB;CAC3C,CAAC;CAID,MAAM,kBAAkB,IAAI,QAAQ,SAAS,OAAO;CACpD,gBAAgB,OAAO,kBAAkB;CACzC,gBAAgB,OAAO,gBAAgB;CAEvC,OAAO,IAAI,SAAS,SAAS,MAAM;EAAE,QAAQ,SAAS;EAAQ,SAAS;CAAgB,CAAC;AAC1F"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shware/http",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -71,13 +71,13 @@
71
71
  "@shware/utils": "^1.5.2"
72
72
  },
73
73
  "devDependencies": {
74
- "@types/node": "^26.1.1",
75
- "@types/react": "^19.2.17",
74
+ "@types/node": "^26.1.2",
75
+ "@types/react": "^19.2.18",
76
76
  "typescript": "^7.0.2",
77
77
  "@repo/typescript-config": "0.0.0"
78
78
  },
79
79
  "peerDependencies": {
80
- "hono": "^4.12.31",
80
+ "hono": "^4.12.33",
81
81
  "i18next": "^26.3.6",
82
82
  "react": "^19.2.8"
83
83
  },