@decentrys/dri-sdk 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Decentrys Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # @decentrys/dri-sdk
2
+
3
+ Digital Recovery Intelligence: fund tracing, attribution, intervention points
4
+ and evidence packages.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install @decentrys/dri-sdk
10
+ ```
11
+
12
+ ## What this is not
13
+
14
+ Decentrys **never takes custody of assets**, and does not freeze, seize,
15
+ transmit or return anything. It is not a law firm, a law enforcement agency or
16
+ a licensed recovery agent, and nothing it returns is legal advice.
17
+
18
+ The **Recovery Index is an analytical estimate** produced by a versioned model
19
+ from the evidence in a case. It is not a prediction, a guarantee, or a promised
20
+ recovery rate. Every response carries that disclaimer, and the client throws
21
+ `MissingDisclosureError` if one ever arrives without it.
22
+
23
+ ## Use
24
+
25
+ ```ts
26
+ const investigation = await dri.createInvestigation({ ... });
27
+ await dri.traceFunds(investigation.id); // one address, one hop
28
+ const points = await dri.identifyInterventionPoints(investigation.id);
29
+ ```
30
+
31
+ Intervention points separate `CUSTODIAL` from `CROSS_CHAIN`: a custodian holds
32
+ the value, a bridge operator holds records of value that has moved on.
33
+ Collapsing them sends legal budget after a paper trail.
34
+
35
+ Attribution beyond a mixer is inference and is labelled as inference. An
36
+ indirect connection is an observation about a counterparty, never an
37
+ accusation about a person.
38
+
39
+ **Secret keys only** — an investigation names third parties.
40
+
41
+ ## Licence
42
+
43
+ MIT © Decentrys Labs
44
+
45
+ ## Links
46
+
47
+ - [decentrys.com](https://decentrys.com) · [SDK overview](https://decentrys.com/sdk) · [Developer API](https://decentrys.com/developers)
48
+ - Source: [github.com/teamdecentrys-byte/Decentrys](https://github.com/teamdecentrys-byte/Decentrys)
@@ -0,0 +1,290 @@
1
+ "use strict";
2
+ var DecentrysDri = (() => {
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
+ DecentrysDri: () => DecentrysDri,
25
+ DriError: () => DriError,
26
+ MissingDisclosureError: () => MissingDisclosureError,
27
+ RECOVERY_BANDS: () => RECOVERY_BANDS,
28
+ SDK_VERSION: () => SDK_VERSION,
29
+ assertAnalyticalDisclosure: () => assertAnalyticalDisclosure,
30
+ custodialPoints: () => custodialPoints,
31
+ inferredPoints: () => inferredPoints
32
+ });
33
+
34
+ // src/model.ts
35
+ var RECOVERY_BANDS = ["HIGH", "MODERATE", "LOW", "VERY_LOW"];
36
+ var MissingDisclosureError = class extends Error {
37
+ constructor(message) {
38
+ super(message);
39
+ this.name = "MissingDisclosureError";
40
+ }
41
+ };
42
+ function assertAnalyticalDisclosure(index) {
43
+ if (index.analyticalOnly !== true) {
44
+ throw new MissingDisclosureError(
45
+ "This Recovery Index is not marked analytical-only. The Recovery Index is an analytical estimate and never a predicted recovery rate; refusing to hand back a score that does not say so."
46
+ );
47
+ }
48
+ if (typeof index.disclaimer !== "string" || index.disclaimer.trim().length === 0) {
49
+ throw new MissingDisclosureError(
50
+ "This Recovery Index arrived without its disclaimer. The disclaimer travels with the score by design, because a score shown without it is read as a promise of recovery."
51
+ );
52
+ }
53
+ return index;
54
+ }
55
+ function custodialPoints(analysis) {
56
+ return analysis.interventionPoints.filter((point) => point.kind === "CUSTODIAL");
57
+ }
58
+ function inferredPoints(analysis) {
59
+ return analysis.interventionPoints.filter((point) => !point.observedPath);
60
+ }
61
+
62
+ // src/client.ts
63
+ var SDK_VERSION = "0.1.0";
64
+ var DEFAULT_BASE_URL = "https://api.decentrys.com";
65
+ var DEFAULT_TIMEOUT_MS = 3e4;
66
+ var DriError = class extends Error {
67
+ constructor(status, message, code) {
68
+ super(message);
69
+ this.status = status;
70
+ this.code = code;
71
+ this.name = "DriError";
72
+ }
73
+ status;
74
+ code;
75
+ };
76
+ var DecentrysDri = class {
77
+ baseUrl;
78
+ apiKey;
79
+ timeoutMs;
80
+ fetchImpl;
81
+ constructor(config) {
82
+ if (!config.apiKey?.trim()) {
83
+ throw new Error("DRI: an apiKey is required. Create one at https://decentrys.com/developers.");
84
+ }
85
+ if (config.apiKey.startsWith("dk_pub_")) {
86
+ throw new Error(
87
+ "DRI: that is a publishable key. An investigation names third parties, and that must never run behind a credential shipped inside a client where anyone who downloads it can read it. Use a secret key, server-side."
88
+ );
89
+ }
90
+ this.apiKey = config.apiKey;
91
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
92
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
93
+ this.fetchImpl = config.fetch ?? resolveFetch();
94
+ }
95
+ // --- Investigations ------------------------------------------------------
96
+ /**
97
+ * Open an investigation seeded on one address.
98
+ *
99
+ * Nothing is traced yet. The seed node exists so the graph has a root before
100
+ * any chain call is made, which means a case can be opened and worked on
101
+ * even while a provider is unreachable.
102
+ */
103
+ createInvestigation(input) {
104
+ return this.request("POST", "/v1/dri/investigations", {
105
+ title: input.title,
106
+ chain: input.chain,
107
+ address: input.address,
108
+ ...input.incidentId ? { incidentId: input.incidentId } : {},
109
+ ...input.maxHops === void 0 ? {} : { maxHops: input.maxHops }
110
+ });
111
+ }
112
+ listInvestigations(limit) {
113
+ const query = limit === void 0 ? "" : `?limit=${encodeURIComponent(String(limit))}`;
114
+ return this.request(
115
+ "GET",
116
+ `/v1/dri/investigations${query}`
117
+ ).then((body) => body.investigations);
118
+ }
119
+ /**
120
+ * Follow the value one address further.
121
+ *
122
+ * One address, one hop, per call — and that is a deliberate limit rather
123
+ * than an unfinished feature. The public endpoints a trace reads are rate
124
+ * limited, and an unattended crawl to four hops exhausts them; the result
125
+ * then reads as "the funds vanished" rather than "we ran out of quota",
126
+ * which on this product is the worst possible way to be wrong. Call it in a
127
+ * loop if you want depth, and read `coverage.truncated` on each result.
128
+ */
129
+ traceFunds(input) {
130
+ return this.request("POST", `/v1/dri/investigations/${encode(input.investigationId)}/trace`, {
131
+ ...input.fromNodeId ? { fromNodeId: input.fromNodeId } : {}
132
+ });
133
+ }
134
+ /** The graph as it stands: every address reached, and how it was reached. */
135
+ getFlowGraph(investigationId) {
136
+ return this.request("GET", `/v1/dri/investigations/${encode(investigationId)}`);
137
+ }
138
+ /**
139
+ * Where a third party could act, and where attribution stops.
140
+ *
141
+ * Read both halves. An intervention point is a party who holds value and can
142
+ * be contacted, served or subpoenaed — whether it does anything is a matter
143
+ * for it, its regulators and any legal process, never for Decentrys. An
144
+ * attribution limit is where the trail stops being provable, and a case that
145
+ * reaches one has established an answer rather than failed.
146
+ *
147
+ * Every point carries `observedPath`. False means the connection to this
148
+ * case runs through mixing or privacy infrastructure and is therefore
149
+ * inference — an observation about a counterparty, never an accusation about
150
+ * a person. `inferredPoints()` isolates them.
151
+ */
152
+ identifyInterventionPoints(investigationId) {
153
+ return this.request(
154
+ "GET",
155
+ `/v1/dri/investigations/${encode(investigationId)}/intervention-points`
156
+ );
157
+ }
158
+ /**
159
+ * What happened on this case, oldest first, with the acting party on every
160
+ * entry.
161
+ *
162
+ * Merges the recovery record in when the incident behind the investigation
163
+ * has a case. Entries attributed to anyone other than Decentrys are recorded
164
+ * as reported by that party — Decentrys did not perform them and does not
165
+ * verify them.
166
+ */
167
+ getInvestigationTimeline(investigationId, limit) {
168
+ const query = limit === void 0 ? "" : `?limit=${encodeURIComponent(String(limit))}`;
169
+ return this.request(
170
+ "GET",
171
+ `/v1/dri/investigations/${encode(investigationId)}/timeline${query}`
172
+ );
173
+ }
174
+ // --- Recovery cases ------------------------------------------------------
175
+ /**
176
+ * The recovery cases of your organisation.
177
+ *
178
+ * A case is opened as part of an engagement, against a declared incident and
179
+ * a stated loss; the tranche ledger behind it is analyst work, so it is not
180
+ * something an API call brings into existence. This is how you find the
181
+ * `recoveryCaseId` that `getRecoveryIndex` and `generateEvidencePackage`
182
+ * take — and `getFlowGraph` reports it too, for an investigation attached to
183
+ * one.
184
+ */
185
+ listCases(limit) {
186
+ const query = limit === void 0 ? "" : `?limit=${encodeURIComponent(String(limit))}`;
187
+ return this.request("GET", `/v1/dri/cases${query}`).then((body) => body.cases);
188
+ }
189
+ /**
190
+ * The Recovery Index for a case.
191
+ *
192
+ * An **analytical estimate** of the current evidentiary and custodial
193
+ * position, produced by a versioned model from the evidence recorded in the
194
+ * case. It is not a probability, not a forecast, and not a representation
195
+ * that any value will be recovered. `modelVersion` and `inputsHash` come
196
+ * back with it so any figure shown to a client can be reproduced exactly
197
+ * from the record it was computed from.
198
+ *
199
+ * The disclaimer travels in the payload, and this method **refuses a
200
+ * response that does not carry it** rather than returning a bare number.
201
+ * That check is deliberately duplicated from the server: an integrator's
202
+ * dashboard renders whatever arrives, and "58 — MODERATE" beside the word
203
+ * recovery is read as a rate by everyone who sees it. If the caveat ever
204
+ * stops arriving, failing here is the only outcome that keeps the score
205
+ * honest.
206
+ */
207
+ getRecoveryIndex(recoveryCaseId) {
208
+ return this.request("GET", `/v1/dri/cases/${encode(recoveryCaseId)}/recovery-index`).then(assertAnalyticalDisclosure);
209
+ }
210
+ /**
211
+ * Prepare an evidence package for a recipient.
212
+ *
213
+ * **Prepared, not sent.** The package goes to you and, at your direction, to
214
+ * your counsel, your insurer or a law-enforcement agency. Decentrys does not
215
+ * contact counterparties on your behalf unless instructed, and recording
216
+ * that a package was transmitted is a separate act that names who sent it.
217
+ *
218
+ * The document is frozen and hashed at generation, because third parties
219
+ * make freezing decisions on the strength of these and one that could be
220
+ * regenerated differently afterwards would be worth nothing. Quote
221
+ * `contentHash` back to a recipient so they can verify what they hold.
222
+ */
223
+ generateEvidencePackage(input) {
224
+ return this.request(
225
+ "POST",
226
+ `/v1/dri/cases/${encode(input.recoveryCaseId)}/evidence-package`,
227
+ {
228
+ recipientType: input.recipientType,
229
+ recipientName: input.recipientName,
230
+ ...input.recoveryLeadId ? { recoveryLeadId: input.recoveryLeadId } : {}
231
+ }
232
+ );
233
+ }
234
+ // --- transport -----------------------------------------------------------
235
+ async request(method, path, body) {
236
+ const controller = new AbortController();
237
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
238
+ try {
239
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
240
+ method,
241
+ headers: {
242
+ "content-type": "application/json",
243
+ "x-api-key": this.apiKey,
244
+ "user-agent": `decentrys-dri/${SDK_VERSION}`
245
+ },
246
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
247
+ signal: controller.signal
248
+ });
249
+ const text = await response.text();
250
+ if (!response.ok) {
251
+ const parsed2 = safeParse(text);
252
+ throw new DriError(
253
+ response.status,
254
+ typeof parsed2?.message === "string" ? parsed2.message : `Decentrys returned HTTP ${response.status}.`,
255
+ typeof parsed2?.code === "string" ? parsed2.code : void 0
256
+ );
257
+ }
258
+ if (!text) return void 0;
259
+ const parsed = safeParse(text);
260
+ if (parsed === null) throw new DriError(response.status, "The response was not valid JSON.");
261
+ return "data" in parsed ? parsed.data : parsed;
262
+ } catch (error) {
263
+ if (error instanceof DriError) throw error;
264
+ if (controller.signal.aborted) throw new DriError(0, `No response within ${this.timeoutMs}ms.`);
265
+ throw new DriError(0, error instanceof Error ? error.message : "Request failed.");
266
+ } finally {
267
+ clearTimeout(timer);
268
+ }
269
+ }
270
+ };
271
+ function encode(segment) {
272
+ return encodeURIComponent(segment);
273
+ }
274
+ function safeParse(text) {
275
+ try {
276
+ const parsed = JSON.parse(text);
277
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+ function resolveFetch() {
283
+ const candidate = globalThis.fetch;
284
+ if (typeof candidate !== "function") {
285
+ throw new Error("DRI: no global fetch was found. Pass one via `new DecentrysDri({ fetch })`.");
286
+ }
287
+ return candidate.bind(globalThis);
288
+ }
289
+ return __toCommonJS(index_exports);
290
+ })();
@@ -0,0 +1,265 @@
1
+ // src/model.ts
2
+ var RECOVERY_BANDS = ["HIGH", "MODERATE", "LOW", "VERY_LOW"];
3
+ var MissingDisclosureError = class extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "MissingDisclosureError";
7
+ }
8
+ };
9
+ function assertAnalyticalDisclosure(index) {
10
+ if (index.analyticalOnly !== true) {
11
+ throw new MissingDisclosureError(
12
+ "This Recovery Index is not marked analytical-only. The Recovery Index is an analytical estimate and never a predicted recovery rate; refusing to hand back a score that does not say so."
13
+ );
14
+ }
15
+ if (typeof index.disclaimer !== "string" || index.disclaimer.trim().length === 0) {
16
+ throw new MissingDisclosureError(
17
+ "This Recovery Index arrived without its disclaimer. The disclaimer travels with the score by design, because a score shown without it is read as a promise of recovery."
18
+ );
19
+ }
20
+ return index;
21
+ }
22
+ function custodialPoints(analysis) {
23
+ return analysis.interventionPoints.filter((point) => point.kind === "CUSTODIAL");
24
+ }
25
+ function inferredPoints(analysis) {
26
+ return analysis.interventionPoints.filter((point) => !point.observedPath);
27
+ }
28
+
29
+ // src/client.ts
30
+ var SDK_VERSION = "0.1.0";
31
+ var DEFAULT_BASE_URL = "https://api.decentrys.com";
32
+ var DEFAULT_TIMEOUT_MS = 3e4;
33
+ var DriError = class extends Error {
34
+ constructor(status, message, code) {
35
+ super(message);
36
+ this.status = status;
37
+ this.code = code;
38
+ this.name = "DriError";
39
+ }
40
+ status;
41
+ code;
42
+ };
43
+ var DecentrysDri = class {
44
+ baseUrl;
45
+ apiKey;
46
+ timeoutMs;
47
+ fetchImpl;
48
+ constructor(config) {
49
+ if (!config.apiKey?.trim()) {
50
+ throw new Error("DRI: an apiKey is required. Create one at https://decentrys.com/developers.");
51
+ }
52
+ if (config.apiKey.startsWith("dk_pub_")) {
53
+ throw new Error(
54
+ "DRI: that is a publishable key. An investigation names third parties, and that must never run behind a credential shipped inside a client where anyone who downloads it can read it. Use a secret key, server-side."
55
+ );
56
+ }
57
+ this.apiKey = config.apiKey;
58
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
59
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
60
+ this.fetchImpl = config.fetch ?? resolveFetch();
61
+ }
62
+ // --- Investigations ------------------------------------------------------
63
+ /**
64
+ * Open an investigation seeded on one address.
65
+ *
66
+ * Nothing is traced yet. The seed node exists so the graph has a root before
67
+ * any chain call is made, which means a case can be opened and worked on
68
+ * even while a provider is unreachable.
69
+ */
70
+ createInvestigation(input) {
71
+ return this.request("POST", "/v1/dri/investigations", {
72
+ title: input.title,
73
+ chain: input.chain,
74
+ address: input.address,
75
+ ...input.incidentId ? { incidentId: input.incidentId } : {},
76
+ ...input.maxHops === void 0 ? {} : { maxHops: input.maxHops }
77
+ });
78
+ }
79
+ listInvestigations(limit) {
80
+ const query = limit === void 0 ? "" : `?limit=${encodeURIComponent(String(limit))}`;
81
+ return this.request(
82
+ "GET",
83
+ `/v1/dri/investigations${query}`
84
+ ).then((body) => body.investigations);
85
+ }
86
+ /**
87
+ * Follow the value one address further.
88
+ *
89
+ * One address, one hop, per call — and that is a deliberate limit rather
90
+ * than an unfinished feature. The public endpoints a trace reads are rate
91
+ * limited, and an unattended crawl to four hops exhausts them; the result
92
+ * then reads as "the funds vanished" rather than "we ran out of quota",
93
+ * which on this product is the worst possible way to be wrong. Call it in a
94
+ * loop if you want depth, and read `coverage.truncated` on each result.
95
+ */
96
+ traceFunds(input) {
97
+ return this.request("POST", `/v1/dri/investigations/${encode(input.investigationId)}/trace`, {
98
+ ...input.fromNodeId ? { fromNodeId: input.fromNodeId } : {}
99
+ });
100
+ }
101
+ /** The graph as it stands: every address reached, and how it was reached. */
102
+ getFlowGraph(investigationId) {
103
+ return this.request("GET", `/v1/dri/investigations/${encode(investigationId)}`);
104
+ }
105
+ /**
106
+ * Where a third party could act, and where attribution stops.
107
+ *
108
+ * Read both halves. An intervention point is a party who holds value and can
109
+ * be contacted, served or subpoenaed — whether it does anything is a matter
110
+ * for it, its regulators and any legal process, never for Decentrys. An
111
+ * attribution limit is where the trail stops being provable, and a case that
112
+ * reaches one has established an answer rather than failed.
113
+ *
114
+ * Every point carries `observedPath`. False means the connection to this
115
+ * case runs through mixing or privacy infrastructure and is therefore
116
+ * inference — an observation about a counterparty, never an accusation about
117
+ * a person. `inferredPoints()` isolates them.
118
+ */
119
+ identifyInterventionPoints(investigationId) {
120
+ return this.request(
121
+ "GET",
122
+ `/v1/dri/investigations/${encode(investigationId)}/intervention-points`
123
+ );
124
+ }
125
+ /**
126
+ * What happened on this case, oldest first, with the acting party on every
127
+ * entry.
128
+ *
129
+ * Merges the recovery record in when the incident behind the investigation
130
+ * has a case. Entries attributed to anyone other than Decentrys are recorded
131
+ * as reported by that party — Decentrys did not perform them and does not
132
+ * verify them.
133
+ */
134
+ getInvestigationTimeline(investigationId, limit) {
135
+ const query = limit === void 0 ? "" : `?limit=${encodeURIComponent(String(limit))}`;
136
+ return this.request(
137
+ "GET",
138
+ `/v1/dri/investigations/${encode(investigationId)}/timeline${query}`
139
+ );
140
+ }
141
+ // --- Recovery cases ------------------------------------------------------
142
+ /**
143
+ * The recovery cases of your organisation.
144
+ *
145
+ * A case is opened as part of an engagement, against a declared incident and
146
+ * a stated loss; the tranche ledger behind it is analyst work, so it is not
147
+ * something an API call brings into existence. This is how you find the
148
+ * `recoveryCaseId` that `getRecoveryIndex` and `generateEvidencePackage`
149
+ * take — and `getFlowGraph` reports it too, for an investigation attached to
150
+ * one.
151
+ */
152
+ listCases(limit) {
153
+ const query = limit === void 0 ? "" : `?limit=${encodeURIComponent(String(limit))}`;
154
+ return this.request("GET", `/v1/dri/cases${query}`).then((body) => body.cases);
155
+ }
156
+ /**
157
+ * The Recovery Index for a case.
158
+ *
159
+ * An **analytical estimate** of the current evidentiary and custodial
160
+ * position, produced by a versioned model from the evidence recorded in the
161
+ * case. It is not a probability, not a forecast, and not a representation
162
+ * that any value will be recovered. `modelVersion` and `inputsHash` come
163
+ * back with it so any figure shown to a client can be reproduced exactly
164
+ * from the record it was computed from.
165
+ *
166
+ * The disclaimer travels in the payload, and this method **refuses a
167
+ * response that does not carry it** rather than returning a bare number.
168
+ * That check is deliberately duplicated from the server: an integrator's
169
+ * dashboard renders whatever arrives, and "58 — MODERATE" beside the word
170
+ * recovery is read as a rate by everyone who sees it. If the caveat ever
171
+ * stops arriving, failing here is the only outcome that keeps the score
172
+ * honest.
173
+ */
174
+ getRecoveryIndex(recoveryCaseId) {
175
+ return this.request("GET", `/v1/dri/cases/${encode(recoveryCaseId)}/recovery-index`).then(assertAnalyticalDisclosure);
176
+ }
177
+ /**
178
+ * Prepare an evidence package for a recipient.
179
+ *
180
+ * **Prepared, not sent.** The package goes to you and, at your direction, to
181
+ * your counsel, your insurer or a law-enforcement agency. Decentrys does not
182
+ * contact counterparties on your behalf unless instructed, and recording
183
+ * that a package was transmitted is a separate act that names who sent it.
184
+ *
185
+ * The document is frozen and hashed at generation, because third parties
186
+ * make freezing decisions on the strength of these and one that could be
187
+ * regenerated differently afterwards would be worth nothing. Quote
188
+ * `contentHash` back to a recipient so they can verify what they hold.
189
+ */
190
+ generateEvidencePackage(input) {
191
+ return this.request(
192
+ "POST",
193
+ `/v1/dri/cases/${encode(input.recoveryCaseId)}/evidence-package`,
194
+ {
195
+ recipientType: input.recipientType,
196
+ recipientName: input.recipientName,
197
+ ...input.recoveryLeadId ? { recoveryLeadId: input.recoveryLeadId } : {}
198
+ }
199
+ );
200
+ }
201
+ // --- transport -----------------------------------------------------------
202
+ async request(method, path, body) {
203
+ const controller = new AbortController();
204
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
205
+ try {
206
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
207
+ method,
208
+ headers: {
209
+ "content-type": "application/json",
210
+ "x-api-key": this.apiKey,
211
+ "user-agent": `decentrys-dri/${SDK_VERSION}`
212
+ },
213
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
214
+ signal: controller.signal
215
+ });
216
+ const text = await response.text();
217
+ if (!response.ok) {
218
+ const parsed2 = safeParse(text);
219
+ throw new DriError(
220
+ response.status,
221
+ typeof parsed2?.message === "string" ? parsed2.message : `Decentrys returned HTTP ${response.status}.`,
222
+ typeof parsed2?.code === "string" ? parsed2.code : void 0
223
+ );
224
+ }
225
+ if (!text) return void 0;
226
+ const parsed = safeParse(text);
227
+ if (parsed === null) throw new DriError(response.status, "The response was not valid JSON.");
228
+ return "data" in parsed ? parsed.data : parsed;
229
+ } catch (error) {
230
+ if (error instanceof DriError) throw error;
231
+ if (controller.signal.aborted) throw new DriError(0, `No response within ${this.timeoutMs}ms.`);
232
+ throw new DriError(0, error instanceof Error ? error.message : "Request failed.");
233
+ } finally {
234
+ clearTimeout(timer);
235
+ }
236
+ }
237
+ };
238
+ function encode(segment) {
239
+ return encodeURIComponent(segment);
240
+ }
241
+ function safeParse(text) {
242
+ try {
243
+ const parsed = JSON.parse(text);
244
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+ function resolveFetch() {
250
+ const candidate = globalThis.fetch;
251
+ if (typeof candidate !== "function") {
252
+ throw new Error("DRI: no global fetch was found. Pass one via `new DecentrysDri({ fetch })`.");
253
+ }
254
+ return candidate.bind(globalThis);
255
+ }
256
+ export {
257
+ DecentrysDri,
258
+ DriError,
259
+ MissingDisclosureError,
260
+ RECOVERY_BANDS,
261
+ SDK_VERSION,
262
+ assertAnalyticalDisclosure,
263
+ custodialPoints,
264
+ inferredPoints
265
+ };