@heightproof/shared 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +13 -2
- package/dist/airnode.d.ts +69 -0
- package/dist/airnode.d.ts.map +1 -0
- package/dist/airnode.js +117 -0
- package/dist/demoEvents.d.ts +6 -0
- package/dist/demoEvents.d.ts.map +1 -0
- package/dist/demoEvents.js +126 -0
- package/dist/detectors.d.ts +154 -0
- package/dist/detectors.d.ts.map +1 -0
- package/dist/detectors.js +287 -0
- package/dist/index.d.ts +862 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +278 -0
- package/dist/rpc.d.ts +111 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +391 -0
- package/package.json +48 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzF,OAAO,EAOL,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EACL,6BAA6B,EAC7B,uBAAuB,EACvB,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAC7B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,2BAA2B,EAC3B,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,eAAe,EACf,YAAY,EACZ,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,WAAW,EACX,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,WAAW,EACX,kBAAkB,EAClB,mBAAmB,EACnB,yBAAyB,EACzB,KAAK,QAAQ,EACb,KAAK,WAAW,EACjB,MAAM,UAAU,CAAC;AAElB,iEAAiE;AACjE,eAAO,MAAM,6BAA6B,QAAS,CAAC;AAEpD,eAAO,MAAM,gBAAgB,yEAQ3B,CAAC;AAEH,eAAO,MAAM,iBAAiB,mEAAiE,CAAC;AAEhG,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAqD7B,mEAAmE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmCvE,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YArCvB,mEAAmE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuCvE,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AACtE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC1D,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAmBF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,WAAW,CAQxE;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAiBvD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAK7D;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,GAAE,eAAoB,GAAG,OAAO,CAajF;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAE,eAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYnH;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;IACjD,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,CAAC,CAAC;AAEzE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,yBAAyB,CAAC,EAAE,MAAM,CAAC;IAGnC,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,aAAa,CAAC,EAAE,oBAAoB,CAAC;IACrC,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,UAAU,CAAC,EAAE,iBAAiB,CAAC;CAChC,CAAC;AAEF,wBAAsB,QAAQ,CAAC,CAAC,EAC9B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,eAAe,EAC3B,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAC1B,OAAO,CAAC,CAAC,CAAC,CAsBZ;AAED,wBAAgB,YAAY,CAC1B,KAAK,EAAE,gBAAgB,EACvB,oBAAoB,SAAI,EACxB,WAAW,GAAE,iBAAsB,GAClC,gBAAgB,CAoFlB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { context, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
2
|
+
import { detectCostWaste, detectDisagreement, detectReorgLag, detectSilentFailure, detectStaleHead, detectThrottling } from "./detectors.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export { airnodePassthroughRequestHash, checkAirnodeAttestation, readAirnodeAttestation, recoverAirnodeSigner, reverifyAttestation } from "./airnode.js";
|
|
5
|
+
export { buildDemoEvents } from "./demoEvents.js";
|
|
6
|
+
export { detectCostWaste, detectDisagreement, detectReorgLag, detectSilentFailure, detectStaleHead, detectThrottling, DEFAULT_DETECTOR_THRESHOLDS } from "./detectors.js";
|
|
7
|
+
export { chainBlockTimeMs, classifyResultShape, extractBlockHash, extractBlockNumber, extractBlockTag, extractRpcError, hashEndpoint, hashRpcResult, methodCostUnits, normalizeRpcResult, pinBlockTag, blockTagParamIndex, isThrottleSignature, isVerifiableMethod, resultCount, VERIFIABLE_METHODS, CHAIN_BLOCK_TIME_MS, DEFAULT_METHOD_COST_UNITS } from "./rpc.js";
|
|
8
|
+
/** Largest signed response body kept as attestation evidence. */
|
|
9
|
+
export const AIRNODE_SIGNED_BODY_MAX_CHARS = 65_536;
|
|
10
|
+
export const HttpMethodSchema = z.enum([
|
|
11
|
+
"GET",
|
|
12
|
+
"POST",
|
|
13
|
+
"PUT",
|
|
14
|
+
"PATCH",
|
|
15
|
+
"DELETE",
|
|
16
|
+
"OPTIONS",
|
|
17
|
+
"HEAD"
|
|
18
|
+
]);
|
|
19
|
+
export const TrafficKindSchema = z.enum(["rest", "graphql", "evm_rpc", "websocket", "webhook"]);
|
|
20
|
+
export const HeightproofEventSchema = z.object({
|
|
21
|
+
id: z.string().min(8),
|
|
22
|
+
traceId: z.string().regex(/^[0-9a-f]{32}$/i).optional(),
|
|
23
|
+
parentSpanId: z.string().regex(/^[0-9a-f]{16}$/i).optional(),
|
|
24
|
+
projectId: z.string().min(1),
|
|
25
|
+
serviceName: z.string().min(1),
|
|
26
|
+
environment: z.string().default("development"),
|
|
27
|
+
timestamp: z.string().datetime(),
|
|
28
|
+
kind: TrafficKindSchema,
|
|
29
|
+
request: z.object({
|
|
30
|
+
method: HttpMethodSchema,
|
|
31
|
+
path: z.string(),
|
|
32
|
+
route: z.string().optional(),
|
|
33
|
+
ip: z.string().optional(),
|
|
34
|
+
userAgent: z.string().optional(),
|
|
35
|
+
headers: z.record(z.string()).default({}),
|
|
36
|
+
query: z.record(z.unknown()).default({}),
|
|
37
|
+
body: z.unknown().optional(),
|
|
38
|
+
auth: z
|
|
39
|
+
.object({
|
|
40
|
+
present: z.boolean(),
|
|
41
|
+
scheme: z.string().optional(),
|
|
42
|
+
failed: z.boolean().default(false)
|
|
43
|
+
})
|
|
44
|
+
.default({ present: false, failed: false })
|
|
45
|
+
}),
|
|
46
|
+
response: z.object({
|
|
47
|
+
statusCode: z.number().int().min(100).max(599),
|
|
48
|
+
latencyMs: z.number().nonnegative(),
|
|
49
|
+
bodyBytes: z.number().nonnegative().optional()
|
|
50
|
+
}),
|
|
51
|
+
graphQL: z
|
|
52
|
+
.object({
|
|
53
|
+
operationName: z.string().optional(),
|
|
54
|
+
operationType: z.string().optional()
|
|
55
|
+
})
|
|
56
|
+
.optional(),
|
|
57
|
+
evmRpc: z
|
|
58
|
+
.object({
|
|
59
|
+
method: z.string(),
|
|
60
|
+
chainId: z.string().optional(),
|
|
61
|
+
provider: z.string().optional(),
|
|
62
|
+
walletAddress: z.string().optional(),
|
|
63
|
+
contractAddress: z.string().optional(),
|
|
64
|
+
// Result capture. Every field below is optional so events produced by older SDK
|
|
65
|
+
// versions keep validating; detectors treat a missing field as "not observed"
|
|
66
|
+
// rather than as a negative observation.
|
|
67
|
+
blockTag: z.string().optional(),
|
|
68
|
+
blockNumber: z.string().optional(),
|
|
69
|
+
blockHash: z.string().optional(),
|
|
70
|
+
resultHash: z.string().optional(),
|
|
71
|
+
resultShape: z.enum(["null", "empty_array", "empty_data", "value", "error_in_body"]).optional(),
|
|
72
|
+
resultCount: z.number().int().nonnegative().optional(),
|
|
73
|
+
/** Whether the response carried a rate-limiting signature (D5). */
|
|
74
|
+
throttled: z.boolean().optional(),
|
|
75
|
+
rpcErrorCode: z.number().int().optional(),
|
|
76
|
+
rpcErrorMessage: z.string().optional(),
|
|
77
|
+
// Hash of the endpoint URL. The URL itself must never be stored: it embeds the key.
|
|
78
|
+
endpointHash: z.string().optional(),
|
|
79
|
+
costUnits: z.number().nonnegative().optional(),
|
|
80
|
+
// Links a verification call back to the primary call it shadows (detector D2).
|
|
81
|
+
shadowOfTraceId: z.string().optional(),
|
|
82
|
+
// A signed answer from an attesting endpoint (an Airnode). Attribution, not truth:
|
|
83
|
+
// detectors compare it like any other peer. The worker re-checks the signature
|
|
84
|
+
// against `signedBody` rather than trusting the SDK's verdict.
|
|
85
|
+
attestation: z
|
|
86
|
+
.object({
|
|
87
|
+
scheme: z.literal("airnode"),
|
|
88
|
+
signer: z.string().regex(/^0x[0-9a-fA-F]{40}$/),
|
|
89
|
+
requestHash: z.string().regex(/^0x[0-9a-fA-F]{64}$/),
|
|
90
|
+
timestamp: z.string().regex(/^\d+$/),
|
|
91
|
+
signature: z.string().regex(/^0x[0-9a-fA-F]{130}$/),
|
|
92
|
+
signatureValid: z.boolean(),
|
|
93
|
+
requestBound: z.boolean(),
|
|
94
|
+
// The response text exactly as signed, kept so the evidence can be re-verified
|
|
95
|
+
// later by anyone. Capped: an attestation over a larger body is kept without it.
|
|
96
|
+
signedBody: z.string().max(AIRNODE_SIGNED_BODY_MAX_CHARS).optional()
|
|
97
|
+
})
|
|
98
|
+
.optional()
|
|
99
|
+
})
|
|
100
|
+
.optional(),
|
|
101
|
+
error: z
|
|
102
|
+
.object({
|
|
103
|
+
type: z.string(),
|
|
104
|
+
message: z.string(),
|
|
105
|
+
stack: z.string().optional()
|
|
106
|
+
})
|
|
107
|
+
.optional()
|
|
108
|
+
});
|
|
109
|
+
export const EventBatchSchema = z.object({
|
|
110
|
+
events: z.array(HeightproofEventSchema).min(1).max(100)
|
|
111
|
+
});
|
|
112
|
+
const defaultSensitiveFields = [
|
|
113
|
+
"authorization",
|
|
114
|
+
"cookie",
|
|
115
|
+
"set-cookie",
|
|
116
|
+
"password",
|
|
117
|
+
"passcode",
|
|
118
|
+
"secret",
|
|
119
|
+
"token",
|
|
120
|
+
"accessToken",
|
|
121
|
+
"refreshToken",
|
|
122
|
+
"apiKey",
|
|
123
|
+
"xApiKey",
|
|
124
|
+
"xHeightproofApiKey",
|
|
125
|
+
"privateKey",
|
|
126
|
+
"mnemonic"
|
|
127
|
+
];
|
|
128
|
+
export function classifyTraffic(path, body) {
|
|
129
|
+
if (path.toLowerCase().includes("graphql"))
|
|
130
|
+
return "graphql";
|
|
131
|
+
if (isRecord(body) && typeof body.jsonrpc === "string" && typeof body.method === "string") {
|
|
132
|
+
return "evm_rpc";
|
|
133
|
+
}
|
|
134
|
+
return "rest";
|
|
135
|
+
}
|
|
136
|
+
export function normalizeRoutePath(path) {
|
|
137
|
+
const cleanPath = path.split("?")[0]?.replace(/\/+$/, "") || "/";
|
|
138
|
+
return cleanPath
|
|
139
|
+
.split("/")
|
|
140
|
+
.map((segment) => {
|
|
141
|
+
if (!segment)
|
|
142
|
+
return segment;
|
|
143
|
+
if (/^\d+$/.test(segment))
|
|
144
|
+
return ":id";
|
|
145
|
+
if (/^[0-9a-f]{24}$/i.test(segment))
|
|
146
|
+
return ":objectId";
|
|
147
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(segment)) {
|
|
148
|
+
return ":uuid";
|
|
149
|
+
}
|
|
150
|
+
if (/^0x[0-9a-f]{40}$/i.test(segment))
|
|
151
|
+
return ":address";
|
|
152
|
+
if (/^0x[0-9a-f]{64}$/i.test(segment))
|
|
153
|
+
return ":hash";
|
|
154
|
+
return segment;
|
|
155
|
+
})
|
|
156
|
+
.join("/");
|
|
157
|
+
}
|
|
158
|
+
export function normalizeErrorMessage(message) {
|
|
159
|
+
return message
|
|
160
|
+
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi, ":uuid")
|
|
161
|
+
.replace(/\b\d+\b/g, "#")
|
|
162
|
+
.trim();
|
|
163
|
+
}
|
|
164
|
+
export function redactValue(value, config = {}) {
|
|
165
|
+
const replacement = config.replacement ?? "[REDACTED]";
|
|
166
|
+
const sensitive = new Set([...defaultSensitiveFields, ...(config.fields ?? [])].map(normalizeKey));
|
|
167
|
+
function visit(input, key) {
|
|
168
|
+
if (key && sensitive.has(normalizeKey(key)))
|
|
169
|
+
return replacement;
|
|
170
|
+
if (Array.isArray(input))
|
|
171
|
+
return input.map((entry) => visit(entry));
|
|
172
|
+
if (!isRecord(input))
|
|
173
|
+
return input;
|
|
174
|
+
return Object.fromEntries(Object.entries(input).map(([childKey, childValue]) => [childKey, visit(childValue, childKey)]));
|
|
175
|
+
}
|
|
176
|
+
return visit(value);
|
|
177
|
+
}
|
|
178
|
+
export function redactHeaders(headers, config = {}) {
|
|
179
|
+
const replacement = config.replacement ?? "[REDACTED]";
|
|
180
|
+
const sensitive = new Set([...defaultSensitiveFields, ...(config.headerFields ?? [])].map(normalizeKey));
|
|
181
|
+
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [
|
|
182
|
+
key,
|
|
183
|
+
sensitive.has(normalizeKey(key)) ? replacement : value
|
|
184
|
+
]));
|
|
185
|
+
}
|
|
186
|
+
export async function withSpan(name, attributes, operation) {
|
|
187
|
+
const tracer = trace.getTracer("heightproof");
|
|
188
|
+
const span = tracer.startSpan(name);
|
|
189
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
190
|
+
if (value !== undefined)
|
|
191
|
+
span.setAttribute(key, value);
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const result = await context.with(trace.setSpan(context.active(), span), operation);
|
|
195
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
span.recordException(error);
|
|
200
|
+
span.setStatus({
|
|
201
|
+
code: SpanStatusCode.ERROR,
|
|
202
|
+
message: error instanceof Error ? error.message : "unknown_error"
|
|
203
|
+
});
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
span.end();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export function assessThreat(event, recentIpRequestCount = 0, web3Context = {}) {
|
|
211
|
+
const signals = [];
|
|
212
|
+
if (event.response.statusCode >= 500) {
|
|
213
|
+
signals.push({ name: "server_error", weight: 18, reason: "Request produced a server error" });
|
|
214
|
+
}
|
|
215
|
+
if ([401, 403].includes(event.response.statusCode) || event.request.auth.failed) {
|
|
216
|
+
signals.push({ name: "auth_failure", weight: 24, reason: "Authentication failed or was rejected" });
|
|
217
|
+
}
|
|
218
|
+
if (event.response.latencyMs > 1500) {
|
|
219
|
+
signals.push({ name: "high_latency", weight: 12, reason: "Request latency exceeded 1500ms" });
|
|
220
|
+
}
|
|
221
|
+
if (recentIpRequestCount > 120) {
|
|
222
|
+
signals.push({ name: "rate_anomaly", weight: 30, reason: "IP exceeded normal request volume" });
|
|
223
|
+
}
|
|
224
|
+
if (event.kind === "evm_rpc" && event.evmRpc?.method.match(/send|sign|private|unlock/i)) {
|
|
225
|
+
signals.push({ name: "sensitive_rpc", weight: 26, reason: "EVM RPC method can affect keys or funds" });
|
|
226
|
+
}
|
|
227
|
+
if (event.kind === "evm_rpc") {
|
|
228
|
+
if ((web3Context.recentMethodCallCount ?? 0) > 200) {
|
|
229
|
+
signals.push({
|
|
230
|
+
name: "rpc_flooding",
|
|
231
|
+
weight: 28,
|
|
232
|
+
reason: `${event.evmRpc?.method ?? "This RPC method"} was called far more often than normal from a single source`
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const isTransactionSubmission = Boolean(event.evmRpc?.method.match(/sendrawtransaction|sendtransaction/i));
|
|
236
|
+
if (isTransactionSubmission &&
|
|
237
|
+
(web3Context.walletRecentHourTxCount ?? 0) >= 10 &&
|
|
238
|
+
(web3Context.walletRecentHourTxCount ?? 0) > (web3Context.walletBaselineHourlyTxCount ?? 0) * 10) {
|
|
239
|
+
signals.push({
|
|
240
|
+
name: "tx_burst",
|
|
241
|
+
weight: 30,
|
|
242
|
+
reason: "Wallet transaction volume spiked well above its historical baseline"
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const baselineLatency = web3Context.providerBaselineP95LatencyMs ?? 0;
|
|
246
|
+
const recentLatency = web3Context.providerRecentP95LatencyMs ?? 0;
|
|
247
|
+
if (baselineLatency > 0 && recentLatency > 500 && recentLatency > baselineLatency * 3) {
|
|
248
|
+
signals.push({
|
|
249
|
+
name: "provider_degradation",
|
|
250
|
+
weight: 20,
|
|
251
|
+
reason: "EVM RPC provider latency has degraded well beyond its recent baseline"
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if ((web3Context.providerRecentFailureRate ?? 0) > 0.2) {
|
|
255
|
+
signals.push({
|
|
256
|
+
name: "provider_failures",
|
|
257
|
+
weight: 22,
|
|
258
|
+
reason: "EVM RPC provider is failing an abnormal share of recent requests"
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
signals.push(...detectStaleHead(event, web3Context.staleHead), ...detectSilentFailure(event, web3Context.silentFailure), ...detectCostWaste(event, web3Context.costWaste), ...detectDisagreement(event, web3Context.disagreement), ...detectReorgLag(event, web3Context.reorg), ...detectThrottling(event, web3Context.throttling));
|
|
262
|
+
}
|
|
263
|
+
if (event.kind === "graphql" && String(event.request.body ?? "").length > 10000) {
|
|
264
|
+
signals.push({ name: "large_graphql_payload", weight: 14, reason: "GraphQL payload is unusually large" });
|
|
265
|
+
}
|
|
266
|
+
const score = Math.min(100, signals.reduce((total, signal) => total + signal.weight, 0));
|
|
267
|
+
return {
|
|
268
|
+
score,
|
|
269
|
+
severity: score >= 75 ? "critical" : score >= 50 ? "high" : score >= 25 ? "medium" : "low",
|
|
270
|
+
signals
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function normalizeKey(key) {
|
|
274
|
+
return key.replace(/[-_]/g, "").toLowerCase();
|
|
275
|
+
}
|
|
276
|
+
function isRecord(value) {
|
|
277
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
278
|
+
}
|
package/dist/rpc.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC result capture primitives.
|
|
3
|
+
*
|
|
4
|
+
* Every content-aware detector (see docs/detectors.md) depends on being able to answer
|
|
5
|
+
* "did these two responses say the same thing?" without storing raw chain state. That
|
|
6
|
+
* reduces to normalizing a JSON-RPC result into a canonical form and hashing it.
|
|
7
|
+
*
|
|
8
|
+
* Normalization has to be exactly as aggressive as JSON-RPC's own ambiguity and no more.
|
|
9
|
+
* Too little and honest providers look like they disagree over formatting; too much and
|
|
10
|
+
* genuinely different values collide into the same hash and real disagreement goes unseen.
|
|
11
|
+
*/
|
|
12
|
+
export type ResultShape = "null" | "empty_array" | "empty_data" | "value" | "error_in_body";
|
|
13
|
+
export type BlockTag = string;
|
|
14
|
+
/**
|
|
15
|
+
* Default compute-unit weights, used for cost accounting (detector D6).
|
|
16
|
+
*
|
|
17
|
+
* These are order-of-magnitude defaults modelled on published provider pricing. Providers
|
|
18
|
+
* change their weights, and they differ between providers, so treat these as a starting
|
|
19
|
+
* point to be overridden per deployment rather than as authoritative billing figures.
|
|
20
|
+
*/
|
|
21
|
+
export declare const DEFAULT_METHOD_COST_UNITS: Record<string, number>;
|
|
22
|
+
/** Cost weight for a method, or `undefined` when the method is not in the table. */
|
|
23
|
+
export declare function methodCostUnits(method: string, table?: Record<string, number>): number | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Nominal block time in milliseconds, used by the stale-head detector (D1) to decide how
|
|
26
|
+
* long a head may stand still before it is suspicious. Unknown chains fall back to a
|
|
27
|
+
* measured median at the call site rather than to a global constant.
|
|
28
|
+
*/
|
|
29
|
+
export declare const CHAIN_BLOCK_TIME_MS: Record<string, number>;
|
|
30
|
+
export declare function chainBlockTimeMs(chainId: string | undefined): number | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Hash of an RPC endpoint URL.
|
|
33
|
+
*
|
|
34
|
+
* Provider URLs embed API keys (`https://eth-mainnet.g.alchemy.com/v2/<KEY>`), so the URL
|
|
35
|
+
* itself must never be persisted or transmitted. This lets two endpoints be told apart
|
|
36
|
+
* without Heightproof ever holding the credential.
|
|
37
|
+
*/
|
|
38
|
+
export declare function hashEndpoint(url: string): string;
|
|
39
|
+
/** Canonical hash of a JSON-RPC result, for same-height comparison across providers. */
|
|
40
|
+
export declare function hashRpcResult(result: unknown, method?: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Reduce a JSON-RPC result to canonical form:
|
|
43
|
+
*
|
|
44
|
+
* - object keys sorted, so key order never affects the hash
|
|
45
|
+
* - `undefined`-valued keys dropped; explicit `null` preserved (they mean different things)
|
|
46
|
+
* - hex lowercased everywhere (hex is case-insensitive, so case is never a real difference)
|
|
47
|
+
* - leading zeros stripped only from values the spec defines as QUANTITY
|
|
48
|
+
* - provider-order-dependent arrays (logs) sorted into chain order
|
|
49
|
+
*/
|
|
50
|
+
export declare function normalizeRpcResult(result: unknown, method?: string): unknown;
|
|
51
|
+
/**
|
|
52
|
+
* Classify a response so silent failures (D3) are visible without storing the result.
|
|
53
|
+
*
|
|
54
|
+
* `empty_array` and `null` are recorded as facts, not as failures — emptiness is often
|
|
55
|
+
* correct. D3 only ever fires on a *change* or a disagreement, never on emptiness alone.
|
|
56
|
+
*/
|
|
57
|
+
export declare function classifyResultShape(result: unknown, errorInBody?: boolean): ResultShape;
|
|
58
|
+
/**
|
|
59
|
+
* Number of entries in an array result, used to spot silently truncated pages (D3).
|
|
60
|
+
* Non-array results have no count rather than a count of zero.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resultCount(result: unknown): number | undefined;
|
|
63
|
+
/** The block tag a call was made against, as written by the caller. */
|
|
64
|
+
export declare function extractBlockTag(method: string, params: unknown): BlockTag | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* The concrete height a call resolved against.
|
|
67
|
+
*
|
|
68
|
+
* Prefers a height carried by the result itself, since that is what the provider actually
|
|
69
|
+
* answered from; falls back to a numeric block tag in the request.
|
|
70
|
+
*/
|
|
71
|
+
export declare function extractBlockNumber(method: string, params: unknown, result: unknown): string | undefined;
|
|
72
|
+
/** The block hash a result was served from, when the response carries one (reorg tracking, D4). */
|
|
73
|
+
export declare function extractBlockHash(result: unknown): string | undefined;
|
|
74
|
+
/** A JSON-RPC error object returned inside an HTTP 200 body. */
|
|
75
|
+
export declare function extractRpcError(body: unknown): {
|
|
76
|
+
code?: number;
|
|
77
|
+
message?: string;
|
|
78
|
+
} | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Methods whose answer at a fixed block height is deterministic, and which are therefore
|
|
81
|
+
* safe to replay against another endpoint for comparison (detector D2).
|
|
82
|
+
*
|
|
83
|
+
* This is an allowlist, never a denylist: shadowing `eth_gasPrice` or `eth_estimateGas`
|
|
84
|
+
* would compare two legitimately different answers and report every sample as a
|
|
85
|
+
* disagreement.
|
|
86
|
+
*/
|
|
87
|
+
export declare const VERIFIABLE_METHODS: Set<string>;
|
|
88
|
+
export declare function isVerifiableMethod(method: string): boolean;
|
|
89
|
+
export declare function blockTagParamIndex(method: string): number | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Rewrite a call's block tag to a concrete height so two endpoints can be asked exactly
|
|
92
|
+
* the same question.
|
|
93
|
+
*
|
|
94
|
+
* This is the single most important step in cross-provider verification. Comparing a
|
|
95
|
+
* `latest` response from one endpoint against a `latest` response from another measures
|
|
96
|
+
* how far apart their heads are, not whether they disagree — and at any real head skew
|
|
97
|
+
* that produces a false positive on essentially every sample.
|
|
98
|
+
*
|
|
99
|
+
* Returns `undefined` when the call cannot be pinned, in which case it must not be
|
|
100
|
+
* shadowed at all.
|
|
101
|
+
*/
|
|
102
|
+
export declare function pinBlockTag(method: string, params: unknown, blockNumber: string): unknown[] | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Whether a response carries a rate-limiting signature (detector D5).
|
|
105
|
+
*
|
|
106
|
+
* Covers the clean case (HTTP 429) and the case that actually matters: throttling
|
|
107
|
+
* delivered as a JSON-RPC error inside an HTTP 200, where nothing watching status codes
|
|
108
|
+
* will ever see it.
|
|
109
|
+
*/
|
|
110
|
+
export declare function isThrottleSignature(statusCode: number | undefined, rpcErrorCode: number | undefined, rpcErrorMessage: string | undefined): boolean;
|
|
111
|
+
//# sourceMappingURL=rpc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,aAAa,GAAG,YAAY,GAAG,OAAO,GAAG,eAAe,CAAC;AAE5F,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC;AA2E9B;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAe5D,CAAC;AAEF,oFAAoF;AACpF,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAA6B,GACxD,MAAM,GAAG,SAAS,CAEpB;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAOtD,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGhF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAGtE;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAS5E;AAwED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,EAAE,WAAW,UAAQ,GAAG,WAAW,CASrF;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE/D;AAED,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,CAMrF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAcvG;AAED,mGAAmG;AACnG,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAWpE;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAW9F;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,aAS7B,CAAC;AAKH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAErE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,EAAE,GAAG,SAAS,CAuCvG;AAgBD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,eAAe,EAAE,MAAM,GAAG,SAAS,GAClC,OAAO,CAIT"}
|