@dvmkit/sdk 0.1.0-rc.4 → 0.1.0-rc.6
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/dist/{chunk-F2L6KIMD.js → chunk-5URG56JJ.js} +5 -143
- package/dist/{chunk-EDU6COY2.js → chunk-7AKBC4PW.js} +754 -150
- package/dist/chunk-ANFX5HEG.js +186 -0
- package/dist/{chunk-4UVRXDNY.js → chunk-CMDI3ENK.js} +342 -176
- package/dist/{chunk-GJD7PVWY.js → chunk-FJDCFHW5.js} +26 -12
- package/dist/{chunk-AAJNGQMC.js → chunk-JGGI65I3.js} +1 -1
- package/dist/{chunk-2K6UXDAN.js → chunk-KSQEFVFJ.js} +623 -691
- package/dist/{chunk-XYTSDAPH.js → chunk-LDTWX7JW.js} +1 -1
- package/dist/chunk-MKI6OVW4.js +194 -0
- package/dist/{chunk-AZBXSXQT.js → chunk-P4RUVDU7.js} +1 -0
- package/dist/chunk-TSWKITGR.js +772 -0
- package/dist/{credit-menu-ClA1JyYW.d.ts → credit-menu-CYkETVM4.d.ts} +22 -3
- package/dist/{fx-ptKVFOwq.d.ts → fx-Pf4Ey4f_.d.ts} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/internal/index.d.ts +4974 -85
- package/dist/internal/index.js +3742 -167
- package/dist/{job-store-DxFqDPYq.d.ts → job-store-BtCaLnvJ.d.ts} +23 -1
- package/dist/server/index.d.ts +5 -5
- package/dist/server/index.js +8 -21
- package/dist/tempo-lifecycle-SQL3KLEZ.js +13 -0
- package/dist/tempo-wallet-QOLEIPCV.js +31 -0
- package/dist/testing/index.d.ts +1 -1
- package/dist/{usd-Cha_j80I.d.ts → usd-D7fW2S7I.d.ts} +1 -1
- package/dist/{x402-XXFQQAAD.js → x402-FTG2GRAQ.js} +6 -4
- package/package.json +1 -1
|
@@ -1,14 +1,50 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
CONFIG_PINNED_FILE,
|
|
2
|
+
IMPLICIT_CREDIT_TTL_MS
|
|
3
|
+
} from "./chunk-5URG56JJ.js";
|
|
4
|
+
import {
|
|
6
5
|
CREDITS_FILE,
|
|
7
6
|
DvmError,
|
|
8
7
|
ensureConfigDir
|
|
9
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-MKI6OVW4.js";
|
|
9
|
+
|
|
10
|
+
// src/lib/output.ts
|
|
11
|
+
import { writeSync } from "fs";
|
|
12
|
+
var _traceId = null;
|
|
13
|
+
function setOutputTraceId(id) {
|
|
14
|
+
_traceId = id;
|
|
15
|
+
}
|
|
16
|
+
function getOutputTraceId() {
|
|
17
|
+
return _traceId;
|
|
18
|
+
}
|
|
19
|
+
function json(data) {
|
|
20
|
+
const out = _traceId !== null && typeof data === "object" && data !== null && !Array.isArray(data) && !("trace_id" in data) ? { trace_id: _traceId, ...data } : data;
|
|
21
|
+
writeAllSync(1, JSON.stringify(out) + "\n");
|
|
22
|
+
}
|
|
23
|
+
function info(msg) {
|
|
24
|
+
writeAllSync(2, msg + "\n");
|
|
25
|
+
}
|
|
26
|
+
function raw(msg) {
|
|
27
|
+
writeAllSync(1, msg + "\n");
|
|
28
|
+
}
|
|
29
|
+
function progress(event) {
|
|
30
|
+
writeAllSync(2, JSON.stringify(event) + "\n");
|
|
31
|
+
}
|
|
32
|
+
function writeAllSync(fd, text) {
|
|
33
|
+
const buf = Buffer.from(text, "utf8");
|
|
34
|
+
let offset = 0;
|
|
35
|
+
while (offset < buf.length) {
|
|
36
|
+
try {
|
|
37
|
+
offset += writeSync(fd, buf, offset, buf.length - offset);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
const code = err.code;
|
|
40
|
+
if (code === "EAGAIN" || code === "EINTR") continue;
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
10
45
|
|
|
11
46
|
// src/lib/payment-rails/types.ts
|
|
47
|
+
var RAIL_FLAGS = ["cashu", "tempo", "x402"];
|
|
12
48
|
var ADVERTISED_AMOUNT_TOLERANCE_BPS = 1000n;
|
|
13
49
|
function maxAdvertisedMicro(localMicro) {
|
|
14
50
|
return localMicro * (10000n + ADVERTISED_AMOUNT_TOLERANCE_BPS) / 10000n;
|
|
@@ -19,9 +55,9 @@ function withinAdvertisedTolerance(advertisedMicro, localMicro) {
|
|
|
19
55
|
|
|
20
56
|
// src/lib/transport/types.ts
|
|
21
57
|
var SUPPORTED_PROTOCOL_VERSION = 1;
|
|
22
|
-
function parseCreditTerms(
|
|
23
|
-
if (!
|
|
24
|
-
const credit =
|
|
58
|
+
function parseCreditTerms(raw2) {
|
|
59
|
+
if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return void 0;
|
|
60
|
+
const credit = raw2;
|
|
25
61
|
if (typeof credit.min_micro !== "number" || typeof credit.max_micro !== "number" || typeof credit.ttl_ms !== "number" || typeof credit.currency !== "string" || !Array.isArray(credit.funding) || !credit.funding.every((method) => typeof method === "string") || credit.lightning_min_micro !== void 0 && typeof credit.lightning_min_micro !== "number")
|
|
26
62
|
return void 0;
|
|
27
63
|
const tempo = credit.tempo === void 0 ? void 0 : parseCreditTempoTerms(credit.tempo);
|
|
@@ -41,10 +77,10 @@ function parseCreditTerms(raw) {
|
|
|
41
77
|
}
|
|
42
78
|
};
|
|
43
79
|
}
|
|
44
|
-
function parseCreditMenu(
|
|
45
|
-
const terms = parseCreditTerms(
|
|
80
|
+
function parseCreditMenu(raw2) {
|
|
81
|
+
const terms = parseCreditTerms(raw2);
|
|
46
82
|
if (!terms) return void 0;
|
|
47
|
-
const credit =
|
|
83
|
+
const credit = raw2;
|
|
48
84
|
return {
|
|
49
85
|
...terms,
|
|
50
86
|
...typeof credit.credit_id === "string" && { credit_id: credit.credit_id },
|
|
@@ -55,9 +91,9 @@ function parseCreditMenu(raw) {
|
|
|
55
91
|
...typeof credit.expiry_ms === "number" && { expiry_ms: credit.expiry_ms }
|
|
56
92
|
};
|
|
57
93
|
}
|
|
58
|
-
function parseCreditTempoTerms(
|
|
59
|
-
if (!
|
|
60
|
-
const block =
|
|
94
|
+
function parseCreditTempoTerms(raw2) {
|
|
95
|
+
if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return void 0;
|
|
96
|
+
const block = raw2;
|
|
61
97
|
if (!Array.isArray(block.methods) || !block.methods.every(isCreditTempoMethod)) return void 0;
|
|
62
98
|
if (block.withheld !== void 0 && (!Array.isArray(block.withheld) || !block.withheld.every(isCreditTempoWithheldMethod)))
|
|
63
99
|
return void 0;
|
|
@@ -75,15 +111,15 @@ function parseCreditTempoTerms(raw) {
|
|
|
75
111
|
}
|
|
76
112
|
};
|
|
77
113
|
}
|
|
78
|
-
function isCreditTempoMethod(
|
|
79
|
-
return !!
|
|
114
|
+
function isCreditTempoMethod(raw2) {
|
|
115
|
+
return !!raw2 && typeof raw2 === "object" && typeof raw2.method === "string" && (raw2.intent === "charge" || raw2.intent === "session");
|
|
80
116
|
}
|
|
81
|
-
function isCreditTempoWithheldMethod(
|
|
82
|
-
return isCreditTempoMethod(
|
|
117
|
+
function isCreditTempoWithheldMethod(raw2) {
|
|
118
|
+
return isCreditTempoMethod(raw2) && typeof raw2.reason === "string";
|
|
83
119
|
}
|
|
84
|
-
function parseCreditX402Terms(
|
|
85
|
-
if (!
|
|
86
|
-
const schemes =
|
|
120
|
+
function parseCreditX402Terms(raw2) {
|
|
121
|
+
if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return void 0;
|
|
122
|
+
const schemes = raw2.schemes;
|
|
87
123
|
if (!Array.isArray(schemes) || !schemes.every(
|
|
88
124
|
(entry) => !!entry && typeof entry === "object" && typeof entry.scheme === "string" && typeof entry.network === "string"
|
|
89
125
|
))
|
|
@@ -96,663 +132,10 @@ function parseCreditX402Terms(raw) {
|
|
|
96
132
|
};
|
|
97
133
|
}
|
|
98
134
|
|
|
99
|
-
// src/lib/output.ts
|
|
100
|
-
import { writeSync } from "fs";
|
|
101
|
-
function info(msg) {
|
|
102
|
-
writeAllSync(2, msg + "\n");
|
|
103
|
-
}
|
|
104
|
-
function progress(event) {
|
|
105
|
-
writeAllSync(2, JSON.stringify(event) + "\n");
|
|
106
|
-
}
|
|
107
|
-
function writeAllSync(fd, text) {
|
|
108
|
-
const buf = Buffer.from(text, "utf8");
|
|
109
|
-
let offset = 0;
|
|
110
|
-
while (offset < buf.length) {
|
|
111
|
-
try {
|
|
112
|
-
offset += writeSync(fd, buf, offset, buf.length - offset);
|
|
113
|
-
} catch (err) {
|
|
114
|
-
const code = err.code;
|
|
115
|
-
if (code === "EAGAIN" || code === "EINTR") continue;
|
|
116
|
-
throw err;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// src/lib/evm-private-key.ts
|
|
122
|
-
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
123
|
-
import { hexToBytes } from "@noble/hashes/utils.js";
|
|
124
|
-
var EVM_PRIVATE_KEY_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
125
|
-
function isValidEvmPrivateKey(value) {
|
|
126
|
-
return EVM_PRIVATE_KEY_RE.test(value) && secp256k1.utils.isValidSecretKey(hexToBytes(value.slice(2)));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// src/lib/config-mutation.ts
|
|
130
|
-
import {
|
|
131
|
-
appendFileSync,
|
|
132
|
-
chmodSync,
|
|
133
|
-
existsSync,
|
|
134
|
-
readFileSync,
|
|
135
|
-
renameSync,
|
|
136
|
-
statSync,
|
|
137
|
-
unlinkSync,
|
|
138
|
-
writeFileSync
|
|
139
|
-
} from "fs";
|
|
140
|
-
var PROTECTED_CONFIG_FIELDS = ["nwcUri", "defaultIdentity", "tempo", "x402"];
|
|
141
|
-
var ProtectedFieldAuthorization = class {
|
|
142
|
-
/** Targets this authorization covers. A field also covers its own leaves. */
|
|
143
|
-
targets;
|
|
144
|
-
/** Prefer {@link authorizeProtectedRemoval}; this exists for it to call. */
|
|
145
|
-
constructor(targets) {
|
|
146
|
-
this.targets = [...targets];
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* True when `target` is one this authorization names. Naming a field covers
|
|
150
|
-
* the leaves under it; naming a leaf covers only that leaf, so a command
|
|
151
|
-
* entitled to clear `tempo.apiKey` still cannot take the whole `tempo` block.
|
|
152
|
-
*/
|
|
153
|
-
covers(target) {
|
|
154
|
-
if (this.targets.includes(target)) return true;
|
|
155
|
-
const field = target.split(".")[0];
|
|
156
|
-
return target !== field && this.targets.includes(field);
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
function authorizeProtectedRemoval(...targets) {
|
|
160
|
-
return new ProtectedFieldAuthorization(targets);
|
|
161
|
-
}
|
|
162
|
-
function applyConfigPatch(base, patch) {
|
|
163
|
-
if (!isPlainObject(patch))
|
|
164
|
-
throw patchShapeError("A change must be an object with `set` and/or `unset`.");
|
|
165
|
-
if (patch.set !== void 0 && !isPlainObject(patch.set)) {
|
|
166
|
-
throw patchShapeError("A change's `set` must be an object of fields to write.");
|
|
167
|
-
}
|
|
168
|
-
if (patch.unset !== void 0 && !Array.isArray(patch.unset)) {
|
|
169
|
-
throw patchShapeError("A change's `unset` must be an array of field names.");
|
|
170
|
-
}
|
|
171
|
-
const set = patch.set ?? {};
|
|
172
|
-
const unset = patch.unset ?? [];
|
|
173
|
-
for (const [field, value] of Object.entries(set)) {
|
|
174
|
-
if (value !== void 0) continue;
|
|
175
|
-
throw new DvmError(
|
|
176
|
-
"config_patch_invalid",
|
|
177
|
-
`Config field '${field}' was declared with no value.`,
|
|
178
|
-
"Give the field a value, or name it in the change's `unset` list to remove it. An undefined value is how a field gets dropped without anyone deciding to drop it."
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
for (const field of unset) {
|
|
182
|
-
if (typeof field !== "string" || field.length === 0) {
|
|
183
|
-
throw patchShapeError("Every entry in a change's `unset` must be a field name.");
|
|
184
|
-
}
|
|
185
|
-
if (!Object.hasOwn(set, field)) continue;
|
|
186
|
-
throw new DvmError(
|
|
187
|
-
"config_patch_invalid",
|
|
188
|
-
`Config field '${field}' is both written and removed by the same change.`,
|
|
189
|
-
"Declare each field once: in `set` with its new value, or in `unset` to remove it."
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
const merged = { ...base, ...set };
|
|
193
|
-
const removing = new Set(unset);
|
|
194
|
-
return Object.fromEntries(
|
|
195
|
-
Object.entries(merged).filter(([field]) => !removing.has(field))
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
function deepFreezeConfig(config) {
|
|
199
|
-
freezeDeep(config);
|
|
200
|
-
return config;
|
|
201
|
-
}
|
|
202
|
-
function protectedRemovals(before, after) {
|
|
203
|
-
const previous = before;
|
|
204
|
-
const next = after;
|
|
205
|
-
const removed = [];
|
|
206
|
-
for (const field of PROTECTED_CONFIG_FIELDS) {
|
|
207
|
-
const held = previous[field];
|
|
208
|
-
if (!isPresentValue(held)) continue;
|
|
209
|
-
const kept = next[field];
|
|
210
|
-
if (!isPresentValue(kept)) {
|
|
211
|
-
removed.push(field);
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
const spec = PROTECTED_FIELDS[field];
|
|
215
|
-
if (spec.credentialLeaves.length === 0) continue;
|
|
216
|
-
if (!isPlainObject(held)) continue;
|
|
217
|
-
if (!isPlainObject(kept)) {
|
|
218
|
-
removed.push(field);
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
for (const leaf of spec.credentialLeaves) {
|
|
222
|
-
if (!isPresentValue(held[leaf]) || isPresentValue(kept[leaf])) continue;
|
|
223
|
-
removed.push(`${field}.${leaf}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return removed;
|
|
227
|
-
}
|
|
228
|
-
function supersededProtectedSecrets(before, after) {
|
|
229
|
-
const previous = before;
|
|
230
|
-
const next = after;
|
|
231
|
-
const superseded = [];
|
|
232
|
-
for (const field of PROTECTED_CONFIG_FIELDS) {
|
|
233
|
-
const spec = PROTECTED_FIELDS[field];
|
|
234
|
-
if (!spec.secret) continue;
|
|
235
|
-
const held = previous[field];
|
|
236
|
-
const kept = next[field];
|
|
237
|
-
if (spec.credentialLeaves.length === 0) {
|
|
238
|
-
if (isPresentValue(held) && isPresentValue(kept) && held !== kept) superseded.push(field);
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
if (!isPlainObject(held) || !isPlainObject(kept)) continue;
|
|
242
|
-
for (const leaf of spec.credentialLeaves) {
|
|
243
|
-
const was = held[leaf];
|
|
244
|
-
const now = kept[leaf];
|
|
245
|
-
if (!isPresentValue(was) || !isPresentValue(now) || was === now) continue;
|
|
246
|
-
superseded.push(`${field}.${leaf}`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return superseded;
|
|
250
|
-
}
|
|
251
|
-
function changedConfigPaths(before, after) {
|
|
252
|
-
const found = /* @__PURE__ */ new Set();
|
|
253
|
-
collectChangedPaths(
|
|
254
|
-
before,
|
|
255
|
-
after,
|
|
256
|
-
CONFIG_PATH_SHAPE,
|
|
257
|
-
"",
|
|
258
|
-
found
|
|
259
|
-
);
|
|
260
|
-
return [...found].sort();
|
|
261
|
-
}
|
|
262
|
-
function assertValidConfig(config, fields) {
|
|
263
|
-
if (!isPlainObject(config)) {
|
|
264
|
-
throw new DvmError(
|
|
265
|
-
"config_invalid",
|
|
266
|
-
"The caller configuration is not a JSON object.",
|
|
267
|
-
`Expected an object at ${CONFIG_FILE}.${rollbackSuffix()}`
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
const record = config;
|
|
271
|
-
const scope = fields ? new Set(fields) : null;
|
|
272
|
-
for (const [field, kind] of Object.entries(CONFIG_FIELD_TYPES)) {
|
|
273
|
-
if (scope && !scope.has(field)) continue;
|
|
274
|
-
const value = record[field];
|
|
275
|
-
if (value === void 0) continue;
|
|
276
|
-
if (!matchesKind(value, kind)) throw invalidFieldError(field, kind);
|
|
277
|
-
}
|
|
278
|
-
for (const [path, kind] of Object.entries(CONFIG_LEAF_TYPES)) {
|
|
279
|
-
const [field, leaf] = path.split(".");
|
|
280
|
-
if (scope && !scope.has(field)) continue;
|
|
281
|
-
const owner = record[field];
|
|
282
|
-
if (!isPlainObject(owner)) continue;
|
|
283
|
-
const value = owner[leaf];
|
|
284
|
-
if (value === void 0 || !matchesKind(value, kind)) throw invalidFieldError(path, kind);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
function readRawConfig() {
|
|
288
|
-
if (!existsSync(CONFIG_FILE)) return null;
|
|
289
|
-
try {
|
|
290
|
-
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
291
|
-
} catch {
|
|
292
|
-
throw new DvmError(
|
|
293
|
-
"config_corrupt",
|
|
294
|
-
"Config file is corrupt.",
|
|
295
|
-
// Not "delete it": those bytes are the only copy on this machine of the
|
|
296
|
-
// NWC connection string and both private keys, however badly they parse.
|
|
297
|
-
`Run 'dvm config rollback' to restore a stored copy, or move ${CONFIG_FILE} aside yourself and run 'dvm init'.${rollbackSuffix()}`
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
function writeConfigFile(config) {
|
|
302
|
-
writeJsonAtomic(CONFIG_FILE, config);
|
|
303
|
-
}
|
|
304
|
-
var CONFIG_SNAPSHOT_SLOTS = ["backup", "pinned"];
|
|
305
|
-
function configSnapshotPath(slot) {
|
|
306
|
-
return slot === "pinned" ? CONFIG_PINNED_FILE : CONFIG_BACKUP_FILE;
|
|
307
|
-
}
|
|
308
|
-
function describeConfigBackup(slot = "backup") {
|
|
309
|
-
const path = configSnapshotPath(slot);
|
|
310
|
-
try {
|
|
311
|
-
if (!existsSync(path)) return null;
|
|
312
|
-
const stat = statSync(path);
|
|
313
|
-
const posix = process.platform !== "win32";
|
|
314
|
-
const mode = stat.mode & 511;
|
|
315
|
-
return {
|
|
316
|
-
path,
|
|
317
|
-
slot,
|
|
318
|
-
bytes: stat.size,
|
|
319
|
-
modifiedAt: stat.mtime.toISOString(),
|
|
320
|
-
modeOctal: posix ? toOctal(mode) : null,
|
|
321
|
-
ownerOnly: !posix || (mode & 63) === 0
|
|
322
|
-
};
|
|
323
|
-
} catch {
|
|
324
|
-
return null;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
function readConfigBackup(slot = "backup") {
|
|
328
|
-
const path = configSnapshotPath(slot);
|
|
329
|
-
if (!existsSync(path)) return null;
|
|
330
|
-
try {
|
|
331
|
-
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
332
|
-
return isPlainObject(parsed) ? parsed : null;
|
|
333
|
-
} catch {
|
|
334
|
-
return null;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
function retainConfigBackup(slot = "backup") {
|
|
338
|
-
if (!existsSync(CONFIG_FILE)) return false;
|
|
339
|
-
ensureConfigDir();
|
|
340
|
-
const path = configSnapshotPath(slot);
|
|
341
|
-
const tmp = `${path}.tmp`;
|
|
342
|
-
writeFileSync(tmp, readFileSync(CONFIG_FILE), { mode: 384 });
|
|
343
|
-
chmodSync(tmp, 384);
|
|
344
|
-
commitTmp(tmp, path);
|
|
345
|
-
return true;
|
|
346
|
-
}
|
|
347
|
-
function writeConfigBackup(config, slot = "backup") {
|
|
348
|
-
writeJsonAtomic(configSnapshotPath(slot), config);
|
|
349
|
-
}
|
|
350
|
-
function removeConfigBackup(slot = "backup") {
|
|
351
|
-
const path = configSnapshotPath(slot);
|
|
352
|
-
if (!existsSync(path)) return false;
|
|
353
|
-
unlinkSync(path);
|
|
354
|
-
return true;
|
|
355
|
-
}
|
|
356
|
-
function protectedTargetValue(config, target) {
|
|
357
|
-
const [field, named] = target.split(".");
|
|
358
|
-
const held = config[field];
|
|
359
|
-
const leaves = PROTECTED_FIELDS[field].credentialLeaves;
|
|
360
|
-
const leaf = named ?? (leaves.length > 0 ? leaves[0] : void 0);
|
|
361
|
-
if (leaf === void 0) return held;
|
|
362
|
-
return isPlainObject(held) ? held[leaf] : void 0;
|
|
363
|
-
}
|
|
364
|
-
function protectedValuesMatch(target, left, right) {
|
|
365
|
-
const normalized = comparableProtectedValue(target, left);
|
|
366
|
-
return normalized !== void 0 && normalized === comparableProtectedValue(target, right);
|
|
367
|
-
}
|
|
368
|
-
function isSecretProtectedTarget(target) {
|
|
369
|
-
return PROTECTED_FIELDS[target.split(".")[0]].secret;
|
|
370
|
-
}
|
|
371
|
-
function withoutProtectedTargets(config, targets) {
|
|
372
|
-
const dropping = /* @__PURE__ */ new Set();
|
|
373
|
-
for (const target of targets) {
|
|
374
|
-
const field = target.split(".")[0];
|
|
375
|
-
dropping.add(field);
|
|
376
|
-
for (const alias of PROTECTED_FIELDS[field].aliases) dropping.add(alias);
|
|
377
|
-
}
|
|
378
|
-
return Object.fromEntries(
|
|
379
|
-
Object.entries(config).filter(([field]) => !dropping.has(field))
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
function appendConfigAudit(record) {
|
|
383
|
-
try {
|
|
384
|
-
ensureConfigDir();
|
|
385
|
-
appendFileSync(CONFIG_AUDIT_FILE, JSON.stringify(record) + "\n", { mode: 384 });
|
|
386
|
-
chmodSync(CONFIG_AUDIT_FILE, 384);
|
|
387
|
-
} catch {
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
function protectedRemovalError(targets, reason) {
|
|
391
|
-
const named = targets.join(", ");
|
|
392
|
-
const one = targets.length === 1;
|
|
393
|
-
return new DvmError(
|
|
394
|
-
"config_protected_field",
|
|
395
|
-
`The '${reason}' change would remove ${named} from the local configuration.`,
|
|
396
|
-
`Nothing was written. ${named} ${one ? "is a credential or identity pointer" : "are credentials or identity pointers"} nothing else on this machine records, so only a command that names ${one ? "it" : "them"} explicitly may remove ${one ? "it" : "them"} \u2014 'dvm wallet disconnect', 'dvm wallet tempo-disconnect' and 'dvm wallet x402-disconnect' are those commands.`,
|
|
397
|
-
{ fields: targets }
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
var PROTECTED_FIELDS = {
|
|
401
|
-
nwcUri: { aliases: [], credentialLeaves: [], secret: true, hexCredential: false },
|
|
402
|
-
// A pointer at a key, not a key: worth protecting from removal, and never
|
|
403
|
-
// worth stripping out of a stored copy — that is recovery lost for nothing.
|
|
404
|
-
defaultIdentity: { aliases: [], credentialLeaves: [], secret: false, hexCredential: false },
|
|
405
|
-
// `mpp` is the pre-internal-review name; a caller who has not run a mutating command
|
|
406
|
-
// since the rename still has their private key under it.
|
|
407
|
-
tempo: { aliases: ["mpp"], credentialLeaves: ["apiKey"], secret: true, hexCredential: true },
|
|
408
|
-
x402: { aliases: [], credentialLeaves: ["privateKey"], secret: true, hexCredential: true }
|
|
409
|
-
};
|
|
410
|
-
var CONFIG_PATH_SHAPE = {
|
|
411
|
-
float: { statedBudget: {}, connectedAt: {}, alias: {}, methods: {} },
|
|
412
|
-
x402: { privateKey: {}, network: {} },
|
|
413
|
-
tempo: { method: {}, accountId: {}, apiKey: {} },
|
|
414
|
-
credit: {
|
|
415
|
-
targetJobs: {},
|
|
416
|
-
posture: {},
|
|
417
|
-
tier0Cap: {},
|
|
418
|
-
tier1Cap: {},
|
|
419
|
-
pocketTarget: {},
|
|
420
|
-
fundingRail: {},
|
|
421
|
-
perDvm: { "*": { targetJobs: {}, posture: {}, fundingRail: {}, dvmId: {}, builderPubkey: {} } },
|
|
422
|
-
trustOverrides: { "*": {} }
|
|
423
|
-
},
|
|
424
|
-
feedback: { posture: {} }
|
|
425
|
-
};
|
|
426
|
-
var CONFIG_FIELD_TYPES = {
|
|
427
|
-
nwcUri: "string",
|
|
428
|
-
float: "object",
|
|
429
|
-
defaultBudget: "string",
|
|
430
|
-
autoPayThreshold: "string",
|
|
431
|
-
defaultMaxPayments: "number",
|
|
432
|
-
cashuMints: "string[]",
|
|
433
|
-
x402: "object",
|
|
434
|
-
tempo: "object",
|
|
435
|
-
defaultRail: "string",
|
|
436
|
-
defaultIdentity: "string",
|
|
437
|
-
credit: "object",
|
|
438
|
-
feedback: "object"
|
|
439
|
-
};
|
|
440
|
-
var CONFIG_LEAF_TYPES = {
|
|
441
|
-
"float.connectedAt": "string",
|
|
442
|
-
"x402.privateKey": "string",
|
|
443
|
-
"tempo.method": "string"
|
|
444
|
-
};
|
|
445
|
-
function collectChangedPaths(before, after, shape, prefix, found) {
|
|
446
|
-
for (const key of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
447
|
-
const held = before[key];
|
|
448
|
-
const kept = after[key];
|
|
449
|
-
if (deepEqual(held, kept)) continue;
|
|
450
|
-
const known = shape !== void 0 && Object.hasOwn(shape, key);
|
|
451
|
-
const wildcard = !known && shape !== void 0 && Object.hasOwn(shape, "*");
|
|
452
|
-
const child = known ? shape[key] : wildcard ? shape["*"] : void 0;
|
|
453
|
-
const path = prefix ? `${prefix}.${wildcard ? "*" : key}` : key;
|
|
454
|
-
const descendable = child !== void 0 && Object.keys(child).length > 0 && (held === void 0 || isPlainObject(held)) && (kept === void 0 || isPlainObject(kept));
|
|
455
|
-
if (descendable) {
|
|
456
|
-
collectChangedPaths(
|
|
457
|
-
isPlainObject(held) ? held : {},
|
|
458
|
-
isPlainObject(kept) ? kept : {},
|
|
459
|
-
child,
|
|
460
|
-
path,
|
|
461
|
-
found
|
|
462
|
-
);
|
|
463
|
-
} else {
|
|
464
|
-
found.add(path);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
function invalidFieldError(path, kind) {
|
|
469
|
-
return new DvmError(
|
|
470
|
-
"config_invalid",
|
|
471
|
-
`Config field '${path}' must be ${describeKind(kind)}.`,
|
|
472
|
-
`Nothing was written. Correct the field in ${CONFIG_FILE}, or run 'dvm doctor' for the full picture.${rollbackSuffix()}`
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
function describeKind(kind) {
|
|
476
|
-
switch (kind) {
|
|
477
|
-
case "string":
|
|
478
|
-
return "a string";
|
|
479
|
-
case "number":
|
|
480
|
-
return "a number";
|
|
481
|
-
case "object":
|
|
482
|
-
return "an object";
|
|
483
|
-
case "string[]":
|
|
484
|
-
return "an array of strings";
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
function matchesKind(value, kind) {
|
|
488
|
-
switch (kind) {
|
|
489
|
-
case "string":
|
|
490
|
-
return typeof value === "string";
|
|
491
|
-
case "number":
|
|
492
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
493
|
-
case "object":
|
|
494
|
-
return isPlainObject(value);
|
|
495
|
-
case "string[]":
|
|
496
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
function rollbackSuffix() {
|
|
500
|
-
const stored = describeConfigBackup() ?? describeConfigBackup("pinned");
|
|
501
|
-
return stored === null ? "" : ` A stored copy of the previous configuration is at ${stored.path}.`;
|
|
502
|
-
}
|
|
503
|
-
function writeJsonAtomic(path, value) {
|
|
504
|
-
ensureConfigDir();
|
|
505
|
-
const tmp = `${path}.tmp`;
|
|
506
|
-
writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
|
|
507
|
-
chmodSync(tmp, 384);
|
|
508
|
-
commitTmp(tmp, path);
|
|
509
|
-
}
|
|
510
|
-
function commitTmp(tmp, path) {
|
|
511
|
-
try {
|
|
512
|
-
renameSync(tmp, path);
|
|
513
|
-
} catch (err) {
|
|
514
|
-
try {
|
|
515
|
-
unlinkSync(tmp);
|
|
516
|
-
} catch {
|
|
517
|
-
}
|
|
518
|
-
throw err;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
function deepEqual(a, b) {
|
|
522
|
-
if (a === b) return true;
|
|
523
|
-
if (Array.isArray(a) || Array.isArray(b)) {
|
|
524
|
-
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
525
|
-
return a.every((item, index) => deepEqual(item, b[index]));
|
|
526
|
-
}
|
|
527
|
-
if (!isPlainObject(a) || !isPlainObject(b)) return false;
|
|
528
|
-
for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
529
|
-
if (!deepEqual(a[key], b[key])) return false;
|
|
530
|
-
}
|
|
531
|
-
return true;
|
|
532
|
-
}
|
|
533
|
-
function isPresentValue(value) {
|
|
534
|
-
if (value === void 0 || value === null) return false;
|
|
535
|
-
return typeof value !== "string" || value.trim().length > 0;
|
|
536
|
-
}
|
|
537
|
-
function comparableProtectedValue(target, value) {
|
|
538
|
-
if (typeof value !== "string") return void 0;
|
|
539
|
-
const trimmed = value.trim();
|
|
540
|
-
if (trimmed.length === 0) return void 0;
|
|
541
|
-
const field = target.split(".")[0];
|
|
542
|
-
return PROTECTED_FIELDS[field].hexCredential ? trimmed.toLowerCase() : trimmed;
|
|
543
|
-
}
|
|
544
|
-
function patchShapeError(message) {
|
|
545
|
-
return new DvmError(
|
|
546
|
-
"config_patch_invalid",
|
|
547
|
-
message,
|
|
548
|
-
"A change declares `set` (fields to write) and `unset` (field names to remove). Anything else cannot be applied, and nothing was written."
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
function freezeDeep(value) {
|
|
552
|
-
if (typeof value !== "object" || value === null || Object.isFrozen(value)) return;
|
|
553
|
-
Object.freeze(value);
|
|
554
|
-
for (const nested of Object.values(value)) freezeDeep(nested);
|
|
555
|
-
}
|
|
556
|
-
function isPlainObject(value) {
|
|
557
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
558
|
-
}
|
|
559
|
-
function toOctal(mode) {
|
|
560
|
-
return "0" + mode.toString(8).padStart(3, "0");
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// src/lib/identity.ts
|
|
564
|
-
import lockfile from "proper-lockfile";
|
|
565
|
-
var CREDIT_POSTURES = ["suggest", "auto", "off"];
|
|
566
|
-
function isCreditPosture(value) {
|
|
567
|
-
return typeof value === "string" && CREDIT_POSTURES.includes(value);
|
|
568
|
-
}
|
|
569
|
-
function migrateCreditConfig(config) {
|
|
570
|
-
const credit = config.credit;
|
|
571
|
-
if (!credit) return config;
|
|
572
|
-
const migrateLayer = (layer) => {
|
|
573
|
-
const railed = migrateFundingRail(layer);
|
|
574
|
-
if (!("disabled" in railed)) return railed;
|
|
575
|
-
const { disabled, ...rest } = railed;
|
|
576
|
-
const migrated = { ...rest };
|
|
577
|
-
if (migrated.posture === void 0 && disabled === true) migrated.posture = "off";
|
|
578
|
-
return migrated;
|
|
579
|
-
};
|
|
580
|
-
const perDvm = credit.perDvm;
|
|
581
|
-
return {
|
|
582
|
-
...config,
|
|
583
|
-
credit: {
|
|
584
|
-
...migrateLayer(credit),
|
|
585
|
-
...perDvm ? {
|
|
586
|
-
perDvm: Object.fromEntries(
|
|
587
|
-
Object.entries(perDvm).map(([endpoint, per]) => [endpoint, migrateLayer(per)])
|
|
588
|
-
)
|
|
589
|
-
} : {}
|
|
590
|
-
}
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
function migrateFundingRail(layer) {
|
|
594
|
-
const loose = layer;
|
|
595
|
-
if (loose.fundingRail !== "mpp") return layer;
|
|
596
|
-
return { ...layer, fundingRail: "tempo" };
|
|
597
|
-
}
|
|
598
|
-
function migrateTempoWalletKey(config) {
|
|
599
|
-
const loose = config;
|
|
600
|
-
if (!loose.mpp || loose.tempo) return config;
|
|
601
|
-
const { mpp, ...rest } = loose;
|
|
602
|
-
return { ...rest, tempo: mpp };
|
|
603
|
-
}
|
|
604
|
-
function migrateConfig(config) {
|
|
605
|
-
return migrateCreditConfig(migrateTempoWalletKey(config));
|
|
606
|
-
}
|
|
607
|
-
function loadConfig() {
|
|
608
|
-
const raw = readRawConfig();
|
|
609
|
-
return raw === null ? null : migrateConfig(raw);
|
|
610
|
-
}
|
|
611
|
-
async function updateConfig(update) {
|
|
612
|
-
return withConfigLock(async () => {
|
|
613
|
-
const raw = readRawConfig();
|
|
614
|
-
const snapshot = raw === null ? {} : structuredClone(raw);
|
|
615
|
-
const base = raw === null ? null : migrateConfig(structuredClone(raw));
|
|
616
|
-
const handed = raw === null ? null : deepFreezeConfig(migrateConfig(structuredClone(raw)));
|
|
617
|
-
const next = applyConfigPatch(base ?? {}, await runPatch(update, handed));
|
|
618
|
-
const changed = changedConfigPaths(snapshot, next);
|
|
619
|
-
if (changed.length === 0) {
|
|
620
|
-
return {
|
|
621
|
-
config: base ?? {},
|
|
622
|
-
previous: base,
|
|
623
|
-
changed: [],
|
|
624
|
-
written: false,
|
|
625
|
-
backup: describeConfigBackup()
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
const removals = protectedRemovals(base ?? {}, next);
|
|
629
|
-
const unauthorized = removals.filter((target) => update.authorize?.covers(target) !== true);
|
|
630
|
-
if (unauthorized.length > 0) throw protectedRemovalError(unauthorized, update.reason);
|
|
631
|
-
assertValidConfig(
|
|
632
|
-
next,
|
|
633
|
-
changed.map((path) => path.split(".")[0])
|
|
634
|
-
);
|
|
635
|
-
const superseded = supersededProtectedSecrets(base ?? {}, next);
|
|
636
|
-
const stripped = [
|
|
637
|
-
.../* @__PURE__ */ new Set([...removals, ...superseded, ...update.sanitizeBackup ?? []])
|
|
638
|
-
].filter(isSecretProtectedTarget);
|
|
639
|
-
const sweeping = update.sweepSlots ?? CONFIG_SNAPSHOT_SLOTS;
|
|
640
|
-
const swept = [];
|
|
641
|
-
if (raw !== null) {
|
|
642
|
-
if (stripped.length > 0) {
|
|
643
|
-
writeConfigBackup(withoutProtectedTargets(raw, stripped));
|
|
644
|
-
swept.push("backup");
|
|
645
|
-
} else {
|
|
646
|
-
retainConfigBackup();
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
if (stripped.length > 0 && sweeping.includes("pinned")) {
|
|
650
|
-
if (sweepPinnedSnapshot(stripped, base ?? {}, update.reason)) swept.push("pinned");
|
|
651
|
-
}
|
|
652
|
-
const backup = describeConfigBackup();
|
|
653
|
-
writeConfigFile(next);
|
|
654
|
-
appendConfigAudit({
|
|
655
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
656
|
-
event: "update",
|
|
657
|
-
reason: update.reason,
|
|
658
|
-
fields: changed,
|
|
659
|
-
...removals.length > 0 ? { removed: removals } : {},
|
|
660
|
-
...superseded.length > 0 ? { superseded } : {},
|
|
661
|
-
...swept.length > 0 ? { sweptSlots: swept } : {},
|
|
662
|
-
backup: backup !== null,
|
|
663
|
-
pid: process.pid
|
|
664
|
-
});
|
|
665
|
-
return { config: next, previous: base, changed, written: true, backup };
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
async function withConfigLock(fn) {
|
|
669
|
-
ensureConfigDir();
|
|
670
|
-
let release;
|
|
671
|
-
try {
|
|
672
|
-
release = await lockfile.lock(CONFIG_FILE, {
|
|
673
|
-
lockfilePath: `${CONFIG_FILE}.lock`,
|
|
674
|
-
realpath: false,
|
|
675
|
-
stale: 3e4,
|
|
676
|
-
retries: { retries: 10, factor: 1, minTimeout: 100, maxTimeout: 100 }
|
|
677
|
-
});
|
|
678
|
-
} catch {
|
|
679
|
-
throw new DvmError(
|
|
680
|
-
"config_locked",
|
|
681
|
-
"Another dvm process is updating the local configuration.",
|
|
682
|
-
"Wait a moment and retry."
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
try {
|
|
686
|
-
return await fn();
|
|
687
|
-
} finally {
|
|
688
|
-
await release().catch(() => void 0);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
async function runPatch(update, current) {
|
|
692
|
-
try {
|
|
693
|
-
return await update.patch(current);
|
|
694
|
-
} catch (err) {
|
|
695
|
-
if (err instanceof TypeError && /read only|not extensible|frozen|cannot delete property|cannot add property/i.test(
|
|
696
|
-
err.message
|
|
697
|
-
)) {
|
|
698
|
-
throw new DvmError(
|
|
699
|
-
"config_patch_invalid",
|
|
700
|
-
`The '${update.reason}' change tried to modify the configuration it was handed.`,
|
|
701
|
-
"Nothing was written. A change declares what it wants through its `set` and `unset` lists; editing the current config in place bypasses the protected-field gate, so the object is frozen."
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
throw err;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
function sweepPinnedSnapshot(targets, departing, reason) {
|
|
708
|
-
if (describeConfigBackup("pinned") === null) return false;
|
|
709
|
-
const pinned = readConfigBackup("pinned");
|
|
710
|
-
if (pinned === null) {
|
|
711
|
-
return retireSlot(`${reason}:unreadable_pin`, "pinned");
|
|
712
|
-
}
|
|
713
|
-
const migrated = migrateConfig(structuredClone(pinned));
|
|
714
|
-
const matching = targets.filter(
|
|
715
|
-
(target) => protectedValuesMatch(
|
|
716
|
-
target,
|
|
717
|
-
protectedTargetValue(departing, target),
|
|
718
|
-
protectedTargetValue(migrated, target)
|
|
719
|
-
)
|
|
720
|
-
);
|
|
721
|
-
if (matching.length === 0) return false;
|
|
722
|
-
const sanitized = withoutProtectedTargets(pinned, matching);
|
|
723
|
-
const changed = changedConfigPaths(pinned, sanitized);
|
|
724
|
-
if (changed.length === 0) return false;
|
|
725
|
-
writeConfigBackup(sanitized, "pinned");
|
|
726
|
-
appendConfigAudit({
|
|
727
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
728
|
-
event: "backup_sanitized",
|
|
729
|
-
reason,
|
|
730
|
-
fields: changed,
|
|
731
|
-
removed: matching,
|
|
732
|
-
slot: "pinned",
|
|
733
|
-
backup: describeConfigBackup() !== null,
|
|
734
|
-
pid: process.pid
|
|
735
|
-
});
|
|
736
|
-
return true;
|
|
737
|
-
}
|
|
738
|
-
function retireSlot(reason, slot) {
|
|
739
|
-
if (!removeConfigBackup(slot)) return false;
|
|
740
|
-
appendConfigAudit({
|
|
741
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
742
|
-
event: "backup_retired",
|
|
743
|
-
reason,
|
|
744
|
-
fields: [],
|
|
745
|
-
slot,
|
|
746
|
-
backup: describeConfigBackup() !== null,
|
|
747
|
-
pid: process.pid
|
|
748
|
-
});
|
|
749
|
-
return true;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
135
|
// src/lib/credit-store.ts
|
|
753
|
-
import { existsSync
|
|
136
|
+
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
754
137
|
import { dirname } from "path";
|
|
755
|
-
import
|
|
138
|
+
import lockfile from "proper-lockfile";
|
|
756
139
|
function withCreditLock(fn, opts = {}) {
|
|
757
140
|
if (lockDepth > 0) {
|
|
758
141
|
lockDepth += 1;
|
|
@@ -834,6 +217,12 @@ function fundingReachableAt(funding, endpoint, credits) {
|
|
|
834
217
|
const origins = dvmLocationOrigins(record);
|
|
835
218
|
return origins.includes(at) && origins.includes(named);
|
|
836
219
|
}
|
|
220
|
+
function pendingDrainFor(record, creditId) {
|
|
221
|
+
return record?.pendingDrains?.find((d) => d.creditId === creditId);
|
|
222
|
+
}
|
|
223
|
+
function pendingDrainById(record, drainId) {
|
|
224
|
+
return record?.pendingDrains?.find((d) => d.drainId === drainId);
|
|
225
|
+
}
|
|
837
226
|
function loadCredit(endpoint, callerPubkey, identity) {
|
|
838
227
|
return withCreditLock(() => {
|
|
839
228
|
const key = normalizeCreditEndpoint(endpoint);
|
|
@@ -847,9 +236,124 @@ function loadCredit(endpoint, callerPubkey, identity) {
|
|
|
847
236
|
return record;
|
|
848
237
|
});
|
|
849
238
|
}
|
|
239
|
+
function clearCreditSelection(endpoint, callerPubkey, creditId, identity) {
|
|
240
|
+
return withCreditLock(() => {
|
|
241
|
+
const key = normalizeCreditEndpoint(endpoint);
|
|
242
|
+
const store = loadStore();
|
|
243
|
+
const record = findDvmLocated(store.credits, {
|
|
244
|
+
endpoint: key,
|
|
245
|
+
...identity ? { identity } : {},
|
|
246
|
+
where: (candidate) => candidate.callerPubkey === callerPubkey && candidate.creditId === creditId
|
|
247
|
+
});
|
|
248
|
+
if (!record) {
|
|
249
|
+
throw new DvmError(
|
|
250
|
+
"credit_not_found",
|
|
251
|
+
`Credit ${creditId} is not the selected local credit at ${key}.`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
attachDvmLocation(record, key, identity);
|
|
255
|
+
record.selectionCleared = true;
|
|
256
|
+
record.updatedAt = Date.now();
|
|
257
|
+
saveStore(store);
|
|
258
|
+
return structuredClone(record);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
850
261
|
function listCredits() {
|
|
851
262
|
return withCreditLock(() => [...loadStore().credits].sort((a, b) => b.updatedAt - a.updatedAt));
|
|
852
263
|
}
|
|
264
|
+
function listImplicitCredits(now = Date.now()) {
|
|
265
|
+
return withCreditLock(() => {
|
|
266
|
+
const store = loadStore();
|
|
267
|
+
const aged = ageImplicitCredits(store.implicitCredits, now);
|
|
268
|
+
const capped = capLostImplicitCredits(store.implicitCredits);
|
|
269
|
+
if (aged || capped.changed) {
|
|
270
|
+
store.implicitCredits = capped.credits;
|
|
271
|
+
saveStore(store);
|
|
272
|
+
}
|
|
273
|
+
return [...store.implicitCredits].sort(compareImplicitCredits);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function loadImplicitCredits(endpoint, callerPubkey, identity, now = Date.now()) {
|
|
277
|
+
return withCreditLock(() => {
|
|
278
|
+
const store = loadStore();
|
|
279
|
+
const key = normalizeCreditEndpoint(endpoint);
|
|
280
|
+
let changed = ageImplicitCredits(store.implicitCredits, now);
|
|
281
|
+
const matches = store.implicitCredits.filter(
|
|
282
|
+
(record) => Boolean(
|
|
283
|
+
findDvmLocated([record], {
|
|
284
|
+
endpoint: key,
|
|
285
|
+
...identity ? { identity } : {},
|
|
286
|
+
where: (candidate) => candidate.callerPubkey === callerPubkey
|
|
287
|
+
})
|
|
288
|
+
)
|
|
289
|
+
);
|
|
290
|
+
for (const record of matches) {
|
|
291
|
+
changed = attachDvmLocation(record, key, identity) || changed;
|
|
292
|
+
}
|
|
293
|
+
const capped = capLostImplicitCredits(store.implicitCredits);
|
|
294
|
+
if (changed || capped.changed) {
|
|
295
|
+
store.implicitCredits = capped.credits;
|
|
296
|
+
saveStore(store);
|
|
297
|
+
}
|
|
298
|
+
return matches.filter((record) => store.implicitCredits.includes(record)).sort(compareImplicitCredits);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
function markImplicitCreditLost(args) {
|
|
302
|
+
return withCreditLock(() => {
|
|
303
|
+
const store = loadStore();
|
|
304
|
+
const key = normalizeCreditEndpoint(args.endpoint);
|
|
305
|
+
const record = findDvmLocated(store.implicitCredits, {
|
|
306
|
+
endpoint: key,
|
|
307
|
+
...args.identity ? { identity: args.identity } : {},
|
|
308
|
+
where: (candidate) => candidate.callerPubkey === args.callerPubkey && candidate.creditId === args.creditId
|
|
309
|
+
});
|
|
310
|
+
if (!record) return void 0;
|
|
311
|
+
attachDvmLocation(record, key, args.identity);
|
|
312
|
+
record.status = "lost";
|
|
313
|
+
record.lossReason = args.reason;
|
|
314
|
+
record.updatedAt = args.now ?? Date.now();
|
|
315
|
+
const capped = capLostImplicitCredits(store.implicitCredits);
|
|
316
|
+
store.implicitCredits = capped.credits;
|
|
317
|
+
saveStore(store);
|
|
318
|
+
return store.implicitCredits.includes(record) ? record : void 0;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
function listFundingMenuSnapshots() {
|
|
322
|
+
return withCreditLock(
|
|
323
|
+
() => [...loadStore().fundingMenuSnapshots].sort((a, b) => b.observedAt - a.observedAt)
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
function recordFundingMenuSnapshot(args) {
|
|
327
|
+
const menu = parseCreditMenu(args.menu);
|
|
328
|
+
if (!menu) return;
|
|
329
|
+
withCreditLock(() => {
|
|
330
|
+
const store = loadStore();
|
|
331
|
+
const key = normalizeCreditEndpoint(args.endpoint);
|
|
332
|
+
const previous = findDvmLocated(store.fundingMenuSnapshots, {
|
|
333
|
+
endpoint: key,
|
|
334
|
+
...args.identity ? { identity: args.identity } : {},
|
|
335
|
+
where: (entry) => entry.callerPubkey === args.callerPubkey
|
|
336
|
+
});
|
|
337
|
+
const index = previous ? store.fundingMenuSnapshots.indexOf(previous) : -1;
|
|
338
|
+
const builderPubkey = args.builderPubkey ?? previous?.builderPubkey;
|
|
339
|
+
const snapshot = {
|
|
340
|
+
// The row keeps the origin it was opened at and learns the new spelling
|
|
341
|
+
// as an alias, so a caller who alternates hostnames still reads one menu.
|
|
342
|
+
endpoint: previous?.endpoint ?? key,
|
|
343
|
+
...previous?.endpointAliases ? { endpointAliases: [...previous.endpointAliases] } : {},
|
|
344
|
+
...previous?.dvmId ? { dvmId: previous.dvmId } : {},
|
|
345
|
+
...previous?.attestedBuilderPubkey ? { attestedBuilderPubkey: previous.attestedBuilderPubkey } : {},
|
|
346
|
+
callerPubkey: args.callerPubkey,
|
|
347
|
+
menu,
|
|
348
|
+
observedAt: args.now ?? Date.now(),
|
|
349
|
+
...builderPubkey ? { builderPubkey } : {}
|
|
350
|
+
};
|
|
351
|
+
attachDvmLocation(snapshot, key, args.identity);
|
|
352
|
+
if (index === -1) store.fundingMenuSnapshots.push(snapshot);
|
|
353
|
+
else store.fundingMenuSnapshots[index] = snapshot;
|
|
354
|
+
saveStore(store);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
853
357
|
function listPendingFundings(args = {}) {
|
|
854
358
|
const endpoint = args.endpoint ? normalizeCreditEndpoint(args.endpoint) : void 0;
|
|
855
359
|
return withCreditLock(() => {
|
|
@@ -945,6 +449,231 @@ function recordFundResponse(args) {
|
|
|
945
449
|
return record;
|
|
946
450
|
});
|
|
947
451
|
}
|
|
452
|
+
function recordBalanceResponse(args) {
|
|
453
|
+
const now = args.now ?? Date.now();
|
|
454
|
+
return withCreditLock(() => {
|
|
455
|
+
const existing = loadCredit(args.endpoint, args.callerPubkey, args.identity);
|
|
456
|
+
const prepaidCredits = args.credits.filter((credit) => !credit.credit_id.startsWith("imp:"));
|
|
457
|
+
const selectedPrimary = args.primary && !args.primary.credit_id.startsWith("imp:") ? args.primary : void 0;
|
|
458
|
+
const primary = selectedPrimary ?? prepaidCredits.find((credit) => credit.credit_id === existing?.creditId);
|
|
459
|
+
if (!primary) {
|
|
460
|
+
if (existing) {
|
|
461
|
+
mutateCredit(
|
|
462
|
+
{
|
|
463
|
+
endpoint: args.endpoint,
|
|
464
|
+
callerPubkey: args.callerPubkey,
|
|
465
|
+
...args.identity ? { identity: args.identity } : {}
|
|
466
|
+
},
|
|
467
|
+
(record2) => {
|
|
468
|
+
record2.siblingCredits = [];
|
|
469
|
+
return void 0;
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
return existing;
|
|
474
|
+
}
|
|
475
|
+
const record = recordFundResponse({
|
|
476
|
+
endpoint: args.endpoint,
|
|
477
|
+
...args.identity ? { identity: args.identity } : {},
|
|
478
|
+
callerPubkey: args.callerPubkey,
|
|
479
|
+
credit: primary,
|
|
480
|
+
targetMicro: args.targetMicro ?? existing?.targetMicro ?? primary.remaining_micro,
|
|
481
|
+
...args.menu ? { menu: args.menu } : {},
|
|
482
|
+
...args.builderPubkey ? { builderPubkey: args.builderPubkey } : {},
|
|
483
|
+
...args.selectPrimary ? { select: true } : {},
|
|
484
|
+
...args.sequenced === false ? { sequenced: false } : {},
|
|
485
|
+
now
|
|
486
|
+
});
|
|
487
|
+
mutateCredit(
|
|
488
|
+
{
|
|
489
|
+
endpoint: args.endpoint,
|
|
490
|
+
callerPubkey: args.callerPubkey,
|
|
491
|
+
...args.identity ? { identity: args.identity } : {}
|
|
492
|
+
},
|
|
493
|
+
(current) => {
|
|
494
|
+
current.siblingCredits = prepaidCredits.filter((credit) => credit.credit_id !== current.creditId).map((credit) => siblingFromWire(credit, now));
|
|
495
|
+
return void 0;
|
|
496
|
+
}
|
|
497
|
+
);
|
|
498
|
+
return loadCredit(args.endpoint, args.callerPubkey, args.identity) ?? record;
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
function applyReceiptCredit(args) {
|
|
502
|
+
const now = args.now ?? Date.now();
|
|
503
|
+
return withCreditLock(() => {
|
|
504
|
+
const store = loadStore();
|
|
505
|
+
const key = normalizeCreditEndpoint(args.endpoint);
|
|
506
|
+
const record = findDvmLocated(store.credits, {
|
|
507
|
+
endpoint: key,
|
|
508
|
+
...args.identity ? { identity: args.identity } : {},
|
|
509
|
+
where: (c) => c.callerPubkey === args.callerPubkey && c.creditId === args.credit.credit_id
|
|
510
|
+
});
|
|
511
|
+
if (!record) return {};
|
|
512
|
+
attachDvmLocation(record, key, args.identity);
|
|
513
|
+
let mismatch;
|
|
514
|
+
const sequential = args.credit.ledger_seq === record.ledgerSeq + 1;
|
|
515
|
+
if (sequential && args.outcome === "completed" && record.status === "active" && !record.provisional && !record.unsequencedBalance) {
|
|
516
|
+
const expected = record.remainingMicro - args.credit.amount;
|
|
517
|
+
if (args.credit.balance_after !== expected) {
|
|
518
|
+
mismatch = {
|
|
519
|
+
atMs: now,
|
|
520
|
+
expectedMicro: expected,
|
|
521
|
+
reportedMicro: args.credit.balance_after,
|
|
522
|
+
drawId: args.credit.draw_id,
|
|
523
|
+
ledgerSeq: args.credit.ledger_seq,
|
|
524
|
+
...record.lastReceiptJobId ? { priorJobId: record.lastReceiptJobId } : {},
|
|
525
|
+
jobId: args.jobId
|
|
526
|
+
};
|
|
527
|
+
record.discrepancy = mismatch;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (args.credit.ledger_seq >= record.ledgerSeq) {
|
|
531
|
+
record.remainingMicro = args.credit.balance_after;
|
|
532
|
+
record.ledgerSeq = args.credit.ledger_seq;
|
|
533
|
+
delete record.unsequencedBalance;
|
|
534
|
+
}
|
|
535
|
+
delete record.provisional;
|
|
536
|
+
if (args.outcome === "completed") record.lastPriceMicro = args.credit.amount;
|
|
537
|
+
if (!mismatch && record.discrepancy && args.credit.ledger_seq > record.discrepancy.ledgerSeq) {
|
|
538
|
+
delete record.discrepancy;
|
|
539
|
+
}
|
|
540
|
+
record.lastReceiptJobId = args.jobId;
|
|
541
|
+
record.updatedAt = now;
|
|
542
|
+
saveStore(store);
|
|
543
|
+
return { credit: record, ...mismatch ? { mismatch } : {} };
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
function applyImplicitReceiptCredit(args) {
|
|
547
|
+
if (!args.credit.credit_id.startsWith("imp:")) return {};
|
|
548
|
+
const now = args.now ?? Date.now();
|
|
549
|
+
return withCreditLock(() => {
|
|
550
|
+
const store = loadStore();
|
|
551
|
+
const key = normalizeCreditEndpoint(args.endpoint);
|
|
552
|
+
let record = findDvmLocated(store.implicitCredits, {
|
|
553
|
+
endpoint: key,
|
|
554
|
+
...args.identity ? { identity: args.identity } : {},
|
|
555
|
+
where: (candidate) => candidate.callerPubkey === args.callerPubkey && candidate.creditId === args.credit.credit_id
|
|
556
|
+
});
|
|
557
|
+
if (!record && (args.outcome === "completed" || args.credit.balance_after <= 0)) return {};
|
|
558
|
+
if (!record) {
|
|
559
|
+
record = {
|
|
560
|
+
endpoint: key,
|
|
561
|
+
...identityFields(args.identity),
|
|
562
|
+
callerPubkey: args.callerPubkey,
|
|
563
|
+
creditId: args.credit.credit_id,
|
|
564
|
+
currency: args.currency,
|
|
565
|
+
remainingMicro: args.credit.balance_after,
|
|
566
|
+
ledgerSeq: args.credit.ledger_seq,
|
|
567
|
+
expiryMs: args.issuedAtMs + IMPLICIT_CREDIT_TTL_MS,
|
|
568
|
+
status: args.issuedAtMs + IMPLICIT_CREDIT_TTL_MS <= now ? "lost" : "active",
|
|
569
|
+
releasedByJobId: args.jobId,
|
|
570
|
+
releasedAt: args.issuedAtMs,
|
|
571
|
+
updatedAt: now,
|
|
572
|
+
lastReceiptJobId: args.jobId
|
|
573
|
+
};
|
|
574
|
+
store.implicitCredits.push(record);
|
|
575
|
+
const capped2 = capLostImplicitCredits(store.implicitCredits);
|
|
576
|
+
store.implicitCredits = capped2.credits;
|
|
577
|
+
saveStore(store);
|
|
578
|
+
return store.implicitCredits.includes(record) ? { credit: record } : {};
|
|
579
|
+
}
|
|
580
|
+
attachDvmLocation(record, key, args.identity);
|
|
581
|
+
let mismatch;
|
|
582
|
+
const sequential = args.credit.ledger_seq === record.ledgerSeq + 1;
|
|
583
|
+
if (sequential && args.outcome === "completed" && record.status === "active") {
|
|
584
|
+
const expected = record.remainingMicro - args.credit.amount;
|
|
585
|
+
if (args.credit.balance_after !== expected) {
|
|
586
|
+
mismatch = {
|
|
587
|
+
atMs: now,
|
|
588
|
+
expectedMicro: expected,
|
|
589
|
+
reportedMicro: args.credit.balance_after,
|
|
590
|
+
drawId: args.credit.draw_id,
|
|
591
|
+
ledgerSeq: args.credit.ledger_seq,
|
|
592
|
+
...record.lastReceiptJobId ? { priorJobId: record.lastReceiptJobId } : {},
|
|
593
|
+
jobId: args.jobId
|
|
594
|
+
};
|
|
595
|
+
record.discrepancy = mismatch;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (args.credit.ledger_seq >= record.ledgerSeq) {
|
|
599
|
+
record.remainingMicro = args.credit.balance_after;
|
|
600
|
+
record.ledgerSeq = args.credit.ledger_seq;
|
|
601
|
+
}
|
|
602
|
+
if (!mismatch && record.discrepancy && args.credit.ledger_seq > record.discrepancy.ledgerSeq) {
|
|
603
|
+
delete record.discrepancy;
|
|
604
|
+
}
|
|
605
|
+
record.currency = args.currency;
|
|
606
|
+
record.lastReceiptJobId = args.jobId;
|
|
607
|
+
record.updatedAt = now;
|
|
608
|
+
if (record.remainingMicro <= 0) {
|
|
609
|
+
store.implicitCredits = store.implicitCredits.filter((candidate) => candidate !== record);
|
|
610
|
+
saveStore(store);
|
|
611
|
+
return { ...mismatch ? { mismatch } : {}, pruned: true };
|
|
612
|
+
}
|
|
613
|
+
if (record.expiryMs <= now) record.status = "lost";
|
|
614
|
+
const capped = capLostImplicitCredits(store.implicitCredits);
|
|
615
|
+
store.implicitCredits = capped.credits;
|
|
616
|
+
saveStore(store);
|
|
617
|
+
return {
|
|
618
|
+
...store.implicitCredits.includes(record) ? { credit: record } : {},
|
|
619
|
+
...mismatch ? { mismatch } : {}
|
|
620
|
+
};
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
function ensureProvisionalCredit(args) {
|
|
624
|
+
const now = args.now ?? Date.now();
|
|
625
|
+
withCreditLock(() => {
|
|
626
|
+
const store = loadStore();
|
|
627
|
+
const key = normalizeCreditEndpoint(args.endpoint);
|
|
628
|
+
let record = findDvmLocated(store.credits, {
|
|
629
|
+
endpoint: key,
|
|
630
|
+
...args.identity ? { identity: args.identity } : {},
|
|
631
|
+
where: (c) => c.callerPubkey === args.callerPubkey
|
|
632
|
+
});
|
|
633
|
+
if (!record) {
|
|
634
|
+
record = {
|
|
635
|
+
endpoint: key,
|
|
636
|
+
...identityFields(args.identity),
|
|
637
|
+
callerPubkey: args.callerPubkey,
|
|
638
|
+
creditId: args.creditId,
|
|
639
|
+
currency: args.currency,
|
|
640
|
+
balanceMicro: 0,
|
|
641
|
+
remainingMicro: 0,
|
|
642
|
+
ledgerSeq: 0,
|
|
643
|
+
expiryMs: 0,
|
|
644
|
+
targetMicro: args.targetMicro,
|
|
645
|
+
status: "active",
|
|
646
|
+
openedAt: now,
|
|
647
|
+
updatedAt: now
|
|
648
|
+
};
|
|
649
|
+
store.credits.push(record);
|
|
650
|
+
}
|
|
651
|
+
attachDvmLocation(record, key, args.identity);
|
|
652
|
+
adoptCreditId(record, args.creditId);
|
|
653
|
+
record.currency = args.currency;
|
|
654
|
+
record.targetMicro = args.targetMicro;
|
|
655
|
+
record.status = "active";
|
|
656
|
+
record.provisional = true;
|
|
657
|
+
record.updatedAt = now;
|
|
658
|
+
if (args.menu) record.menu = args.menu;
|
|
659
|
+
saveStore(store);
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
function markCredit(endpoint, callerPubkey, status, opts = {}) {
|
|
663
|
+
mutateCredit(
|
|
664
|
+
{ endpoint, callerPubkey, ...opts.identity ? { identity: opts.identity } : {} },
|
|
665
|
+
(record) => {
|
|
666
|
+
if (opts.ifCreditId !== void 0 && record.creditId !== opts.ifCreditId) return "skip";
|
|
667
|
+
record.status = status;
|
|
668
|
+
if (status !== "active") {
|
|
669
|
+
delete record.refillUnavailableRail;
|
|
670
|
+
delete record.refillDrainNoticed;
|
|
671
|
+
}
|
|
672
|
+
if (status === "drained") record.remainingMicro = 0;
|
|
673
|
+
return void 0;
|
|
674
|
+
}
|
|
675
|
+
);
|
|
676
|
+
}
|
|
948
677
|
function loadCreditX402Binding(endpoint, callerPubkey, creditId, identity) {
|
|
949
678
|
const record = loadCredit(endpoint, callerPubkey, identity);
|
|
950
679
|
if (record?.creditId !== creditId) return {};
|
|
@@ -970,6 +699,127 @@ function recordCreditX402Instrument(endpoint, callerPubkey, creditId, instrument
|
|
|
970
699
|
return void 0;
|
|
971
700
|
});
|
|
972
701
|
}
|
|
702
|
+
function recordRefusedDrawBalance(args) {
|
|
703
|
+
const remaining = safeMicro(args.remainingMicro);
|
|
704
|
+
if (remaining === void 0) return;
|
|
705
|
+
const balance = safeMicro(args.balanceMicro);
|
|
706
|
+
mutateCredit(mutationAt(args.endpoint, args.callerPubkey, args.identity), (record) => {
|
|
707
|
+
if (record.creditId !== args.creditId) return "skip";
|
|
708
|
+
const unchanged = record.remainingMicro === remaining && (balance === void 0 || record.balanceMicro === balance) && record.unsequencedBalance === true && !record.provisional;
|
|
709
|
+
if (unchanged) return "skip";
|
|
710
|
+
record.remainingMicro = remaining;
|
|
711
|
+
if (balance !== void 0) record.balanceMicro = balance;
|
|
712
|
+
record.unsequencedBalance = true;
|
|
713
|
+
delete record.provisional;
|
|
714
|
+
return void 0;
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
function markRefillRailUnavailable(endpoint, callerPubkey, creditId, rail, identity) {
|
|
718
|
+
let firstObservation = false;
|
|
719
|
+
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
720
|
+
if (record.creditId !== creditId) return "skip";
|
|
721
|
+
if (record.refillUnavailableRail === rail) return "skip";
|
|
722
|
+
record.refillUnavailableRail = rail;
|
|
723
|
+
firstObservation = true;
|
|
724
|
+
return void 0;
|
|
725
|
+
});
|
|
726
|
+
return firstObservation;
|
|
727
|
+
}
|
|
728
|
+
function markRefillDrainNoticed(endpoint, callerPubkey, creditId, drainId, identity) {
|
|
729
|
+
let firstObservation = false;
|
|
730
|
+
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
731
|
+
if (record.creditId !== creditId) return "skip";
|
|
732
|
+
if (record.refillDrainNoticed === drainId) return "skip";
|
|
733
|
+
record.refillDrainNoticed = drainId;
|
|
734
|
+
firstObservation = true;
|
|
735
|
+
return void 0;
|
|
736
|
+
});
|
|
737
|
+
return firstObservation;
|
|
738
|
+
}
|
|
739
|
+
function clearRefillRailUnavailable(endpoint, callerPubkey, creditId, identity) {
|
|
740
|
+
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
741
|
+
if (record.creditId !== creditId) return "skip";
|
|
742
|
+
if (record.refillUnavailableRail === void 0) return "skip";
|
|
743
|
+
delete record.refillUnavailableRail;
|
|
744
|
+
return void 0;
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
function forgetCredit(endpoint, callerPubkey, identity) {
|
|
748
|
+
withCreditLock(() => {
|
|
749
|
+
const store = loadStore();
|
|
750
|
+
const key = normalizeCreditEndpoint(endpoint);
|
|
751
|
+
const record = findDvmLocated(store.credits, {
|
|
752
|
+
endpoint: key,
|
|
753
|
+
...identity ? { identity } : {},
|
|
754
|
+
where: (c) => c.callerPubkey === callerPubkey
|
|
755
|
+
});
|
|
756
|
+
if (!record) return;
|
|
757
|
+
saveStore({ ...store, credits: store.credits.filter((c) => c !== record) });
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
function recordPendingDrain(endpoint, callerPubkey, drain, identity) {
|
|
761
|
+
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
762
|
+
const drains = record.pendingDrains ?? [];
|
|
763
|
+
const index = drains.findIndex(
|
|
764
|
+
(d) => d.drainId === drain.drainId && d.creditId === drain.creditId
|
|
765
|
+
);
|
|
766
|
+
if (index === -1) drains.push(drain);
|
|
767
|
+
else drains[index] = drain;
|
|
768
|
+
record.pendingDrains = capPendingDrains(drains, record.creditId);
|
|
769
|
+
return void 0;
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
function clearPendingDrain(endpoint, callerPubkey, drain, identity) {
|
|
773
|
+
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
774
|
+
const before = record.pendingDrains ?? [];
|
|
775
|
+
const remaining = before.filter(
|
|
776
|
+
(d) => !(d.drainId === drain.drainId && d.creditId === drain.creditId)
|
|
777
|
+
);
|
|
778
|
+
if (remaining.length === before.length) return "skip";
|
|
779
|
+
if (remaining.length === 0) delete record.pendingDrains;
|
|
780
|
+
else record.pendingDrains = remaining;
|
|
781
|
+
return void 0;
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
function recordDrainReceipts(args) {
|
|
785
|
+
const {
|
|
786
|
+
endpoint,
|
|
787
|
+
identity,
|
|
788
|
+
callerPubkey,
|
|
789
|
+
drainId,
|
|
790
|
+
creditId,
|
|
791
|
+
receipts,
|
|
792
|
+
read,
|
|
793
|
+
now = Date.now()
|
|
794
|
+
} = args;
|
|
795
|
+
if (receipts.length === 0) return;
|
|
796
|
+
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
797
|
+
const evidence = record.drainEvidence ??= [];
|
|
798
|
+
let bundle = evidence.find((e) => e.drainId === drainId);
|
|
799
|
+
if (!bundle) {
|
|
800
|
+
bundle = { drainId, creditId, receipts: [], collectedAt: now };
|
|
801
|
+
evidence.push(bundle);
|
|
802
|
+
}
|
|
803
|
+
bundle.creditId = creditId;
|
|
804
|
+
bundle.receipts = receipts;
|
|
805
|
+
bundle.collectedAt = now;
|
|
806
|
+
if (read.anchor) bundle.anchor = read.anchor;
|
|
807
|
+
else if (read.reachable) delete bundle.anchor;
|
|
808
|
+
record.drainEvidence = capDrainEvidence(evidence, endpoint);
|
|
809
|
+
return void 0;
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
function settleDrainEvidence(args) {
|
|
813
|
+
const { endpoint, identity, callerPubkey, drainId, now = Date.now() } = args;
|
|
814
|
+
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
815
|
+
const evidence = record.drainEvidence ?? [];
|
|
816
|
+
const bundle = evidence.find((e) => e.drainId === drainId);
|
|
817
|
+
if (!bundle || bundle.settledAt !== void 0) return "skip";
|
|
818
|
+
bundle.settledAt = now;
|
|
819
|
+
record.drainEvidence = capDrainEvidence(evidence, endpoint);
|
|
820
|
+
return void 0;
|
|
821
|
+
});
|
|
822
|
+
}
|
|
973
823
|
function identityFields(identity) {
|
|
974
824
|
if (!identity) return {};
|
|
975
825
|
return { dvmId: identity.dvmId, attestedBuilderPubkey: identity.builderPubkey };
|
|
@@ -1029,21 +879,23 @@ function adoptCreditId(record, creditId) {
|
|
|
1029
879
|
delete record.discrepancy;
|
|
1030
880
|
delete record.lastReceiptJobId;
|
|
1031
881
|
}
|
|
882
|
+
var MAX_PENDING_DRAINS = 8;
|
|
883
|
+
var MAX_SETTLED_DRAIN_EVIDENCE = 8;
|
|
1032
884
|
var MAX_LOST_IMPLICIT_CREDITS = 8;
|
|
1033
885
|
var MAX_DATE_MS = 864e13;
|
|
1034
886
|
function loadStore() {
|
|
1035
|
-
if (!
|
|
887
|
+
if (!existsSync(CREDITS_FILE)) {
|
|
1036
888
|
return { credits: [], implicitCredits: [], pendingFundings: [], fundingMenuSnapshots: [] };
|
|
1037
889
|
}
|
|
1038
|
-
let
|
|
890
|
+
let raw2;
|
|
1039
891
|
try {
|
|
1040
|
-
|
|
892
|
+
raw2 = readFileSync(CREDITS_FILE, "utf-8");
|
|
1041
893
|
} catch (cause) {
|
|
1042
894
|
throw unreadableStore(cause);
|
|
1043
895
|
}
|
|
1044
896
|
let data;
|
|
1045
897
|
try {
|
|
1046
|
-
const parsed = JSON.parse(
|
|
898
|
+
const parsed = JSON.parse(raw2);
|
|
1047
899
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1048
900
|
throw new Error("the file parsed as JSON but is not a credit-store object");
|
|
1049
901
|
}
|
|
@@ -1118,6 +970,18 @@ function dedupeImplicitCredits(credits) {
|
|
|
1118
970
|
const capped = capLostImplicitCredits(merged);
|
|
1119
971
|
return { credits: capped.credits, changed: changed || capped.changed };
|
|
1120
972
|
}
|
|
973
|
+
function ageImplicitCredits(credits, now) {
|
|
974
|
+
let changed = false;
|
|
975
|
+
for (const credit of credits) {
|
|
976
|
+
if (credit.status === "active" && credit.expiryMs <= now) {
|
|
977
|
+
credit.status = "lost";
|
|
978
|
+
credit.lossReason = "expired";
|
|
979
|
+
credit.updatedAt = now;
|
|
980
|
+
changed = true;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return changed;
|
|
984
|
+
}
|
|
1121
985
|
function capLostImplicitCredits(credits) {
|
|
1122
986
|
const lostByOwner = /* @__PURE__ */ new Map();
|
|
1123
987
|
for (const credit of credits) {
|
|
@@ -1136,6 +1000,10 @@ function capLostImplicitCredits(credits) {
|
|
|
1136
1000
|
}
|
|
1137
1001
|
return dropped.size === 0 ? { credits, changed: false } : { credits: credits.filter((credit) => !dropped.has(credit)), changed: true };
|
|
1138
1002
|
}
|
|
1003
|
+
function compareImplicitCredits(a, b) {
|
|
1004
|
+
if (a.status !== b.status) return a.status === "active" ? -1 : 1;
|
|
1005
|
+
return a.status === "active" ? a.expiryMs - b.expiryMs : b.updatedAt - a.updatedAt;
|
|
1006
|
+
}
|
|
1139
1007
|
function mergeDuplicateSnapshots(snapshots, credits) {
|
|
1140
1008
|
const identities = originIdentityMap(credits);
|
|
1141
1009
|
const merged = [];
|
|
@@ -1339,6 +1207,21 @@ function isFundingMenuSnapshot(value) {
|
|
|
1339
1207
|
const snapshot = value;
|
|
1340
1208
|
return typeof snapshot.endpoint === "string" && typeof snapshot.callerPubkey === "string" && typeof snapshot.observedAt === "number" && Number.isFinite(snapshot.observedAt) && Math.abs(snapshot.observedAt) <= MAX_DATE_MS && parseCreditMenu(snapshot.menu) !== void 0 && (snapshot.builderPubkey === void 0 || typeof snapshot.builderPubkey === "string");
|
|
1341
1209
|
}
|
|
1210
|
+
function siblingFromWire(credit, observedAt) {
|
|
1211
|
+
return {
|
|
1212
|
+
creditId: credit.credit_id,
|
|
1213
|
+
status: credit.status ?? "active",
|
|
1214
|
+
currency: credit.currency,
|
|
1215
|
+
balanceMicro: credit.balance_micro,
|
|
1216
|
+
remainingMicro: credit.remaining_micro,
|
|
1217
|
+
ledgerSeq: credit.ledger_seq,
|
|
1218
|
+
expiryMs: credit.expiry_ms,
|
|
1219
|
+
expired: credit.expired,
|
|
1220
|
+
drainable: credit.drainable,
|
|
1221
|
+
...credit.reconciliation_pending ? { reconciliationPending: true } : {},
|
|
1222
|
+
observedAt
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1342
1225
|
function normalizeRailKey(record) {
|
|
1343
1226
|
const loose = record;
|
|
1344
1227
|
if (loose.rail === LEGACY_TEMPO_RAIL_KEY) loose.rail = "tempo";
|
|
@@ -1402,15 +1285,37 @@ function isEvidenceish(value) {
|
|
|
1402
1285
|
function isDrainish(value) {
|
|
1403
1286
|
return typeof value === "object" && value !== null && typeof value.drainId === "string";
|
|
1404
1287
|
}
|
|
1288
|
+
function capPendingDrains(drains, creditId) {
|
|
1289
|
+
if (drains.length <= MAX_PENDING_DRAINS) return drains;
|
|
1290
|
+
const evictable = drains.filter((d) => d.creditId !== creditId).sort((a, b) => a.requestedAt - b.requestedAt);
|
|
1291
|
+
const dropped = new Set(evictable.slice(0, drains.length - MAX_PENDING_DRAINS));
|
|
1292
|
+
for (const d of dropped) {
|
|
1293
|
+
info(
|
|
1294
|
+
`Dropping the oldest unclaimed drain record (${d.drainId}${d.creditId ? ` on credit ${d.creditId}` : ""}) \u2014 ${String(MAX_PENDING_DRAINS)} is the most this DVM's record keeps.`
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
return drains.filter((d) => !dropped.has(d));
|
|
1298
|
+
}
|
|
1299
|
+
function capDrainEvidence(evidence, endpoint) {
|
|
1300
|
+
const settled = evidence.filter((e) => e.settledAt !== void 0).sort((a, b) => (a.settledAt ?? 0) - (b.settledAt ?? 0));
|
|
1301
|
+
if (settled.length <= MAX_SETTLED_DRAIN_EVIDENCE) return evidence;
|
|
1302
|
+
const dropped = new Set(settled.slice(0, settled.length - MAX_SETTLED_DRAIN_EVIDENCE));
|
|
1303
|
+
for (const e of dropped) {
|
|
1304
|
+
info(
|
|
1305
|
+
`Dropping the oldest completed reclaim's receipts (drain ${e.drainId}${e.creditId ? ` on credit ${e.creditId}` : ""}) \u2014 ${String(MAX_SETTLED_DRAIN_EVIDENCE)} is the most this DVM's record archives. Export the ones still held before they age out too: 'dvm credit receipts ${endpoint} --export'.`
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
return evidence.filter((e) => !dropped.has(e));
|
|
1309
|
+
}
|
|
1405
1310
|
function saveStore(store) {
|
|
1406
1311
|
ensureConfigDir();
|
|
1407
1312
|
const tmp = `${CREDITS_FILE}.${String(process.pid)}.tmp`;
|
|
1408
|
-
|
|
1313
|
+
writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
|
|
1409
1314
|
try {
|
|
1410
|
-
|
|
1315
|
+
renameSync(tmp, CREDITS_FILE);
|
|
1411
1316
|
} catch (err) {
|
|
1412
1317
|
try {
|
|
1413
|
-
|
|
1318
|
+
unlinkSync(tmp);
|
|
1414
1319
|
} catch {
|
|
1415
1320
|
}
|
|
1416
1321
|
throw err;
|
|
@@ -1431,7 +1336,7 @@ function quarantineCorruptStore(cause) {
|
|
|
1431
1336
|
const quarantined = `${CREDITS_FILE}.corrupt-${stamp}-${String(process.pid)}`;
|
|
1432
1337
|
let moved = false;
|
|
1433
1338
|
try {
|
|
1434
|
-
|
|
1339
|
+
renameSync(CREDITS_FILE, quarantined);
|
|
1435
1340
|
moved = true;
|
|
1436
1341
|
} catch {
|
|
1437
1342
|
}
|
|
@@ -1448,6 +1353,9 @@ function quarantineCorruptStore(cause) {
|
|
|
1448
1353
|
`The local record of your prepaid credits is damaged. It was set aside at ${kept} rather than overwritten, so any signed proof of a refund you are owed is still in that file. Your credits are held by each DVM, not by this file, so re-reading a balance will restore what the CLI shows.`
|
|
1449
1354
|
);
|
|
1450
1355
|
}
|
|
1356
|
+
function safeMicro(value) {
|
|
1357
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
1358
|
+
}
|
|
1451
1359
|
function mutateCredit(location, mutate) {
|
|
1452
1360
|
withCreditLock(() => {
|
|
1453
1361
|
const store = loadStore();
|
|
@@ -1480,7 +1388,7 @@ function acquireCreditLock(opts) {
|
|
|
1480
1388
|
let lastError;
|
|
1481
1389
|
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
1482
1390
|
try {
|
|
1483
|
-
return
|
|
1391
|
+
return lockfile.lockSync(CREDITS_FILE, {
|
|
1484
1392
|
lockfilePath: lockPath,
|
|
1485
1393
|
stale,
|
|
1486
1394
|
realpath: false
|
|
@@ -1532,26 +1440,50 @@ function sleepSync(ms) {
|
|
|
1532
1440
|
}
|
|
1533
1441
|
|
|
1534
1442
|
export {
|
|
1443
|
+
setOutputTraceId,
|
|
1444
|
+
getOutputTraceId,
|
|
1445
|
+
json,
|
|
1535
1446
|
info,
|
|
1447
|
+
raw,
|
|
1536
1448
|
progress,
|
|
1449
|
+
RAIL_FLAGS,
|
|
1537
1450
|
maxAdvertisedMicro,
|
|
1538
1451
|
withinAdvertisedTolerance,
|
|
1539
|
-
isValidEvmPrivateKey,
|
|
1540
|
-
authorizeProtectedRemoval,
|
|
1541
|
-
isCreditPosture,
|
|
1542
|
-
loadConfig,
|
|
1543
|
-
updateConfig,
|
|
1544
1452
|
SUPPORTED_PROTOCOL_VERSION,
|
|
1545
1453
|
parseCreditTerms,
|
|
1546
1454
|
parseCreditMenu,
|
|
1547
1455
|
normalizeCreditEndpoint,
|
|
1456
|
+
dvmLocationIdentity,
|
|
1548
1457
|
fundingReachableAt,
|
|
1458
|
+
pendingDrainFor,
|
|
1459
|
+
pendingDrainById,
|
|
1460
|
+
loadCredit,
|
|
1461
|
+
clearCreditSelection,
|
|
1549
1462
|
listCredits,
|
|
1463
|
+
listImplicitCredits,
|
|
1464
|
+
loadImplicitCredits,
|
|
1465
|
+
markImplicitCreditLost,
|
|
1466
|
+
listFundingMenuSnapshots,
|
|
1467
|
+
recordFundingMenuSnapshot,
|
|
1550
1468
|
listPendingFundings,
|
|
1551
1469
|
loadPendingFunding,
|
|
1552
1470
|
recordPendingFunding,
|
|
1553
1471
|
clearPendingFunding,
|
|
1554
1472
|
recordFundResponse,
|
|
1473
|
+
recordBalanceResponse,
|
|
1474
|
+
applyReceiptCredit,
|
|
1475
|
+
applyImplicitReceiptCredit,
|
|
1476
|
+
ensureProvisionalCredit,
|
|
1477
|
+
markCredit,
|
|
1555
1478
|
loadCreditX402Binding,
|
|
1556
|
-
recordCreditX402Instrument
|
|
1479
|
+
recordCreditX402Instrument,
|
|
1480
|
+
recordRefusedDrawBalance,
|
|
1481
|
+
markRefillRailUnavailable,
|
|
1482
|
+
markRefillDrainNoticed,
|
|
1483
|
+
clearRefillRailUnavailable,
|
|
1484
|
+
forgetCredit,
|
|
1485
|
+
recordPendingDrain,
|
|
1486
|
+
clearPendingDrain,
|
|
1487
|
+
recordDrainReceipts,
|
|
1488
|
+
settleDrainEvidence
|
|
1557
1489
|
};
|