@decentrys/protect 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.
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/browser/decentrys-protect.js +901 -0
- package/dist/browser/decentrys-protect.mjs +876 -0
- package/dist/cache.d.ts +41 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +75 -0
- package/dist/cache.js.map +1 -0
- package/dist/classify.d.ts +58 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/classify.js +269 -0
- package/dist/classify.js.map +1 -0
- package/dist/client.d.ts +132 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +307 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/model.d.ts +156 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +80 -0
- package/dist/model.js.map +1 -0
- package/dist/simulation.d.ts +57 -0
- package/dist/simulation.d.ts.map +1 -0
- package/dist/simulation.js +23 -0
- package/dist/simulation.js.map +1 -0
- package/dist/transport.d.ts +61 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +151 -0
- package/dist/transport.js.map +1 -0
- package/dist/wire.d.ts +63 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +274 -0
- package/dist/wire.js.map +1 -0
- package/package.json +64 -0
- package/src/cache.test.ts +67 -0
- package/src/cache.ts +87 -0
- package/src/classify.test.ts +294 -0
- package/src/classify.ts +323 -0
- package/src/client.test.ts +224 -0
- package/src/client.ts +420 -0
- package/src/index.ts +7 -0
- package/src/model.ts +237 -0
- package/src/simulation.ts +71 -0
- package/src/transport.test.ts +129 -0
- package/src/transport.ts +203 -0
- package/src/wire.test.ts +172 -0
- package/src/wire.ts +321 -0
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var DecentrysProtect = (() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
COVERAGE_SIGNAL_TYPES: () => COVERAGE_SIGNAL_TYPES,
|
|
25
|
+
DEFAULT_POLICY: () => DEFAULT_POLICY,
|
|
26
|
+
Decentrys: () => Decentrys,
|
|
27
|
+
HISTORY_STATUS_MEANING: () => HISTORY_STATUS_MEANING,
|
|
28
|
+
HttpTransport: () => HttpTransport,
|
|
29
|
+
MIN_RAISING_CONFIDENCE: () => MIN_RAISING_CONFIDENCE,
|
|
30
|
+
PROTECT_MODEL_VERSION: () => PROTECT_MODEL_VERSION,
|
|
31
|
+
RISK_LEVELS: () => RISK_LEVELS,
|
|
32
|
+
RISK_LEVEL_MEANING: () => RISK_LEVEL_MEANING,
|
|
33
|
+
SDK_VERSION: () => SDK_VERSION,
|
|
34
|
+
TransportError: () => TransportError,
|
|
35
|
+
TtlCache: () => TtlCache,
|
|
36
|
+
applyPolicy: () => applyPolicy,
|
|
37
|
+
cacheKey: () => cacheKey,
|
|
38
|
+
classify: () => classify,
|
|
39
|
+
isAtLeast: () => isAtLeast,
|
|
40
|
+
joinUrl: () => joinUrl,
|
|
41
|
+
normalizeEvidence: () => normalizeEvidence,
|
|
42
|
+
unavailableAssessment: () => unavailableAssessment,
|
|
43
|
+
unavailableSimulation: () => unavailableSimulation
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// src/model.ts
|
|
47
|
+
var RISK_LEVELS = [
|
|
48
|
+
"NO_CRITICAL_RISK_DETECTED",
|
|
49
|
+
"INFORMATIONAL",
|
|
50
|
+
"CAUTION",
|
|
51
|
+
"ELEVATED_RISK",
|
|
52
|
+
"HIGH_RISK",
|
|
53
|
+
"CRITICAL_THREAT",
|
|
54
|
+
"KNOWN_MALICIOUS"
|
|
55
|
+
];
|
|
56
|
+
var RISK_ORDER = {
|
|
57
|
+
NO_CRITICAL_RISK_DETECTED: 0,
|
|
58
|
+
INFORMATIONAL: 1,
|
|
59
|
+
CAUTION: 2,
|
|
60
|
+
ELEVATED_RISK: 3,
|
|
61
|
+
HIGH_RISK: 4,
|
|
62
|
+
CRITICAL_THREAT: 5,
|
|
63
|
+
KNOWN_MALICIOUS: 6
|
|
64
|
+
};
|
|
65
|
+
function isAtLeast(level, floor) {
|
|
66
|
+
return RISK_ORDER[level] >= RISK_ORDER[floor];
|
|
67
|
+
}
|
|
68
|
+
var RISK_LEVEL_MEANING = {
|
|
69
|
+
NO_CRITICAL_RISK_DETECTED: "No known critical threat evidence was identified. This is not an assurance of safety.",
|
|
70
|
+
INFORMATIONAL: "Facts worth knowing before proceeding. Nothing here indicates danger.",
|
|
71
|
+
CAUTION: "A security-sensitive capability or behaviour exists that deserves attention.",
|
|
72
|
+
ELEVATED_RISK: "Several meaningful risk signals are present together.",
|
|
73
|
+
HIGH_RISK: "Strong technical or behavioural evidence of significant danger.",
|
|
74
|
+
CRITICAL_THREAT: "Severe threat supported by concrete evidence.",
|
|
75
|
+
KNOWN_MALICIOUS: "Confirmed malicious infrastructure or behaviour, verified against evidence."
|
|
76
|
+
};
|
|
77
|
+
var HISTORY_STATUS_MEANING = {
|
|
78
|
+
ESTABLISHED: "Substantial on-chain history is available.",
|
|
79
|
+
MODERATE: "Some history is available.",
|
|
80
|
+
LIMITED: "Little history is available yet. This is normal for anything recently deployed and is not a risk finding.",
|
|
81
|
+
NONE: "No history is available. This is not a risk finding."
|
|
82
|
+
};
|
|
83
|
+
var PROTECT_MODEL_VERSION = "protect-1.0.0";
|
|
84
|
+
|
|
85
|
+
// src/classify.ts
|
|
86
|
+
var RAISING_STATUSES = ["ACTIVE"];
|
|
87
|
+
var MIN_RAISING_CONFIDENCE = 0.5;
|
|
88
|
+
function classify(input) {
|
|
89
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
90
|
+
const facts = input.facts ?? [];
|
|
91
|
+
const capabilities = input.capabilities ?? [];
|
|
92
|
+
const unknowns = input.unknowns ?? [];
|
|
93
|
+
const historyStatus = input.historyStatus ?? "LIMITED";
|
|
94
|
+
const threatSignals = (input.threatSignals ?? []).map((signal) => applyDecay(signal, now));
|
|
95
|
+
const raising = threatSignals.filter(
|
|
96
|
+
(s) => RAISING_STATUSES.includes(s.status) && s.confidence >= MIN_RAISING_CONFIDENCE
|
|
97
|
+
);
|
|
98
|
+
const explanation = [];
|
|
99
|
+
let level = "NO_CRITICAL_RISK_DETECTED";
|
|
100
|
+
const confirmed = raising.filter(
|
|
101
|
+
(s) => s.severity === "CRITICAL" && s.evidence.some((e) => e.analystVerified)
|
|
102
|
+
);
|
|
103
|
+
const confirmedMalicious = confirmed.length > 0;
|
|
104
|
+
if (confirmedMalicious) {
|
|
105
|
+
level = "KNOWN_MALICIOUS";
|
|
106
|
+
for (const signal of confirmed) {
|
|
107
|
+
explanation.push(`${signal.explanation} (verified by an analyst)`);
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
const critical = raising.filter((s) => s.severity === "CRITICAL");
|
|
111
|
+
const high = raising.filter((s) => s.severity === "HIGH");
|
|
112
|
+
const medium = raising.filter((s) => s.severity === "MEDIUM");
|
|
113
|
+
if (critical.length > 0) {
|
|
114
|
+
level = "CRITICAL_THREAT";
|
|
115
|
+
for (const s of critical) explanation.push(s.explanation);
|
|
116
|
+
} else if (high.length > 0) {
|
|
117
|
+
level = high.length > 1 ? "HIGH_RISK" : "ELEVATED_RISK";
|
|
118
|
+
for (const s of high) explanation.push(s.explanation);
|
|
119
|
+
} else if (medium.length > 1) {
|
|
120
|
+
level = "ELEVATED_RISK";
|
|
121
|
+
for (const s of medium) explanation.push(s.explanation);
|
|
122
|
+
} else if (medium.length === 1) {
|
|
123
|
+
level = "CAUTION";
|
|
124
|
+
explanation.push(medium[0].explanation);
|
|
125
|
+
}
|
|
126
|
+
const significant = capabilities.filter((c) => c.severity === "SIGNIFICANT");
|
|
127
|
+
if (significant.length > 0 && rank(level) < rank("CAUTION")) {
|
|
128
|
+
level = "CAUTION";
|
|
129
|
+
for (const c of significant) explanation.push(c.statement);
|
|
130
|
+
} else if (rank(level) < rank("INFORMATIONAL") && capabilities.length > 0) {
|
|
131
|
+
level = "INFORMATIONAL";
|
|
132
|
+
for (const c of capabilities.slice(0, 3)) explanation.push(c.statement);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const signal of threatSignals) {
|
|
136
|
+
if (raising.includes(signal)) continue;
|
|
137
|
+
if (signal.status !== "ACTIVE") {
|
|
138
|
+
explanation.push(
|
|
139
|
+
`${signal.explanation} \u2014 this signal is ${signal.status.toLowerCase()} and did not affect the assessment.`
|
|
140
|
+
);
|
|
141
|
+
} else if (signal.confidence < MIN_RAISING_CONFIDENCE) {
|
|
142
|
+
explanation.push(
|
|
143
|
+
`${signal.explanation} \u2014 reported at ${Math.round(signal.confidence * 100)}% confidence, which is too low to raise the risk level on its own.`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (level === "NO_CRITICAL_RISK_DETECTED" && facts.length > 0) {
|
|
148
|
+
level = "INFORMATIONAL";
|
|
149
|
+
}
|
|
150
|
+
if (historyStatus === "LIMITED" || historyStatus === "NONE") {
|
|
151
|
+
explanation.push(HISTORY_STATUS_MEANING[historyStatus]);
|
|
152
|
+
}
|
|
153
|
+
if (unknowns.length > 0) {
|
|
154
|
+
explanation.push(
|
|
155
|
+
`${unknowns.length} ${unknowns.length === 1 ? "attribute is" : "attributes are"} unknown. Unknown is reported as unknown; it does not contribute to risk.`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (explanation.length === 0) {
|
|
159
|
+
explanation.push(RISK_LEVEL_MEANING[level]);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
riskLevel: level,
|
|
163
|
+
confirmedMalicious,
|
|
164
|
+
confidence: confidenceFor(level, raising, historyStatus),
|
|
165
|
+
historyStatus,
|
|
166
|
+
facts,
|
|
167
|
+
capabilities,
|
|
168
|
+
threatSignals,
|
|
169
|
+
unknowns,
|
|
170
|
+
components: {
|
|
171
|
+
technicalRisk: technicalRisk(capabilities),
|
|
172
|
+
behavioralRisk: behavioralRisk(raising),
|
|
173
|
+
threatIntelligenceRisk: threatIntelligenceRisk(raising),
|
|
174
|
+
// Coverage, reported separately so it cannot be summed into a risk total.
|
|
175
|
+
historyConfidence: input.historyConfidence ?? historyConfidenceFor(historyStatus)
|
|
176
|
+
},
|
|
177
|
+
explanation,
|
|
178
|
+
modelVersion: PROTECT_MODEL_VERSION,
|
|
179
|
+
assessedAt: now.toISOString()
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function rank(level) {
|
|
183
|
+
return [
|
|
184
|
+
"NO_CRITICAL_RISK_DETECTED",
|
|
185
|
+
"INFORMATIONAL",
|
|
186
|
+
"CAUTION",
|
|
187
|
+
"ELEVATED_RISK",
|
|
188
|
+
"HIGH_RISK",
|
|
189
|
+
"CRITICAL_THREAT",
|
|
190
|
+
"KNOWN_MALICIOUS"
|
|
191
|
+
].indexOf(level);
|
|
192
|
+
}
|
|
193
|
+
function applyDecay(signal, now) {
|
|
194
|
+
if (signal.status !== "ACTIVE") return signal;
|
|
195
|
+
if (!signal.expiresAt) return signal;
|
|
196
|
+
return Date.parse(signal.expiresAt) <= now.getTime() ? { ...signal, status: "STALE" } : signal;
|
|
197
|
+
}
|
|
198
|
+
function confidenceFor(level, raising, history) {
|
|
199
|
+
if (raising.length > 0) {
|
|
200
|
+
const best = Math.max(...raising.map((s) => s.confidence));
|
|
201
|
+
return Number(best.toFixed(2));
|
|
202
|
+
}
|
|
203
|
+
switch (history) {
|
|
204
|
+
case "ESTABLISHED":
|
|
205
|
+
return 0.85;
|
|
206
|
+
case "MODERATE":
|
|
207
|
+
return 0.7;
|
|
208
|
+
case "LIMITED":
|
|
209
|
+
return 0.5;
|
|
210
|
+
default:
|
|
211
|
+
return 0.35;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function technicalRisk(capabilities) {
|
|
215
|
+
const weight = { INFO: 4, NOTABLE: 12, SIGNIFICANT: 25 };
|
|
216
|
+
return Math.min(100, capabilities.reduce((sum, c) => sum + weight[c.severity], 0));
|
|
217
|
+
}
|
|
218
|
+
function behavioralRisk(raising) {
|
|
219
|
+
const weight = { LOW: 5, MEDIUM: 20, HIGH: 40, CRITICAL: 70 };
|
|
220
|
+
return Math.min(100, raising.filter((s) => s.hops === 0).reduce((sum, s) => sum + weight[s.severity] * s.confidence, 0));
|
|
221
|
+
}
|
|
222
|
+
function threatIntelligenceRisk(raising) {
|
|
223
|
+
const weight = { LOW: 5, MEDIUM: 15, HIGH: 35, CRITICAL: 60 };
|
|
224
|
+
return Math.min(100, raising.reduce((sum, s) => {
|
|
225
|
+
const decay = Math.pow(0.6, Math.max(0, s.hops));
|
|
226
|
+
return sum + weight[s.severity] * s.confidence * decay;
|
|
227
|
+
}, 0));
|
|
228
|
+
}
|
|
229
|
+
function historyConfidenceFor(status) {
|
|
230
|
+
switch (status) {
|
|
231
|
+
case "ESTABLISHED":
|
|
232
|
+
return 90;
|
|
233
|
+
case "MODERATE":
|
|
234
|
+
return 60;
|
|
235
|
+
case "LIMITED":
|
|
236
|
+
return 25;
|
|
237
|
+
default:
|
|
238
|
+
return 5;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
var DEFAULT_POLICY = {
|
|
242
|
+
NO_CRITICAL_RISK_DETECTED: "allow",
|
|
243
|
+
INFORMATIONAL: "inform",
|
|
244
|
+
CAUTION: "warn",
|
|
245
|
+
ELEVATED_RISK: "warn_strong",
|
|
246
|
+
HIGH_RISK: "require_confirmation",
|
|
247
|
+
CRITICAL_THREAT: "require_confirmation",
|
|
248
|
+
KNOWN_MALICIOUS: "block"
|
|
249
|
+
};
|
|
250
|
+
function applyPolicy(assessment, policy = {}) {
|
|
251
|
+
const action = policy[assessment.riskLevel] ?? DEFAULT_POLICY[assessment.riskLevel];
|
|
252
|
+
return {
|
|
253
|
+
action,
|
|
254
|
+
reason: assessment.explanation[0] ?? RISK_LEVEL_MEANING[assessment.riskLevel]
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function unavailableAssessment(failMode, reason, now = /* @__PURE__ */ new Date()) {
|
|
258
|
+
return {
|
|
259
|
+
riskLevel: "NO_CRITICAL_RISK_DETECTED",
|
|
260
|
+
confirmedMalicious: false,
|
|
261
|
+
confidence: 0,
|
|
262
|
+
historyStatus: "NONE",
|
|
263
|
+
facts: [],
|
|
264
|
+
capabilities: [],
|
|
265
|
+
threatSignals: [],
|
|
266
|
+
unknowns: [{
|
|
267
|
+
field: "assessment",
|
|
268
|
+
reason: "PROVIDER_UNAVAILABLE",
|
|
269
|
+
statement: `Decentrys could not be reached: ${reason}. Nothing was checked.`
|
|
270
|
+
}],
|
|
271
|
+
components: { technicalRisk: 0, behavioralRisk: 0, threatIntelligenceRisk: 0, historyConfidence: 0 },
|
|
272
|
+
explanation: [
|
|
273
|
+
`Decentrys could not be reached: ${reason}.`,
|
|
274
|
+
failMode === "closed" ? "This deployment is configured to refuse unverified transactions." : failMode === "warn" ? "No security check was performed. Proceed with the care you would use without any tool." : "No security check was performed."
|
|
275
|
+
],
|
|
276
|
+
modelVersion: PROTECT_MODEL_VERSION,
|
|
277
|
+
assessedAt: now.toISOString()
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/wire.ts
|
|
282
|
+
var COVERAGE_SIGNAL_TYPES = /* @__PURE__ */ new Set([
|
|
283
|
+
"NEW_ADDRESS",
|
|
284
|
+
"NEW_CONTRACT",
|
|
285
|
+
"NEW_DEPLOYMENT",
|
|
286
|
+
"LOW_ACTIVITY",
|
|
287
|
+
"LIMITED_HISTORY",
|
|
288
|
+
"NO_HISTORY",
|
|
289
|
+
"ESTABLISHED_HISTORY",
|
|
290
|
+
"UNVERIFIED_SOURCE",
|
|
291
|
+
"UNVERIFIED_CONTRACT",
|
|
292
|
+
"NO_AUDIT",
|
|
293
|
+
"UNAUDITED",
|
|
294
|
+
"ANONYMOUS_DEPLOYER",
|
|
295
|
+
"ANONYMOUS_TEAM",
|
|
296
|
+
"UNKNOWN_DEPLOYER",
|
|
297
|
+
"LOW_LIQUIDITY",
|
|
298
|
+
"THIN_LIQUIDITY",
|
|
299
|
+
"SMALL_MARKET_CAP",
|
|
300
|
+
"HOLDER_CONCENTRATION",
|
|
301
|
+
"LOW_HOLDER_COUNT",
|
|
302
|
+
"NOT_ON_TOKEN_LIST",
|
|
303
|
+
"UNKNOWN_TOKEN",
|
|
304
|
+
"NO_SOCIAL_PRESENCE"
|
|
305
|
+
]);
|
|
306
|
+
var NO_HISTORY_TYPES = /* @__PURE__ */ new Set(["NO_HISTORY", "UNKNOWN_TOKEN"]);
|
|
307
|
+
function normalizeEvidence(raw, fallback) {
|
|
308
|
+
const body = isRecord(raw) ? raw : {};
|
|
309
|
+
const demotedSignals = [];
|
|
310
|
+
const facts = asArray(body.facts).map(toFact).filter(isPresent);
|
|
311
|
+
const capabilities = asArray(body.capabilities).map(toCapability).filter(isPresent);
|
|
312
|
+
const unknowns = asArray(body.unknowns).map(toUnknown).filter(isPresent);
|
|
313
|
+
const threatSignals = [];
|
|
314
|
+
let noHistoryObserved = false;
|
|
315
|
+
for (const item of asArray(body.threatSignals)) {
|
|
316
|
+
const signal = toSignal(item);
|
|
317
|
+
if (!signal) continue;
|
|
318
|
+
if (COVERAGE_SIGNAL_TYPES.has(signal.type)) {
|
|
319
|
+
demotedSignals.push(signal.type);
|
|
320
|
+
if (NO_HISTORY_TYPES.has(signal.type)) noHistoryObserved = true;
|
|
321
|
+
facts.push({
|
|
322
|
+
type: signal.type,
|
|
323
|
+
value: null,
|
|
324
|
+
statement: signal.explanation,
|
|
325
|
+
source: signal.evidence[0]?.source ?? "decentrys",
|
|
326
|
+
observedAt: signal.lastSeen
|
|
327
|
+
});
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
threatSignals.push(signal);
|
|
331
|
+
}
|
|
332
|
+
const declaredHistory = asHistoryStatus(body.historyStatus);
|
|
333
|
+
const historyStatus = declaredHistory ?? (noHistoryObserved ? "NONE" : "LIMITED");
|
|
334
|
+
return {
|
|
335
|
+
subject: toSubject(body.subject, fallback),
|
|
336
|
+
facts,
|
|
337
|
+
capabilities,
|
|
338
|
+
threatSignals,
|
|
339
|
+
unknowns,
|
|
340
|
+
historyStatus,
|
|
341
|
+
historyConfidence: asNumber(body.historyConfidence) ?? void 0,
|
|
342
|
+
producedAt: asString(body.producedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
343
|
+
demotedSignals
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function toSubject(raw, fallback) {
|
|
347
|
+
if (!isRecord(raw)) return fallback;
|
|
348
|
+
const kind = asString(raw.kind);
|
|
349
|
+
return {
|
|
350
|
+
kind: isSubjectKind(kind) ? kind : fallback.kind,
|
|
351
|
+
chain: asString(raw.chain) ?? fallback.chain,
|
|
352
|
+
identifier: asString(raw.identifier) ?? fallback.identifier
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function toFact(raw) {
|
|
356
|
+
if (!isRecord(raw)) return null;
|
|
357
|
+
const type = asString(raw.type);
|
|
358
|
+
const statement = asString(raw.statement);
|
|
359
|
+
if (!type || !statement) return null;
|
|
360
|
+
return {
|
|
361
|
+
type,
|
|
362
|
+
value: asFactValue(raw.value),
|
|
363
|
+
statement,
|
|
364
|
+
source: asString(raw.source) ?? "decentrys",
|
|
365
|
+
observedAt: asString(raw.observedAt) ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function toCapability(raw) {
|
|
369
|
+
if (!isRecord(raw)) return null;
|
|
370
|
+
const type = asString(raw.type);
|
|
371
|
+
const statement = asString(raw.statement);
|
|
372
|
+
if (!type || !statement) return null;
|
|
373
|
+
const severity = asString(raw.severity);
|
|
374
|
+
return {
|
|
375
|
+
type,
|
|
376
|
+
// An unrecognised severity becomes INFO, the least consequential value.
|
|
377
|
+
// Guessing upward would let a typo raise someone's risk level.
|
|
378
|
+
severity: severity === "NOTABLE" || severity === "SIGNIFICANT" ? severity : "INFO",
|
|
379
|
+
statement,
|
|
380
|
+
grantedBy: asString(raw.grantedBy) ?? void 0
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function toSignal(raw) {
|
|
384
|
+
if (!isRecord(raw)) return null;
|
|
385
|
+
const type = asString(raw.type);
|
|
386
|
+
const explanation = asString(raw.explanation);
|
|
387
|
+
if (!type || !explanation) return null;
|
|
388
|
+
const evidence = asArray(raw.evidence).map(toEvidence).filter(isPresent);
|
|
389
|
+
const createdAt = asString(raw.createdAt) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
390
|
+
return {
|
|
391
|
+
type,
|
|
392
|
+
severity: asSeverity(raw.severity),
|
|
393
|
+
confidence: clamp01(asNumber(raw.confidence) ?? 0),
|
|
394
|
+
explanation,
|
|
395
|
+
// A missing hop count means we do not know how far away this is, and an
|
|
396
|
+
// unknown distance is an inference, not a direct observation.
|
|
397
|
+
hops: Math.max(0, Math.trunc(asNumber(raw.hops) ?? 1)),
|
|
398
|
+
evidence,
|
|
399
|
+
status: asSignalStatus(raw.status),
|
|
400
|
+
createdAt,
|
|
401
|
+
lastSeen: asString(raw.lastSeen) ?? createdAt,
|
|
402
|
+
expiresAt: asString(raw.expiresAt) ?? void 0
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function toEvidence(raw) {
|
|
406
|
+
if (!isRecord(raw)) return null;
|
|
407
|
+
const id = asString(raw.id);
|
|
408
|
+
const type = asString(raw.type);
|
|
409
|
+
if (!id || !type) return null;
|
|
410
|
+
return {
|
|
411
|
+
id,
|
|
412
|
+
type,
|
|
413
|
+
source: asString(raw.source) ?? "decentrys",
|
|
414
|
+
chain: asString(raw.chain) ?? void 0,
|
|
415
|
+
txHash: asString(raw.txHash) ?? void 0,
|
|
416
|
+
contract: asString(raw.contract) ?? void 0,
|
|
417
|
+
address: asString(raw.address) ?? void 0,
|
|
418
|
+
observedAt: asString(raw.observedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
419
|
+
confidence: clamp01(asNumber(raw.confidence) ?? 0),
|
|
420
|
+
// Defaults to false. `analystVerified` is what gates KNOWN_MALICIOUS, so
|
|
421
|
+
// an absent field must never be read as a human having checked.
|
|
422
|
+
analystVerified: raw.analystVerified === true,
|
|
423
|
+
metadata: isRecord(raw.metadata) ? raw.metadata : void 0
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function toUnknown(raw) {
|
|
427
|
+
if (!isRecord(raw)) return null;
|
|
428
|
+
const field = asString(raw.field);
|
|
429
|
+
const statement = asString(raw.statement);
|
|
430
|
+
if (!field || !statement) return null;
|
|
431
|
+
const reason = asString(raw.reason);
|
|
432
|
+
const reasons = ["UNKNOWN", "INSUFFICIENT_DATA", "PROVIDER_UNAVAILABLE", "NOT_APPLICABLE"];
|
|
433
|
+
return {
|
|
434
|
+
field,
|
|
435
|
+
reason: reasons.includes(reason) ? reason : "UNKNOWN",
|
|
436
|
+
statement
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function isRecord(value) {
|
|
440
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
441
|
+
}
|
|
442
|
+
function asArray(value) {
|
|
443
|
+
return Array.isArray(value) ? value : [];
|
|
444
|
+
}
|
|
445
|
+
function asString(value) {
|
|
446
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
447
|
+
}
|
|
448
|
+
function asNumber(value) {
|
|
449
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
450
|
+
}
|
|
451
|
+
function asFactValue(value) {
|
|
452
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
453
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
function asSeverity(value) {
|
|
457
|
+
const severities = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
458
|
+
return severities.includes(value) ? value : "LOW";
|
|
459
|
+
}
|
|
460
|
+
function asSignalStatus(value) {
|
|
461
|
+
const statuses = ["ACTIVE", "STALE", "RESOLVED", "DISPUTED_FACT", "REMOVED"];
|
|
462
|
+
return statuses.includes(value) ? value : "ACTIVE";
|
|
463
|
+
}
|
|
464
|
+
function asHistoryStatus(value) {
|
|
465
|
+
const statuses = ["ESTABLISHED", "MODERATE", "LIMITED", "NONE"];
|
|
466
|
+
return statuses.includes(value) ? value : null;
|
|
467
|
+
}
|
|
468
|
+
function isSubjectKind(value) {
|
|
469
|
+
return value === "address" || value === "contract" || value === "token" || value === "transaction" || value === "approval" || value === "dapp";
|
|
470
|
+
}
|
|
471
|
+
function clamp01(value) {
|
|
472
|
+
return Math.min(1, Math.max(0, value));
|
|
473
|
+
}
|
|
474
|
+
function isPresent(value) {
|
|
475
|
+
return value !== null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/simulation.ts
|
|
479
|
+
function unavailableSimulation(reason, now = /* @__PURE__ */ new Date()) {
|
|
480
|
+
return {
|
|
481
|
+
outcome: "UNAVAILABLE",
|
|
482
|
+
balanceChanges: [],
|
|
483
|
+
approvalChanges: [],
|
|
484
|
+
contractsCalled: [],
|
|
485
|
+
unavailableReason: reason,
|
|
486
|
+
simulatedAt: now.toISOString()
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/transport.ts
|
|
491
|
+
var TransportError = class extends Error {
|
|
492
|
+
failure;
|
|
493
|
+
status;
|
|
494
|
+
constructor(failure, message, status) {
|
|
495
|
+
super(message);
|
|
496
|
+
this.name = "TransportError";
|
|
497
|
+
this.failure = failure;
|
|
498
|
+
this.status = status;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
function failureForStatus(status) {
|
|
502
|
+
if (status === 401) return "unauthorized";
|
|
503
|
+
if (status === 403) return "forbidden";
|
|
504
|
+
if (status === 429) return "rate_limited";
|
|
505
|
+
if (status >= 400 && status < 500) return "invalid_request";
|
|
506
|
+
return "server_error";
|
|
507
|
+
}
|
|
508
|
+
var RETRYABLE = ["network", "timeout", "server_error", "rate_limited"];
|
|
509
|
+
var HttpTransport = class {
|
|
510
|
+
constructor(config) {
|
|
511
|
+
this.config = config;
|
|
512
|
+
}
|
|
513
|
+
config;
|
|
514
|
+
async request(options) {
|
|
515
|
+
const attempts = options.idempotent ? this.config.retries + 1 : 1;
|
|
516
|
+
let last = new TransportError("network", "No attempt was made.");
|
|
517
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
518
|
+
try {
|
|
519
|
+
return await this.attempt(options);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
last = error instanceof TransportError ? error : new TransportError("network", error instanceof Error ? error.message : "Unknown error.");
|
|
522
|
+
if (!RETRYABLE.includes(last.failure)) throw last;
|
|
523
|
+
if (attempt === attempts - 1) throw last;
|
|
524
|
+
await delay(120 * (attempt + 1));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
throw last;
|
|
528
|
+
}
|
|
529
|
+
async attempt(options) {
|
|
530
|
+
const controller = new AbortController();
|
|
531
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
532
|
+
const onAbort = () => controller.abort();
|
|
533
|
+
options.signal?.addEventListener("abort", onAbort);
|
|
534
|
+
try {
|
|
535
|
+
const response = await this.config.fetch(joinUrl(this.config.baseUrl, options.path), {
|
|
536
|
+
method: "POST",
|
|
537
|
+
headers: {
|
|
538
|
+
"content-type": "application/json",
|
|
539
|
+
"x-api-key": this.config.apiKey,
|
|
540
|
+
"user-agent": this.config.userAgent
|
|
541
|
+
},
|
|
542
|
+
body: JSON.stringify(options.body ?? {}),
|
|
543
|
+
signal: controller.signal
|
|
544
|
+
});
|
|
545
|
+
const text = await response.text();
|
|
546
|
+
if (!response.ok) {
|
|
547
|
+
throw new TransportError(
|
|
548
|
+
failureForStatus(response.status),
|
|
549
|
+
messageFrom(text) ?? `Decentrys returned HTTP ${response.status}.`,
|
|
550
|
+
response.status
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
let parsed;
|
|
554
|
+
try {
|
|
555
|
+
parsed = JSON.parse(text);
|
|
556
|
+
} catch {
|
|
557
|
+
throw new TransportError("malformed_response", "The response was not valid JSON.");
|
|
558
|
+
}
|
|
559
|
+
const envelope = parsed;
|
|
560
|
+
return envelope && typeof envelope === "object" && "data" in envelope ? envelope.data : parsed;
|
|
561
|
+
} catch (error) {
|
|
562
|
+
if (error instanceof TransportError) throw error;
|
|
563
|
+
if (isAbort(error)) {
|
|
564
|
+
throw options.signal?.aborted ? new TransportError("network", "The request was cancelled by the caller.") : new TransportError("timeout", `No response within ${this.config.timeoutMs}ms.`);
|
|
565
|
+
}
|
|
566
|
+
throw new TransportError("network", error instanceof Error ? error.message : "Network request failed.");
|
|
567
|
+
} finally {
|
|
568
|
+
clearTimeout(timer);
|
|
569
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
function isAbort(error) {
|
|
574
|
+
return typeof error === "object" && error !== null && (error.name === "AbortError" || error.code === "ABORT_ERR");
|
|
575
|
+
}
|
|
576
|
+
function messageFrom(text) {
|
|
577
|
+
try {
|
|
578
|
+
const body = JSON.parse(text);
|
|
579
|
+
if (typeof body.message === "string" && body.message.trim()) return body.message;
|
|
580
|
+
if (typeof body.error === "string" && body.error.trim()) return body.error;
|
|
581
|
+
} catch {
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
function joinUrl(base, path) {
|
|
586
|
+
return `${base.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
587
|
+
}
|
|
588
|
+
function delay(ms) {
|
|
589
|
+
return new Promise((resolve) => {
|
|
590
|
+
setTimeout(resolve, ms);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/cache.ts
|
|
595
|
+
var TtlCache = class {
|
|
596
|
+
constructor(options) {
|
|
597
|
+
this.options = options;
|
|
598
|
+
this.now = options.now ?? (() => Date.now());
|
|
599
|
+
}
|
|
600
|
+
options;
|
|
601
|
+
entries = /* @__PURE__ */ new Map();
|
|
602
|
+
now;
|
|
603
|
+
get(key) {
|
|
604
|
+
const entry = this.entries.get(key);
|
|
605
|
+
if (!entry) return void 0;
|
|
606
|
+
if (entry.expiresAt <= this.now()) {
|
|
607
|
+
this.entries.delete(key);
|
|
608
|
+
return void 0;
|
|
609
|
+
}
|
|
610
|
+
this.entries.delete(key);
|
|
611
|
+
this.entries.set(key, entry);
|
|
612
|
+
return entry.value;
|
|
613
|
+
}
|
|
614
|
+
set(key, value, ttlMs = this.options.ttlMs) {
|
|
615
|
+
if (ttlMs <= 0 || this.options.maxEntries <= 0) return;
|
|
616
|
+
this.entries.delete(key);
|
|
617
|
+
this.entries.set(key, { value, expiresAt: this.now() + ttlMs });
|
|
618
|
+
while (this.entries.size > this.options.maxEntries) {
|
|
619
|
+
const oldest = this.entries.keys().next();
|
|
620
|
+
if (oldest.done) break;
|
|
621
|
+
this.entries.delete(oldest.value);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
delete(key) {
|
|
625
|
+
this.entries.delete(key);
|
|
626
|
+
}
|
|
627
|
+
clear() {
|
|
628
|
+
this.entries.clear();
|
|
629
|
+
}
|
|
630
|
+
get size() {
|
|
631
|
+
return this.entries.size;
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
function cacheKey(kind, parts) {
|
|
635
|
+
return [kind, ...parts.map((p) => p === void 0 || p === null ? "" : String(p))].join("|");
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/client.ts
|
|
639
|
+
var SDK_VERSION = "0.1.0";
|
|
640
|
+
var DEFAULT_BASE_URL = "https://api.decentrys.com";
|
|
641
|
+
var DEFAULT_TIMEOUT_MS = 4e3;
|
|
642
|
+
var DEFAULT_CACHE_TTL_MS = 12e4;
|
|
643
|
+
var DEFAULT_CACHE_ENTRIES = 500;
|
|
644
|
+
var Decentrys = class {
|
|
645
|
+
transport;
|
|
646
|
+
failMode;
|
|
647
|
+
policy;
|
|
648
|
+
cache;
|
|
649
|
+
constructor(config) {
|
|
650
|
+
if (!config.apiKey || !config.apiKey.trim()) {
|
|
651
|
+
throw new Error("Decentrys: an apiKey is required. Create one at https://decentrys.com/developers.");
|
|
652
|
+
}
|
|
653
|
+
this.failMode = config.failMode ?? "warn";
|
|
654
|
+
this.policy = config.policy ?? {};
|
|
655
|
+
this.cache = new TtlCache({
|
|
656
|
+
ttlMs: config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
|
|
657
|
+
maxEntries: config.cacheMaxEntries ?? DEFAULT_CACHE_ENTRIES
|
|
658
|
+
});
|
|
659
|
+
this.transport = config.transport ?? new HttpTransport({
|
|
660
|
+
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
|
|
661
|
+
apiKey: config.apiKey,
|
|
662
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
663
|
+
retries: config.retries ?? 1,
|
|
664
|
+
fetch: config.fetch ?? resolveFetch(),
|
|
665
|
+
userAgent: `decentrys-protect/${SDK_VERSION}`
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
// -------------------------------------------------------------------------
|
|
669
|
+
// Assessments
|
|
670
|
+
// -------------------------------------------------------------------------
|
|
671
|
+
/** Pre-sign analysis of a transaction the user is about to approve. */
|
|
672
|
+
async assessTransaction(tx, options = {}) {
|
|
673
|
+
return this.assess({
|
|
674
|
+
path: "/v1/protect/transaction",
|
|
675
|
+
body: tx,
|
|
676
|
+
subject: { kind: "transaction", chain: tx.chain, identifier: tx.to ?? tx.from },
|
|
677
|
+
// A transaction's assessment depends on its calldata and its moment.
|
|
678
|
+
// Caching one would serve a stale answer for a different transaction.
|
|
679
|
+
cacheable: false,
|
|
680
|
+
idempotent: true,
|
|
681
|
+
options
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
/** What a contract *can* do — capabilities, proxy status, admin controls. */
|
|
685
|
+
async scanContract(request, options = {}) {
|
|
686
|
+
return this.assess({
|
|
687
|
+
path: "/v1/protect/contract",
|
|
688
|
+
body: request,
|
|
689
|
+
subject: { kind: "contract", chain: request.chain, identifier: request.address },
|
|
690
|
+
cacheable: true,
|
|
691
|
+
idempotent: true,
|
|
692
|
+
options
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
async screenAddress(request, options = {}) {
|
|
696
|
+
return this.assess({
|
|
697
|
+
path: "/v1/protect/address",
|
|
698
|
+
body: request,
|
|
699
|
+
subject: { kind: "address", chain: request.chain, identifier: request.address },
|
|
700
|
+
cacheable: true,
|
|
701
|
+
idempotent: true,
|
|
702
|
+
options
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
async screenToken(request, options = {}) {
|
|
706
|
+
return this.assess({
|
|
707
|
+
path: "/v1/protect/token",
|
|
708
|
+
body: request,
|
|
709
|
+
subject: { kind: "token", chain: request.chain, identifier: request.address },
|
|
710
|
+
cacheable: true,
|
|
711
|
+
idempotent: true,
|
|
712
|
+
options
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* An approval is assessed on the spender and the allowance together.
|
|
717
|
+
*
|
|
718
|
+
* Not cached: the same spender with an unlimited allowance and with a
|
|
719
|
+
* one-off allowance are different decisions, and the amount is the part a
|
|
720
|
+
* user most needs told.
|
|
721
|
+
*/
|
|
722
|
+
async screenApproval(request, options = {}) {
|
|
723
|
+
return this.assess({
|
|
724
|
+
path: "/v1/protect/approval",
|
|
725
|
+
body: request,
|
|
726
|
+
subject: { kind: "approval", chain: request.chain, identifier: request.spender },
|
|
727
|
+
cacheable: false,
|
|
728
|
+
idempotent: true,
|
|
729
|
+
options
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
async assessDapp(request, options = {}) {
|
|
733
|
+
return this.assess({
|
|
734
|
+
path: "/v1/protect/dapp",
|
|
735
|
+
body: request,
|
|
736
|
+
subject: { kind: "dapp", chain: request.chain ?? "multi", identifier: request.origin },
|
|
737
|
+
cacheable: true,
|
|
738
|
+
idempotent: true,
|
|
739
|
+
options
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* The threat signals on a subject, without a classification.
|
|
744
|
+
*
|
|
745
|
+
* For integrators building their own presentation. An empty array means no
|
|
746
|
+
* signals were found — which is not the same as safe, and the SDK will not
|
|
747
|
+
* pretend otherwise on their behalf.
|
|
748
|
+
*/
|
|
749
|
+
async getThreatSignals(request, options = {}) {
|
|
750
|
+
try {
|
|
751
|
+
const raw = await this.transport.request({
|
|
752
|
+
path: "/v1/protect/signals",
|
|
753
|
+
body: request,
|
|
754
|
+
idempotent: true,
|
|
755
|
+
signal: options.signal
|
|
756
|
+
});
|
|
757
|
+
return normalizeEvidence(raw, {
|
|
758
|
+
kind: "address",
|
|
759
|
+
chain: request.chain,
|
|
760
|
+
identifier: request.address
|
|
761
|
+
}).threatSignals;
|
|
762
|
+
} catch {
|
|
763
|
+
return [];
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
// -------------------------------------------------------------------------
|
|
767
|
+
// Decoding and simulation
|
|
768
|
+
// -------------------------------------------------------------------------
|
|
769
|
+
/** What this transaction does, in the words a user would use. */
|
|
770
|
+
async explainTransaction(tx, options = {}) {
|
|
771
|
+
try {
|
|
772
|
+
const raw = await this.transport.request({
|
|
773
|
+
path: "/v1/protect/explain",
|
|
774
|
+
body: tx,
|
|
775
|
+
idempotent: true,
|
|
776
|
+
signal: options.signal
|
|
777
|
+
});
|
|
778
|
+
return {
|
|
779
|
+
summary: typeof raw?.summary === "string" && raw.summary ? raw.summary : "This transaction could not be decoded.",
|
|
780
|
+
actions: stringList(raw?.actions),
|
|
781
|
+
exposure: stringList(raw?.exposure),
|
|
782
|
+
undecoded: stringList(raw?.undecoded)
|
|
783
|
+
};
|
|
784
|
+
} catch (error) {
|
|
785
|
+
return {
|
|
786
|
+
summary: "This transaction could not be decoded.",
|
|
787
|
+
actions: [],
|
|
788
|
+
exposure: [],
|
|
789
|
+
undecoded: [`Decentrys could not be reached: ${describe(error)}.`]
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/** Execute the transaction against a fork and report what would change. */
|
|
794
|
+
async simulateTransaction(tx, options = {}) {
|
|
795
|
+
try {
|
|
796
|
+
const raw = await this.transport.request({
|
|
797
|
+
path: "/v1/protect/simulate",
|
|
798
|
+
body: tx,
|
|
799
|
+
idempotent: true,
|
|
800
|
+
signal: options.signal
|
|
801
|
+
});
|
|
802
|
+
return normalizeSimulation(raw);
|
|
803
|
+
} catch (error) {
|
|
804
|
+
return unavailableSimulation(describe(error));
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
// -------------------------------------------------------------------------
|
|
808
|
+
// Cache control
|
|
809
|
+
// -------------------------------------------------------------------------
|
|
810
|
+
/** Drop cached evidence. Call after a user reports a stale result. */
|
|
811
|
+
clearCache() {
|
|
812
|
+
this.cache.clear();
|
|
813
|
+
}
|
|
814
|
+
// -------------------------------------------------------------------------
|
|
815
|
+
// Internals
|
|
816
|
+
// -------------------------------------------------------------------------
|
|
817
|
+
async assess(params) {
|
|
818
|
+
const key = params.cacheable ? cacheKey(params.subject.kind, [params.subject.chain, params.subject.identifier]) : null;
|
|
819
|
+
if (key && !params.options.skipCache) {
|
|
820
|
+
const hit = this.cache.get(key);
|
|
821
|
+
if (hit) return this.finish(hit, true);
|
|
822
|
+
}
|
|
823
|
+
let evidence;
|
|
824
|
+
try {
|
|
825
|
+
const raw = await this.transport.request({
|
|
826
|
+
path: params.path,
|
|
827
|
+
body: params.body,
|
|
828
|
+
idempotent: params.idempotent,
|
|
829
|
+
signal: params.options.signal
|
|
830
|
+
});
|
|
831
|
+
evidence = normalizeEvidence(raw, params.subject);
|
|
832
|
+
} catch (error) {
|
|
833
|
+
return {
|
|
834
|
+
subject: params.subject,
|
|
835
|
+
assessment: unavailableAssessment(this.failMode, describe(error)),
|
|
836
|
+
decision: {
|
|
837
|
+
// `closed` is the only mode where unavailability is itself a stop.
|
|
838
|
+
// The others must not fabricate a risk level to justify blocking.
|
|
839
|
+
action: this.failMode === "closed" ? "block" : "warn",
|
|
840
|
+
reason: `Decentrys could not be reached: ${describe(error)}.`
|
|
841
|
+
},
|
|
842
|
+
cached: false,
|
|
843
|
+
demotedSignals: []
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
if (key) this.cache.set(key, evidence);
|
|
847
|
+
return this.finish(evidence, false);
|
|
848
|
+
}
|
|
849
|
+
finish(evidence, cached) {
|
|
850
|
+
const assessment = classify({
|
|
851
|
+
facts: evidence.facts,
|
|
852
|
+
capabilities: evidence.capabilities,
|
|
853
|
+
threatSignals: evidence.threatSignals,
|
|
854
|
+
unknowns: evidence.unknowns,
|
|
855
|
+
historyStatus: evidence.historyStatus,
|
|
856
|
+
historyConfidence: evidence.historyConfidence
|
|
857
|
+
});
|
|
858
|
+
return {
|
|
859
|
+
subject: evidence.subject,
|
|
860
|
+
assessment,
|
|
861
|
+
decision: applyPolicy(assessment, this.policy),
|
|
862
|
+
cached,
|
|
863
|
+
demotedSignals: evidence.demotedSignals
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
function resolveFetch() {
|
|
868
|
+
const candidate = globalThis.fetch;
|
|
869
|
+
if (typeof candidate !== "function") {
|
|
870
|
+
throw new Error(
|
|
871
|
+
"Decentrys: no global fetch was found. Pass one via `new Decentrys({ fetch })` (Node 18+, modern browsers and React Native provide one)."
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
return candidate.bind(globalThis);
|
|
875
|
+
}
|
|
876
|
+
function describe(error) {
|
|
877
|
+
if (error instanceof TransportError) return error.message;
|
|
878
|
+
if (error instanceof Error) return error.message;
|
|
879
|
+
return "unknown error";
|
|
880
|
+
}
|
|
881
|
+
function stringList(value) {
|
|
882
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.length > 0) : [];
|
|
883
|
+
}
|
|
884
|
+
function normalizeSimulation(raw) {
|
|
885
|
+
const body = typeof raw === "object" && raw !== null ? raw : {};
|
|
886
|
+
const outcomes = ["SUCCESS", "REVERT", "NOT_SUPPORTED", "UNAVAILABLE"];
|
|
887
|
+
return {
|
|
888
|
+
// An unrecognised outcome is not a success. Defaulting the other way would
|
|
889
|
+
// let a malformed response read as "this transaction is fine".
|
|
890
|
+
outcome: outcomes.includes(body.outcome) ? body.outcome : "UNAVAILABLE",
|
|
891
|
+
revertReason: typeof body.revertReason === "string" ? body.revertReason : void 0,
|
|
892
|
+
balanceChanges: Array.isArray(body.balanceChanges) ? body.balanceChanges : [],
|
|
893
|
+
approvalChanges: Array.isArray(body.approvalChanges) ? body.approvalChanges : [],
|
|
894
|
+
contractsCalled: stringList(body.contractsCalled),
|
|
895
|
+
gasUsed: typeof body.gasUsed === "string" ? body.gasUsed : void 0,
|
|
896
|
+
unavailableReason: typeof body.unavailableReason === "string" ? body.unavailableReason : void 0,
|
|
897
|
+
simulatedAt: typeof body.simulatedAt === "string" ? body.simulatedAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
return __toCommonJS(index_exports);
|
|
901
|
+
})();
|