@astrasyncai/verification-gateway 5.21.1 → 5.22.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +19 -7
  3. package/dist/adapters/express.js +75 -9
  4. package/dist/adapters/express.js.map +1 -1
  5. package/dist/adapters/express.mjs +75 -9
  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 +75 -9
  12. package/dist/adapters/nextjs.js.map +1 -1
  13. package/dist/adapters/nextjs.mjs +75 -9
  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 +60 -13
  50. package/dist/edge-core/index.js.map +1 -1
  51. package/dist/edge-core/index.mjs +60 -13
  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 +27 -6
  58. package/dist/index.d.ts +27 -6
  59. package/dist/index.js +75 -9
  60. package/dist/index.js.map +1 -1
  61. package/dist/index.mjs +75 -9
  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 +20 -1
  70. package/dist/platform-signatures.d.ts +20 -1
  71. package/dist/platform-signatures.js +41 -6
  72. package/dist/platform-signatures.js.map +1 -1
  73. package/dist/platform-signatures.mjs +41 -6
  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/dist/index.d.mts CHANGED
@@ -155,9 +155,11 @@ declare function extractPlatformHeaders(sanitized: Record<string, string> | unde
155
155
  * `connection` keys the edge adapters populate natively, so SDK-only
156
156
  * deployments still get IP / geo / TLS attribution. Precedence per key:
157
157
  *
158
- * - `ip`: `cf-connecting-ip` > `fastly-client-ip` > `true-client-ip` >
159
- * `x-real-ip` > `cloudfront-viewer-address` (port stripped) > leftmost
160
- * `x-forwarded-for`
158
+ * - `ip`: `cf-connecting-ip` > `fastly-client-ip` > `incap-client-ip`
159
+ * (Imperva) > `true-client-ip` (Akamai et al.) > `x-real-ip` >
160
+ * `cloudfront-viewer-address` (port stripped) > leftmost `x-forwarded-for`
161
+ * - Akamai `x-akamai-edgescape` fills country / region / city / lat / long /
162
+ * timeZone / asn / continent where no header above supplied them
161
163
  * - `country`: `cf-ipcountry` > `x-vercel-ip-country` >
162
164
  * `cloudfront-viewer-country` > `fastly-geo-country`
163
165
  * - `region` / `city` / `timeZone`: Vercel then CloudFront viewer headers
@@ -5322,7 +5324,7 @@ declare function _resetEdgeConfigCache(): void;
5322
5324
  * Pure regex — no Node built-ins, edge-safe. Detection is regex-only for
5323
5325
  * v2.9 — TLS fingerprinting / behavioural signatures are deferred.
5324
5326
  */
5325
- type PlatformAgentVendor = 'claude' | 'chatgpt' | 'gemini' | 'cursor' | 'goose' | 'perplexity' | 'chatgpt-atlas' | 'perplexity-comet' | 'astrasync-bridge' | 'astrasync-sdk' | 'unknown';
5327
+ type PlatformAgentVendor = 'claude' | 'chatgpt' | 'gemini' | 'cursor' | 'goose' | 'perplexity' | 'grok' | 'meta-ai' | 'chatgpt-atlas' | 'perplexity-comet' | 'astrasync-bridge' | 'astrasync-sdk' | 'unknown';
5326
5328
  interface PlatformFingerprint {
5327
5329
  vendor: PlatformAgentVendor;
5328
5330
  /** A stable identifier used as the orphan-agent dedup key. */
@@ -5358,6 +5360,13 @@ interface PlatformDetectionInput {
5358
5360
  * provider — registry validation rejects ASN-only signatures.
5359
5361
  */
5360
5362
  asn?: string;
5363
+ /**
5364
+ * Round 31 — bot labels a customer's WAF attached to the request (AWS Bot
5365
+ * Control `…:bot:name:x`, Akamai Bot Manager bot name, Cloudflare
5366
+ * verified-bot category, Imperva client classification). Present only on
5367
+ * rows read from WAF security logs. Matched against `wafLabelPatterns`.
5368
+ */
5369
+ wafLabels?: string[];
5361
5370
  }
5362
5371
  /**
5363
5372
  * A signature definition, generic over the vendor key. The seed constant
@@ -5386,6 +5395,8 @@ interface PlatformSignatureDef<V extends string = string> {
5386
5395
  ja4Patterns?: RegExp[];
5387
5396
  /** Exact ASN strings this signature matches (never regex). */
5388
5397
  asnList?: string[];
5398
+ /** Patterns tested against each WAF bot label (round 31; see PlatformDetectionInput.wafLabels). */
5399
+ wafLabelPatterns?: RegExp[];
5389
5400
  }
5390
5401
  /**
5391
5402
  * A fingerprint whose vendor may be a dynamically-promoted (non-seed) key.
@@ -5402,6 +5413,16 @@ interface DynamicPlatformFingerprint {
5402
5413
  * Patterns are intentionally permissive (case-insensitive substring match) since
5403
5414
  * platform UA strings drift between versions. False positives are tolerable —
5404
5415
  * the orphan record is informational, not enforcement.
5416
+ *
5417
+ * Round 31: `wafLabelPatterns` attribute rows read from a customer's WAF
5418
+ * security logs by the bot name THEIR WAF assigned; they never match inline
5419
+ * traffic (no labels there).
5420
+ *
5421
+ * NOT detectable here, by design (Oct 2026): Meta's Muse and OpenAI's Dots
5422
+ * publish no user agent and browse through real Chromium sessions that are
5423
+ * indistinguishable from a person's. There is no signature to find — don't
5424
+ * chase one. Verified identity (an ASTRA-id the agent presents) is the only
5425
+ * durable way to know such an agent is there; that is the product's point.
5405
5426
  */
5406
5427
  declare const PLATFORM_AGENT_SIGNATURES: ReadonlyArray<PlatformSignatureDef<PlatformAgentVendor>>;
5407
5428
  /**
@@ -5466,7 +5487,7 @@ declare function detectRuntime(userAgent: string | undefined | null): string | u
5466
5487
  * silently (never throwing), so this is a no-op there — which is correct:
5467
5488
  * in a browser the page's real UA is the honest identity.
5468
5489
  */
5469
- declare const SDK_USER_AGENT = "astrasync-sdk/5.21.1";
5490
+ declare const SDK_USER_AGENT = "astrasync-sdk/5.22.0";
5470
5491
  declare const sdkFetch: typeof fetch;
5471
5492
 
5472
5493
  interface LocalPolicy {
@@ -5584,7 +5605,7 @@ declare function parseYaml(yamlContent: string): ParseResult;
5584
5605
  * is fine — bumping both in the release-ceremony commit keeps them
5585
5606
  * lockstep.
5586
5607
  */
5587
- declare const SDK_VERSION = "5.21.1";
5608
+ declare const SDK_VERSION = "5.22.0";
5588
5609
 
5589
5610
  /**
5590
5611
  * 5.15.0 — the `UCP-Agent` request header (UCP 2026-08-25): an RFC 8941
package/dist/index.d.ts CHANGED
@@ -155,9 +155,11 @@ declare function extractPlatformHeaders(sanitized: Record<string, string> | unde
155
155
  * `connection` keys the edge adapters populate natively, so SDK-only
156
156
  * deployments still get IP / geo / TLS attribution. Precedence per key:
157
157
  *
158
- * - `ip`: `cf-connecting-ip` > `fastly-client-ip` > `true-client-ip` >
159
- * `x-real-ip` > `cloudfront-viewer-address` (port stripped) > leftmost
160
- * `x-forwarded-for`
158
+ * - `ip`: `cf-connecting-ip` > `fastly-client-ip` > `incap-client-ip`
159
+ * (Imperva) > `true-client-ip` (Akamai et al.) > `x-real-ip` >
160
+ * `cloudfront-viewer-address` (port stripped) > leftmost `x-forwarded-for`
161
+ * - Akamai `x-akamai-edgescape` fills country / region / city / lat / long /
162
+ * timeZone / asn / continent where no header above supplied them
161
163
  * - `country`: `cf-ipcountry` > `x-vercel-ip-country` >
162
164
  * `cloudfront-viewer-country` > `fastly-geo-country`
163
165
  * - `region` / `city` / `timeZone`: Vercel then CloudFront viewer headers
@@ -5322,7 +5324,7 @@ declare function _resetEdgeConfigCache(): void;
5322
5324
  * Pure regex — no Node built-ins, edge-safe. Detection is regex-only for
5323
5325
  * v2.9 — TLS fingerprinting / behavioural signatures are deferred.
5324
5326
  */
5325
- type PlatformAgentVendor = 'claude' | 'chatgpt' | 'gemini' | 'cursor' | 'goose' | 'perplexity' | 'chatgpt-atlas' | 'perplexity-comet' | 'astrasync-bridge' | 'astrasync-sdk' | 'unknown';
5327
+ type PlatformAgentVendor = 'claude' | 'chatgpt' | 'gemini' | 'cursor' | 'goose' | 'perplexity' | 'grok' | 'meta-ai' | 'chatgpt-atlas' | 'perplexity-comet' | 'astrasync-bridge' | 'astrasync-sdk' | 'unknown';
5326
5328
  interface PlatformFingerprint {
5327
5329
  vendor: PlatformAgentVendor;
5328
5330
  /** A stable identifier used as the orphan-agent dedup key. */
@@ -5358,6 +5360,13 @@ interface PlatformDetectionInput {
5358
5360
  * provider — registry validation rejects ASN-only signatures.
5359
5361
  */
5360
5362
  asn?: string;
5363
+ /**
5364
+ * Round 31 — bot labels a customer's WAF attached to the request (AWS Bot
5365
+ * Control `…:bot:name:x`, Akamai Bot Manager bot name, Cloudflare
5366
+ * verified-bot category, Imperva client classification). Present only on
5367
+ * rows read from WAF security logs. Matched against `wafLabelPatterns`.
5368
+ */
5369
+ wafLabels?: string[];
5361
5370
  }
5362
5371
  /**
5363
5372
  * A signature definition, generic over the vendor key. The seed constant
@@ -5386,6 +5395,8 @@ interface PlatformSignatureDef<V extends string = string> {
5386
5395
  ja4Patterns?: RegExp[];
5387
5396
  /** Exact ASN strings this signature matches (never regex). */
5388
5397
  asnList?: string[];
5398
+ /** Patterns tested against each WAF bot label (round 31; see PlatformDetectionInput.wafLabels). */
5399
+ wafLabelPatterns?: RegExp[];
5389
5400
  }
5390
5401
  /**
5391
5402
  * A fingerprint whose vendor may be a dynamically-promoted (non-seed) key.
@@ -5402,6 +5413,16 @@ interface DynamicPlatformFingerprint {
5402
5413
  * Patterns are intentionally permissive (case-insensitive substring match) since
5403
5414
  * platform UA strings drift between versions. False positives are tolerable —
5404
5415
  * the orphan record is informational, not enforcement.
5416
+ *
5417
+ * Round 31: `wafLabelPatterns` attribute rows read from a customer's WAF
5418
+ * security logs by the bot name THEIR WAF assigned; they never match inline
5419
+ * traffic (no labels there).
5420
+ *
5421
+ * NOT detectable here, by design (Oct 2026): Meta's Muse and OpenAI's Dots
5422
+ * publish no user agent and browse through real Chromium sessions that are
5423
+ * indistinguishable from a person's. There is no signature to find — don't
5424
+ * chase one. Verified identity (an ASTRA-id the agent presents) is the only
5425
+ * durable way to know such an agent is there; that is the product's point.
5405
5426
  */
5406
5427
  declare const PLATFORM_AGENT_SIGNATURES: ReadonlyArray<PlatformSignatureDef<PlatformAgentVendor>>;
5407
5428
  /**
@@ -5466,7 +5487,7 @@ declare function detectRuntime(userAgent: string | undefined | null): string | u
5466
5487
  * silently (never throwing), so this is a no-op there — which is correct:
5467
5488
  * in a browser the page's real UA is the honest identity.
5468
5489
  */
5469
- declare const SDK_USER_AGENT = "astrasync-sdk/5.21.1";
5490
+ declare const SDK_USER_AGENT = "astrasync-sdk/5.22.0";
5470
5491
  declare const sdkFetch: typeof fetch;
5471
5492
 
5472
5493
  interface LocalPolicy {
@@ -5584,7 +5605,7 @@ declare function parseYaml(yamlContent: string): ParseResult;
5584
5605
  * is fine — bumping both in the release-ceremony commit keeps them
5585
5606
  * lockstep.
5586
5607
  */
5587
- declare const SDK_VERSION = "5.21.1";
5608
+ declare const SDK_VERSION = "5.22.0";
5588
5609
 
5589
5610
  /**
5590
5611
  * 5.15.0 — the `UCP-Agent` request header (UCP 2026-08-25): an RFC 8941
package/dist/index.js CHANGED
@@ -229,7 +229,7 @@ function getTrustLevel(score) {
229
229
  }
230
230
 
231
231
  // src/version.ts
232
- var SDK_VERSION = "5.21.1";
232
+ var SDK_VERSION = "5.22.0";
233
233
 
234
234
  // src/http.ts
235
235
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -1404,7 +1404,11 @@ var PLATFORM_HEADER_NAMES = /* @__PURE__ */ new Set([
1404
1404
  "x-goog-api-client",
1405
1405
  "user-agent",
1406
1406
  "x-agent-card-url",
1407
- "x-agent-provider"
1407
+ "x-agent-provider",
1408
+ // Round 31 — Web Bot Auth's Signature-Agent names the signing agent's
1409
+ // directory host (a public URL, never a secret). It attributes a signed
1410
+ // agent even when its UA is stock Chrome.
1411
+ "signature-agent"
1408
1412
  ]);
1409
1413
  var PLATFORM_HEADER_PREFIXES = [
1410
1414
  "x-goog-",
@@ -1535,7 +1539,12 @@ function extractPlatformHeaders(sanitized) {
1535
1539
  return out;
1536
1540
  }
1537
1541
  var CONNECTION_HEADER_SOURCES = [
1538
- ["ip", ["cf-connecting-ip", "fastly-client-ip", "true-client-ip", "x-real-ip"]],
1542
+ // Round 31: `incap-client-ip` = Imperva Cloud WAF; `true-client-ip` is
1543
+ // also Akamai's client-IP header when the property enables it.
1544
+ [
1545
+ "ip",
1546
+ ["cf-connecting-ip", "fastly-client-ip", "incap-client-ip", "true-client-ip", "x-real-ip"]
1547
+ ],
1539
1548
  [
1540
1549
  "country",
1541
1550
  ["cf-ipcountry", "x-vercel-ip-country", "cloudfront-viewer-country", "fastly-geo-country"]
@@ -1586,6 +1595,28 @@ function deriveConnectionFromHeaders(headers) {
1586
1595
  if (portMatch) connection.sourcePort = portMatch[1];
1587
1596
  if (!connection.ip && addr) connection.ip = addr.replace(/:\d+$/, "");
1588
1597
  }
1598
+ {
1599
+ const edgescape = (lower["x-akamai-edgescape"] ?? lower["akamai-edgescape"])?.trim();
1600
+ if (edgescape) {
1601
+ const fields = /* @__PURE__ */ new Map();
1602
+ for (const pair of edgescape.split(",")) {
1603
+ const i = pair.indexOf("=");
1604
+ if (i > 0) fields.set(pair.slice(0, i).trim().toLowerCase(), pair.slice(i + 1).trim());
1605
+ }
1606
+ const fill = (key, field) => {
1607
+ const v = fields.get(field);
1608
+ if (v && !connection[key]) connection[key] = v.slice(0, MAX_CONNECTION_VALUE_CHARS);
1609
+ };
1610
+ fill("country", "country_code");
1611
+ fill("region", "region_code");
1612
+ fill("city", "city");
1613
+ fill("latitude", "lat");
1614
+ fill("longitude", "long");
1615
+ fill("timeZone", "timezone");
1616
+ fill("asn", "asnum");
1617
+ fill("continent", "continent");
1618
+ }
1619
+ }
1589
1620
  if (!connection.ip) {
1590
1621
  const xff = lower["x-forwarded-for"];
1591
1622
  const client = xff?.split(",")[0]?.trim();
@@ -2284,7 +2315,8 @@ var PLATFORM_AGENT_SIGNATURES = [
2284
2315
  uaPatterns: [/claude/i, /anthropic/i],
2285
2316
  agentCardPatterns: [/anthropic\.com/i, /claude\.ai/i],
2286
2317
  apiKeyFormatPatterns: [/^sk-ant/i],
2287
- headerPatterns: [/^anthropic-version/i, /^anthropic-beta/i, /^x-anthropic-/i]
2318
+ headerPatterns: [/^anthropic-version/i, /^anthropic-beta/i, /^x-anthropic-/i],
2319
+ wafLabelPatterns: [/\bclaude/i, /\banthropic/i]
2288
2320
  },
2289
2321
  {
2290
2322
  vendor: "chatgpt",
@@ -2295,7 +2327,14 @@ var PLATFORM_AGENT_SIGNATURES = [
2295
2327
  uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i, /\bGPTBot\b/i, /\bOAI-SearchBot\b/i],
2296
2328
  agentCardPatterns: [/openai\.com/i, /chatgpt\.com/i],
2297
2329
  apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],
2298
- headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i]
2330
+ headerPatterns: [
2331
+ /^openai-organization/i,
2332
+ /^openai-project/i,
2333
+ /^openai-version/i,
2334
+ // ChatGPT agent signs with Web Bot Auth under this directory host.
2335
+ /^signature-agent: "?https:\/\/chatgpt\.com/i
2336
+ ],
2337
+ wafLabelPatterns: [/\bgptbot\b/i, /\bchatgpt/i, /\boai-searchbot\b/i, /\bopenai/i]
2299
2338
  },
2300
2339
  {
2301
2340
  vendor: "gemini",
@@ -2316,7 +2355,8 @@ var PLATFORM_AGENT_SIGNATURES = [
2316
2355
  ],
2317
2356
  agentCardPatterns: [/gemini\.google\.com/i, /ai\.google\.dev/i],
2318
2357
  apiKeyFormatPatterns: [/^aiza/i],
2319
- headerPatterns: [/^x-goog-user-project/i, /^x-goog-api-client/i]
2358
+ headerPatterns: [/^x-goog-user-project/i, /^x-goog-api-client/i],
2359
+ wafLabelPatterns: [/\bgoogle-extended\b/i, /\bgoogleother\b/i, /\bgemini/i]
2320
2360
  },
2321
2361
  {
2322
2362
  vendor: "cursor",
@@ -2335,7 +2375,31 @@ var PLATFORM_AGENT_SIGNATURES = [
2335
2375
  displayName: "Perplexity",
2336
2376
  uaPatterns: [/perplexity/i],
2337
2377
  agentCardPatterns: [/perplexity\.ai/i],
2338
- apiKeyFormatPatterns: [/^pplx-/i]
2378
+ apiKeyFormatPatterns: [/^pplx-/i],
2379
+ wafLabelPatterns: [/\bperplexity/i]
2380
+ },
2381
+ // Round 30.1: xAI's documented crawlers (GrokBot/1.0, xAI-Grok/1.0,
2382
+ // Grok-DeepSearch/1.0). Anchored, versioned tokens — never in a stock UA.
2383
+ {
2384
+ vendor: "grok",
2385
+ displayName: "xAI Grok",
2386
+ uaPatterns: [/\bGrokBot\//i, /\bxAI-Grok\//i, /\bGrok-DeepSearch\//i],
2387
+ agentCardPatterns: [/\bx\.ai\b/i, /grok\.com/i],
2388
+ apiKeyFormatPatterns: [/^xai-/i],
2389
+ wafLabelPatterns: [/\bgrok/i, /\bxai\b/i]
2390
+ },
2391
+ // Round 30.1: Meta AI's index and fetch crawlers. facebookexternalhit
2392
+ // (link previews) is deliberately NOT here — it stays a generic crawler.
2393
+ {
2394
+ vendor: "meta-ai",
2395
+ displayName: "Meta AI",
2396
+ uaPatterns: [/\bMeta-WebIndexer\b/i, /\bMeta-ExternalFetcher\b/i],
2397
+ agentCardPatterns: [/meta\.ai/i],
2398
+ wafLabelPatterns: [
2399
+ /\bmeta-webindexer\b/i,
2400
+ /\bmeta-externalfetcher\b/i,
2401
+ /\bmeta-externalagent\b/i
2402
+ ]
2339
2403
  },
2340
2404
  // Agentic browsers — versioned-token patterns ONLY (a stock Chrome UA must
2341
2405
  // never match). Distinct vendor keys (not 'chatgpt'/'perplexity'): the
@@ -2383,6 +2447,7 @@ function matchPlatformSignature(signatures, input) {
2383
2447
  const apiKeyFormat = input.apiKeyFormat ?? "";
2384
2448
  const ja4 = input.ja4 ?? "";
2385
2449
  const asn = input.asn ?? "";
2450
+ const wafLabels = input.wafLabels ?? [];
2386
2451
  const headerLines = [];
2387
2452
  if (input.platformHeaders) {
2388
2453
  for (const [name, value] of Object.entries(input.platformHeaders)) {
@@ -2396,7 +2461,8 @@ function matchPlatformSignature(signatures, input) {
2396
2461
  const headerMatch = headerLines.length > 0 && sig.headerPatterns?.some((p) => headerLines.some((line) => p.test(line)));
2397
2462
  const ja4Match = ja4.length > 0 && sig.ja4Patterns?.some((p) => p.test(ja4));
2398
2463
  const asnMatch = asn.length > 0 && sig.asnList?.includes(asn);
2399
- if (uaMatch || cardMatch || keyMatch || headerMatch || ja4Match || asnMatch) {
2464
+ const wafMatch = wafLabels.length > 0 && sig.wafLabelPatterns?.some((p) => wafLabels.some((label) => p.test(label)));
2465
+ if (uaMatch || cardMatch || keyMatch || headerMatch || ja4Match || asnMatch || wafMatch) {
2400
2466
  const uaBase = ua.split(/[\s/]/)[0]?.toLowerCase() ?? "";
2401
2467
  const cardHost = (() => {
2402
2468
  try {
@@ -2405,7 +2471,7 @@ function matchPlatformSignature(signatures, input) {
2405
2471
  return "";
2406
2472
  }
2407
2473
  })();
2408
- const keyBasis = uaBase || cardHost || apiKeyFormat || ja4 || asn && `asn${asn}` || "unknown";
2474
+ const keyBasis = uaBase || cardHost || apiKeyFormat || ja4 || asn && `asn${asn}` || wafMatch && `waf:${wafLabels[0]?.toLowerCase()}` || "unknown";
2409
2475
  return {
2410
2476
  vendor: sig.vendor,
2411
2477
  fingerprintKey: `${sig.vendor}:${keyBasis}`,