@heightproof/shared 0.0.0-stage → 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
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"}