@astrasyncai/verification-gateway 5.21.2 → 5.22.1

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +19 -7
  3. package/dist/adapters/express.js +60 -11
  4. package/dist/adapters/express.js.map +1 -1
  5. package/dist/adapters/express.mjs +60 -11
  6. package/dist/adapters/express.mjs.map +1 -1
  7. package/dist/adapters/mcp.js +34 -3
  8. package/dist/adapters/mcp.js.map +1 -1
  9. package/dist/adapters/mcp.mjs +34 -3
  10. package/dist/adapters/mcp.mjs.map +1 -1
  11. package/dist/adapters/nextjs.js +60 -11
  12. package/dist/adapters/nextjs.js.map +1 -1
  13. package/dist/adapters/nextjs.mjs +60 -11
  14. package/dist/adapters/nextjs.mjs.map +1 -1
  15. package/dist/adapters/sdk.js +1 -1
  16. package/dist/adapters/sdk.js.map +1 -1
  17. package/dist/adapters/sdk.mjs +1 -1
  18. package/dist/adapters/sdk.mjs.map +1 -1
  19. package/dist/agent/index.js +1 -1
  20. package/dist/agent/index.js.map +1 -1
  21. package/dist/agent/index.mjs +1 -1
  22. package/dist/agent/index.mjs.map +1 -1
  23. package/dist/bin/astrasync-claude-hook.js +1 -1
  24. package/dist/bin/astrasync-codex-hook.js +1 -1
  25. package/dist/bin/astrasync-guard.js +1 -1
  26. package/dist/bin/astrasync.js +1 -1
  27. package/dist/browser/background.js +1 -1
  28. package/dist/browser/background.js.map +1 -1
  29. package/dist/browser/background.mjs +1 -1
  30. package/dist/browser/background.mjs.map +1 -1
  31. package/dist/cli/index.js +1 -1
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/index.mjs +1 -1
  34. package/dist/cli/index.mjs.map +1 -1
  35. package/dist/codex/index.js +1 -1
  36. package/dist/codex/index.js.map +1 -1
  37. package/dist/codex/index.mjs +1 -1
  38. package/dist/codex/index.mjs.map +1 -1
  39. package/dist/cursor/extension.js +1 -1
  40. package/dist/cursor/extension.js.map +1 -1
  41. package/dist/cursor/extension.mjs +1 -1
  42. package/dist/cursor/extension.mjs.map +1 -1
  43. package/dist/edge-config.js +1 -1
  44. package/dist/edge-config.js.map +1 -1
  45. package/dist/edge-config.mjs +1 -1
  46. package/dist/edge-config.mjs.map +1 -1
  47. package/dist/edge-core/index.d.mts +4 -1
  48. package/dist/edge-core/index.d.ts +4 -1
  49. package/dist/edge-core/index.js +45 -15
  50. package/dist/edge-core/index.js.map +1 -1
  51. package/dist/edge-core/index.mjs +45 -15
  52. package/dist/edge-core/index.mjs.map +1 -1
  53. package/dist/gateway/gateway.js +1 -1
  54. package/dist/gateway/gateway.js.map +1 -1
  55. package/dist/gateway/gateway.mjs +1 -1
  56. package/dist/gateway/gateway.mjs.map +1 -1
  57. package/dist/index.d.mts +44 -6
  58. package/dist/index.d.ts +44 -6
  59. package/dist/index.js +71 -14
  60. package/dist/index.js.map +1 -1
  61. package/dist/index.mjs +71 -14
  62. package/dist/index.mjs.map +1 -1
  63. package/dist/metadata-capture.d.mts +5 -3
  64. package/dist/metadata-capture.d.ts +5 -3
  65. package/dist/metadata-capture.js +33 -2
  66. package/dist/metadata-capture.js.map +1 -1
  67. package/dist/metadata-capture.mjs +33 -2
  68. package/dist/metadata-capture.mjs.map +1 -1
  69. package/dist/platform-signatures.d.mts +13 -0
  70. package/dist/platform-signatures.d.ts +13 -0
  71. package/dist/platform-signatures.js +26 -8
  72. package/dist/platform-signatures.js.map +1 -1
  73. package/dist/platform-signatures.mjs +26 -8
  74. package/dist/platform-signatures.mjs.map +1 -1
  75. package/dist/registration/index.js +1 -1
  76. package/dist/registration/index.js.map +1 -1
  77. package/dist/registration/index.mjs +1 -1
  78. package/dist/registration/index.mjs.map +1 -1
  79. package/dist/transport/index.js +1 -1
  80. package/dist/transport/index.js.map +1 -1
  81. package/dist/transport/index.mjs +1 -1
  82. package/dist/transport/index.mjs.map +1 -1
  83. package/dist/verify.js +1 -1
  84. package/dist/verify.js.map +1 -1
  85. package/dist/verify.mjs +1 -1
  86. package/dist/verify.mjs.map +1 -1
  87. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -8,6 +8,26 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
8
8
  Full architectural context for each release lives in the platform spec at
9
9
  `docs/research/adapter-architecture-technical-requirements.md`.
10
10
 
11
+ ## [5.22.1] — 2026-11-02 — pricingContextFrom: provisional by design, opt-in strict; kit header
12
+
13
+ ### Added
14
+
15
+ - `pricingContextFrom(headers, { verification })`: pass the result of your own verification (for example `req.agentVerification` from the Express or Next.js adapter). A result that isn't identity-verified, or that names a different agent, forces `verified: false`.
16
+ - The agent-commerce kit (`createAgentCommerce`) sends `X-AstraSync-Kit: agent-commerce` when it fetches its rules. AstraSync uses it to tell your kit from any other client: only a kit fetch marks your endpoint as applying cardholder prices.
17
+
18
+ ### Documented
19
+
20
+ - `verified` from `pricingContextFrom` is provisional. It decides what an agent is shown (member and cardholder prices), not what it may buy. A bare `x-astra-id` still counts, so your catalogue and your checkout kit price alike. The purchase is verified at confirm, and AstraSync refuses a cardholder-priced checkout for anyone who isn't that programme's verified cardholder. No change is needed to keep today's behaviour.
21
+
22
+ ## [5.22.0] — 2026-10-26 — edge verdicts on beacons, WAF bot labels, Akamai and Imperva headers
23
+
24
+ ### Added
25
+
26
+ - **Edge verdict on beacons.** Edge beacons now carry `edgeDecision` (`allow` | `deny`) and `edgeMode`, decided before the beacon is sent. An enforce-mode deny at the edge is recorded as a deny in Visit Intelligence instead of looking like a pass.
27
+ - **`wafLabelPatterns`** on `PlatformSignatureDef` and **`wafLabels`** on `PlatformDetectionInput`: attribute a request by the bot name a customer's WAF assigned (AWS Bot Control, Akamai Bot Manager, Cloudflare verified-bot category, Imperva client app). Seed patterns are added for Claude, ChatGPT, Gemini, Perplexity, Grok and Meta AI. They only ever match rows read from WAF security logs.
28
+ - **`signature-agent`** is now a platform-signal header, and ChatGPT's Web Bot Auth directory host attributes a signed agent whose user agent is stock Chrome.
29
+ - `deriveConnectionFromHeaders`: `Incap-Client-IP` (Imperva) in the client-IP precedence, and Akamai's `X-Akamai-Edgescape` filling country, region, city, lat/long, time zone, ASN and continent where no direct header supplied them.
30
+
11
31
  ## [5.21.2] — 2026-10-25 — xAI Grok and Meta AI crawler signatures
12
32
 
13
33
  ### Added
package/README.md CHANGED
@@ -1083,15 +1083,27 @@ Both consume the same dashboard-managed policy (`./edge-config`), the same platf
1083
1083
 
1084
1084
  Header-level capture (`ObservedMetadata.headers` etc.) is identical everywhere: **automatic on every verify** for the edge adapters and the `express` / `nextjs` / `mcp` SDK adapters (no config, no opt-out). What differs is the **connection layer** — and the deciding variable is _who operates the CDN in front of your origin_:
1085
1085
 
1086
- | Integration | Connection layer | TLS fingerprint (JA3/JA4) |
1087
- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1088
- | **Edge adapter** (`adapter-lambda`, `adapter-edge`) | **Full** — native platform signals (IP, ASN, geo, TLS, HTTP version, device class, header structure) | CloudFront: **JA3 + JA4** (2.2.0+, via the template's origin request policy). Cloudflare Workers: Bot Management zones only. Vercel: JA4 with BotID. Fastly: none |
1089
- | **SDK behind a CDN you control** (your own Cloudflare / Fastly / Vercel / CloudFront distribution) | **Derived** — geo/ASN/TLS promoted from the CDN's injected headers (`deriveConnectionFromHeaders`) | Cloudflare: JA3 + JA4 (`cf-ja3-hash` / `cf-ja4`, plan-dependent). CloudFront: JA3 + JA4 — add the `CloudFront-Viewer-JA3/JA4-Fingerprint` headers to your origin request policy |
1090
- | **SDK behind a platform-managed CDN** (Railway, Render, Fly, Heroku, …) | **IP only** — platform CDNs forward `x-forwarded-for` but NOT their provider-specific geo/TLS headers to customer origins | None — no fingerprint header reaches your origin |
1091
- | **SDK, no CDN** | Absent (IP from `x-forwarded-for` when a proxy sets it) | None |
1086
+ | Integration | Connection layer | TLS fingerprint (JA3/JA4) |
1087
+ | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1088
+ | **Edge adapter** (`adapter-lambda`, `adapter-edge`) | **Full** — native platform signals (IP, ASN, geo, TLS, HTTP version, device class, header structure) | CloudFront: **JA3 + JA4** (2.2.0+, via the template's origin request policy). Cloudflare Workers: Bot Management zones only. Vercel: JA4 with BotID. Fastly: none |
1089
+ | **SDK behind a CDN you control** (your own Cloudflare / Fastly / Vercel / CloudFront distribution) | **Derived** — geo/ASN/TLS promoted from the CDN's injected headers (`deriveConnectionFromHeaders`) | Cloudflare: JA3 + JA4 (`cf-ja3-hash` / `cf-ja4`, plan-dependent). CloudFront: JA3 + JA4 — add the `CloudFront-Viewer-JA3/JA4-Fingerprint` headers to your origin request policy |
1090
+ | **SDK behind Akamai or Imperva** | **Derived** — IP from `True-Client-IP` / `Incap-Client-IP`; geo/ASN from Akamai's `X-Akamai-Edgescape` when the property adds it | None |
1091
+ | **SDK behind a platform-managed CDN** (Railway, Render, Fly, Heroku, …) | **IP only** — platform CDNs forward `x-forwarded-for` but NOT their provider-specific geo/TLS headers to customer origins | None — no fingerprint header reaches your origin |
1092
+ | **SDK, no CDN** | Absent (IP from `x-forwarded-for` when a proxy sets it) | None |
1092
1093
 
1093
1094
  > "Behind Fastly" ≠ "receives Fastly's headers": platform-managed CDNs are operated by the platform, and they do not forward `Fastly-Client-IP`-style headers to your origin. If connection-layer signals or fingerprints matter to you on a managed platform, deploy the edge adapter in front (bucket 1) — that is the whole point of the TAG.
1094
1095
 
1096
+ > **Blocked at the edge.** Every row above sees only traffic the CDN's own
1097
+ > firewall and bot rules let through: those rules run first. Requests they
1098
+ > block reach no AstraSync code. Connect the CDN or WAF's security logs (an
1099
+ > edge-log connector on the endpoint page) to see them in Visit Intelligence
1100
+ > as "Blocked at your edge", classified from the logs and never
1101
+ > cryptographically verified.
1102
+ >
1103
+ > Edge beacons also carry the gateway's own verdict (`edgeDecision`,
1104
+ > `edgeMode`; 5.22.0), so an enforce-mode deny at the edge is recorded as a
1105
+ > deny.
1106
+
1095
1107
  > **Note:** `runCommercePipeline` (on the `./transport` subpath) is the **server-side** orchestrator of commerce-protocol verification — it runs inside the AstraSync verify-access service. The edge adapters forward raw commerce artifacts to verify-access and never bundle or call the pipeline themselves.
1096
1108
 
1097
1109
  ### Which `EdgeConfig` fields each integration consumes
@@ -1117,7 +1129,7 @@ The ONE sanitiser deciding what is safe to store from an inbound agent's request
1117
1129
  - `sanitizeHeaders(raw)` → `{ headers, apiKeyFormat?, platformHeaders? }` — drops genuine secrets (cookies, signatures, AstraSync credential headers), reduces credential headers to a safe **format prefix** (e.g. `sk-ant-api03` — a platform signal, never the key), keeps everything else verbatim, and enforces size caps (`MAX_HEADERS`, `MAX_HEADER_VALUE_BYTES`, `MAX_TOTAL_BYTES`).
1118
1130
  - `extractApiKeyFormat(value)` — the safe key-format prefix of a credential header value, or `undefined` for unrecognised/opaque tokens.
1119
1131
  - `extractPlatformHeaders(sanitized)` — the platform-signal subset (`openai-organization`, `anthropic-version`, `x-goog-user-project`, …) of an already-sanitised map.
1120
- - `deriveConnectionFromHeaders(headers)` — promotes CDN-injected headers (Cloudflare / Fastly / Vercel / CloudFront geo, client-IP, TLS-fingerprint, and header-structure headers — incl. `cf-ja3-hash` / `cf-ja4` and `cloudfront-viewer-ja3-fingerprint` / `-ja4-fingerprint` from 4.3.0) into the `connection` block, so SDK-only deployments behind a CDN **you control** still get connection-layer attribution (see the capture matrix for the platform-managed-CDN caveat). Values are truncated to `MAX_CONNECTION_VALUE_CHARS` (256 — the platform bound). Native edge signals remain authoritative; this is the SDK-level fallback.
1132
+ - `deriveConnectionFromHeaders(headers)` — promotes CDN-injected headers (Cloudflare / Fastly / Vercel / CloudFront / Akamai EdgeScape geo, client-IP incl. Imperva `incap-client-ip` (5.22.0), TLS-fingerprint, and header-structure headers — incl. `cf-ja3-hash` / `cf-ja4` and `cloudfront-viewer-ja3-fingerprint` / `-ja4-fingerprint` from 4.3.0) into the `connection` block, so SDK-only deployments behind a CDN **you control** still get connection-layer attribution (see the capture matrix for the platform-managed-CDN caveat). Values are truncated to `MAX_CONNECTION_VALUE_CHARS` (256 — the platform bound). Native edge signals remain authoritative; this is the SDK-level fallback.
1121
1133
  - `buildSdkObservedMetadata(rawHeaders)` — the one builder the express / nextjs / mcp adapters share: sanitised headers + `apiKeyFormat` + `platformHeaders` + derived `connection` + `schemaVersion`.
1122
1134
  - `CAPTURE_SCHEMA_VERSION` — the capture-semantics version stamped on every `ObservedMetadata` (currently `1`). Capture semantics are **frozen within a major version**: `schemaVersion` bumps only on a semantic change to what is kept/dropped/derived, never for additive signals. The verify body's `sdkVersion` disambiguates builds.
1123
1135
  - Types: `ObservedMetadata` (`{ headers, apiKeyFormat?, platformHeaders?, connection?, schemaVersion? }` — `connection` is populated natively by edge adapters and via CDN-header derivation by SDK adapters), `SanitizeHeadersResult`.
@@ -60,7 +60,11 @@ var PLATFORM_HEADER_NAMES = /* @__PURE__ */ new Set([
60
60
  "x-goog-api-client",
61
61
  "user-agent",
62
62
  "x-agent-card-url",
63
- "x-agent-provider"
63
+ "x-agent-provider",
64
+ // Round 31 — Web Bot Auth's Signature-Agent names the signing agent's
65
+ // directory host (a public URL, never a secret). It attributes a signed
66
+ // agent even when its UA is stock Chrome.
67
+ "signature-agent"
64
68
  ]);
65
69
  var PLATFORM_HEADER_PREFIXES = [
66
70
  "x-goog-",
@@ -177,7 +181,12 @@ function sanitizeHeaders(raw) {
177
181
  return result;
178
182
  }
179
183
  var CONNECTION_HEADER_SOURCES = [
180
- ["ip", ["cf-connecting-ip", "fastly-client-ip", "true-client-ip", "x-real-ip"]],
184
+ // Round 31: `incap-client-ip` = Imperva Cloud WAF; `true-client-ip` is
185
+ // also Akamai's client-IP header when the property enables it.
186
+ [
187
+ "ip",
188
+ ["cf-connecting-ip", "fastly-client-ip", "incap-client-ip", "true-client-ip", "x-real-ip"]
189
+ ],
181
190
  [
182
191
  "country",
183
192
  ["cf-ipcountry", "x-vercel-ip-country", "cloudfront-viewer-country", "fastly-geo-country"]
@@ -228,6 +237,28 @@ function deriveConnectionFromHeaders(headers) {
228
237
  if (portMatch) connection.sourcePort = portMatch[1];
229
238
  if (!connection.ip && addr) connection.ip = addr.replace(/:\d+$/, "");
230
239
  }
240
+ {
241
+ const edgescape = (lower["x-akamai-edgescape"] ?? lower["akamai-edgescape"])?.trim();
242
+ if (edgescape) {
243
+ const fields = /* @__PURE__ */ new Map();
244
+ for (const pair of edgescape.split(",")) {
245
+ const i = pair.indexOf("=");
246
+ if (i > 0) fields.set(pair.slice(0, i).trim().toLowerCase(), pair.slice(i + 1).trim());
247
+ }
248
+ const fill = (key, field) => {
249
+ const v = fields.get(field);
250
+ if (v && !connection[key]) connection[key] = v.slice(0, MAX_CONNECTION_VALUE_CHARS);
251
+ };
252
+ fill("country", "country_code");
253
+ fill("region", "region_code");
254
+ fill("city", "city");
255
+ fill("latitude", "lat");
256
+ fill("longitude", "long");
257
+ fill("timeZone", "timezone");
258
+ fill("asn", "asnum");
259
+ fill("continent", "continent");
260
+ }
261
+ }
231
262
  if (!connection.ip) {
232
263
  const xff = lower["x-forwarded-for"];
233
264
  const client = xff?.split(",")[0]?.trim();
@@ -479,7 +510,7 @@ function routePolicyOf(route) {
479
510
  }
480
511
 
481
512
  // src/version.ts
482
- var SDK_VERSION = "5.21.2";
513
+ var SDK_VERSION = "5.22.1";
483
514
 
484
515
  // src/http.ts
485
516
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -1900,7 +1931,8 @@ var PLATFORM_AGENT_SIGNATURES = [
1900
1931
  uaPatterns: [/claude/i, /anthropic/i],
1901
1932
  agentCardPatterns: [/anthropic\.com/i, /claude\.ai/i],
1902
1933
  apiKeyFormatPatterns: [/^sk-ant/i],
1903
- headerPatterns: [/^anthropic-version/i, /^anthropic-beta/i, /^x-anthropic-/i]
1934
+ headerPatterns: [/^anthropic-version/i, /^anthropic-beta/i, /^x-anthropic-/i],
1935
+ wafLabelPatterns: [/\bclaude/i, /\banthropic/i]
1904
1936
  },
1905
1937
  {
1906
1938
  vendor: "chatgpt",
@@ -1911,7 +1943,14 @@ var PLATFORM_AGENT_SIGNATURES = [
1911
1943
  uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i, /\bGPTBot\b/i, /\bOAI-SearchBot\b/i],
1912
1944
  agentCardPatterns: [/openai\.com/i, /chatgpt\.com/i],
1913
1945
  apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],
1914
- headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i]
1946
+ headerPatterns: [
1947
+ /^openai-organization/i,
1948
+ /^openai-project/i,
1949
+ /^openai-version/i,
1950
+ // ChatGPT agent signs with Web Bot Auth under this directory host.
1951
+ /^signature-agent: "?https:\/\/chatgpt\.com/i
1952
+ ],
1953
+ wafLabelPatterns: [/\bgptbot\b/i, /\bchatgpt/i, /\boai-searchbot\b/i, /\bopenai/i]
1915
1954
  },
1916
1955
  {
1917
1956
  vendor: "gemini",
@@ -1932,7 +1971,8 @@ var PLATFORM_AGENT_SIGNATURES = [
1932
1971
  ],
1933
1972
  agentCardPatterns: [/gemini\.google\.com/i, /ai\.google\.dev/i],
1934
1973
  apiKeyFormatPatterns: [/^aiza/i],
1935
- headerPatterns: [/^x-goog-user-project/i, /^x-goog-api-client/i]
1974
+ headerPatterns: [/^x-goog-user-project/i, /^x-goog-api-client/i],
1975
+ wafLabelPatterns: [/\bgoogle-extended\b/i, /\bgoogleother\b/i, /\bgemini/i]
1936
1976
  },
1937
1977
  {
1938
1978
  vendor: "cursor",
@@ -1951,7 +1991,8 @@ var PLATFORM_AGENT_SIGNATURES = [
1951
1991
  displayName: "Perplexity",
1952
1992
  uaPatterns: [/perplexity/i],
1953
1993
  agentCardPatterns: [/perplexity\.ai/i],
1954
- apiKeyFormatPatterns: [/^pplx-/i]
1994
+ apiKeyFormatPatterns: [/^pplx-/i],
1995
+ wafLabelPatterns: [/\bperplexity/i]
1955
1996
  },
1956
1997
  // Round 30.1: xAI's documented crawlers (GrokBot/1.0, xAI-Grok/1.0,
1957
1998
  // Grok-DeepSearch/1.0). Anchored, versioned tokens — never in a stock UA.
@@ -1960,7 +2001,8 @@ var PLATFORM_AGENT_SIGNATURES = [
1960
2001
  displayName: "xAI Grok",
1961
2002
  uaPatterns: [/\bGrokBot\//i, /\bxAI-Grok\//i, /\bGrok-DeepSearch\//i],
1962
2003
  agentCardPatterns: [/\bx\.ai\b/i, /grok\.com/i],
1963
- apiKeyFormatPatterns: [/^xai-/i]
2004
+ apiKeyFormatPatterns: [/^xai-/i],
2005
+ wafLabelPatterns: [/\bgrok/i, /\bxai\b/i]
1964
2006
  },
1965
2007
  // Round 30.1: Meta AI's index and fetch crawlers. facebookexternalhit
1966
2008
  // (link previews) is deliberately NOT here — it stays a generic crawler.
@@ -1968,7 +2010,12 @@ var PLATFORM_AGENT_SIGNATURES = [
1968
2010
  vendor: "meta-ai",
1969
2011
  displayName: "Meta AI",
1970
2012
  uaPatterns: [/\bMeta-WebIndexer\b/i, /\bMeta-ExternalFetcher\b/i],
1971
- agentCardPatterns: [/meta\.ai/i]
2013
+ agentCardPatterns: [/meta\.ai/i],
2014
+ wafLabelPatterns: [
2015
+ /\bmeta-webindexer\b/i,
2016
+ /\bmeta-externalfetcher\b/i,
2017
+ /\bmeta-externalagent\b/i
2018
+ ]
1972
2019
  },
1973
2020
  // Agentic browsers — versioned-token patterns ONLY (a stock Chrome UA must
1974
2021
  // never match). Distinct vendor keys (not 'chatgpt'/'perplexity'): the
@@ -2016,6 +2063,7 @@ function matchPlatformSignature(signatures, input) {
2016
2063
  const apiKeyFormat = input.apiKeyFormat ?? "";
2017
2064
  const ja4 = input.ja4 ?? "";
2018
2065
  const asn = input.asn ?? "";
2066
+ const wafLabels = input.wafLabels ?? [];
2019
2067
  const headerLines = [];
2020
2068
  if (input.platformHeaders) {
2021
2069
  for (const [name, value] of Object.entries(input.platformHeaders)) {
@@ -2029,7 +2077,8 @@ function matchPlatformSignature(signatures, input) {
2029
2077
  const headerMatch = headerLines.length > 0 && sig.headerPatterns?.some((p) => headerLines.some((line) => p.test(line)));
2030
2078
  const ja4Match = ja4.length > 0 && sig.ja4Patterns?.some((p) => p.test(ja4));
2031
2079
  const asnMatch = asn.length > 0 && sig.asnList?.includes(asn);
2032
- if (uaMatch || cardMatch || keyMatch || headerMatch || ja4Match || asnMatch) {
2080
+ const wafMatch = wafLabels.length > 0 && sig.wafLabelPatterns?.some((p) => wafLabels.some((label) => p.test(label)));
2081
+ if (uaMatch || cardMatch || keyMatch || headerMatch || ja4Match || asnMatch || wafMatch) {
2033
2082
  const uaBase = ua.split(/[\s/]/)[0]?.toLowerCase() ?? "";
2034
2083
  const cardHost = (() => {
2035
2084
  try {
@@ -2038,7 +2087,7 @@ function matchPlatformSignature(signatures, input) {
2038
2087
  return "";
2039
2088
  }
2040
2089
  })();
2041
- const keyBasis = uaBase || cardHost || apiKeyFormat || ja4 || asn && `asn${asn}` || "unknown";
2090
+ const keyBasis = uaBase || cardHost || apiKeyFormat || ja4 || asn && `asn${asn}` || wafMatch && `waf:${wafLabels[0]?.toLowerCase()}` || "unknown";
2042
2091
  return {
2043
2092
  vendor: sig.vendor,
2044
2093
  fingerprintKey: `${sig.vendor}:${keyBasis}`,