@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,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulation and plain-language decoding.
|
|
3
|
+
*
|
|
4
|
+
* These answer a different question from risk. "Is this dangerous?" is the
|
|
5
|
+
* classifier's job; "what does this transaction actually do?" is this one's,
|
|
6
|
+
* and for most users it is the more useful of the two. A person who can read
|
|
7
|
+
* "this grants unlimited spending of your USDC to an address you have never
|
|
8
|
+
* interacted with" does not need to be told a score.
|
|
9
|
+
*/
|
|
10
|
+
export interface BalanceChange {
|
|
11
|
+
/** Whose balance moves. Usually the signer, sometimes a contract. */
|
|
12
|
+
address: string;
|
|
13
|
+
asset: string;
|
|
14
|
+
symbol?: string;
|
|
15
|
+
decimals?: number;
|
|
16
|
+
/** Signed, base units. Negative leaves the address. */
|
|
17
|
+
delta: string;
|
|
18
|
+
usdValue?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface ApprovalChange {
|
|
21
|
+
owner: string;
|
|
22
|
+
spender: string;
|
|
23
|
+
token: string;
|
|
24
|
+
symbol?: string;
|
|
25
|
+
/** Base units, or `unlimited`. */
|
|
26
|
+
amount: string;
|
|
27
|
+
unlimited: boolean;
|
|
28
|
+
}
|
|
29
|
+
export type SimulationOutcome = 'SUCCESS' | 'REVERT' | 'NOT_SUPPORTED' | 'UNAVAILABLE';
|
|
30
|
+
export interface SimulationResult {
|
|
31
|
+
outcome: SimulationOutcome;
|
|
32
|
+
/** Present when the outcome is REVERT and the chain reported a reason. */
|
|
33
|
+
revertReason?: string;
|
|
34
|
+
balanceChanges: BalanceChange[];
|
|
35
|
+
approvalChanges: ApprovalChange[];
|
|
36
|
+
/** Contracts the transaction touches, in call order. */
|
|
37
|
+
contractsCalled: string[];
|
|
38
|
+
gasUsed?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Why simulation could not run, when it could not. Stated rather than
|
|
41
|
+
* silently returning an empty result that reads like "nothing happens".
|
|
42
|
+
*/
|
|
43
|
+
unavailableReason?: string;
|
|
44
|
+
simulatedAt: string;
|
|
45
|
+
}
|
|
46
|
+
export interface TransactionExplanation {
|
|
47
|
+
/** One sentence, in the words a user would use. */
|
|
48
|
+
summary: string;
|
|
49
|
+
/** Each distinct thing the transaction does, in order. */
|
|
50
|
+
actions: string[];
|
|
51
|
+
/** What the signer gives up if this is not what they intended. */
|
|
52
|
+
exposure: string[];
|
|
53
|
+
/** Anything the decoder could not resolve, stated as unresolved. */
|
|
54
|
+
undecoded: string[];
|
|
55
|
+
}
|
|
56
|
+
export declare function unavailableSimulation(reason: string, now?: Date): SimulationResult;
|
|
57
|
+
//# sourceMappingURL=simulation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"simulation.d.ts","sourceRoot":"","sources":["../src/simulation.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,aAAa;IAC5B,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,QAAQ,GAAG,eAAe,GAAG,aAAa,CAAC;AAEvF,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,wDAAwD;IACxD,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,sBAAsB;IACrC,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oEAAoE;IACpE,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAa,GAAG,gBAAgB,CASxF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Simulation and plain-language decoding.
|
|
4
|
+
*
|
|
5
|
+
* These answer a different question from risk. "Is this dangerous?" is the
|
|
6
|
+
* classifier's job; "what does this transaction actually do?" is this one's,
|
|
7
|
+
* and for most users it is the more useful of the two. A person who can read
|
|
8
|
+
* "this grants unlimited spending of your USDC to an address you have never
|
|
9
|
+
* interacted with" does not need to be told a score.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.unavailableSimulation = unavailableSimulation;
|
|
13
|
+
function unavailableSimulation(reason, now = new Date()) {
|
|
14
|
+
return {
|
|
15
|
+
outcome: 'UNAVAILABLE',
|
|
16
|
+
balanceChanges: [],
|
|
17
|
+
approvalChanges: [],
|
|
18
|
+
contractsCalled: [],
|
|
19
|
+
unavailableReason: reason,
|
|
20
|
+
simulatedAt: now.toISOString(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=simulation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"simulation.js","sourceRoot":"","sources":["../src/simulation.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAqDH,sDASC;AATD,SAAgB,qBAAqB,CAAC,MAAc,EAAE,GAAG,GAAG,IAAI,IAAI,EAAE;IACpE,OAAO;QACL,OAAO,EAAE,aAAa;QACtB,cAAc,EAAE,EAAE;QAClB,eAAe,EAAE,EAAE;QACnB,eAAe,EAAE,EAAE;QACnB,iBAAiB,EAAE,MAAM;QACzB,WAAW,EAAE,GAAG,CAAC,WAAW,EAAE;KAC/B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP transport for the Protect SDK.
|
|
3
|
+
*
|
|
4
|
+
* Three properties matter more than anything else here, because this code runs
|
|
5
|
+
* inside a wallet on the path to signing a transaction:
|
|
6
|
+
*
|
|
7
|
+
* 1. **It always returns.** A hung request is worse than a failed one — the
|
|
8
|
+
* user is staring at a spinner holding a signature. Every request carries
|
|
9
|
+
* a deadline it cannot exceed.
|
|
10
|
+
* 2. **It never retries a write.** Retrying an idempotent lookup is free;
|
|
11
|
+
* retrying anything else risks doing the thing twice.
|
|
12
|
+
* 3. **It has no ambient dependencies.** `fetch` is injected rather than
|
|
13
|
+
* reached for, so the same code runs in a browser extension, React Native,
|
|
14
|
+
* a Node service and a test without a global to patch.
|
|
15
|
+
*/
|
|
16
|
+
export type FetchLike = (url: string, init: {
|
|
17
|
+
method: string;
|
|
18
|
+
headers: Record<string, string>;
|
|
19
|
+
body?: string;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}) => Promise<{
|
|
22
|
+
ok: boolean;
|
|
23
|
+
status: number;
|
|
24
|
+
text: () => Promise<string>;
|
|
25
|
+
}>;
|
|
26
|
+
export type TransportFailure = 'timeout' | 'network' | 'unauthorized' | 'forbidden' | 'rate_limited' | 'invalid_request' | 'server_error' | 'malformed_response';
|
|
27
|
+
export declare class TransportError extends Error {
|
|
28
|
+
readonly failure: TransportFailure;
|
|
29
|
+
readonly status?: number;
|
|
30
|
+
constructor(failure: TransportFailure, message: string, status?: number);
|
|
31
|
+
}
|
|
32
|
+
export interface RequestOptions {
|
|
33
|
+
path: string;
|
|
34
|
+
body?: unknown;
|
|
35
|
+
/**
|
|
36
|
+
* Safe to repeat. Only idempotent requests are retried; everything else
|
|
37
|
+
* fails on the first attempt rather than risking a duplicate.
|
|
38
|
+
*/
|
|
39
|
+
idempotent?: boolean;
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
}
|
|
42
|
+
export interface TransportConfig {
|
|
43
|
+
baseUrl: string;
|
|
44
|
+
apiKey: string;
|
|
45
|
+
timeoutMs: number;
|
|
46
|
+
/** Retries for idempotent requests only. 0 disables retrying entirely. */
|
|
47
|
+
retries: number;
|
|
48
|
+
fetch: FetchLike;
|
|
49
|
+
userAgent: string;
|
|
50
|
+
}
|
|
51
|
+
export interface Transport {
|
|
52
|
+
request<T>(options: RequestOptions): Promise<T>;
|
|
53
|
+
}
|
|
54
|
+
export declare class HttpTransport implements Transport {
|
|
55
|
+
private readonly config;
|
|
56
|
+
constructor(config: TransportConfig);
|
|
57
|
+
request<T>(options: RequestOptions): Promise<T>;
|
|
58
|
+
private attempt;
|
|
59
|
+
}
|
|
60
|
+
export declare function joinUrl(base: string, path: string): string;
|
|
61
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,MAAM,SAAS,GAAG,CACtB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IACJ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,KACE,OAAO,CAAC;IACX,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7B,CAAC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,SAAS,GACT,cAAc,GACd,WAAW,GACX,cAAc,GACd,iBAAiB,GACjB,cAAc,GACd,oBAAoB,CAAC;AAEzB,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;gBAEb,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAMxE;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACjD;AAaD,qBAAa,aAAc,YAAW,SAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,eAAe;IAE9C,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;YAwBvC,OAAO;CA0DtB;AAuBD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* HTTP transport for the Protect SDK.
|
|
4
|
+
*
|
|
5
|
+
* Three properties matter more than anything else here, because this code runs
|
|
6
|
+
* inside a wallet on the path to signing a transaction:
|
|
7
|
+
*
|
|
8
|
+
* 1. **It always returns.** A hung request is worse than a failed one — the
|
|
9
|
+
* user is staring at a spinner holding a signature. Every request carries
|
|
10
|
+
* a deadline it cannot exceed.
|
|
11
|
+
* 2. **It never retries a write.** Retrying an idempotent lookup is free;
|
|
12
|
+
* retrying anything else risks doing the thing twice.
|
|
13
|
+
* 3. **It has no ambient dependencies.** `fetch` is injected rather than
|
|
14
|
+
* reached for, so the same code runs in a browser extension, React Native,
|
|
15
|
+
* a Node service and a test without a global to patch.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.HttpTransport = exports.TransportError = void 0;
|
|
19
|
+
exports.joinUrl = joinUrl;
|
|
20
|
+
class TransportError extends Error {
|
|
21
|
+
failure;
|
|
22
|
+
status;
|
|
23
|
+
constructor(failure, message, status) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'TransportError';
|
|
26
|
+
this.failure = failure;
|
|
27
|
+
this.status = status;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.TransportError = TransportError;
|
|
31
|
+
/** 4xx that will never succeed on a repeat. Retrying these only adds latency. */
|
|
32
|
+
function failureForStatus(status) {
|
|
33
|
+
if (status === 401)
|
|
34
|
+
return 'unauthorized';
|
|
35
|
+
if (status === 403)
|
|
36
|
+
return 'forbidden';
|
|
37
|
+
if (status === 429)
|
|
38
|
+
return 'rate_limited';
|
|
39
|
+
if (status >= 400 && status < 500)
|
|
40
|
+
return 'invalid_request';
|
|
41
|
+
return 'server_error';
|
|
42
|
+
}
|
|
43
|
+
const RETRYABLE = ['network', 'timeout', 'server_error', 'rate_limited'];
|
|
44
|
+
class HttpTransport {
|
|
45
|
+
config;
|
|
46
|
+
constructor(config) {
|
|
47
|
+
this.config = config;
|
|
48
|
+
}
|
|
49
|
+
async request(options) {
|
|
50
|
+
const attempts = options.idempotent ? this.config.retries + 1 : 1;
|
|
51
|
+
let last = new TransportError('network', 'No attempt was made.');
|
|
52
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
53
|
+
try {
|
|
54
|
+
return await this.attempt(options);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
last = error instanceof TransportError
|
|
58
|
+
? error
|
|
59
|
+
: new TransportError('network', error instanceof Error ? error.message : 'Unknown error.');
|
|
60
|
+
if (!RETRYABLE.includes(last.failure))
|
|
61
|
+
throw last;
|
|
62
|
+
if (attempt === attempts - 1)
|
|
63
|
+
throw last;
|
|
64
|
+
// A short, bounded backoff. Anything longer defeats the point: the
|
|
65
|
+
// user is waiting on a signing screen, not a batch job.
|
|
66
|
+
await delay(120 * (attempt + 1));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw last;
|
|
70
|
+
}
|
|
71
|
+
async attempt(options) {
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
74
|
+
// The caller's own cancellation (the user navigated away, the popup
|
|
75
|
+
// closed) has to reach the request too, or it keeps running after the
|
|
76
|
+
// screen that wanted it is gone.
|
|
77
|
+
const onAbort = () => controller.abort();
|
|
78
|
+
options.signal?.addEventListener('abort', onAbort);
|
|
79
|
+
try {
|
|
80
|
+
const response = await this.config.fetch(joinUrl(this.config.baseUrl, options.path), {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
'content-type': 'application/json',
|
|
84
|
+
'x-api-key': this.config.apiKey,
|
|
85
|
+
'user-agent': this.config.userAgent,
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify(options.body ?? {}),
|
|
88
|
+
signal: controller.signal,
|
|
89
|
+
});
|
|
90
|
+
const text = await response.text();
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new TransportError(failureForStatus(response.status), messageFrom(text) ?? `Decentrys returned HTTP ${response.status}.`, response.status);
|
|
93
|
+
}
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(text);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new TransportError('malformed_response', 'The response was not valid JSON.');
|
|
100
|
+
}
|
|
101
|
+
// The platform wraps every successful body as `{ data: ... }`. Accepting
|
|
102
|
+
// a bare body too keeps the SDK usable against a proxy that unwraps it.
|
|
103
|
+
const envelope = parsed;
|
|
104
|
+
return (envelope && typeof envelope === 'object' && 'data' in envelope
|
|
105
|
+
? envelope.data
|
|
106
|
+
: parsed);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error instanceof TransportError)
|
|
110
|
+
throw error;
|
|
111
|
+
if (isAbort(error)) {
|
|
112
|
+
throw options.signal?.aborted
|
|
113
|
+
? new TransportError('network', 'The request was cancelled by the caller.')
|
|
114
|
+
: new TransportError('timeout', `No response within ${this.config.timeoutMs}ms.`);
|
|
115
|
+
}
|
|
116
|
+
throw new TransportError('network', error instanceof Error ? error.message : 'Network request failed.');
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.HttpTransport = HttpTransport;
|
|
125
|
+
function isAbort(error) {
|
|
126
|
+
return (typeof error === 'object'
|
|
127
|
+
&& error !== null
|
|
128
|
+
&& (error.name === 'AbortError'
|
|
129
|
+
|| error.code === 'ABORT_ERR'));
|
|
130
|
+
}
|
|
131
|
+
/** Surface the API's own message when it sent one; it is better than ours. */
|
|
132
|
+
function messageFrom(text) {
|
|
133
|
+
try {
|
|
134
|
+
const body = JSON.parse(text);
|
|
135
|
+
if (typeof body.message === 'string' && body.message.trim())
|
|
136
|
+
return body.message;
|
|
137
|
+
if (typeof body.error === 'string' && body.error.trim())
|
|
138
|
+
return body.error;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Not JSON. Nothing to surface.
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
function joinUrl(base, path) {
|
|
146
|
+
return `${base.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
|
|
147
|
+
}
|
|
148
|
+
function delay(ms) {
|
|
149
|
+
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAsLH,0BAEC;AA9JD,MAAa,cAAe,SAAQ,KAAK;IAC9B,OAAO,CAAmB;IAC1B,MAAM,CAAU;IAEzB,YAAY,OAAyB,EAAE,OAAe,EAAE,MAAe;QACrE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAVD,wCAUC;AA2BD,iFAAiF;AACjF,SAAS,gBAAgB,CAAC,MAAc;IACtC,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,cAAc,CAAC;IAC1C,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,WAAW,CAAC;IACvC,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,cAAc,CAAC;IAC1C,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG;QAAE,OAAO,iBAAiB,CAAC;IAC5D,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,MAAM,SAAS,GAAuB,CAAC,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,cAAc,CAAC,CAAC;AAE7F,MAAa,aAAa;IACK;IAA7B,YAA6B,MAAuB;QAAvB,WAAM,GAAN,MAAM,CAAiB;IAAG,CAAC;IAExD,KAAK,CAAC,OAAO,CAAI,OAAuB;QACtC,MAAM,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAClE,IAAI,IAAI,GAAmB,IAAI,cAAc,CAAC,SAAS,EAAE,sBAAsB,CAAC,CAAC;QAEjF,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,QAAQ,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;YACvD,IAAI,CAAC;gBACH,OAAO,MAAM,IAAI,CAAC,OAAO,CAAI,OAAO,CAAC,CAAC;YACxC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,GAAG,KAAK,YAAY,cAAc;oBACpC,CAAC,CAAC,KAAK;oBACP,CAAC,CAAC,IAAI,cAAc,CAAC,SAAS,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC;gBAE7F,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC;oBAAE,MAAM,IAAI,CAAC;gBAClD,IAAI,OAAO,KAAK,QAAQ,GAAG,CAAC;oBAAE,MAAM,IAAI,CAAC;gBAEzC,mEAAmE;gBACnE,wDAAwD;gBACxD,MAAM,KAAK,CAAC,GAAG,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;QAED,MAAM,IAAI,CAAC;IACb,CAAC;IAEO,KAAK,CAAC,OAAO,CAAI,OAAuB;QAC9C,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAE1E,oEAAoE;QACpE,sEAAsE;QACtE,iCAAiC;QACjC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACzC,OAAO,CAAC,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAEnD,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE;gBACnF,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;oBAC/B,YAAY,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS;iBACpC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;gBACxC,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YAEH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YAEnC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,cAAc,CACtB,gBAAgB,CAAC,QAAQ,CAAC,MAAM,CAAC,EACjC,WAAW,CAAC,IAAI,CAAC,IAAI,2BAA2B,QAAQ,CAAC,MAAM,GAAG,EAClE,QAAQ,CAAC,MAAM,CAChB,CAAC;YACJ,CAAC;YAED,IAAI,MAAe,CAAC;YACpB,IAAI,CAAC;gBACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;YACvC,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,IAAI,cAAc,CAAC,oBAAoB,EAAE,kCAAkC,CAAC,CAAC;YACrF,CAAC;YAED,yEAAyE;YACzE,wEAAwE;YACxE,MAAM,QAAQ,GAAG,MAA4B,CAAC;YAC9C,OAAO,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,MAAM,IAAI,QAAQ;gBACpE,CAAC,CAAC,QAAQ,CAAC,IAAI;gBACf,CAAC,CAAC,MAAM,CAAM,CAAC;QACnB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,cAAc;gBAAE,MAAM,KAAK,CAAC;YACjD,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBACnB,MAAM,OAAO,CAAC,MAAM,EAAE,OAAO;oBAC3B,CAAC,CAAC,IAAI,cAAc,CAAC,SAAS,EAAE,0CAA0C,CAAC;oBAC3E,CAAC,CAAC,IAAI,cAAc,CAAC,SAAS,EAAE,sBAAsB,IAAI,CAAC,MAAM,CAAC,SAAS,KAAK,CAAC,CAAC;YACtF,CAAC;YACD,MAAM,IAAI,cAAc,CAAC,SAAS,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC;QAC1G,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,OAAO,CAAC,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;CACF;AArFD,sCAqFC;AAED,SAAS,OAAO,CAAC,KAAc;IAC7B,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;WACtB,KAAK,KAAK,IAAI;WACd,CAAE,KAA2B,CAAC,IAAI,KAAK,YAAY;eAChD,KAA2B,CAAC,IAAI,KAAK,WAAW,CAAC,CACxD,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,SAAS,WAAW,CAAC,IAAY;IAC/B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAA2C,CAAC;QACxE,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC;QACjF,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACP,gCAAgC;IAClC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAgB,OAAO,CAAC,IAAY,EAAE,IAAY;IAChD,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;AACnE,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,GAAG,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChE,CAAC"}
|
package/dist/wire.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire contract, and the boundary that defends the rule.
|
|
3
|
+
*
|
|
4
|
+
* Decentrys sends **evidence**, not a verdict. The SDK classifies locally with
|
|
5
|
+
* `classify()`. That is not a stylistic choice — it has two consequences that
|
|
6
|
+
* matter:
|
|
7
|
+
*
|
|
8
|
+
* - An integrator can audit exactly why a level was reached, in code they can
|
|
9
|
+
* read, without trusting a remote score.
|
|
10
|
+
* - The rule is enforced on the client, where it cannot be undone by a server
|
|
11
|
+
* change, a proxy, or a future endpoint written by someone who has not read
|
|
12
|
+
* the model.
|
|
13
|
+
*
|
|
14
|
+
* That last point is concrete rather than theoretical. Decentrys also operates
|
|
15
|
+
* an AML engine for exchanges and custodians, and in *that* product a new
|
|
16
|
+
* address legitimately raises a score — money-laundering typologies genuinely
|
|
17
|
+
* care about freshly created accounts, and regulators expect it to be flagged.
|
|
18
|
+
* The same signal reaching a consumer wallet unchanged would tax every new
|
|
19
|
+
* project on earth. So the boundary below refuses coverage signals no matter
|
|
20
|
+
* who sends them, and turns them into what they actually are: history.
|
|
21
|
+
*/
|
|
22
|
+
import type { HistoryStatus, ObservedFact, TechnicalCapability, ThreatSignal, UnknownField } from './model';
|
|
23
|
+
export type SubjectKind = 'address' | 'contract' | 'token' | 'transaction' | 'approval' | 'dapp';
|
|
24
|
+
export interface SubjectRef {
|
|
25
|
+
kind: SubjectKind;
|
|
26
|
+
chain: string;
|
|
27
|
+
identifier: string;
|
|
28
|
+
}
|
|
29
|
+
/** What every Protect endpoint returns. Evidence in, classification local. */
|
|
30
|
+
export interface EvidencePayload {
|
|
31
|
+
subject: SubjectRef;
|
|
32
|
+
facts: ObservedFact[];
|
|
33
|
+
capabilities: TechnicalCapability[];
|
|
34
|
+
threatSignals: ThreatSignal[];
|
|
35
|
+
unknowns: UnknownField[];
|
|
36
|
+
historyStatus: HistoryStatus;
|
|
37
|
+
historyConfidence?: number;
|
|
38
|
+
producedAt: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Signal types that describe how much *we* know, not how dangerous a subject
|
|
42
|
+
* is. If one arrives as a threat signal it is demoted to a fact and its
|
|
43
|
+
* explanation is preserved, so nothing is hidden — it simply stops being able
|
|
44
|
+
* to raise a risk level.
|
|
45
|
+
*
|
|
46
|
+
* Every entry here is a real signal name used somewhere in the industry, and
|
|
47
|
+
* several are emitted by Decentrys' own AML engine.
|
|
48
|
+
*/
|
|
49
|
+
export declare const COVERAGE_SIGNAL_TYPES: Set<string>;
|
|
50
|
+
export interface NormalizedEvidence extends EvidencePayload {
|
|
51
|
+
/** Signals demoted at the boundary, kept so an integrator can see it happen. */
|
|
52
|
+
demotedSignals: string[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Coerce an untrusted payload into the model's shape.
|
|
56
|
+
*
|
|
57
|
+
* A wallet must not crash because a field arrived as a number instead of a
|
|
58
|
+
* string, or because a proxy inserted something. Anything unusable is dropped
|
|
59
|
+
* rather than guessed at, and a dropped item is simply absent — never
|
|
60
|
+
* converted into a finding.
|
|
61
|
+
*/
|
|
62
|
+
export declare function normalizeEvidence(raw: unknown, fallback: SubjectRef): NormalizedEvidence;
|
|
63
|
+
//# sourceMappingURL=wire.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../src/wire.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EACW,aAAa,EAAE,YAAY,EAAgB,mBAAmB,EACnE,YAAY,EAAE,YAAY,EAC3C,MAAM,SAAS,CAAC;AAEjB,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,aAAa,GAAG,UAAU,GAAG,MAAM,CAAC;AAEjG,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,8EAA8E;AAC9E,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,UAAU,CAAC;IACpB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,YAAY,EAAE,mBAAmB,EAAE,CAAC;IACpC,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,aAAa,EAAE,aAAa,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;CACpB;AAMD;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,aAuBhC,CAAC;AAUH,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,gFAAgF;IAChF,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAMD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,GAAG,kBAAkB,CA+CxF"}
|
package/dist/wire.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The wire contract, and the boundary that defends the rule.
|
|
4
|
+
*
|
|
5
|
+
* Decentrys sends **evidence**, not a verdict. The SDK classifies locally with
|
|
6
|
+
* `classify()`. That is not a stylistic choice — it has two consequences that
|
|
7
|
+
* matter:
|
|
8
|
+
*
|
|
9
|
+
* - An integrator can audit exactly why a level was reached, in code they can
|
|
10
|
+
* read, without trusting a remote score.
|
|
11
|
+
* - The rule is enforced on the client, where it cannot be undone by a server
|
|
12
|
+
* change, a proxy, or a future endpoint written by someone who has not read
|
|
13
|
+
* the model.
|
|
14
|
+
*
|
|
15
|
+
* That last point is concrete rather than theoretical. Decentrys also operates
|
|
16
|
+
* an AML engine for exchanges and custodians, and in *that* product a new
|
|
17
|
+
* address legitimately raises a score — money-laundering typologies genuinely
|
|
18
|
+
* care about freshly created accounts, and regulators expect it to be flagged.
|
|
19
|
+
* The same signal reaching a consumer wallet unchanged would tax every new
|
|
20
|
+
* project on earth. So the boundary below refuses coverage signals no matter
|
|
21
|
+
* who sends them, and turns them into what they actually are: history.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.COVERAGE_SIGNAL_TYPES = void 0;
|
|
25
|
+
exports.normalizeEvidence = normalizeEvidence;
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Coverage signals — never threat signals
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
/**
|
|
30
|
+
* Signal types that describe how much *we* know, not how dangerous a subject
|
|
31
|
+
* is. If one arrives as a threat signal it is demoted to a fact and its
|
|
32
|
+
* explanation is preserved, so nothing is hidden — it simply stops being able
|
|
33
|
+
* to raise a risk level.
|
|
34
|
+
*
|
|
35
|
+
* Every entry here is a real signal name used somewhere in the industry, and
|
|
36
|
+
* several are emitted by Decentrys' own AML engine.
|
|
37
|
+
*/
|
|
38
|
+
exports.COVERAGE_SIGNAL_TYPES = new Set([
|
|
39
|
+
'NEW_ADDRESS',
|
|
40
|
+
'NEW_CONTRACT',
|
|
41
|
+
'NEW_DEPLOYMENT',
|
|
42
|
+
'LOW_ACTIVITY',
|
|
43
|
+
'LIMITED_HISTORY',
|
|
44
|
+
'NO_HISTORY',
|
|
45
|
+
'ESTABLISHED_HISTORY',
|
|
46
|
+
'UNVERIFIED_SOURCE',
|
|
47
|
+
'UNVERIFIED_CONTRACT',
|
|
48
|
+
'NO_AUDIT',
|
|
49
|
+
'UNAUDITED',
|
|
50
|
+
'ANONYMOUS_DEPLOYER',
|
|
51
|
+
'ANONYMOUS_TEAM',
|
|
52
|
+
'UNKNOWN_DEPLOYER',
|
|
53
|
+
'LOW_LIQUIDITY',
|
|
54
|
+
'THIN_LIQUIDITY',
|
|
55
|
+
'SMALL_MARKET_CAP',
|
|
56
|
+
'HOLDER_CONCENTRATION',
|
|
57
|
+
'LOW_HOLDER_COUNT',
|
|
58
|
+
'NOT_ON_TOKEN_LIST',
|
|
59
|
+
'UNKNOWN_TOKEN',
|
|
60
|
+
'NO_SOCIAL_PRESENCE',
|
|
61
|
+
]);
|
|
62
|
+
/**
|
|
63
|
+
* The coverage signals that say there is *no* record at all, as opposed to a
|
|
64
|
+
* thin one. Used only when the server sent no `historyStatus`: without this,
|
|
65
|
+
* demoting the signal would throw away the one thing it told us, and a subject
|
|
66
|
+
* we know nothing about would be reported as merely thinly covered.
|
|
67
|
+
*/
|
|
68
|
+
const NO_HISTORY_TYPES = new Set(['NO_HISTORY', 'UNKNOWN_TOKEN']);
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Defensive normalization
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
/**
|
|
73
|
+
* Coerce an untrusted payload into the model's shape.
|
|
74
|
+
*
|
|
75
|
+
* A wallet must not crash because a field arrived as a number instead of a
|
|
76
|
+
* string, or because a proxy inserted something. Anything unusable is dropped
|
|
77
|
+
* rather than guessed at, and a dropped item is simply absent — never
|
|
78
|
+
* converted into a finding.
|
|
79
|
+
*/
|
|
80
|
+
function normalizeEvidence(raw, fallback) {
|
|
81
|
+
const body = isRecord(raw) ? raw : {};
|
|
82
|
+
const demotedSignals = [];
|
|
83
|
+
const facts = asArray(body.facts).map(toFact).filter(isPresent);
|
|
84
|
+
const capabilities = asArray(body.capabilities).map(toCapability).filter(isPresent);
|
|
85
|
+
const unknowns = asArray(body.unknowns).map(toUnknown).filter(isPresent);
|
|
86
|
+
const threatSignals = [];
|
|
87
|
+
let noHistoryObserved = false;
|
|
88
|
+
for (const item of asArray(body.threatSignals)) {
|
|
89
|
+
const signal = toSignal(item);
|
|
90
|
+
if (!signal)
|
|
91
|
+
continue;
|
|
92
|
+
if (exports.COVERAGE_SIGNAL_TYPES.has(signal.type)) {
|
|
93
|
+
demotedSignals.push(signal.type);
|
|
94
|
+
if (NO_HISTORY_TYPES.has(signal.type))
|
|
95
|
+
noHistoryObserved = true;
|
|
96
|
+
facts.push({
|
|
97
|
+
type: signal.type,
|
|
98
|
+
value: null,
|
|
99
|
+
statement: signal.explanation,
|
|
100
|
+
source: signal.evidence[0]?.source ?? 'decentrys',
|
|
101
|
+
observedAt: signal.lastSeen,
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
threatSignals.push(signal);
|
|
106
|
+
}
|
|
107
|
+
// LIMITED is the neutral default, and deliberately so: it is the correct
|
|
108
|
+
// state for anything recently deployed and carries no accusation.
|
|
109
|
+
const declaredHistory = asHistoryStatus(body.historyStatus);
|
|
110
|
+
const historyStatus = declaredHistory ?? (noHistoryObserved ? 'NONE' : 'LIMITED');
|
|
111
|
+
return {
|
|
112
|
+
subject: toSubject(body.subject, fallback),
|
|
113
|
+
facts,
|
|
114
|
+
capabilities,
|
|
115
|
+
threatSignals,
|
|
116
|
+
unknowns,
|
|
117
|
+
historyStatus,
|
|
118
|
+
historyConfidence: asNumber(body.historyConfidence) ?? undefined,
|
|
119
|
+
producedAt: asString(body.producedAt) ?? new Date().toISOString(),
|
|
120
|
+
demotedSignals,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Field coercion
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
function toSubject(raw, fallback) {
|
|
127
|
+
if (!isRecord(raw))
|
|
128
|
+
return fallback;
|
|
129
|
+
const kind = asString(raw.kind);
|
|
130
|
+
return {
|
|
131
|
+
kind: isSubjectKind(kind) ? kind : fallback.kind,
|
|
132
|
+
chain: asString(raw.chain) ?? fallback.chain,
|
|
133
|
+
identifier: asString(raw.identifier) ?? fallback.identifier,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function toFact(raw) {
|
|
137
|
+
if (!isRecord(raw))
|
|
138
|
+
return null;
|
|
139
|
+
const type = asString(raw.type);
|
|
140
|
+
const statement = asString(raw.statement);
|
|
141
|
+
if (!type || !statement)
|
|
142
|
+
return null;
|
|
143
|
+
return {
|
|
144
|
+
type,
|
|
145
|
+
value: asFactValue(raw.value),
|
|
146
|
+
statement,
|
|
147
|
+
source: asString(raw.source) ?? 'decentrys',
|
|
148
|
+
observedAt: asString(raw.observedAt) ?? new Date().toISOString(),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function toCapability(raw) {
|
|
152
|
+
if (!isRecord(raw))
|
|
153
|
+
return null;
|
|
154
|
+
const type = asString(raw.type);
|
|
155
|
+
const statement = asString(raw.statement);
|
|
156
|
+
if (!type || !statement)
|
|
157
|
+
return null;
|
|
158
|
+
const severity = asString(raw.severity);
|
|
159
|
+
return {
|
|
160
|
+
type,
|
|
161
|
+
// An unrecognised severity becomes INFO, the least consequential value.
|
|
162
|
+
// Guessing upward would let a typo raise someone's risk level.
|
|
163
|
+
severity: severity === 'NOTABLE' || severity === 'SIGNIFICANT' ? severity : 'INFO',
|
|
164
|
+
statement,
|
|
165
|
+
grantedBy: asString(raw.grantedBy) ?? undefined,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function toSignal(raw) {
|
|
169
|
+
if (!isRecord(raw))
|
|
170
|
+
return null;
|
|
171
|
+
const type = asString(raw.type);
|
|
172
|
+
const explanation = asString(raw.explanation);
|
|
173
|
+
if (!type || !explanation)
|
|
174
|
+
return null;
|
|
175
|
+
const evidence = asArray(raw.evidence).map(toEvidence).filter(isPresent);
|
|
176
|
+
const createdAt = asString(raw.createdAt) ?? new Date().toISOString();
|
|
177
|
+
return {
|
|
178
|
+
type,
|
|
179
|
+
severity: asSeverity(raw.severity),
|
|
180
|
+
confidence: clamp01(asNumber(raw.confidence) ?? 0),
|
|
181
|
+
explanation,
|
|
182
|
+
// A missing hop count means we do not know how far away this is, and an
|
|
183
|
+
// unknown distance is an inference, not a direct observation.
|
|
184
|
+
hops: Math.max(0, Math.trunc(asNumber(raw.hops) ?? 1)),
|
|
185
|
+
evidence,
|
|
186
|
+
status: asSignalStatus(raw.status),
|
|
187
|
+
createdAt,
|
|
188
|
+
lastSeen: asString(raw.lastSeen) ?? createdAt,
|
|
189
|
+
expiresAt: asString(raw.expiresAt) ?? undefined,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function toEvidence(raw) {
|
|
193
|
+
if (!isRecord(raw))
|
|
194
|
+
return null;
|
|
195
|
+
const id = asString(raw.id);
|
|
196
|
+
const type = asString(raw.type);
|
|
197
|
+
if (!id || !type)
|
|
198
|
+
return null;
|
|
199
|
+
return {
|
|
200
|
+
id,
|
|
201
|
+
type,
|
|
202
|
+
source: asString(raw.source) ?? 'decentrys',
|
|
203
|
+
chain: asString(raw.chain) ?? undefined,
|
|
204
|
+
txHash: asString(raw.txHash) ?? undefined,
|
|
205
|
+
contract: asString(raw.contract) ?? undefined,
|
|
206
|
+
address: asString(raw.address) ?? undefined,
|
|
207
|
+
observedAt: asString(raw.observedAt) ?? new Date().toISOString(),
|
|
208
|
+
confidence: clamp01(asNumber(raw.confidence) ?? 0),
|
|
209
|
+
// Defaults to false. `analystVerified` is what gates KNOWN_MALICIOUS, so
|
|
210
|
+
// an absent field must never be read as a human having checked.
|
|
211
|
+
analystVerified: raw.analystVerified === true,
|
|
212
|
+
metadata: isRecord(raw.metadata) ? raw.metadata : undefined,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function toUnknown(raw) {
|
|
216
|
+
if (!isRecord(raw))
|
|
217
|
+
return null;
|
|
218
|
+
const field = asString(raw.field);
|
|
219
|
+
const statement = asString(raw.statement);
|
|
220
|
+
if (!field || !statement)
|
|
221
|
+
return null;
|
|
222
|
+
const reason = asString(raw.reason);
|
|
223
|
+
const reasons = ['UNKNOWN', 'INSUFFICIENT_DATA', 'PROVIDER_UNAVAILABLE', 'NOT_APPLICABLE'];
|
|
224
|
+
return {
|
|
225
|
+
field,
|
|
226
|
+
reason: reasons.includes(reason) ? reason : 'UNKNOWN',
|
|
227
|
+
statement,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Primitives
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
function isRecord(value) {
|
|
234
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
235
|
+
}
|
|
236
|
+
function asArray(value) {
|
|
237
|
+
return Array.isArray(value) ? value : [];
|
|
238
|
+
}
|
|
239
|
+
function asString(value) {
|
|
240
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
241
|
+
}
|
|
242
|
+
function asNumber(value) {
|
|
243
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
244
|
+
}
|
|
245
|
+
function asFactValue(value) {
|
|
246
|
+
if (typeof value === 'string' || typeof value === 'boolean')
|
|
247
|
+
return value;
|
|
248
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
249
|
+
return value;
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
function asSeverity(value) {
|
|
253
|
+
const severities = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];
|
|
254
|
+
return severities.includes(value) ? value : 'LOW';
|
|
255
|
+
}
|
|
256
|
+
function asSignalStatus(value) {
|
|
257
|
+
const statuses = ['ACTIVE', 'STALE', 'RESOLVED', 'DISPUTED_FACT', 'REMOVED'];
|
|
258
|
+
return statuses.includes(value) ? value : 'ACTIVE';
|
|
259
|
+
}
|
|
260
|
+
function asHistoryStatus(value) {
|
|
261
|
+
const statuses = ['ESTABLISHED', 'MODERATE', 'LIMITED', 'NONE'];
|
|
262
|
+
return statuses.includes(value) ? value : null;
|
|
263
|
+
}
|
|
264
|
+
function isSubjectKind(value) {
|
|
265
|
+
return value === 'address' || value === 'contract' || value === 'token'
|
|
266
|
+
|| value === 'transaction' || value === 'approval' || value === 'dapp';
|
|
267
|
+
}
|
|
268
|
+
function clamp01(value) {
|
|
269
|
+
return Math.min(1, Math.max(0, value));
|
|
270
|
+
}
|
|
271
|
+
function isPresent(value) {
|
|
272
|
+
return value !== null;
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=wire.js.map
|