From b8402dd6222aa2d505967ba232aaf40628c21a2d Mon Sep 17 00:00:00 2001 From: macjayz Date: Thu, 29 Oct 2026 17:56:28 +0100 Subject: [PATCH] fix(sdk): bound the event queue and make the packages publishable HeightproofClient re-queued every failed batch with no limit, so a Heightproof outage grew memory in the host application without bound, and re-queued batches the server had rejected (401, 400, 413), which then sat at the head of the queue and blocked every later batch for good. Requests also had no timeout. - Cap the in-memory queue (maxQueueSize, default 5000), dropping the oldest. - Retry only network errors, timeouts, 408, 429 and 5xx; drop other 4xx. - Abort ingestion requests after requestTimeoutMs (default 5000). - Report drops through onDrop(count, reason) and stats(). @heightproof/web3 marked every failed RPC call as an auth failure, which added the auth_failure signal (+24) to ordinary RPC errors. Only 401/403 count now. Publishing: shared, sdk-node and web3 get descriptions, license, repository, exports, a files list (dist, README, LICENSE only), publishConfig access public, prepublishOnly build, and READMEs. express becomes an optional peer of sdk-node, since it imports only Express types; installing @heightproof/web3 no longer pulls in Express. --- packages/sdk-node/LICENSE | 21 ++++++ packages/sdk-node/README.md | 52 +++++++++++++ packages/sdk-node/package.json | 40 +++++++++- packages/sdk-node/src/index.test.ts | 110 +++++++++++++++++++++++++++- packages/sdk-node/src/index.ts | 61 ++++++++++++++- packages/shared/LICENSE | 21 ++++++ packages/shared/README.md | 14 ++++ packages/shared/package.json | 35 ++++++++- packages/web3/LICENSE | 21 ++++++ packages/web3/README.md | 88 ++++++++++++++++++++++ packages/web3/package.json | 39 +++++++++- packages/web3/src/index.test.ts | 4 +- packages/web3/src/index.ts | 4 +- 13 files changed, 499 insertions(+), 11 deletions(-) create mode 100644 packages/sdk-node/LICENSE create mode 100644 packages/sdk-node/README.md create mode 100644 packages/shared/LICENSE create mode 100644 packages/shared/README.md create mode 100644 packages/web3/LICENSE create mode 100644 packages/web3/README.md diff --git a/packages/sdk-node/LICENSE b/packages/sdk-node/LICENSE new file mode 100644 index 0000000..e5f0160 --- /dev/null +++ b/packages/sdk-node/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Jayces + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/sdk-node/README.md b/packages/sdk-node/README.md new file mode 100644 index 0000000..a380353 --- /dev/null +++ b/packages/sdk-node/README.md @@ -0,0 +1,52 @@ +# @heightproof/sdk-node + +Node.js client for a self-hosted [Heightproof](https://github.com/macjayz/heightproof) instance: +batched, non-blocking event delivery, plus Express middleware that records REST and GraphQL +requests so they can be correlated with RPC incidents. + +For EVM JSON-RPC monitoring, use [`@heightproof/web3`](https://www.npmjs.com/package/@heightproof/web3), +which builds on this package. + +```bash +npm install @heightproof/sdk-node +``` + +## Express + +```ts +import { heightproofErrorHandler, heightproofExpress } from "@heightproof/sdk-node"; + +app.use( + heightproofExpress({ + projectId: "my-project", + apiKey: process.env.HEIGHTPROOF_API_KEY!, + endpoint: "https://heightproof.internal", + serviceName: "payments-api" + }) +); + +// ...routes... + +app.use(heightproofErrorHandler()); // captures thrown errors for error grouping +``` + +Sensitive headers and body fields (authorization, cookies, passwords, private keys, mnemonics) +are redacted before anything leaves the process. Add your own with the `redaction` option. + +## Delivery options + +| Option | Default | | +|---|---|---| +| `flushIntervalMs` | `5000` | How often queued events are sent | +| `maxBatchSize` | `25` | Events per request; reaching it also triggers a send | +| `maxQueueSize` | `5000` | Events held while Heightproof is unreachable; oldest dropped beyond this | +| `requestTimeoutMs` | `5000` | Abort an ingestion request after this long | +| `onDrop` | — | `(count, reason) => void`, called when events are discarded (`queue_full` or `rejected`) | + +Network failures, timeouts, `408`, `429` and `5xx` responses are retried on the next flush. Other +`4xx` responses are dropped, since they would be rejected identically every time. +`HeightproofClient#stats()` reports how many events are queued and how many have been dropped. + +## License + +MIT diff --git a/packages/sdk-node/package.json b/packages/sdk-node/package.json index 230ea8f..e5c06b2 100644 --- a/packages/sdk-node/package.json +++ b/packages/sdk-node/package.json @@ -1,12 +1,45 @@ { "name": "@heightproof/sdk-node", "version": "0.1.0", + "description": "Heightproof SDK for Node.js: batched, non-blocking event delivery and Express middleware.", + "keywords": [ + "heightproof", + "express", + "observability", + "apm" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/macjayz/heightproof.git", + "directory": "packages/sdk-node" + }, + "homepage": "https://github.com/macjayz/heightproof#readme", + "bugs": "https://github.com/macjayz/heightproof/issues", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "engines": { + "node": ">=20" + }, + "publishConfig": { + "access": "public" + }, "scripts": { "build": "tsc -p tsconfig.json", - "test": "vitest run --passWithNoTests" + "test": "vitest run --passWithNoTests", + "prepublishOnly": "npm run build" }, "dependencies": { "@heightproof/shared": "0.1.0" @@ -14,6 +47,11 @@ "peerDependencies": { "express": ">=4" }, + "peerDependenciesMeta": { + "express": { + "optional": true + } + }, "devDependencies": { "@types/express": "^4.17.21", "express": "^5.2.1" diff --git a/packages/sdk-node/src/index.test.ts b/packages/sdk-node/src/index.test.ts index 794394b..2d1210c 100644 --- a/packages/sdk-node/src/index.test.ts +++ b/packages/sdk-node/src/index.test.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "node:events"; import type { Request, Response } from "express"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { HeightproofClient, heightproofErrorHandler, heightproofExpress } from "./index.js"; function createFakeRequest(overrides: Partial = {}): Request { @@ -98,3 +98,111 @@ describe("heightproofExpress error capture", () => { vi.restoreAllMocks(); }); }); + +describe("HeightproofClient delivery", () => { + const options = { + projectId: "demo", + apiKey: "dev-heightproof-key", + endpoint: "http://localhost:8080", + serviceName: "example-api", + flushIntervalMs: 60_000, + maxBatchSize: 2 + }; + + function event(id: string) { + return { + id, + projectId: "demo", + serviceName: "example-api", + environment: "test", + timestamp: "2026-09-01T20:12:00.000Z", + kind: "rest" as const, + request: { + method: "GET" as const, + path: "/", + headers: {}, + query: {}, + auth: { present: false, failed: false } + }, + response: { statusCode: 200, latencyMs: 1 } + }; + } + + function respondWith(...statuses: Array) { + const fetchMock = vi.fn(); + for (const status of statuses) { + if (status instanceof Error) fetchMock.mockRejectedValueOnce(status); + else fetchMock.mockResolvedValueOnce({ ok: status < 300, status }); + } + vi.stubGlobal("fetch", fetchMock); + return fetchMock; + } + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("caps memory while Heightproof is unreachable by dropping the oldest events", () => { + const drops: Array<[number, string]> = []; + const client = new HeightproofClient({ + ...options, + maxBatchSize: 100, + maxQueueSize: 3, + onDrop: (count, reason) => drops.push([count, reason]) + }); + + for (const id of ["e1", "e2", "e3", "e4", "e5"]) client.capture(event(id)); + + expect(client.stats()).toEqual({ queued: 3, dropped: 2 }); + expect(drops).toEqual([ + [1, "queue_full"], + [1, "queue_full"] + ]); + client.stop(); + }); + + it("keeps a batch for retry when the network fails or the server is overloaded", async () => { + respondWith(new TypeError("fetch failed"), 503, 429); + const client = new HeightproofClient(options); + client.capture(event("e1")); + + await client.flush(); + await client.flush(); + await client.flush(); + + expect(client.stats()).toEqual({ queued: 1, dropped: 0 }); + client.stop(); + }); + + it("drops a batch the server rejects instead of retrying it forever", async () => { + const fetchMock = respondWith(401, 202); + const drops: Array<[number, string]> = []; + const client = new HeightproofClient({ + ...options, + maxBatchSize: 1, + onDrop: (c, r) => drops.push([c, r]) + }); + client.capture(event("rejected")); + client.capture(event("next")); + + await client.flush(); + await client.flush(); + + expect(drops).toEqual([[1, "rejected"]]); + expect(client.stats()).toEqual({ queued: 0, dropped: 1 }); + expect(JSON.parse(fetchMock.mock.calls[1][1].body).events[0].id).toBe("next"); + client.stop(); + }); + + it("aborts an ingestion request that hangs", async () => { + const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 202 }); + vi.stubGlobal("fetch", fetchMock); + const client = new HeightproofClient({ ...options, requestTimeoutMs: 1234 }); + client.capture(event("e1")); + + await client.flush(); + + expect(fetchMock.mock.calls[0][1].signal).toBeInstanceOf(AbortSignal); + client.stop(); + }); +}); diff --git a/packages/sdk-node/src/index.ts b/packages/sdk-node/src/index.ts index b12f7f7..968df6c 100644 --- a/packages/sdk-node/src/index.ts +++ b/packages/sdk-node/src/index.ts @@ -19,8 +19,22 @@ export type HeightproofMiddlewareOptions = { redaction?: RedactionConfig; flushIntervalMs?: number; maxBatchSize?: number; + /** + * Most events held in memory while Heightproof is unreachable. Beyond this the oldest are + * dropped, so an outage costs telemetry, never the host application's memory. Default 5000. + */ + maxQueueSize?: number; + /** Abort an ingestion request after this long. Default 5000ms. */ + requestTimeoutMs?: number; + /** Called when events are discarded, with how many and why. */ + onDrop?: (count: number, reason: DropReason) => void; }; +export type DropReason = "queue_full" | "rejected"; + +const DEFAULT_MAX_QUEUE_SIZE = 5000; +const DEFAULT_REQUEST_TIMEOUT_MS = 5000; + type RequestWithCapturedError = Request & { heightproofError?: unknown }; export function heightproofErrorHandler() { @@ -124,6 +138,7 @@ function getTraceId(req: Request) { export class HeightproofClient { private queue: HeightproofEvent[] = []; private timer?: NodeJS.Timeout; + private dropped = 0; constructor(private readonly options: HeightproofMiddlewareOptions) { this.timer = setInterval(() => void this.flush(), options.flushIntervalMs ?? 5000); @@ -132,11 +147,17 @@ export class HeightproofClient { capture(event: HeightproofEvent): void { this.queue.push(event); + this.enforceQueueLimit(); if (this.queue.length >= (this.options.maxBatchSize ?? 25)) { void this.flush(); } } + /** Events waiting to be sent, and events discarded since the client started. */ + stats(): { queued: number; dropped: number } { + return { queued: this.queue.length, dropped: this.dropped }; + } + async flush(): Promise { if (this.queue.length === 0) return; const events = this.queue.splice(0, this.options.maxBatchSize ?? 25); @@ -149,14 +170,42 @@ export class HeightproofClient { "content-type": "application/json", "x-heightproof-api-key": this.options.apiKey }, - body: JSON.stringify(payload) + body: JSON.stringify(payload), + signal: AbortSignal.timeout(this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS) }); - if (!response.ok) { - this.queue.unshift(...events); + if (response.ok) return; + if (isRetryableStatus(response.status)) { + this.requeue(events); + } else { + // A 400, 401, 403 or 413 will be rejected identically on every retry. Re-queueing it + // would put it back at the head of the queue and block every later batch for good. + this.drop(events.length, "rejected"); } } catch { - this.queue.unshift(...events); + // Network failure or timeout: Heightproof may come back, so keep the batch. + this.requeue(events); + } + } + + private requeue(events: HeightproofEvent[]) { + this.queue.unshift(...events); + this.enforceQueueLimit(); + } + + private enforceQueueLimit() { + const overflow = this.queue.length - (this.options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE); + if (overflow <= 0) return; + this.queue.splice(0, overflow); + this.drop(overflow, "queue_full"); + } + + private drop(count: number, reason: DropReason) { + this.dropped += count; + try { + this.options.onDrop?.(count, reason); + } catch { + // A throwing callback must not break capture in the host application. } } @@ -165,6 +214,10 @@ export class HeightproofClient { } } +function isRetryableStatus(status: number) { + return status === 408 || status === 429 || status >= 500; +} + function normalizeHeaders(headers: Request["headers"]): Record { return Object.fromEntries( Object.entries(headers).map(([key, value]) => [ diff --git a/packages/shared/LICENSE b/packages/shared/LICENSE new file mode 100644 index 0000000..e5f0160 --- /dev/null +++ b/packages/shared/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Jayces + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/shared/README.md b/packages/shared/README.md new file mode 100644 index 0000000..dda46b2 --- /dev/null +++ b/packages/shared/README.md @@ -0,0 +1,14 @@ +# @heightproof/shared + +Internal building blocks shared by the [Heightproof](https://github.com/macjayz/heightproof) +SDKs and services: the `HeightproofEvent` schema (Zod), JSON-RPC result normalization and +hashing, redaction helpers, and the content-aware RPC reliability detectors. + +You normally do not install this directly; it is a dependency of +[`@heightproof/web3`](https://www.npmjs.com/package/@heightproof/web3) and +[`@heightproof/sdk-node`](https://www.npmjs.com/package/@heightproof/sdk-node). Its API follows +those packages and may change between minor versions while Heightproof is pre-1.0. + +## License + +MIT diff --git a/packages/shared/package.json b/packages/shared/package.json index 9859201..326fb31 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,12 +1,45 @@ { "name": "@heightproof/shared", "version": "0.1.0", + "description": "Shared event schema, JSON-RPC normalization and reliability detectors for Heightproof.", + "keywords": [ + "heightproof", + "ethereum", + "json-rpc", + "observability" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/macjayz/heightproof.git", + "directory": "packages/shared" + }, + "homepage": "https://github.com/macjayz/heightproof#readme", + "bugs": "https://github.com/macjayz/heightproof/issues", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "engines": { + "node": ">=20" + }, + "publishConfig": { + "access": "public" + }, "scripts": { "build": "tsc -p tsconfig.json", - "test": "vitest run" + "test": "vitest run", + "prepublishOnly": "npm run build" }, "dependencies": { "@opentelemetry/api": "^1.9.1", diff --git a/packages/web3/LICENSE b/packages/web3/LICENSE new file mode 100644 index 0000000..e5f0160 --- /dev/null +++ b/packages/web3/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Jayces + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/web3/README.md b/packages/web3/README.md new file mode 100644 index 0000000..f9e6d3a --- /dev/null +++ b/packages/web3/README.md @@ -0,0 +1,88 @@ +# @heightproof/web3 + +Report every EVM JSON-RPC call your application makes to a self-hosted +[Heightproof](https://github.com/macjayz/heightproof) instance, which detects the provider +failures that still return `200 OK`: stale chain heads, providers disagreeing at the same block +height, JSON-RPC errors inside success responses, reorg lag, and rate limiting disguised as +success. + +```bash +npm install @heightproof/web3 +``` + +## Wrap an EIP-1193 provider + +```ts +import { wrapEip1193Provider } from "@heightproof/web3"; + +const provider = wrapEip1193Provider(window.ethereum, { + projectId: "my-project", + apiKey: process.env.HEIGHTPROOF_API_KEY!, + endpoint: "https://heightproof.internal", + chainId: 1, + provider: "alchemy" +}); +``` + +## Or use the viem transport + +```ts +import { createPublicClient } from "viem"; +import { mainnet } from "viem/chains"; +import { heightproofTransport } from "@heightproof/web3"; + +const client = createPublicClient({ + chain: mainnet, + transport: heightproofTransport({ + projectId: "my-project", + apiKey: process.env.HEIGHTPROOF_API_KEY!, + endpoint: "https://heightproof.internal", + rpcUrl: process.env.EVM_RPC_URL!, + chainId: 1, + provider: "alchemy" + }) +}); +``` + +`createHeightproofRpcClient(options)` gives the same recording around a plain `request()` call +when you are not using viem or an injected provider. + +## What it records + +Method, chain, provider label, latency, status, the block the call resolved against, a hash of the +normalized result, its shape (`value`, `null`, `empty_array`, `empty_data`, `error_in_body`), any +JSON-RPC error code, and whether the response looked like rate limiting. Raw results are not sent. + +Your RPC URL never leaves your process: Heightproof receives a hash of the endpoint, never the URL, +because provider URLs embed API keys. + +## Cross-provider verification + +Off unless configured, because it sends real requests to the endpoints you name: + +```ts +wrapEip1193Provider(provider, { + // ... + verify: { + endpoints: [{ rpcUrl: process.env.SECONDARY_RPC_URL!, provider: "infura" }], + sampleRate: 0.01, // fraction of deterministic reads to re-check + maxCallsPerMinute: 60 // hard ceiling on extra requests + } +}); +``` + +Sampled reads are re-issued against every endpoint pinned to the same block height, so a +mismatch is a real disagreement rather than head skew. Verification runs after your call returns +and never delays or fails it. + +## Delivery + +Events are batched in memory and sent every 5 seconds or every 25 events. Recording never blocks +or throws into your RPC calls. If Heightproof is unreachable, batches are retried; the in-memory +queue is capped (5,000 events by default, oldest dropped first) so an outage costs telemetry, +not your application's memory. Batches the server rejects outright (bad key, invalid payload) are +dropped rather than retried forever. + +## License + +MIT diff --git a/packages/web3/package.json b/packages/web3/package.json index 016d429..457e728 100644 --- a/packages/web3/package.json +++ b/packages/web3/package.json @@ -1,12 +1,49 @@ { "name": "@heightproof/web3", "version": "0.1.0", + "description": "Detect EVM RPC failures that return 200 OK: wrap an EIP-1193 provider or viem transport and report every call to Heightproof.", + "keywords": [ + "heightproof", + "ethereum", + "evm", + "json-rpc", + "rpc", + "viem", + "eip-1193", + "monitoring" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/macjayz/heightproof.git", + "directory": "packages/web3" + }, + "homepage": "https://github.com/macjayz/heightproof#readme", + "bugs": "https://github.com/macjayz/heightproof/issues", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "engines": { + "node": ">=20" + }, + "publishConfig": { + "access": "public" + }, "scripts": { "build": "tsc -p tsconfig.json", - "test": "vitest run" + "test": "vitest run", + "prepublishOnly": "npm run build" }, "dependencies": { "@heightproof/sdk-node": "0.1.0", diff --git a/packages/web3/src/index.test.ts b/packages/web3/src/index.test.ts index 1cb7d9f..5a1de82 100644 --- a/packages/web3/src/index.test.ts +++ b/packages/web3/src/index.test.ts @@ -44,7 +44,7 @@ describe("web3 sdk", () => { }); }); - it("marks failed json-rpc calls as failed auth-style rpc events", async () => { + it("records a failed call as a 500 without calling it an auth failure", async () => { captured.length = 0; const provider = wrapEip1193Provider( { @@ -62,7 +62,7 @@ describe("web3 sdk", () => { await expect(provider.request({ method: "eth_sendRawTransaction", params: [] })).rejects.toThrow("provider failed"); expect(captured[0]).toMatchObject({ evmRpc: { method: "eth_sendRawTransaction" }, - request: { auth: { failed: true } }, + request: { auth: { failed: false } }, response: { statusCode: 500 } }); }); diff --git a/packages/web3/src/index.ts b/packages/web3/src/index.ts index 5c88f25..885cff9 100644 --- a/packages/web3/src/index.ts +++ b/packages/web3/src/index.ts @@ -216,7 +216,9 @@ class RpcRecorder { auth: { present: true, scheme: "RPC", - failed + // Only a rejected credential is an auth failure. Marking every failed call would add the + // auth_failure signal to ordinary RPC errors and inflate their threat score. + failed: outcome.statusCode === 401 || outcome.statusCode === 403 } }, response: {