@dvmkit/sdk 0.1.0-rc.3 → 0.1.0-rc.5
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-FT7IM66W.js → chunk-4KB3LTOS.js} +1343 -1258
- package/dist/{chunk-XY5Y5REG.js → chunk-AAJNGQMC.js} +1 -1
- package/dist/{chunk-RHP3BRTH.js → chunk-AB3L6B52.js} +3 -3
- package/dist/{chunk-C2DC4FKR.js → chunk-ATQIII6K.js} +330 -9
- package/dist/{chunk-5SO7ZVOH.js → chunk-F2L6KIMD.js} +5 -0
- package/dist/{chunk-YD3TZNXV.js → chunk-ZMZTZFRC.js} +17 -2
- package/dist/{credit-menu-DONAtGVf.d.ts → credit-menu-ClA1JyYW.d.ts} +2 -2
- package/dist/{fx-Bq4cvn16.d.ts → fx-ptKVFOwq.d.ts} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/internal/index.d.ts +236 -7
- package/dist/internal/index.js +45 -6
- package/dist/{job-store-m2pYmvbr.d.ts → job-store-DxFqDPYq.d.ts} +6 -1
- package/dist/server/index.d.ts +5 -5
- package/dist/server/index.js +5 -213
- package/dist/testing/index.d.ts +1 -1
- package/dist/{usd-DjVAPMlf.d.ts → usd-Cha_j80I.d.ts} +1 -1
- package/dist/{x402-5EVIUSEP.js → x402-3VQ64MCS.js} +4 -4
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
CREDITS_FILE,
|
|
7
7
|
DvmError,
|
|
8
8
|
ensureConfigDir
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-F2L6KIMD.js";
|
|
10
10
|
|
|
11
11
|
// src/lib/payment-rails/types.ts
|
|
12
12
|
var ADVERTISED_AMOUNT_TOLERANCE_BPS = 1000n;
|
|
@@ -96,6 +96,11 @@ function parseCreditX402Terms(raw) {
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// src/lib/credit-store.ts
|
|
100
|
+
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
101
|
+
import { dirname } from "path";
|
|
102
|
+
import lockfile from "proper-lockfile";
|
|
103
|
+
|
|
99
104
|
// src/lib/output.ts
|
|
100
105
|
import { writeSync } from "fs";
|
|
101
106
|
function info(msg) {
|
|
@@ -118,1417 +123,1492 @@ function writeAllSync(fd, text) {
|
|
|
118
123
|
}
|
|
119
124
|
}
|
|
120
125
|
|
|
121
|
-
// src/lib/
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
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];
|
|
126
|
+
// src/lib/credit-store.ts
|
|
127
|
+
function withCreditLock(fn, opts = {}) {
|
|
128
|
+
if (lockDepth > 0) {
|
|
129
|
+
lockDepth += 1;
|
|
130
|
+
try {
|
|
131
|
+
return rejectAsyncBody(fn());
|
|
132
|
+
} finally {
|
|
133
|
+
lockDepth -= 1;
|
|
134
|
+
}
|
|
147
135
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
136
|
+
const release = acquireCreditLock(opts);
|
|
137
|
+
lockDepth = 1;
|
|
138
|
+
try {
|
|
139
|
+
return rejectAsyncBody(fn());
|
|
140
|
+
} finally {
|
|
141
|
+
lockDepth = 0;
|
|
142
|
+
try {
|
|
143
|
+
release();
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
157
146
|
}
|
|
158
|
-
};
|
|
159
|
-
function authorizeProtectedRemoval(...targets) {
|
|
160
|
-
return new ProtectedFieldAuthorization(targets);
|
|
161
147
|
}
|
|
162
|
-
function
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
);
|
|
148
|
+
function normalizeCreditEndpoint(endpoint) {
|
|
149
|
+
try {
|
|
150
|
+
return new URL(endpoint).origin;
|
|
151
|
+
} catch {
|
|
152
|
+
return endpoint.replace(/\/+$/, "");
|
|
180
153
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
154
|
+
}
|
|
155
|
+
function dvmLocationIdentity(record) {
|
|
156
|
+
if (typeof record.dvmId !== "string" || record.dvmId.length === 0) return void 0;
|
|
157
|
+
const pubkey = typeof record.attestedBuilderPubkey === "string" ? record.attestedBuilderPubkey.trim().toLowerCase() : "";
|
|
158
|
+
if (!/^[0-9a-f]{64}$/.test(pubkey)) return void 0;
|
|
159
|
+
return { dvmId: record.dvmId, builderPubkey: pubkey };
|
|
160
|
+
}
|
|
161
|
+
function findDvmLocated(records, args) {
|
|
162
|
+
const where = args.where ?? (() => true);
|
|
163
|
+
const identity = args.identity;
|
|
164
|
+
if (identity) {
|
|
165
|
+
const attested = records.find(
|
|
166
|
+
(record) => where(record) && matchesDvmIdentity(record, identity)
|
|
190
167
|
);
|
|
168
|
+
if (attested) return attested;
|
|
191
169
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return Object.fromEntries(
|
|
195
|
-
Object.entries(merged).filter(([field]) => !removing.has(field))
|
|
170
|
+
return records.find(
|
|
171
|
+
(record) => where(record) && matchesDvmOrigin(record, args.endpoint, identity)
|
|
196
172
|
);
|
|
197
173
|
}
|
|
198
|
-
function
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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;
|
|
174
|
+
function attachDvmLocation(record, endpoint, identity) {
|
|
175
|
+
let changed = false;
|
|
176
|
+
if (identity) {
|
|
177
|
+
if (record.dvmId !== identity.dvmId) {
|
|
178
|
+
record.dvmId = identity.dvmId;
|
|
179
|
+
changed = true;
|
|
220
180
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
181
|
+
if (record.attestedBuilderPubkey !== identity.builderPubkey) {
|
|
182
|
+
record.attestedBuilderPubkey = identity.builderPubkey;
|
|
183
|
+
changed = true;
|
|
224
184
|
}
|
|
225
185
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
}
|
|
186
|
+
if (endpoint !== record.endpoint && !dvmLocationOrigins(record).includes(endpoint)) {
|
|
187
|
+
record.endpointAliases = [...record.endpointAliases ?? [], endpoint];
|
|
188
|
+
changed = true;
|
|
248
189
|
}
|
|
249
|
-
return
|
|
190
|
+
return changed;
|
|
250
191
|
}
|
|
251
|
-
function
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
192
|
+
function dvmLocationOrigins(record) {
|
|
193
|
+
const aliases = Array.isArray(record.endpointAliases) ? record.endpointAliases.filter((endpoint) => typeof endpoint === "string") : [];
|
|
194
|
+
const primary = typeof record.endpoint === "string" ? [record.endpoint] : [];
|
|
195
|
+
return [...primary, ...aliases].map(normalizeCreditEndpoint);
|
|
196
|
+
}
|
|
197
|
+
function fundingReachableAt(funding, endpoint, credits) {
|
|
198
|
+
const at = normalizeCreditEndpoint(funding.endpoint);
|
|
199
|
+
const named = normalizeCreditEndpoint(endpoint);
|
|
200
|
+
if (at === named) return true;
|
|
201
|
+
const record = credits.find(
|
|
202
|
+
(c) => c.callerPubkey === funding.callerPubkey && c.creditId === funding.creditId
|
|
259
203
|
);
|
|
260
|
-
|
|
204
|
+
if (!record) return false;
|
|
205
|
+
const origins = dvmLocationOrigins(record);
|
|
206
|
+
return origins.includes(at) && origins.includes(named);
|
|
261
207
|
}
|
|
262
|
-
function
|
|
263
|
-
|
|
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
|
-
}
|
|
208
|
+
function pendingDrainFor(record, creditId) {
|
|
209
|
+
return record?.pendingDrains?.find((d) => d.creditId === creditId);
|
|
286
210
|
}
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
211
|
+
function loadCredit(endpoint, callerPubkey, identity) {
|
|
212
|
+
return withCreditLock(() => {
|
|
213
|
+
const key = normalizeCreditEndpoint(endpoint);
|
|
214
|
+
const store = loadStore();
|
|
215
|
+
const record = findDvmLocated(store.credits, {
|
|
216
|
+
endpoint: key,
|
|
217
|
+
...identity ? { identity } : {},
|
|
218
|
+
where: (c) => c.callerPubkey === callerPubkey
|
|
219
|
+
});
|
|
220
|
+
if (record && attachDvmLocation(record, key, identity)) saveStore(store);
|
|
221
|
+
return record;
|
|
222
|
+
});
|
|
300
223
|
}
|
|
301
|
-
function
|
|
302
|
-
|
|
224
|
+
function listCredits() {
|
|
225
|
+
return withCreditLock(() => [...loadStore().credits].sort((a, b) => b.updatedAt - a.updatedAt));
|
|
303
226
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return
|
|
227
|
+
function listPendingFundings(args = {}) {
|
|
228
|
+
const endpoint = args.endpoint ? normalizeCreditEndpoint(args.endpoint) : void 0;
|
|
229
|
+
return withCreditLock(() => {
|
|
230
|
+
const store = loadStore();
|
|
231
|
+
return store.pendingFundings.filter(
|
|
232
|
+
(funding) => (!endpoint || fundingReachableAt(funding, endpoint, store.credits)) && (!args.callerPubkey || funding.callerPubkey === args.callerPubkey) && (!args.creditId || funding.creditId === args.creditId)
|
|
233
|
+
).sort((a, b) => a.requestedAt - b.requestedAt);
|
|
234
|
+
});
|
|
307
235
|
}
|
|
308
|
-
function
|
|
309
|
-
|
|
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
|
-
}
|
|
236
|
+
function loadPendingFunding(endpoint, callerPubkey, creditId) {
|
|
237
|
+
return listPendingFundings({ endpoint, callerPubkey, ...creditId ? { creditId } : {} })[0];
|
|
326
238
|
}
|
|
327
|
-
function
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
239
|
+
function recordPendingFunding(funding) {
|
|
240
|
+
withCreditLock(() => {
|
|
241
|
+
const store = loadStore();
|
|
242
|
+
const normalized = { ...funding, endpoint: normalizeCreditEndpoint(funding.endpoint) };
|
|
243
|
+
const index = store.pendingFundings.findIndex(
|
|
244
|
+
(entry) => entry.callerPubkey === normalized.callerPubkey && entry.creditId === normalized.creditId && entry.fundId === normalized.fundId && fundingReachableAt(entry, normalized.endpoint, store.credits)
|
|
245
|
+
);
|
|
246
|
+
if (index === -1) store.pendingFundings.push(normalized);
|
|
247
|
+
else
|
|
248
|
+
store.pendingFundings[index] = {
|
|
249
|
+
...normalized,
|
|
250
|
+
endpoint: store.pendingFundings[index].endpoint
|
|
251
|
+
};
|
|
252
|
+
saveStore(store);
|
|
253
|
+
});
|
|
336
254
|
}
|
|
337
|
-
function
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
255
|
+
function clearPendingFunding(endpoint, callerPubkey, creditId, fundId) {
|
|
256
|
+
withCreditLock(() => {
|
|
257
|
+
const store = loadStore();
|
|
258
|
+
const normalized = normalizeCreditEndpoint(endpoint);
|
|
259
|
+
const remaining = store.pendingFundings.filter(
|
|
260
|
+
(entry) => !(entry.callerPubkey === callerPubkey && entry.creditId === creditId && entry.fundId === fundId && fundingReachableAt(entry, normalized, store.credits))
|
|
261
|
+
);
|
|
262
|
+
if (remaining.length === store.pendingFundings.length) return;
|
|
263
|
+
saveStore({ ...store, pendingFundings: remaining });
|
|
264
|
+
});
|
|
346
265
|
}
|
|
347
|
-
function
|
|
348
|
-
|
|
266
|
+
function recordFundResponse(args) {
|
|
267
|
+
const now = args.now ?? Date.now();
|
|
268
|
+
return withCreditLock(() => {
|
|
269
|
+
const store = loadStore();
|
|
270
|
+
const key = normalizeCreditEndpoint(args.endpoint);
|
|
271
|
+
let record = findDvmLocated(store.credits, {
|
|
272
|
+
endpoint: key,
|
|
273
|
+
...args.identity ? { identity: args.identity } : {},
|
|
274
|
+
where: (c) => c.callerPubkey === args.callerPubkey
|
|
275
|
+
});
|
|
276
|
+
if (!record) {
|
|
277
|
+
record = {
|
|
278
|
+
endpoint: key,
|
|
279
|
+
...identityFields(args.identity),
|
|
280
|
+
callerPubkey: args.callerPubkey,
|
|
281
|
+
creditId: args.credit.credit_id,
|
|
282
|
+
currency: args.credit.currency,
|
|
283
|
+
balanceMicro: 0,
|
|
284
|
+
remainingMicro: 0,
|
|
285
|
+
ledgerSeq: 0,
|
|
286
|
+
expiryMs: 0,
|
|
287
|
+
targetMicro: args.targetMicro,
|
|
288
|
+
status: "active",
|
|
289
|
+
openedAt: now,
|
|
290
|
+
updatedAt: now
|
|
291
|
+
};
|
|
292
|
+
store.credits.push(record);
|
|
293
|
+
}
|
|
294
|
+
attachDvmLocation(record, key, args.identity);
|
|
295
|
+
adoptCreditId(record, args.credit.credit_id);
|
|
296
|
+
record.currency = args.credit.currency;
|
|
297
|
+
record.balanceMicro = args.credit.balance_micro;
|
|
298
|
+
record.remainingMicro = args.credit.remaining_micro;
|
|
299
|
+
record.ledgerSeq = Math.max(record.ledgerSeq, args.credit.ledger_seq);
|
|
300
|
+
record.expiryMs = args.credit.expiry_ms;
|
|
301
|
+
record.targetMicro = args.targetMicro;
|
|
302
|
+
record.status = args.credit.status === "unbacked" ? "unbacked" : args.credit.expired ? "expired" : "active";
|
|
303
|
+
record.reconciliationPending = args.credit.reconciliation_pending;
|
|
304
|
+
record.updatedAt = now;
|
|
305
|
+
delete record.provisional;
|
|
306
|
+
if (args.sequenced === false) record.unsequencedBalance = true;
|
|
307
|
+
else delete record.unsequencedBalance;
|
|
308
|
+
if (args.menu) record.menu = args.menu;
|
|
309
|
+
if (args.rail) {
|
|
310
|
+
record.rail = args.rail;
|
|
311
|
+
delete record.refillUnavailableRail;
|
|
312
|
+
}
|
|
313
|
+
if (args.x402Instrument) record.x402Instrument = args.x402Instrument;
|
|
314
|
+
if (args.x402ChannelNetwork) record.x402ChannelNetwork = args.x402ChannelNetwork;
|
|
315
|
+
if (args.builderPubkey) record.builderPubkey = args.builderPubkey;
|
|
316
|
+
if (args.select) delete record.selectionCleared;
|
|
317
|
+
delete record.discrepancy;
|
|
318
|
+
saveStore(store);
|
|
319
|
+
return record;
|
|
320
|
+
});
|
|
349
321
|
}
|
|
350
|
-
function
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
322
|
+
function ensureProvisionalCredit(args) {
|
|
323
|
+
const now = args.now ?? Date.now();
|
|
324
|
+
withCreditLock(() => {
|
|
325
|
+
const store = loadStore();
|
|
326
|
+
const key = normalizeCreditEndpoint(args.endpoint);
|
|
327
|
+
let record = findDvmLocated(store.credits, {
|
|
328
|
+
endpoint: key,
|
|
329
|
+
...args.identity ? { identity: args.identity } : {},
|
|
330
|
+
where: (c) => c.callerPubkey === args.callerPubkey
|
|
331
|
+
});
|
|
332
|
+
if (!record) {
|
|
333
|
+
record = {
|
|
334
|
+
endpoint: key,
|
|
335
|
+
...identityFields(args.identity),
|
|
336
|
+
callerPubkey: args.callerPubkey,
|
|
337
|
+
creditId: args.creditId,
|
|
338
|
+
currency: args.currency,
|
|
339
|
+
balanceMicro: 0,
|
|
340
|
+
remainingMicro: 0,
|
|
341
|
+
ledgerSeq: 0,
|
|
342
|
+
expiryMs: 0,
|
|
343
|
+
targetMicro: args.targetMicro,
|
|
344
|
+
status: "active",
|
|
345
|
+
openedAt: now,
|
|
346
|
+
updatedAt: now
|
|
347
|
+
};
|
|
348
|
+
store.credits.push(record);
|
|
349
|
+
}
|
|
350
|
+
attachDvmLocation(record, key, args.identity);
|
|
351
|
+
adoptCreditId(record, args.creditId);
|
|
352
|
+
record.currency = args.currency;
|
|
353
|
+
record.targetMicro = args.targetMicro;
|
|
354
|
+
record.status = "active";
|
|
355
|
+
record.provisional = true;
|
|
356
|
+
record.updatedAt = now;
|
|
357
|
+
if (args.menu) record.menu = args.menu;
|
|
358
|
+
saveStore(store);
|
|
359
|
+
});
|
|
355
360
|
}
|
|
356
|
-
function
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
361
|
+
function loadCreditX402Binding(endpoint, callerPubkey, creditId, identity) {
|
|
362
|
+
const record = loadCredit(endpoint, callerPubkey, identity);
|
|
363
|
+
if (record?.creditId !== creditId) return {};
|
|
364
|
+
if (record.status !== "active" && record.status !== "expired") return {};
|
|
365
|
+
return {
|
|
366
|
+
...record.x402Instrument ? { instrument: record.x402Instrument } : {},
|
|
367
|
+
...record.x402ChannelNetwork ? { channelNetwork: record.x402ChannelNetwork } : {}
|
|
368
|
+
};
|
|
363
369
|
}
|
|
364
|
-
function
|
|
365
|
-
|
|
366
|
-
|
|
370
|
+
function recordCreditX402Instrument(endpoint, callerPubkey, creditId, instrument, identity, channelNetwork) {
|
|
371
|
+
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
372
|
+
if (record.creditId !== creditId) return "skip";
|
|
373
|
+
const instrumentChanged = record.x402Instrument !== instrument;
|
|
374
|
+
const channelNetworkChanged = instrument === "x402_channel" && channelNetwork !== void 0 && record.x402ChannelNetwork !== channelNetwork;
|
|
375
|
+
if (!instrumentChanged && !channelNetworkChanged) return "skip";
|
|
376
|
+
if (instrumentChanged) {
|
|
377
|
+
record.x402Instrument = instrument;
|
|
378
|
+
delete record.x402ChannelNetwork;
|
|
379
|
+
}
|
|
380
|
+
if (instrument === "x402_channel" && channelNetwork !== void 0) {
|
|
381
|
+
record.x402ChannelNetwork = channelNetwork;
|
|
382
|
+
}
|
|
383
|
+
return void 0;
|
|
384
|
+
});
|
|
367
385
|
}
|
|
368
|
-
function
|
|
369
|
-
|
|
386
|
+
function recordPendingDrain(endpoint, callerPubkey, drain, identity) {
|
|
387
|
+
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
388
|
+
const drains = record.pendingDrains ?? [];
|
|
389
|
+
const index = drains.findIndex(
|
|
390
|
+
(d) => d.drainId === drain.drainId && d.creditId === drain.creditId
|
|
391
|
+
);
|
|
392
|
+
if (index === -1) drains.push(drain);
|
|
393
|
+
else drains[index] = drain;
|
|
394
|
+
record.pendingDrains = capPendingDrains(drains, record.creditId);
|
|
395
|
+
return void 0;
|
|
396
|
+
});
|
|
370
397
|
}
|
|
371
|
-
function
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
398
|
+
function clearPendingDrain(endpoint, callerPubkey, drain, identity) {
|
|
399
|
+
mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
|
|
400
|
+
const before = record.pendingDrains ?? [];
|
|
401
|
+
const remaining = before.filter(
|
|
402
|
+
(d) => !(d.drainId === drain.drainId && d.creditId === drain.creditId)
|
|
403
|
+
);
|
|
404
|
+
if (remaining.length === before.length) return "skip";
|
|
405
|
+
if (remaining.length === 0) delete record.pendingDrains;
|
|
406
|
+
else record.pendingDrains = remaining;
|
|
407
|
+
return void 0;
|
|
408
|
+
});
|
|
381
409
|
}
|
|
382
|
-
function
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
appendFileSync(CONFIG_AUDIT_FILE, JSON.stringify(record) + "\n", { mode: 384 });
|
|
386
|
-
chmodSync(CONFIG_AUDIT_FILE, 384);
|
|
387
|
-
} catch {
|
|
388
|
-
}
|
|
410
|
+
function identityFields(identity) {
|
|
411
|
+
if (!identity) return {};
|
|
412
|
+
return { dvmId: identity.dvmId, attestedBuilderPubkey: identity.builderPubkey };
|
|
389
413
|
}
|
|
390
|
-
function
|
|
391
|
-
|
|
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
|
-
);
|
|
414
|
+
function mutationAt(endpoint, callerPubkey, identity) {
|
|
415
|
+
return { endpoint, callerPubkey, ...identity ? { identity } : {} };
|
|
399
416
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
);
|
|
417
|
+
function matchesDvmIdentity(record, identity) {
|
|
418
|
+
const stored = dvmLocationIdentity(record);
|
|
419
|
+
return stored?.dvmId === identity.dvmId && stored.builderPubkey === identity.builderPubkey;
|
|
474
420
|
}
|
|
475
|
-
function
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
case "number":
|
|
480
|
-
return "a number";
|
|
481
|
-
case "object":
|
|
482
|
-
return "an object";
|
|
483
|
-
case "string[]":
|
|
484
|
-
return "an array of strings";
|
|
421
|
+
function matchesDvmOrigin(record, endpoint, identity) {
|
|
422
|
+
if (identity) {
|
|
423
|
+
if (dvmLocationIdentity(record) !== void 0) return false;
|
|
424
|
+
if (record.dvmId !== void 0 && record.dvmId !== identity.dvmId) return false;
|
|
485
425
|
}
|
|
426
|
+
return dvmLocationOrigins(record).includes(endpoint);
|
|
486
427
|
}
|
|
487
|
-
function
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
428
|
+
function adoptCreditId(record, creditId) {
|
|
429
|
+
if (record.creditId === creditId) return;
|
|
430
|
+
if (record.status === "active" || record.status === "expired" || record.status === "unbacked") {
|
|
431
|
+
const prior = {
|
|
432
|
+
creditId: record.creditId,
|
|
433
|
+
status: record.status === "unbacked" ? "unbacked" : "active",
|
|
434
|
+
currency: record.currency,
|
|
435
|
+
balanceMicro: record.balanceMicro,
|
|
436
|
+
remainingMicro: record.remainingMicro,
|
|
437
|
+
ledgerSeq: record.ledgerSeq,
|
|
438
|
+
expiryMs: record.expiryMs,
|
|
439
|
+
expired: record.status === "expired",
|
|
440
|
+
drainable: record.status !== "unbacked" && record.balanceMicro > 0,
|
|
441
|
+
...record.reconciliationPending ? { reconciliationPending: true } : {},
|
|
442
|
+
observedAt: record.updatedAt
|
|
443
|
+
};
|
|
444
|
+
record.siblingCredits = [
|
|
445
|
+
...(record.siblingCredits ?? []).filter(
|
|
446
|
+
(sibling) => sibling.creditId !== prior.creditId && sibling.creditId !== creditId
|
|
447
|
+
),
|
|
448
|
+
prior
|
|
449
|
+
];
|
|
450
|
+
} else if (record.siblingCredits) {
|
|
451
|
+
record.siblingCredits = record.siblingCredits.filter(
|
|
452
|
+
(sibling) => sibling.creditId !== creditId
|
|
453
|
+
);
|
|
497
454
|
}
|
|
455
|
+
record.creditId = creditId;
|
|
456
|
+
record.ledgerSeq = 0;
|
|
457
|
+
record.balanceMicro = 0;
|
|
458
|
+
record.remainingMicro = 0;
|
|
459
|
+
record.expiryMs = 0;
|
|
460
|
+
delete record.unsequencedBalance;
|
|
461
|
+
delete record.rail;
|
|
462
|
+
delete record.x402Instrument;
|
|
463
|
+
delete record.x402ChannelNetwork;
|
|
464
|
+
delete record.refillUnavailableRail;
|
|
465
|
+
delete record.refillDrainNoticed;
|
|
466
|
+
delete record.discrepancy;
|
|
467
|
+
delete record.lastReceiptJobId;
|
|
498
468
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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;
|
|
469
|
+
var MAX_PENDING_DRAINS = 8;
|
|
470
|
+
var MAX_LOST_IMPLICIT_CREDITS = 8;
|
|
471
|
+
var MAX_DATE_MS = 864e13;
|
|
472
|
+
function loadStore() {
|
|
473
|
+
if (!existsSync(CREDITS_FILE)) {
|
|
474
|
+
return { credits: [], implicitCredits: [], pendingFundings: [], fundingMenuSnapshots: [] };
|
|
519
475
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
return a.every((item, index) => deepEqual(item, b[index]));
|
|
476
|
+
let raw;
|
|
477
|
+
try {
|
|
478
|
+
raw = readFileSync(CREDITS_FILE, "utf-8");
|
|
479
|
+
} catch (cause) {
|
|
480
|
+
throw unreadableStore(cause);
|
|
526
481
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
482
|
+
let data;
|
|
483
|
+
try {
|
|
484
|
+
const parsed = JSON.parse(raw);
|
|
485
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
486
|
+
throw new Error("the file parsed as JSON but is not a credit-store object");
|
|
487
|
+
}
|
|
488
|
+
data = parsed;
|
|
489
|
+
} catch (cause) {
|
|
490
|
+
throw quarantineCorruptStore(cause);
|
|
530
491
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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."
|
|
492
|
+
const merged = mergeDuplicateCredits(
|
|
493
|
+
Array.isArray(data.credits) ? data.credits.filter((record) => isObjectRecord(record)).map(
|
|
494
|
+
(record) => normalizeRailKey(normalizeEvidence(normalizeDrains(normalizeSiblingCredits(record))))
|
|
495
|
+
) : []
|
|
496
|
+
);
|
|
497
|
+
const snapshots = mergeDuplicateSnapshots(
|
|
498
|
+
Array.isArray(data.fundingMenuSnapshots) ? data.fundingMenuSnapshots.filter(isFundingMenuSnapshot) : [],
|
|
499
|
+
merged.credits
|
|
549
500
|
);
|
|
501
|
+
const fundings = dedupePendingFundings(
|
|
502
|
+
Array.isArray(data.pendingFundings) ? data.pendingFundings.filter((funding) => isObjectRecord(funding)).map(normalizeRailKey) : []
|
|
503
|
+
);
|
|
504
|
+
const implicit = dedupeImplicitCredits(
|
|
505
|
+
Array.isArray(data.implicitCredits) ? data.implicitCredits.filter(isStoredImplicitCredit) : []
|
|
506
|
+
);
|
|
507
|
+
const store = {
|
|
508
|
+
credits: merged.credits,
|
|
509
|
+
implicitCredits: implicit.credits,
|
|
510
|
+
pendingFundings: fundings.fundings,
|
|
511
|
+
fundingMenuSnapshots: snapshots.snapshots
|
|
512
|
+
};
|
|
513
|
+
if (merged.changed || snapshots.changed || fundings.changed || implicit.changed) saveStore(store);
|
|
514
|
+
return store;
|
|
550
515
|
}
|
|
551
|
-
function
|
|
552
|
-
if (
|
|
553
|
-
|
|
554
|
-
for (const nested of Object.values(value)) freezeDeep(nested);
|
|
516
|
+
function isStoredImplicitCredit(value) {
|
|
517
|
+
if (!isObjectRecord(value)) return false;
|
|
518
|
+
return typeof value.endpoint === "string" && typeof value.callerPubkey === "string" && typeof value.creditId === "string" && value.creditId.startsWith("imp:") && typeof value.currency === "string" && typeof value.remainingMicro === "number" && Number.isFinite(value.remainingMicro) && typeof value.ledgerSeq === "number" && Number.isInteger(value.ledgerSeq) && typeof value.expiryMs === "number" && Number.isFinite(value.expiryMs) && (value.status === "active" || value.status === "lost") && typeof value.releasedByJobId === "string" && typeof value.releasedAt === "number" && Number.isFinite(value.releasedAt) && typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt);
|
|
555
519
|
}
|
|
556
|
-
function
|
|
557
|
-
|
|
520
|
+
function dedupeImplicitCredits(credits) {
|
|
521
|
+
const merged = [];
|
|
522
|
+
let changed = false;
|
|
523
|
+
for (const candidate of credits) {
|
|
524
|
+
candidate.endpoint = normalizeCreditEndpoint(candidate.endpoint);
|
|
525
|
+
const identity = dvmLocationIdentity(candidate);
|
|
526
|
+
const existing = findDvmLocated(merged, {
|
|
527
|
+
endpoint: candidate.endpoint,
|
|
528
|
+
...identity ? { identity } : {},
|
|
529
|
+
where: (record) => record.callerPubkey === candidate.callerPubkey && record.creditId === candidate.creditId
|
|
530
|
+
});
|
|
531
|
+
if (!existing) {
|
|
532
|
+
merged.push(candidate);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
changed = true;
|
|
536
|
+
for (const origin of dvmLocationOrigins(candidate)) {
|
|
537
|
+
attachDvmLocation(existing, origin, identity);
|
|
538
|
+
}
|
|
539
|
+
if (candidate.ledgerSeq > existing.ledgerSeq || candidate.ledgerSeq === existing.ledgerSeq && candidate.updatedAt > existing.updatedAt) {
|
|
540
|
+
existing.currency = candidate.currency;
|
|
541
|
+
existing.remainingMicro = candidate.remainingMicro;
|
|
542
|
+
existing.ledgerSeq = candidate.ledgerSeq;
|
|
543
|
+
existing.status = candidate.status;
|
|
544
|
+
if (candidate.lossReason) existing.lossReason = candidate.lossReason;
|
|
545
|
+
else delete existing.lossReason;
|
|
546
|
+
existing.updatedAt = candidate.updatedAt;
|
|
547
|
+
existing.lastReceiptJobId = candidate.lastReceiptJobId;
|
|
548
|
+
existing.discrepancy = candidate.discrepancy;
|
|
549
|
+
}
|
|
550
|
+
existing.expiryMs = Math.min(existing.expiryMs, candidate.expiryMs);
|
|
551
|
+
if (candidate.releasedAt < existing.releasedAt) {
|
|
552
|
+
existing.releasedAt = candidate.releasedAt;
|
|
553
|
+
existing.releasedByJobId = candidate.releasedByJobId;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const capped = capLostImplicitCredits(merged);
|
|
557
|
+
return { credits: capped.credits, changed: changed || capped.changed };
|
|
558
558
|
}
|
|
559
|
-
function
|
|
560
|
-
|
|
559
|
+
function capLostImplicitCredits(credits) {
|
|
560
|
+
const lostByOwner = /* @__PURE__ */ new Map();
|
|
561
|
+
for (const credit of credits) {
|
|
562
|
+
if (credit.status !== "lost") continue;
|
|
563
|
+
const identity = dvmLocationIdentity(credit);
|
|
564
|
+
const dvmKey = identity ? `${identity.dvmId}\0${identity.builderPubkey}` : normalizeCreditEndpoint(credit.endpoint);
|
|
565
|
+
const key = `${credit.callerPubkey}\0${dvmKey}`;
|
|
566
|
+
const group = lostByOwner.get(key) ?? [];
|
|
567
|
+
group.push(credit);
|
|
568
|
+
lostByOwner.set(key, group);
|
|
569
|
+
}
|
|
570
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
571
|
+
for (const group of lostByOwner.values()) {
|
|
572
|
+
group.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
573
|
+
for (const credit of group.slice(MAX_LOST_IMPLICIT_CREDITS)) dropped.add(credit);
|
|
574
|
+
}
|
|
575
|
+
return dropped.size === 0 ? { credits, changed: false } : { credits: credits.filter((credit) => !dropped.has(credit)), changed: true };
|
|
561
576
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
} : {}
|
|
577
|
+
function mergeDuplicateSnapshots(snapshots, credits) {
|
|
578
|
+
const identities = originIdentityMap(credits);
|
|
579
|
+
const merged = [];
|
|
580
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
581
|
+
let changed = false;
|
|
582
|
+
for (const snapshot of snapshots) {
|
|
583
|
+
const stamped = identities.get(`${snapshot.callerPubkey}\0${snapshot.endpoint}`);
|
|
584
|
+
if (stamped !== void 0 && snapshot.dvmId === void 0) {
|
|
585
|
+
snapshot.dvmId = stamped.dvmId;
|
|
586
|
+
if (stamped.attestedBuilderPubkey !== void 0) {
|
|
587
|
+
snapshot.attestedBuilderPubkey = stamped.attestedBuilderPubkey;
|
|
588
|
+
}
|
|
589
|
+
changed = true;
|
|
590
590
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
return
|
|
591
|
+
if (snapshot.dvmId === void 0) {
|
|
592
|
+
merged.push(snapshot);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const key = `${snapshot.callerPubkey}\0${snapshot.dvmId}\0${snapshot.attestedBuilderPubkey ?? ""}`;
|
|
596
|
+
const index = byIdentity.get(key);
|
|
597
|
+
if (index === void 0) {
|
|
598
|
+
byIdentity.set(key, merged.length);
|
|
599
|
+
merged.push(snapshot);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
merged[index] = mergeSnapshots(merged[index], snapshot);
|
|
603
|
+
changed = true;
|
|
604
|
+
}
|
|
605
|
+
return { snapshots: merged, changed };
|
|
606
606
|
}
|
|
607
|
-
function
|
|
608
|
-
const
|
|
609
|
-
|
|
607
|
+
function mergeSnapshots(left, right) {
|
|
608
|
+
const leftSeen = Number.isFinite(left.observedAt) ? left.observedAt : 0;
|
|
609
|
+
const rightSeen = Number.isFinite(right.observedAt) ? right.observedAt : 0;
|
|
610
|
+
const [newer, older] = rightSeen >= leftSeen ? [right, left] : [left, right];
|
|
611
|
+
const snapshot = { ...newer };
|
|
612
|
+
if (snapshot.builderPubkey === void 0 && older.builderPubkey !== void 0) {
|
|
613
|
+
snapshot.builderPubkey = older.builderPubkey;
|
|
614
|
+
}
|
|
615
|
+
const origins = dvmLocationOrigins(newer).concat(dvmLocationOrigins(older));
|
|
616
|
+
const aliases = [...new Set(origins)].filter((origin) => origin !== snapshot.endpoint);
|
|
617
|
+
if (aliases.length > 0) snapshot.endpointAliases = aliases;
|
|
618
|
+
else delete snapshot.endpointAliases;
|
|
619
|
+
return snapshot;
|
|
610
620
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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();
|
|
621
|
+
function originIdentityMap(credits) {
|
|
622
|
+
const identities = /* @__PURE__ */ new Map();
|
|
623
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
624
|
+
for (const record of credits) {
|
|
625
|
+
if (typeof record.dvmId !== "string" || typeof record.callerPubkey !== "string") continue;
|
|
626
|
+
const identity = {
|
|
627
|
+
dvmId: record.dvmId,
|
|
628
|
+
...record.attestedBuilderPubkey !== void 0 ? { attestedBuilderPubkey: record.attestedBuilderPubkey } : {}
|
|
629
|
+
};
|
|
630
|
+
for (const origin of dvmLocationOrigins(record)) {
|
|
631
|
+
const key = `${record.callerPubkey}\0${origin}`;
|
|
632
|
+
const seen = identities.get(key);
|
|
633
|
+
if (seen !== void 0 && (seen.dvmId !== identity.dvmId || seen.attestedBuilderPubkey !== identity.attestedBuilderPubkey)) {
|
|
634
|
+
ambiguous.add(key);
|
|
635
|
+
continue;
|
|
647
636
|
}
|
|
637
|
+
identities.set(key, identity);
|
|
648
638
|
}
|
|
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
639
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
640
|
+
for (const key of ambiguous) identities.delete(key);
|
|
641
|
+
return identities;
|
|
642
|
+
}
|
|
643
|
+
function dedupePendingFundings(fundings) {
|
|
644
|
+
const kept = [];
|
|
645
|
+
const byIntent = /* @__PURE__ */ new Map();
|
|
646
|
+
let changed = false;
|
|
647
|
+
for (const funding of fundings) {
|
|
648
|
+
if (typeof funding.callerPubkey !== "string" || typeof funding.creditId !== "string" || typeof funding.fundId !== "string") {
|
|
649
|
+
kept.push(funding);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const key = `${funding.callerPubkey}\0${funding.creditId}\0${funding.fundId}`;
|
|
653
|
+
const index = byIntent.get(key);
|
|
654
|
+
if (index === void 0) {
|
|
655
|
+
byIntent.set(key, kept.length);
|
|
656
|
+
kept.push(funding);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
const seen = kept[index];
|
|
660
|
+
const seenAt = Number.isFinite(seen.requestedAt) ? seen.requestedAt : 0;
|
|
661
|
+
const at = Number.isFinite(funding.requestedAt) ? funding.requestedAt : 0;
|
|
662
|
+
if (at > seenAt) kept[index] = funding;
|
|
663
|
+
changed = true;
|
|
689
664
|
}
|
|
665
|
+
return { fundings: kept, changed };
|
|
690
666
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
);
|
|
667
|
+
function isObjectRecord(value) {
|
|
668
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
669
|
+
}
|
|
670
|
+
function mergeDuplicateCredits(credits) {
|
|
671
|
+
const merged = [];
|
|
672
|
+
const byCredit = /* @__PURE__ */ new Map();
|
|
673
|
+
let changed = false;
|
|
674
|
+
for (const record of credits) {
|
|
675
|
+
if (typeof record.creditId !== "string" || typeof record.callerPubkey !== "string") {
|
|
676
|
+
merged.push(record);
|
|
677
|
+
continue;
|
|
703
678
|
}
|
|
704
|
-
|
|
679
|
+
const key = `${record.callerPubkey}\0${record.creditId}`;
|
|
680
|
+
const index = byCredit.get(key);
|
|
681
|
+
if (index === void 0) {
|
|
682
|
+
byCredit.set(key, merged.length);
|
|
683
|
+
merged.push(record);
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
merged[index] = mergeCreditRecords(merged[index], record);
|
|
687
|
+
changed = true;
|
|
705
688
|
}
|
|
689
|
+
return { credits: merged, changed };
|
|
706
690
|
}
|
|
707
|
-
function
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
691
|
+
function mergeCreditRecords(left, right) {
|
|
692
|
+
const leftUpdated = Number.isFinite(left.updatedAt) ? left.updatedAt : 0;
|
|
693
|
+
const rightUpdated = Number.isFinite(right.updatedAt) ? right.updatedAt : 0;
|
|
694
|
+
const [newer, older] = rightUpdated >= leftUpdated ? [right, left] : [left, right];
|
|
695
|
+
const record = { ...newer };
|
|
696
|
+
if (record.dvmId === void 0 && older.dvmId !== void 0) record.dvmId = older.dvmId;
|
|
697
|
+
if (record.attestedBuilderPubkey === void 0 && older.attestedBuilderPubkey !== void 0 && record.dvmId === older.dvmId) {
|
|
698
|
+
record.attestedBuilderPubkey = older.attestedBuilderPubkey;
|
|
712
699
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
);
|
|
721
|
-
if (
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
700
|
+
if (record.rail === void 0 && older.rail !== void 0) record.rail = older.rail;
|
|
701
|
+
if (record.x402Instrument === void 0 && older.x402Instrument !== void 0) {
|
|
702
|
+
record.x402Instrument = older.x402Instrument;
|
|
703
|
+
}
|
|
704
|
+
if (record.x402ChannelNetwork === void 0 && older.x402ChannelNetwork !== void 0) {
|
|
705
|
+
record.x402ChannelNetwork = older.x402ChannelNetwork;
|
|
706
|
+
}
|
|
707
|
+
if (record.menu === void 0 && older.menu !== void 0) record.menu = older.menu;
|
|
708
|
+
if (record.builderPubkey === void 0 && older.builderPubkey !== void 0) {
|
|
709
|
+
record.builderPubkey = older.builderPubkey;
|
|
710
|
+
}
|
|
711
|
+
const endpoints = dvmLocationOrigins(newer).concat(dvmLocationOrigins(older));
|
|
712
|
+
const aliases = [...new Set(endpoints)].filter((endpoint) => endpoint !== record.endpoint);
|
|
713
|
+
if (aliases.length > 0) record.endpointAliases = aliases;
|
|
714
|
+
else delete record.endpointAliases;
|
|
715
|
+
const pendingDrains = unionPendingDrains(newer.pendingDrains, older.pendingDrains);
|
|
716
|
+
if (pendingDrains.length > 0) record.pendingDrains = pendingDrains;
|
|
717
|
+
else delete record.pendingDrains;
|
|
718
|
+
const drainEvidence = unionDrainEvidence(newer.drainEvidence, older.drainEvidence);
|
|
719
|
+
if (drainEvidence.length > 0) record.drainEvidence = drainEvidence;
|
|
720
|
+
else delete record.drainEvidence;
|
|
721
|
+
const siblings = Array.isArray(newer.siblingCredits) ? newer.siblingCredits : older.siblingCredits;
|
|
722
|
+
if (siblings) {
|
|
723
|
+
record.siblingCredits = siblings.filter((sibling) => sibling.creditId !== record.creditId);
|
|
724
|
+
} else {
|
|
725
|
+
delete record.siblingCredits;
|
|
726
|
+
}
|
|
727
|
+
return record;
|
|
737
728
|
}
|
|
738
|
-
function
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
slot,
|
|
746
|
-
backup: describeConfigBackup() !== null,
|
|
747
|
-
pid: process.pid
|
|
748
|
-
});
|
|
749
|
-
return true;
|
|
729
|
+
function unionPendingDrains(preferred, fallback) {
|
|
730
|
+
const byDrain = /* @__PURE__ */ new Map();
|
|
731
|
+
for (const drain of [...fallback ?? [], ...preferred ?? []]) {
|
|
732
|
+
const key = `${drain.drainId}\0${drain.creditId ?? ""}`;
|
|
733
|
+
byDrain.set(key, { ...byDrain.get(key) ?? {}, ...drain });
|
|
734
|
+
}
|
|
735
|
+
return [...byDrain.values()];
|
|
750
736
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
try {
|
|
760
|
-
return rejectAsyncBody(fn());
|
|
761
|
-
} finally {
|
|
762
|
-
lockDepth -= 1;
|
|
737
|
+
function unionDrainEvidence(preferred, fallback) {
|
|
738
|
+
const byDrain = /* @__PURE__ */ new Map();
|
|
739
|
+
for (const evidence of [...fallback ?? [], ...preferred ?? []]) {
|
|
740
|
+
const key = `${evidence.drainId}\0${evidence.creditId}`;
|
|
741
|
+
const existing = byDrain.get(key);
|
|
742
|
+
if (!existing) {
|
|
743
|
+
byDrain.set(key, evidence);
|
|
744
|
+
continue;
|
|
763
745
|
}
|
|
746
|
+
const [newer, older] = evidence.collectedAt >= existing.collectedAt ? [evidence, existing] : [existing, evidence];
|
|
747
|
+
const receipts = unionUnknown(newer.receipts, older.receipts);
|
|
748
|
+
byDrain.set(key, {
|
|
749
|
+
...older,
|
|
750
|
+
...newer,
|
|
751
|
+
receipts,
|
|
752
|
+
collectedAt: Math.max(existing.collectedAt, evidence.collectedAt),
|
|
753
|
+
...existing.settledAt !== void 0 || evidence.settledAt !== void 0 ? { settledAt: Math.max(existing.settledAt ?? 0, evidence.settledAt ?? 0) } : {}
|
|
754
|
+
});
|
|
764
755
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
756
|
+
return [...byDrain.values()];
|
|
757
|
+
}
|
|
758
|
+
function unionUnknown(preferred, fallback) {
|
|
759
|
+
const seen = /* @__PURE__ */ new Set();
|
|
760
|
+
const values = [];
|
|
761
|
+
for (const value of [...preferred, ...fallback]) {
|
|
762
|
+
let key;
|
|
771
763
|
try {
|
|
772
|
-
|
|
764
|
+
const serialized = JSON.stringify(value);
|
|
765
|
+
key = typeof serialized === "string" ? serialized : String(value);
|
|
773
766
|
} catch {
|
|
767
|
+
key = String(value);
|
|
774
768
|
}
|
|
769
|
+
if (seen.has(key)) continue;
|
|
770
|
+
seen.add(key);
|
|
771
|
+
values.push(value);
|
|
775
772
|
}
|
|
773
|
+
return values;
|
|
776
774
|
}
|
|
777
|
-
function
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
return endpoint.replace(/\/+$/, "");
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
function dvmLocationIdentity(record) {
|
|
785
|
-
if (typeof record.dvmId !== "string" || record.dvmId.length === 0) return void 0;
|
|
786
|
-
const pubkey = typeof record.attestedBuilderPubkey === "string" ? record.attestedBuilderPubkey.trim().toLowerCase() : "";
|
|
787
|
-
if (!/^[0-9a-f]{64}$/.test(pubkey)) return void 0;
|
|
788
|
-
return { dvmId: record.dvmId, builderPubkey: pubkey };
|
|
789
|
-
}
|
|
790
|
-
function findDvmLocated(records, args) {
|
|
791
|
-
const where = args.where ?? (() => true);
|
|
792
|
-
const identity = args.identity;
|
|
793
|
-
if (identity) {
|
|
794
|
-
const attested = records.find(
|
|
795
|
-
(record) => where(record) && matchesDvmIdentity(record, identity)
|
|
796
|
-
);
|
|
797
|
-
if (attested) return attested;
|
|
798
|
-
}
|
|
799
|
-
return records.find(
|
|
800
|
-
(record) => where(record) && matchesDvmOrigin(record, args.endpoint, identity)
|
|
801
|
-
);
|
|
775
|
+
function isFundingMenuSnapshot(value) {
|
|
776
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
777
|
+
const snapshot = value;
|
|
778
|
+
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");
|
|
802
779
|
}
|
|
803
|
-
function
|
|
804
|
-
|
|
805
|
-
if (
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
if (record.attestedBuilderPubkey !== identity.builderPubkey) {
|
|
811
|
-
record.attestedBuilderPubkey = identity.builderPubkey;
|
|
812
|
-
changed = true;
|
|
780
|
+
function normalizeRailKey(record) {
|
|
781
|
+
const loose = record;
|
|
782
|
+
if (loose.rail === LEGACY_TEMPO_RAIL_KEY) loose.rail = "tempo";
|
|
783
|
+
if (loose.refillUnavailableRail === LEGACY_TEMPO_RAIL_KEY) loose.refillUnavailableRail = "tempo";
|
|
784
|
+
if (Array.isArray(loose.pendingDrains)) {
|
|
785
|
+
for (const drain of loose.pendingDrains) {
|
|
786
|
+
if (drain?.method === LEGACY_TEMPO_RAIL_KEY) drain.method = "tempo";
|
|
813
787
|
}
|
|
814
788
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
789
|
+
return record;
|
|
790
|
+
}
|
|
791
|
+
var LEGACY_TEMPO_RAIL_KEY = "mpp";
|
|
792
|
+
function normalizeSiblingCredits(record) {
|
|
793
|
+
if (!Array.isArray(record.siblingCredits)) {
|
|
794
|
+
delete record.siblingCredits;
|
|
795
|
+
return record;
|
|
818
796
|
}
|
|
819
|
-
|
|
797
|
+
record.siblingCredits = record.siblingCredits.filter(
|
|
798
|
+
(sibling) => isSiblingCredit(sibling) && sibling.creditId !== record.creditId
|
|
799
|
+
);
|
|
800
|
+
return record;
|
|
820
801
|
}
|
|
821
|
-
function
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
802
|
+
function isSiblingCredit(value) {
|
|
803
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
804
|
+
const sibling = value;
|
|
805
|
+
const status = value.status;
|
|
806
|
+
return typeof sibling.creditId === "string" && typeof sibling.currency === "string" && Number.isSafeInteger(sibling.balanceMicro) && Number.isSafeInteger(sibling.remainingMicro) && Number.isSafeInteger(sibling.ledgerSeq) && Number.isFinite(sibling.expiryMs) && Math.abs(sibling.expiryMs) <= MAX_DATE_MS && typeof sibling.expired === "boolean" && typeof sibling.drainable === "boolean" && (status === void 0 || status === "active" || status === "unbacked") && (sibling.reconciliationPending === void 0 || typeof sibling.reconciliationPending === "boolean") && Number.isFinite(sibling.observedAt) && Math.abs(sibling.observedAt) <= MAX_DATE_MS;
|
|
825
807
|
}
|
|
826
|
-
function
|
|
827
|
-
const
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
const record = credits.find(
|
|
831
|
-
(c) => c.callerPubkey === funding.callerPubkey && c.creditId === funding.creditId
|
|
808
|
+
function normalizeDrains(record) {
|
|
809
|
+
const legacy = record;
|
|
810
|
+
const drains = (Array.isArray(record.pendingDrains) ? record.pendingDrains : []).filter(
|
|
811
|
+
isDrainish
|
|
832
812
|
);
|
|
833
|
-
if (
|
|
834
|
-
|
|
835
|
-
|
|
813
|
+
if (isDrainish(legacy.pendingDrain)) drains.unshift(legacy.pendingDrain);
|
|
814
|
+
delete legacy.pendingDrain;
|
|
815
|
+
if (drains.length === 0) delete record.pendingDrains;
|
|
816
|
+
else record.pendingDrains = drains;
|
|
817
|
+
return record;
|
|
836
818
|
}
|
|
837
|
-
function
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const store = loadStore();
|
|
841
|
-
const record = findDvmLocated(store.credits, {
|
|
842
|
-
endpoint: key,
|
|
843
|
-
...identity ? { identity } : {},
|
|
844
|
-
where: (c) => c.callerPubkey === callerPubkey
|
|
845
|
-
});
|
|
846
|
-
if (record && attachDvmLocation(record, key, identity)) saveStore(store);
|
|
819
|
+
function normalizeEvidence(record) {
|
|
820
|
+
if (!Array.isArray(record.drainEvidence)) {
|
|
821
|
+
delete record.drainEvidence;
|
|
847
822
|
return record;
|
|
823
|
+
}
|
|
824
|
+
const evidence = record.drainEvidence.filter(isEvidenceish).map((e) => {
|
|
825
|
+
const bundle = {
|
|
826
|
+
...e,
|
|
827
|
+
creditId: typeof e.creditId === "string" ? e.creditId : "",
|
|
828
|
+
collectedAt: Number.isFinite(e.collectedAt) ? e.collectedAt : 0
|
|
829
|
+
};
|
|
830
|
+
if (!Number.isFinite(bundle.settledAt)) delete bundle.settledAt;
|
|
831
|
+
return bundle;
|
|
848
832
|
});
|
|
833
|
+
if (evidence.length === 0) delete record.drainEvidence;
|
|
834
|
+
else record.drainEvidence = evidence;
|
|
835
|
+
return record;
|
|
849
836
|
}
|
|
850
|
-
function
|
|
851
|
-
return
|
|
852
|
-
}
|
|
853
|
-
function listPendingFundings(args = {}) {
|
|
854
|
-
const endpoint = args.endpoint ? normalizeCreditEndpoint(args.endpoint) : void 0;
|
|
855
|
-
return withCreditLock(() => {
|
|
856
|
-
const store = loadStore();
|
|
857
|
-
return store.pendingFundings.filter(
|
|
858
|
-
(funding) => (!endpoint || fundingReachableAt(funding, endpoint, store.credits)) && (!args.callerPubkey || funding.callerPubkey === args.callerPubkey) && (!args.creditId || funding.creditId === args.creditId)
|
|
859
|
-
).sort((a, b) => a.requestedAt - b.requestedAt);
|
|
860
|
-
});
|
|
837
|
+
function isEvidenceish(value) {
|
|
838
|
+
return typeof value === "object" && value !== null && typeof value.drainId === "string" && Array.isArray(value.receipts);
|
|
861
839
|
}
|
|
862
|
-
function
|
|
863
|
-
return
|
|
840
|
+
function isDrainish(value) {
|
|
841
|
+
return typeof value === "object" && value !== null && typeof value.drainId === "string";
|
|
864
842
|
}
|
|
865
|
-
function
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
843
|
+
function capPendingDrains(drains, creditId) {
|
|
844
|
+
if (drains.length <= MAX_PENDING_DRAINS) return drains;
|
|
845
|
+
const evictable = drains.filter((d) => d.creditId !== creditId).sort((a, b) => a.requestedAt - b.requestedAt);
|
|
846
|
+
const dropped = new Set(evictable.slice(0, drains.length - MAX_PENDING_DRAINS));
|
|
847
|
+
for (const d of dropped) {
|
|
848
|
+
info(
|
|
849
|
+
`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.`
|
|
871
850
|
);
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
store.pendingFundings[index] = {
|
|
875
|
-
...normalized,
|
|
876
|
-
endpoint: store.pendingFundings[index].endpoint
|
|
877
|
-
};
|
|
878
|
-
saveStore(store);
|
|
879
|
-
});
|
|
851
|
+
}
|
|
852
|
+
return drains.filter((d) => !dropped.has(d));
|
|
880
853
|
}
|
|
881
|
-
function
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
854
|
+
function saveStore(store) {
|
|
855
|
+
ensureConfigDir();
|
|
856
|
+
const tmp = `${CREDITS_FILE}.${String(process.pid)}.tmp`;
|
|
857
|
+
writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
|
|
858
|
+
try {
|
|
859
|
+
renameSync(tmp, CREDITS_FILE);
|
|
860
|
+
} catch (err) {
|
|
861
|
+
try {
|
|
862
|
+
unlinkSync(tmp);
|
|
863
|
+
} catch {
|
|
864
|
+
}
|
|
865
|
+
throw err;
|
|
866
|
+
}
|
|
891
867
|
}
|
|
892
|
-
function
|
|
893
|
-
const
|
|
894
|
-
return
|
|
868
|
+
function unreadableStore(cause) {
|
|
869
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
870
|
+
return new DvmError(
|
|
871
|
+
"credit_store_unreadable",
|
|
872
|
+
`The prepaid-credit record at ${CREDITS_FILE} exists but could not be opened: ${detail}`,
|
|
873
|
+
`The file was left untouched. Check that you own it and can read it ('ls -l ${CREDITS_FILE}'), then retry \u2014 'chmod 600 ${CREDITS_FILE}' fixes the common case. Do not delete it: it may hold DVM-signed drain receipts for a refund that is still owed.`,
|
|
874
|
+
{ credits_file: CREDITS_FILE, cause: detail },
|
|
875
|
+
`Your local record of prepaid credits could not be opened \u2014 this looks like a file-permissions problem on this machine, not damage to the record. Nothing was changed or deleted, so any signed proof of a refund you are owed is still there.`
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
function quarantineCorruptStore(cause) {
|
|
879
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
880
|
+
const quarantined = `${CREDITS_FILE}.corrupt-${stamp}-${String(process.pid)}`;
|
|
881
|
+
let moved = false;
|
|
882
|
+
try {
|
|
883
|
+
renameSync(CREDITS_FILE, quarantined);
|
|
884
|
+
moved = true;
|
|
885
|
+
} catch {
|
|
886
|
+
}
|
|
887
|
+
const kept = moved ? quarantined : CREDITS_FILE;
|
|
888
|
+
return new DvmError(
|
|
889
|
+
"credit_store_corrupt",
|
|
890
|
+
moved ? `The prepaid-credit record at ${CREDITS_FILE} is damaged and could not be parsed; it was moved aside to ${quarantined}.` : `The prepaid-credit record at ${CREDITS_FILE} is damaged and could not be parsed, and could not be moved aside.`,
|
|
891
|
+
`Do not delete ${kept} \u2014 it may hold DVM-signed drain receipts for a refund that is still owed. Balances themselves live on the DVM, so 'dvm credit balance <dvm>' re-reads them.`,
|
|
892
|
+
{
|
|
893
|
+
credits_file: CREDITS_FILE,
|
|
894
|
+
...moved ? { quarantined_path: quarantined } : {},
|
|
895
|
+
cause: cause instanceof Error ? cause.message : String(cause)
|
|
896
|
+
},
|
|
897
|
+
`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.`
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
function mutateCredit(location, mutate) {
|
|
901
|
+
withCreditLock(() => {
|
|
895
902
|
const store = loadStore();
|
|
896
|
-
const key = normalizeCreditEndpoint(
|
|
897
|
-
|
|
903
|
+
const key = normalizeCreditEndpoint(location.endpoint);
|
|
904
|
+
const record = findDvmLocated(store.credits, {
|
|
898
905
|
endpoint: key,
|
|
899
|
-
...
|
|
900
|
-
where: (c) => c.callerPubkey ===
|
|
906
|
+
...location.identity ? { identity: location.identity } : {},
|
|
907
|
+
where: (c) => c.callerPubkey === location.callerPubkey
|
|
901
908
|
});
|
|
902
|
-
if (!record)
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
creditId: args.credit.credit_id,
|
|
908
|
-
currency: args.credit.currency,
|
|
909
|
-
balanceMicro: 0,
|
|
910
|
-
remainingMicro: 0,
|
|
911
|
-
ledgerSeq: 0,
|
|
912
|
-
expiryMs: 0,
|
|
913
|
-
targetMicro: args.targetMicro,
|
|
914
|
-
status: "active",
|
|
915
|
-
openedAt: now,
|
|
916
|
-
updatedAt: now
|
|
917
|
-
};
|
|
918
|
-
store.credits.push(record);
|
|
919
|
-
}
|
|
920
|
-
attachDvmLocation(record, key, args.identity);
|
|
921
|
-
adoptCreditId(record, args.credit.credit_id);
|
|
922
|
-
record.currency = args.credit.currency;
|
|
923
|
-
record.balanceMicro = args.credit.balance_micro;
|
|
924
|
-
record.remainingMicro = args.credit.remaining_micro;
|
|
925
|
-
record.ledgerSeq = Math.max(record.ledgerSeq, args.credit.ledger_seq);
|
|
926
|
-
record.expiryMs = args.credit.expiry_ms;
|
|
927
|
-
record.targetMicro = args.targetMicro;
|
|
928
|
-
record.status = args.credit.status === "unbacked" ? "unbacked" : args.credit.expired ? "expired" : "active";
|
|
929
|
-
record.reconciliationPending = args.credit.reconciliation_pending;
|
|
930
|
-
record.updatedAt = now;
|
|
931
|
-
delete record.provisional;
|
|
932
|
-
if (args.sequenced === false) record.unsequencedBalance = true;
|
|
933
|
-
else delete record.unsequencedBalance;
|
|
934
|
-
if (args.menu) record.menu = args.menu;
|
|
935
|
-
if (args.rail) {
|
|
936
|
-
record.rail = args.rail;
|
|
937
|
-
delete record.refillUnavailableRail;
|
|
909
|
+
if (!record) return;
|
|
910
|
+
const locationChanged = attachDvmLocation(record, key, location.identity);
|
|
911
|
+
if (mutate(record) === "skip") {
|
|
912
|
+
if (locationChanged) saveStore(store);
|
|
913
|
+
return;
|
|
938
914
|
}
|
|
939
|
-
|
|
940
|
-
if (args.x402ChannelNetwork) record.x402ChannelNetwork = args.x402ChannelNetwork;
|
|
941
|
-
if (args.builderPubkey) record.builderPubkey = args.builderPubkey;
|
|
942
|
-
if (args.select) delete record.selectionCleared;
|
|
943
|
-
delete record.discrepancy;
|
|
915
|
+
record.updatedAt = Date.now();
|
|
944
916
|
saveStore(store);
|
|
945
|
-
return record;
|
|
946
917
|
});
|
|
947
918
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
919
|
+
var DEFAULT_LOCK_STALE_MS = 1e4;
|
|
920
|
+
var DEFAULT_LOCK_RETRIES = 600;
|
|
921
|
+
var DEFAULT_LOCK_RETRY_INTERVAL_MS = 25;
|
|
922
|
+
var lockDepth = 0;
|
|
923
|
+
function acquireCreditLock(opts) {
|
|
924
|
+
ensureConfigDir();
|
|
925
|
+
const lockPath = `${CREDITS_FILE}.lock`;
|
|
926
|
+
const stale = opts.stale ?? DEFAULT_LOCK_STALE_MS;
|
|
927
|
+
const retries = opts.retries ?? DEFAULT_LOCK_RETRIES;
|
|
928
|
+
const interval = opts.retryIntervalMs ?? DEFAULT_LOCK_RETRY_INTERVAL_MS;
|
|
929
|
+
let lastError;
|
|
930
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
931
|
+
try {
|
|
932
|
+
return lockfile.lockSync(CREDITS_FILE, {
|
|
933
|
+
lockfilePath: lockPath,
|
|
934
|
+
stale,
|
|
935
|
+
realpath: false
|
|
936
|
+
});
|
|
937
|
+
} catch (err) {
|
|
938
|
+
lastError = err;
|
|
939
|
+
if (err.code !== "ELOCKED") {
|
|
940
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
941
|
+
const configDir = dirname(lockPath);
|
|
942
|
+
throw new DvmError(
|
|
943
|
+
"credit_store_lock_failed",
|
|
944
|
+
`Could not create the prepaid-credit lock at ${lockPath}: ${detail}`,
|
|
945
|
+
`This is not contention \u2014 nothing is holding the lock. Check that ${configDir} exists and is writable by you ('ls -ld ${configDir}'), then retry.`,
|
|
946
|
+
{
|
|
947
|
+
lock_file: lockPath,
|
|
948
|
+
config_dir: configDir,
|
|
949
|
+
underlying: detail,
|
|
950
|
+
errno_code: err.code
|
|
951
|
+
},
|
|
952
|
+
`The dvm CLI could not create the small lock file it uses to keep two commands from writing your credit record at once. Its folder looks unwritable \u2014 nothing about your credits is wrong, and nothing was changed.`
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
if (attempt === retries) break;
|
|
956
|
+
sleepSync(interval);
|
|
966
957
|
}
|
|
967
|
-
|
|
968
|
-
|
|
958
|
+
}
|
|
959
|
+
throw new DvmError(
|
|
960
|
+
"credit_store_locked",
|
|
961
|
+
`Could not acquire the prepaid-credit lock at ${lockPath} within the retry budget.`,
|
|
962
|
+
`Another dvm CLI may be writing to ${CREDITS_FILE}. Wait a moment and retry. If no other dvm CLI is running, remove ${lockPath} and try again.`,
|
|
963
|
+
{
|
|
964
|
+
lock_file: lockPath,
|
|
965
|
+
underlying: lastError instanceof Error ? lastError.message : String(lastError)
|
|
969
966
|
}
|
|
970
|
-
|
|
971
|
-
});
|
|
967
|
+
);
|
|
972
968
|
}
|
|
973
|
-
function
|
|
974
|
-
if (
|
|
975
|
-
|
|
969
|
+
function rejectAsyncBody(result) {
|
|
970
|
+
if (typeof result?.then !== "function") {
|
|
971
|
+
return result;
|
|
972
|
+
}
|
|
973
|
+
throw new DvmError(
|
|
974
|
+
"credit_store_lock_misuse",
|
|
975
|
+
"withCreditLock was called with an asynchronous body. The lock is released when the body returns, so a promise's writes would run unprotected.",
|
|
976
|
+
"Make the locked body synchronous \u2014 the credit store's whole surface is sync \u2014 and do any awaiting outside the lock."
|
|
977
|
+
);
|
|
976
978
|
}
|
|
977
|
-
function
|
|
978
|
-
|
|
979
|
+
function sleepSync(ms) {
|
|
980
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
979
981
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
982
|
+
|
|
983
|
+
// src/lib/evm-private-key.ts
|
|
984
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
985
|
+
import { hexToBytes } from "@noble/hashes/utils.js";
|
|
986
|
+
var EVM_PRIVATE_KEY_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
987
|
+
function isValidEvmPrivateKey(value) {
|
|
988
|
+
return EVM_PRIVATE_KEY_RE.test(value) && secp256k1.utils.isValidSecretKey(hexToBytes(value.slice(2)));
|
|
983
989
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
990
|
+
|
|
991
|
+
// src/lib/config-mutation.ts
|
|
992
|
+
import {
|
|
993
|
+
appendFileSync,
|
|
994
|
+
chmodSync,
|
|
995
|
+
existsSync as existsSync2,
|
|
996
|
+
readFileSync as readFileSync2,
|
|
997
|
+
renameSync as renameSync2,
|
|
998
|
+
statSync,
|
|
999
|
+
unlinkSync as unlinkSync2,
|
|
1000
|
+
writeFileSync as writeFileSync2
|
|
1001
|
+
} from "fs";
|
|
1002
|
+
var PROTECTED_CONFIG_FIELDS = ["nwcUri", "defaultIdentity", "tempo", "x402"];
|
|
1003
|
+
var ProtectedFieldAuthorization = class {
|
|
1004
|
+
/** Targets this authorization covers. A field also covers its own leaves. */
|
|
1005
|
+
targets;
|
|
1006
|
+
/** Prefer {@link authorizeProtectedRemoval}; this exists for it to call. */
|
|
1007
|
+
constructor(targets) {
|
|
1008
|
+
this.targets = [...targets];
|
|
988
1009
|
}
|
|
989
|
-
|
|
1010
|
+
/**
|
|
1011
|
+
* True when `target` is one this authorization names. Naming a field covers
|
|
1012
|
+
* the leaves under it; naming a leaf covers only that leaf, so a command
|
|
1013
|
+
* entitled to clear `tempo.apiKey` still cannot take the whole `tempo` block.
|
|
1014
|
+
*/
|
|
1015
|
+
covers(target) {
|
|
1016
|
+
if (this.targets.includes(target)) return true;
|
|
1017
|
+
const field = target.split(".")[0];
|
|
1018
|
+
return target !== field && this.targets.includes(field);
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
function authorizeProtectedRemoval(...targets) {
|
|
1022
|
+
return new ProtectedFieldAuthorization(targets);
|
|
990
1023
|
}
|
|
991
|
-
function
|
|
992
|
-
if (
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
status: record.status === "unbacked" ? "unbacked" : "active",
|
|
997
|
-
currency: record.currency,
|
|
998
|
-
balanceMicro: record.balanceMicro,
|
|
999
|
-
remainingMicro: record.remainingMicro,
|
|
1000
|
-
ledgerSeq: record.ledgerSeq,
|
|
1001
|
-
expiryMs: record.expiryMs,
|
|
1002
|
-
expired: record.status === "expired",
|
|
1003
|
-
drainable: record.status !== "unbacked" && record.balanceMicro > 0,
|
|
1004
|
-
...record.reconciliationPending ? { reconciliationPending: true } : {},
|
|
1005
|
-
observedAt: record.updatedAt
|
|
1006
|
-
};
|
|
1007
|
-
record.siblingCredits = [
|
|
1008
|
-
...(record.siblingCredits ?? []).filter(
|
|
1009
|
-
(sibling) => sibling.creditId !== prior.creditId && sibling.creditId !== creditId
|
|
1010
|
-
),
|
|
1011
|
-
prior
|
|
1012
|
-
];
|
|
1013
|
-
} else if (record.siblingCredits) {
|
|
1014
|
-
record.siblingCredits = record.siblingCredits.filter(
|
|
1015
|
-
(sibling) => sibling.creditId !== creditId
|
|
1016
|
-
);
|
|
1024
|
+
function applyConfigPatch(base, patch) {
|
|
1025
|
+
if (!isPlainObject(patch))
|
|
1026
|
+
throw patchShapeError("A change must be an object with `set` and/or `unset`.");
|
|
1027
|
+
if (patch.set !== void 0 && !isPlainObject(patch.set)) {
|
|
1028
|
+
throw patchShapeError("A change's `set` must be an object of fields to write.");
|
|
1017
1029
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
record.balanceMicro = 0;
|
|
1021
|
-
record.remainingMicro = 0;
|
|
1022
|
-
record.expiryMs = 0;
|
|
1023
|
-
delete record.unsequencedBalance;
|
|
1024
|
-
delete record.rail;
|
|
1025
|
-
delete record.x402Instrument;
|
|
1026
|
-
delete record.x402ChannelNetwork;
|
|
1027
|
-
delete record.refillUnavailableRail;
|
|
1028
|
-
delete record.refillDrainNoticed;
|
|
1029
|
-
delete record.discrepancy;
|
|
1030
|
-
delete record.lastReceiptJobId;
|
|
1031
|
-
}
|
|
1032
|
-
var MAX_LOST_IMPLICIT_CREDITS = 8;
|
|
1033
|
-
var MAX_DATE_MS = 864e13;
|
|
1034
|
-
function loadStore() {
|
|
1035
|
-
if (!existsSync2(CREDITS_FILE)) {
|
|
1036
|
-
return { credits: [], implicitCredits: [], pendingFundings: [], fundingMenuSnapshots: [] };
|
|
1030
|
+
if (patch.unset !== void 0 && !Array.isArray(patch.unset)) {
|
|
1031
|
+
throw patchShapeError("A change's `unset` must be an array of field names.");
|
|
1037
1032
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
throw
|
|
1033
|
+
const set = patch.set ?? {};
|
|
1034
|
+
const unset = patch.unset ?? [];
|
|
1035
|
+
for (const [field, value] of Object.entries(set)) {
|
|
1036
|
+
if (value !== void 0) continue;
|
|
1037
|
+
throw new DvmError(
|
|
1038
|
+
"config_patch_invalid",
|
|
1039
|
+
`Config field '${field}' was declared with no value.`,
|
|
1040
|
+
"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."
|
|
1041
|
+
);
|
|
1043
1042
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1048
|
-
throw new Error("the file parsed as JSON but is not a credit-store object");
|
|
1043
|
+
for (const field of unset) {
|
|
1044
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
1045
|
+
throw patchShapeError("Every entry in a change's `unset` must be a field name.");
|
|
1049
1046
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1047
|
+
if (!Object.hasOwn(set, field)) continue;
|
|
1048
|
+
throw new DvmError(
|
|
1049
|
+
"config_patch_invalid",
|
|
1050
|
+
`Config field '${field}' is both written and removed by the same change.`,
|
|
1051
|
+
"Declare each field once: in `set` with its new value, or in `unset` to remove it."
|
|
1052
|
+
);
|
|
1053
1053
|
}
|
|
1054
|
-
const merged =
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
)
|
|
1058
|
-
);
|
|
1059
|
-
const snapshots = mergeDuplicateSnapshots(
|
|
1060
|
-
Array.isArray(data.fundingMenuSnapshots) ? data.fundingMenuSnapshots.filter(isFundingMenuSnapshot) : [],
|
|
1061
|
-
merged.credits
|
|
1062
|
-
);
|
|
1063
|
-
const fundings = dedupePendingFundings(
|
|
1064
|
-
Array.isArray(data.pendingFundings) ? data.pendingFundings.filter((funding) => isObjectRecord(funding)).map(normalizeRailKey) : []
|
|
1065
|
-
);
|
|
1066
|
-
const implicit = dedupeImplicitCredits(
|
|
1067
|
-
Array.isArray(data.implicitCredits) ? data.implicitCredits.filter(isStoredImplicitCredit) : []
|
|
1054
|
+
const merged = { ...base, ...set };
|
|
1055
|
+
const removing = new Set(unset);
|
|
1056
|
+
return Object.fromEntries(
|
|
1057
|
+
Object.entries(merged).filter(([field]) => !removing.has(field))
|
|
1068
1058
|
);
|
|
1069
|
-
const store = {
|
|
1070
|
-
credits: merged.credits,
|
|
1071
|
-
implicitCredits: implicit.credits,
|
|
1072
|
-
pendingFundings: fundings.fundings,
|
|
1073
|
-
fundingMenuSnapshots: snapshots.snapshots
|
|
1074
|
-
};
|
|
1075
|
-
if (merged.changed || snapshots.changed || fundings.changed || implicit.changed) saveStore(store);
|
|
1076
|
-
return store;
|
|
1077
1059
|
}
|
|
1078
|
-
function
|
|
1079
|
-
|
|
1080
|
-
return
|
|
1060
|
+
function deepFreezeConfig(config) {
|
|
1061
|
+
freezeDeep(config);
|
|
1062
|
+
return config;
|
|
1081
1063
|
}
|
|
1082
|
-
function
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
});
|
|
1093
|
-
if (!existing) {
|
|
1094
|
-
merged.push(candidate);
|
|
1064
|
+
function protectedRemovals(before, after) {
|
|
1065
|
+
const previous = before;
|
|
1066
|
+
const next = after;
|
|
1067
|
+
const removed = [];
|
|
1068
|
+
for (const field of PROTECTED_CONFIG_FIELDS) {
|
|
1069
|
+
const held = previous[field];
|
|
1070
|
+
if (!isPresentValue(held)) continue;
|
|
1071
|
+
const kept = next[field];
|
|
1072
|
+
if (!isPresentValue(kept)) {
|
|
1073
|
+
removed.push(field);
|
|
1095
1074
|
continue;
|
|
1096
1075
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
existing.remainingMicro = candidate.remainingMicro;
|
|
1104
|
-
existing.ledgerSeq = candidate.ledgerSeq;
|
|
1105
|
-
existing.status = candidate.status;
|
|
1106
|
-
if (candidate.lossReason) existing.lossReason = candidate.lossReason;
|
|
1107
|
-
else delete existing.lossReason;
|
|
1108
|
-
existing.updatedAt = candidate.updatedAt;
|
|
1109
|
-
existing.lastReceiptJobId = candidate.lastReceiptJobId;
|
|
1110
|
-
existing.discrepancy = candidate.discrepancy;
|
|
1076
|
+
const spec = PROTECTED_FIELDS[field];
|
|
1077
|
+
if (spec.credentialLeaves.length === 0) continue;
|
|
1078
|
+
if (!isPlainObject(held)) continue;
|
|
1079
|
+
if (!isPlainObject(kept)) {
|
|
1080
|
+
removed.push(field);
|
|
1081
|
+
continue;
|
|
1111
1082
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
existing.releasedByJobId = candidate.releasedByJobId;
|
|
1083
|
+
for (const leaf of spec.credentialLeaves) {
|
|
1084
|
+
if (!isPresentValue(held[leaf]) || isPresentValue(kept[leaf])) continue;
|
|
1085
|
+
removed.push(`${field}.${leaf}`);
|
|
1116
1086
|
}
|
|
1117
1087
|
}
|
|
1118
|
-
|
|
1119
|
-
return { credits: capped.credits, changed: changed || capped.changed };
|
|
1120
|
-
}
|
|
1121
|
-
function capLostImplicitCredits(credits) {
|
|
1122
|
-
const lostByOwner = /* @__PURE__ */ new Map();
|
|
1123
|
-
for (const credit of credits) {
|
|
1124
|
-
if (credit.status !== "lost") continue;
|
|
1125
|
-
const identity = dvmLocationIdentity(credit);
|
|
1126
|
-
const dvmKey = identity ? `${identity.dvmId}\0${identity.builderPubkey}` : normalizeCreditEndpoint(credit.endpoint);
|
|
1127
|
-
const key = `${credit.callerPubkey}\0${dvmKey}`;
|
|
1128
|
-
const group = lostByOwner.get(key) ?? [];
|
|
1129
|
-
group.push(credit);
|
|
1130
|
-
lostByOwner.set(key, group);
|
|
1131
|
-
}
|
|
1132
|
-
const dropped = /* @__PURE__ */ new Set();
|
|
1133
|
-
for (const group of lostByOwner.values()) {
|
|
1134
|
-
group.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1135
|
-
for (const credit of group.slice(MAX_LOST_IMPLICIT_CREDITS)) dropped.add(credit);
|
|
1136
|
-
}
|
|
1137
|
-
return dropped.size === 0 ? { credits, changed: false } : { credits: credits.filter((credit) => !dropped.has(credit)), changed: true };
|
|
1088
|
+
return removed;
|
|
1138
1089
|
}
|
|
1139
|
-
function
|
|
1140
|
-
const
|
|
1141
|
-
const
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
}
|
|
1151
|
-
changed = true;
|
|
1152
|
-
}
|
|
1153
|
-
if (snapshot.dvmId === void 0) {
|
|
1154
|
-
merged.push(snapshot);
|
|
1090
|
+
function supersededProtectedSecrets(before, after) {
|
|
1091
|
+
const previous = before;
|
|
1092
|
+
const next = after;
|
|
1093
|
+
const superseded = [];
|
|
1094
|
+
for (const field of PROTECTED_CONFIG_FIELDS) {
|
|
1095
|
+
const spec = PROTECTED_FIELDS[field];
|
|
1096
|
+
if (!spec.secret) continue;
|
|
1097
|
+
const held = previous[field];
|
|
1098
|
+
const kept = next[field];
|
|
1099
|
+
if (spec.credentialLeaves.length === 0) {
|
|
1100
|
+
if (isPresentValue(held) && isPresentValue(kept) && held !== kept) superseded.push(field);
|
|
1155
1101
|
continue;
|
|
1156
1102
|
}
|
|
1157
|
-
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1103
|
+
if (!isPlainObject(held) || !isPlainObject(kept)) continue;
|
|
1104
|
+
for (const leaf of spec.credentialLeaves) {
|
|
1105
|
+
const was = held[leaf];
|
|
1106
|
+
const now = kept[leaf];
|
|
1107
|
+
if (!isPresentValue(was) || !isPresentValue(now) || was === now) continue;
|
|
1108
|
+
superseded.push(`${field}.${leaf}`);
|
|
1163
1109
|
}
|
|
1164
|
-
merged[index] = mergeSnapshots(merged[index], snapshot);
|
|
1165
|
-
changed = true;
|
|
1166
1110
|
}
|
|
1167
|
-
return
|
|
1111
|
+
return superseded;
|
|
1168
1112
|
}
|
|
1169
|
-
function
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1113
|
+
function changedConfigPaths(before, after) {
|
|
1114
|
+
const found = /* @__PURE__ */ new Set();
|
|
1115
|
+
collectChangedPaths(
|
|
1116
|
+
before,
|
|
1117
|
+
after,
|
|
1118
|
+
CONFIG_PATH_SHAPE,
|
|
1119
|
+
"",
|
|
1120
|
+
found
|
|
1121
|
+
);
|
|
1122
|
+
return [...found].sort();
|
|
1123
|
+
}
|
|
1124
|
+
function assertValidConfig(config, fields) {
|
|
1125
|
+
if (!isPlainObject(config)) {
|
|
1126
|
+
throw new DvmError(
|
|
1127
|
+
"config_invalid",
|
|
1128
|
+
"The caller configuration is not a JSON object.",
|
|
1129
|
+
`Expected an object at ${CONFIG_FILE}.${rollbackSuffix()}`
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
const record = config;
|
|
1133
|
+
const scope = fields ? new Set(fields) : null;
|
|
1134
|
+
for (const [field, kind] of Object.entries(CONFIG_FIELD_TYPES)) {
|
|
1135
|
+
if (scope && !scope.has(field)) continue;
|
|
1136
|
+
const value = record[field];
|
|
1137
|
+
if (value === void 0) continue;
|
|
1138
|
+
if (!matchesKind(value, kind)) throw invalidFieldError(field, kind);
|
|
1139
|
+
}
|
|
1140
|
+
for (const [path, kind] of Object.entries(CONFIG_LEAF_TYPES)) {
|
|
1141
|
+
const [field, leaf] = path.split(".");
|
|
1142
|
+
if (scope && !scope.has(field)) continue;
|
|
1143
|
+
const owner = record[field];
|
|
1144
|
+
if (!isPlainObject(owner)) continue;
|
|
1145
|
+
const value = owner[leaf];
|
|
1146
|
+
if (value === void 0 || !matchesKind(value, kind)) throw invalidFieldError(path, kind);
|
|
1176
1147
|
}
|
|
1177
|
-
const origins = dvmLocationOrigins(newer).concat(dvmLocationOrigins(older));
|
|
1178
|
-
const aliases = [...new Set(origins)].filter((origin) => origin !== snapshot.endpoint);
|
|
1179
|
-
if (aliases.length > 0) snapshot.endpointAliases = aliases;
|
|
1180
|
-
else delete snapshot.endpointAliases;
|
|
1181
|
-
return snapshot;
|
|
1182
1148
|
}
|
|
1183
|
-
function
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1149
|
+
function readRawConfig() {
|
|
1150
|
+
if (!existsSync2(CONFIG_FILE)) return null;
|
|
1151
|
+
try {
|
|
1152
|
+
return JSON.parse(readFileSync2(CONFIG_FILE, "utf-8"));
|
|
1153
|
+
} catch {
|
|
1154
|
+
throw new DvmError(
|
|
1155
|
+
"config_corrupt",
|
|
1156
|
+
"Config file is corrupt.",
|
|
1157
|
+
// Not "delete it": those bytes are the only copy on this machine of the
|
|
1158
|
+
// NWC connection string and both private keys, however badly they parse.
|
|
1159
|
+
`Run 'dvm config rollback' to restore a stored copy, or move ${CONFIG_FILE} aside yourself and run 'dvm init'.${rollbackSuffix()}`
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
function writeConfigFile(config) {
|
|
1164
|
+
writeJsonAtomic(CONFIG_FILE, config);
|
|
1165
|
+
}
|
|
1166
|
+
var CONFIG_SNAPSHOT_SLOTS = ["backup", "pinned"];
|
|
1167
|
+
function configSnapshotPath(slot) {
|
|
1168
|
+
return slot === "pinned" ? CONFIG_PINNED_FILE : CONFIG_BACKUP_FILE;
|
|
1169
|
+
}
|
|
1170
|
+
function describeConfigBackup(slot = "backup") {
|
|
1171
|
+
const path = configSnapshotPath(slot);
|
|
1172
|
+
try {
|
|
1173
|
+
if (!existsSync2(path)) return null;
|
|
1174
|
+
const stat = statSync(path);
|
|
1175
|
+
const posix = process.platform !== "win32";
|
|
1176
|
+
const mode = stat.mode & 511;
|
|
1177
|
+
return {
|
|
1178
|
+
path,
|
|
1179
|
+
slot,
|
|
1180
|
+
bytes: stat.size,
|
|
1181
|
+
modifiedAt: stat.mtime.toISOString(),
|
|
1182
|
+
modeOctal: posix ? toOctal(mode) : null,
|
|
1183
|
+
ownerOnly: !posix || (mode & 63) === 0
|
|
1191
1184
|
};
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
const seen = identities.get(key);
|
|
1195
|
-
if (seen !== void 0 && (seen.dvmId !== identity.dvmId || seen.attestedBuilderPubkey !== identity.attestedBuilderPubkey)) {
|
|
1196
|
-
ambiguous.add(key);
|
|
1197
|
-
continue;
|
|
1198
|
-
}
|
|
1199
|
-
identities.set(key, identity);
|
|
1200
|
-
}
|
|
1185
|
+
} catch {
|
|
1186
|
+
return null;
|
|
1201
1187
|
}
|
|
1202
|
-
for (const key of ambiguous) identities.delete(key);
|
|
1203
|
-
return identities;
|
|
1204
1188
|
}
|
|
1205
|
-
function
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1189
|
+
function readConfigBackup(slot = "backup") {
|
|
1190
|
+
const path = configSnapshotPath(slot);
|
|
1191
|
+
if (!existsSync2(path)) return null;
|
|
1192
|
+
try {
|
|
1193
|
+
const parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
1194
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
1195
|
+
} catch {
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
function retainConfigBackup(slot = "backup") {
|
|
1200
|
+
if (!existsSync2(CONFIG_FILE)) return false;
|
|
1201
|
+
ensureConfigDir();
|
|
1202
|
+
const path = configSnapshotPath(slot);
|
|
1203
|
+
const tmp = `${path}.tmp`;
|
|
1204
|
+
writeFileSync2(tmp, readFileSync2(CONFIG_FILE), { mode: 384 });
|
|
1205
|
+
chmodSync(tmp, 384);
|
|
1206
|
+
commitTmp(tmp, path);
|
|
1207
|
+
return true;
|
|
1208
|
+
}
|
|
1209
|
+
function writeConfigBackup(config, slot = "backup") {
|
|
1210
|
+
writeJsonAtomic(configSnapshotPath(slot), config);
|
|
1211
|
+
}
|
|
1212
|
+
function removeConfigBackup(slot = "backup") {
|
|
1213
|
+
const path = configSnapshotPath(slot);
|
|
1214
|
+
if (!existsSync2(path)) return false;
|
|
1215
|
+
unlinkSync2(path);
|
|
1216
|
+
return true;
|
|
1217
|
+
}
|
|
1218
|
+
function protectedTargetValue(config, target) {
|
|
1219
|
+
const [field, named] = target.split(".");
|
|
1220
|
+
const held = config[field];
|
|
1221
|
+
const leaves = PROTECTED_FIELDS[field].credentialLeaves;
|
|
1222
|
+
const leaf = named ?? (leaves.length > 0 ? leaves[0] : void 0);
|
|
1223
|
+
if (leaf === void 0) return held;
|
|
1224
|
+
return isPlainObject(held) ? held[leaf] : void 0;
|
|
1225
|
+
}
|
|
1226
|
+
function protectedValuesMatch(target, left, right) {
|
|
1227
|
+
const normalized = comparableProtectedValue(target, left);
|
|
1228
|
+
return normalized !== void 0 && normalized === comparableProtectedValue(target, right);
|
|
1229
|
+
}
|
|
1230
|
+
function isSecretProtectedTarget(target) {
|
|
1231
|
+
return PROTECTED_FIELDS[target.split(".")[0]].secret;
|
|
1232
|
+
}
|
|
1233
|
+
function withoutProtectedTargets(config, targets) {
|
|
1234
|
+
const dropping = /* @__PURE__ */ new Set();
|
|
1235
|
+
for (const target of targets) {
|
|
1236
|
+
const field = target.split(".")[0];
|
|
1237
|
+
dropping.add(field);
|
|
1238
|
+
for (const alias of PROTECTED_FIELDS[field].aliases) dropping.add(alias);
|
|
1239
|
+
}
|
|
1240
|
+
return Object.fromEntries(
|
|
1241
|
+
Object.entries(config).filter(([field]) => !dropping.has(field))
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
function appendConfigAudit(record) {
|
|
1245
|
+
try {
|
|
1246
|
+
ensureConfigDir();
|
|
1247
|
+
appendFileSync(CONFIG_AUDIT_FILE, JSON.stringify(record) + "\n", { mode: 384 });
|
|
1248
|
+
chmodSync(CONFIG_AUDIT_FILE, 384);
|
|
1249
|
+
} catch {
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
function protectedRemovalError(targets, reason) {
|
|
1253
|
+
const named = targets.join(", ");
|
|
1254
|
+
const one = targets.length === 1;
|
|
1255
|
+
return new DvmError(
|
|
1256
|
+
"config_protected_field",
|
|
1257
|
+
`The '${reason}' change would remove ${named} from the local configuration.`,
|
|
1258
|
+
`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.`,
|
|
1259
|
+
{ fields: targets }
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
var PROTECTED_FIELDS = {
|
|
1263
|
+
nwcUri: { aliases: [], credentialLeaves: [], secret: true, hexCredential: false },
|
|
1264
|
+
// A pointer at a key, not a key: worth protecting from removal, and never
|
|
1265
|
+
// worth stripping out of a stored copy — that is recovery lost for nothing.
|
|
1266
|
+
defaultIdentity: { aliases: [], credentialLeaves: [], secret: false, hexCredential: false },
|
|
1267
|
+
// `mpp` is the pre-internal-review name; a caller who has not run a mutating command
|
|
1268
|
+
// since the rename still has their private key under it.
|
|
1269
|
+
tempo: { aliases: ["mpp"], credentialLeaves: ["apiKey"], secret: true, hexCredential: true },
|
|
1270
|
+
x402: { aliases: [], credentialLeaves: ["privateKey"], secret: true, hexCredential: true }
|
|
1271
|
+
};
|
|
1272
|
+
var CONFIG_PATH_SHAPE = {
|
|
1273
|
+
float: { statedBudget: {}, connectedAt: {}, alias: {}, methods: {} },
|
|
1274
|
+
x402: { privateKey: {}, network: {} },
|
|
1275
|
+
tempo: { method: {}, accountId: {}, apiKey: {} },
|
|
1276
|
+
credit: {
|
|
1277
|
+
targetJobs: {},
|
|
1278
|
+
posture: {},
|
|
1279
|
+
tier0Cap: {},
|
|
1280
|
+
tier1Cap: {},
|
|
1281
|
+
pocketTarget: {},
|
|
1282
|
+
fundingRail: {},
|
|
1283
|
+
perDvm: { "*": { targetJobs: {}, posture: {}, fundingRail: {}, dvmId: {}, builderPubkey: {} } },
|
|
1284
|
+
trustOverrides: { "*": {} }
|
|
1285
|
+
},
|
|
1286
|
+
feedback: { posture: {} }
|
|
1287
|
+
};
|
|
1288
|
+
var CONFIG_FIELD_TYPES = {
|
|
1289
|
+
nwcUri: "string",
|
|
1290
|
+
float: "object",
|
|
1291
|
+
defaultBudget: "string",
|
|
1292
|
+
autoPayThreshold: "string",
|
|
1293
|
+
defaultMaxPayments: "number",
|
|
1294
|
+
cashuMints: "string[]",
|
|
1295
|
+
x402: "object",
|
|
1296
|
+
tempo: "object",
|
|
1297
|
+
defaultRail: "string",
|
|
1298
|
+
defaultIdentity: "string",
|
|
1299
|
+
credit: "object",
|
|
1300
|
+
feedback: "object"
|
|
1301
|
+
};
|
|
1302
|
+
var CONFIG_LEAF_TYPES = {
|
|
1303
|
+
"float.connectedAt": "string",
|
|
1304
|
+
"x402.privateKey": "string",
|
|
1305
|
+
"tempo.method": "string"
|
|
1306
|
+
};
|
|
1307
|
+
function collectChangedPaths(before, after, shape, prefix, found) {
|
|
1308
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
1309
|
+
const held = before[key];
|
|
1310
|
+
const kept = after[key];
|
|
1311
|
+
if (deepEqual(held, kept)) continue;
|
|
1312
|
+
const known = shape !== void 0 && Object.hasOwn(shape, key);
|
|
1313
|
+
const wildcard = !known && shape !== void 0 && Object.hasOwn(shape, "*");
|
|
1314
|
+
const child = known ? shape[key] : wildcard ? shape["*"] : void 0;
|
|
1315
|
+
const path = prefix ? `${prefix}.${wildcard ? "*" : key}` : key;
|
|
1316
|
+
const descendable = child !== void 0 && Object.keys(child).length > 0 && (held === void 0 || isPlainObject(held)) && (kept === void 0 || isPlainObject(kept));
|
|
1317
|
+
if (descendable) {
|
|
1318
|
+
collectChangedPaths(
|
|
1319
|
+
isPlainObject(held) ? held : {},
|
|
1320
|
+
isPlainObject(kept) ? kept : {},
|
|
1321
|
+
child,
|
|
1322
|
+
path,
|
|
1323
|
+
found
|
|
1324
|
+
);
|
|
1325
|
+
} else {
|
|
1326
|
+
found.add(path);
|
|
1327
|
+
}
|
|
1226
1328
|
}
|
|
1227
|
-
return { fundings: kept, changed };
|
|
1228
1329
|
}
|
|
1229
|
-
function
|
|
1230
|
-
return
|
|
1330
|
+
function invalidFieldError(path, kind) {
|
|
1331
|
+
return new DvmError(
|
|
1332
|
+
"config_invalid",
|
|
1333
|
+
`Config field '${path}' must be ${describeKind(kind)}.`,
|
|
1334
|
+
`Nothing was written. Correct the field in ${CONFIG_FILE}, or run 'dvm doctor' for the full picture.${rollbackSuffix()}`
|
|
1335
|
+
);
|
|
1231
1336
|
}
|
|
1232
|
-
function
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const index = byCredit.get(key);
|
|
1243
|
-
if (index === void 0) {
|
|
1244
|
-
byCredit.set(key, merged.length);
|
|
1245
|
-
merged.push(record);
|
|
1246
|
-
continue;
|
|
1247
|
-
}
|
|
1248
|
-
merged[index] = mergeCreditRecords(merged[index], record);
|
|
1249
|
-
changed = true;
|
|
1337
|
+
function describeKind(kind) {
|
|
1338
|
+
switch (kind) {
|
|
1339
|
+
case "string":
|
|
1340
|
+
return "a string";
|
|
1341
|
+
case "number":
|
|
1342
|
+
return "a number";
|
|
1343
|
+
case "object":
|
|
1344
|
+
return "an object";
|
|
1345
|
+
case "string[]":
|
|
1346
|
+
return "an array of strings";
|
|
1250
1347
|
}
|
|
1251
|
-
return { credits: merged, changed };
|
|
1252
1348
|
}
|
|
1253
|
-
function
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
if (record.x402Instrument === void 0 && older.x402Instrument !== void 0) {
|
|
1264
|
-
record.x402Instrument = older.x402Instrument;
|
|
1265
|
-
}
|
|
1266
|
-
if (record.x402ChannelNetwork === void 0 && older.x402ChannelNetwork !== void 0) {
|
|
1267
|
-
record.x402ChannelNetwork = older.x402ChannelNetwork;
|
|
1268
|
-
}
|
|
1269
|
-
if (record.menu === void 0 && older.menu !== void 0) record.menu = older.menu;
|
|
1270
|
-
if (record.builderPubkey === void 0 && older.builderPubkey !== void 0) {
|
|
1271
|
-
record.builderPubkey = older.builderPubkey;
|
|
1272
|
-
}
|
|
1273
|
-
const endpoints = dvmLocationOrigins(newer).concat(dvmLocationOrigins(older));
|
|
1274
|
-
const aliases = [...new Set(endpoints)].filter((endpoint) => endpoint !== record.endpoint);
|
|
1275
|
-
if (aliases.length > 0) record.endpointAliases = aliases;
|
|
1276
|
-
else delete record.endpointAliases;
|
|
1277
|
-
const pendingDrains = unionPendingDrains(newer.pendingDrains, older.pendingDrains);
|
|
1278
|
-
if (pendingDrains.length > 0) record.pendingDrains = pendingDrains;
|
|
1279
|
-
else delete record.pendingDrains;
|
|
1280
|
-
const drainEvidence = unionDrainEvidence(newer.drainEvidence, older.drainEvidence);
|
|
1281
|
-
if (drainEvidence.length > 0) record.drainEvidence = drainEvidence;
|
|
1282
|
-
else delete record.drainEvidence;
|
|
1283
|
-
const siblings = Array.isArray(newer.siblingCredits) ? newer.siblingCredits : older.siblingCredits;
|
|
1284
|
-
if (siblings) {
|
|
1285
|
-
record.siblingCredits = siblings.filter((sibling) => sibling.creditId !== record.creditId);
|
|
1286
|
-
} else {
|
|
1287
|
-
delete record.siblingCredits;
|
|
1349
|
+
function matchesKind(value, kind) {
|
|
1350
|
+
switch (kind) {
|
|
1351
|
+
case "string":
|
|
1352
|
+
return typeof value === "string";
|
|
1353
|
+
case "number":
|
|
1354
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1355
|
+
case "object":
|
|
1356
|
+
return isPlainObject(value);
|
|
1357
|
+
case "string[]":
|
|
1358
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
1288
1359
|
}
|
|
1289
|
-
return record;
|
|
1290
1360
|
}
|
|
1291
|
-
function
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
const key = `${drain.drainId}\0${drain.creditId ?? ""}`;
|
|
1295
|
-
byDrain.set(key, { ...byDrain.get(key) ?? {}, ...drain });
|
|
1296
|
-
}
|
|
1297
|
-
return [...byDrain.values()];
|
|
1361
|
+
function rollbackSuffix() {
|
|
1362
|
+
const stored = describeConfigBackup() ?? describeConfigBackup("pinned");
|
|
1363
|
+
return stored === null ? "" : ` A stored copy of the previous configuration is at ${stored.path}.`;
|
|
1298
1364
|
}
|
|
1299
|
-
function
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
byDrain.set(key, evidence);
|
|
1306
|
-
continue;
|
|
1307
|
-
}
|
|
1308
|
-
const [newer, older] = evidence.collectedAt >= existing.collectedAt ? [evidence, existing] : [existing, evidence];
|
|
1309
|
-
const receipts = unionUnknown(newer.receipts, older.receipts);
|
|
1310
|
-
byDrain.set(key, {
|
|
1311
|
-
...older,
|
|
1312
|
-
...newer,
|
|
1313
|
-
receipts,
|
|
1314
|
-
collectedAt: Math.max(existing.collectedAt, evidence.collectedAt),
|
|
1315
|
-
...existing.settledAt !== void 0 || evidence.settledAt !== void 0 ? { settledAt: Math.max(existing.settledAt ?? 0, evidence.settledAt ?? 0) } : {}
|
|
1316
|
-
});
|
|
1317
|
-
}
|
|
1318
|
-
return [...byDrain.values()];
|
|
1365
|
+
function writeJsonAtomic(path, value) {
|
|
1366
|
+
ensureConfigDir();
|
|
1367
|
+
const tmp = `${path}.tmp`;
|
|
1368
|
+
writeFileSync2(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
|
|
1369
|
+
chmodSync(tmp, 384);
|
|
1370
|
+
commitTmp(tmp, path);
|
|
1319
1371
|
}
|
|
1320
|
-
function
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
let key;
|
|
1372
|
+
function commitTmp(tmp, path) {
|
|
1373
|
+
try {
|
|
1374
|
+
renameSync2(tmp, path);
|
|
1375
|
+
} catch (err) {
|
|
1325
1376
|
try {
|
|
1326
|
-
|
|
1327
|
-
key = typeof serialized === "string" ? serialized : String(value);
|
|
1377
|
+
unlinkSync2(tmp);
|
|
1328
1378
|
} catch {
|
|
1329
|
-
key = String(value);
|
|
1330
1379
|
}
|
|
1331
|
-
|
|
1332
|
-
seen.add(key);
|
|
1333
|
-
values.push(value);
|
|
1380
|
+
throw err;
|
|
1334
1381
|
}
|
|
1335
|
-
return values;
|
|
1336
|
-
}
|
|
1337
|
-
function isFundingMenuSnapshot(value) {
|
|
1338
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1339
|
-
const snapshot = value;
|
|
1340
|
-
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
1382
|
}
|
|
1342
|
-
function
|
|
1343
|
-
|
|
1344
|
-
if (
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
for (const drain of loose.pendingDrains) {
|
|
1348
|
-
if (drain?.method === LEGACY_TEMPO_RAIL_KEY) drain.method = "tempo";
|
|
1349
|
-
}
|
|
1383
|
+
function deepEqual(a, b) {
|
|
1384
|
+
if (a === b) return true;
|
|
1385
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
1386
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
1387
|
+
return a.every((item, index) => deepEqual(item, b[index]));
|
|
1350
1388
|
}
|
|
1351
|
-
return
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
function normalizeSiblingCredits(record) {
|
|
1355
|
-
if (!Array.isArray(record.siblingCredits)) {
|
|
1356
|
-
delete record.siblingCredits;
|
|
1357
|
-
return record;
|
|
1389
|
+
if (!isPlainObject(a) || !isPlainObject(b)) return false;
|
|
1390
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
1391
|
+
if (!deepEqual(a[key], b[key])) return false;
|
|
1358
1392
|
}
|
|
1359
|
-
|
|
1360
|
-
(sibling) => isSiblingCredit(sibling) && sibling.creditId !== record.creditId
|
|
1361
|
-
);
|
|
1362
|
-
return record;
|
|
1393
|
+
return true;
|
|
1363
1394
|
}
|
|
1364
|
-
function
|
|
1365
|
-
if (
|
|
1366
|
-
|
|
1367
|
-
const status = value.status;
|
|
1368
|
-
return typeof sibling.creditId === "string" && typeof sibling.currency === "string" && Number.isSafeInteger(sibling.balanceMicro) && Number.isSafeInteger(sibling.remainingMicro) && Number.isSafeInteger(sibling.ledgerSeq) && Number.isFinite(sibling.expiryMs) && Math.abs(sibling.expiryMs) <= MAX_DATE_MS && typeof sibling.expired === "boolean" && typeof sibling.drainable === "boolean" && (status === void 0 || status === "active" || status === "unbacked") && (sibling.reconciliationPending === void 0 || typeof sibling.reconciliationPending === "boolean") && Number.isFinite(sibling.observedAt) && Math.abs(sibling.observedAt) <= MAX_DATE_MS;
|
|
1395
|
+
function isPresentValue(value) {
|
|
1396
|
+
if (value === void 0 || value === null) return false;
|
|
1397
|
+
return typeof value !== "string" || value.trim().length > 0;
|
|
1369
1398
|
}
|
|
1370
|
-
function
|
|
1371
|
-
|
|
1372
|
-
const
|
|
1373
|
-
|
|
1399
|
+
function comparableProtectedValue(target, value) {
|
|
1400
|
+
if (typeof value !== "string") return void 0;
|
|
1401
|
+
const trimmed = value.trim();
|
|
1402
|
+
if (trimmed.length === 0) return void 0;
|
|
1403
|
+
const field = target.split(".")[0];
|
|
1404
|
+
return PROTECTED_FIELDS[field].hexCredential ? trimmed.toLowerCase() : trimmed;
|
|
1405
|
+
}
|
|
1406
|
+
function patchShapeError(message) {
|
|
1407
|
+
return new DvmError(
|
|
1408
|
+
"config_patch_invalid",
|
|
1409
|
+
message,
|
|
1410
|
+
"A change declares `set` (fields to write) and `unset` (field names to remove). Anything else cannot be applied, and nothing was written."
|
|
1374
1411
|
);
|
|
1375
|
-
if (isDrainish(legacy.pendingDrain)) drains.unshift(legacy.pendingDrain);
|
|
1376
|
-
delete legacy.pendingDrain;
|
|
1377
|
-
if (drains.length === 0) delete record.pendingDrains;
|
|
1378
|
-
else record.pendingDrains = drains;
|
|
1379
|
-
return record;
|
|
1380
1412
|
}
|
|
1381
|
-
function
|
|
1382
|
-
if (
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
}
|
|
1386
|
-
const evidence = record.drainEvidence.filter(isEvidenceish).map((e) => {
|
|
1387
|
-
const bundle = {
|
|
1388
|
-
...e,
|
|
1389
|
-
creditId: typeof e.creditId === "string" ? e.creditId : "",
|
|
1390
|
-
collectedAt: Number.isFinite(e.collectedAt) ? e.collectedAt : 0
|
|
1391
|
-
};
|
|
1392
|
-
if (!Number.isFinite(bundle.settledAt)) delete bundle.settledAt;
|
|
1393
|
-
return bundle;
|
|
1394
|
-
});
|
|
1395
|
-
if (evidence.length === 0) delete record.drainEvidence;
|
|
1396
|
-
else record.drainEvidence = evidence;
|
|
1397
|
-
return record;
|
|
1413
|
+
function freezeDeep(value) {
|
|
1414
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value)) return;
|
|
1415
|
+
Object.freeze(value);
|
|
1416
|
+
for (const nested of Object.values(value)) freezeDeep(nested);
|
|
1398
1417
|
}
|
|
1399
|
-
function
|
|
1400
|
-
return typeof value === "object" && value !== null &&
|
|
1418
|
+
function isPlainObject(value) {
|
|
1419
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1420
|
+
}
|
|
1421
|
+
function toOctal(mode) {
|
|
1422
|
+
return "0" + mode.toString(8).padStart(3, "0");
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// src/lib/identity.ts
|
|
1426
|
+
import lockfile2 from "proper-lockfile";
|
|
1427
|
+
var CREDIT_POSTURES = ["suggest", "auto", "off"];
|
|
1428
|
+
function isCreditPosture(value) {
|
|
1429
|
+
return typeof value === "string" && CREDIT_POSTURES.includes(value);
|
|
1430
|
+
}
|
|
1431
|
+
function migrateCreditConfig(config) {
|
|
1432
|
+
const credit = config.credit;
|
|
1433
|
+
if (!credit) return config;
|
|
1434
|
+
const migrateLayer = (layer) => {
|
|
1435
|
+
const railed = migrateFundingRail(layer);
|
|
1436
|
+
if (!("disabled" in railed)) return railed;
|
|
1437
|
+
const { disabled, ...rest } = railed;
|
|
1438
|
+
const migrated = { ...rest };
|
|
1439
|
+
if (migrated.posture === void 0 && disabled === true) migrated.posture = "off";
|
|
1440
|
+
return migrated;
|
|
1441
|
+
};
|
|
1442
|
+
const perDvm = credit.perDvm;
|
|
1443
|
+
return {
|
|
1444
|
+
...config,
|
|
1445
|
+
credit: {
|
|
1446
|
+
...migrateLayer(credit),
|
|
1447
|
+
...perDvm ? {
|
|
1448
|
+
perDvm: Object.fromEntries(
|
|
1449
|
+
Object.entries(perDvm).map(([endpoint, per]) => [endpoint, migrateLayer(per)])
|
|
1450
|
+
)
|
|
1451
|
+
} : {}
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1401
1454
|
}
|
|
1402
|
-
function
|
|
1403
|
-
|
|
1455
|
+
function migrateFundingRail(layer) {
|
|
1456
|
+
const loose = layer;
|
|
1457
|
+
if (loose.fundingRail !== "mpp") return layer;
|
|
1458
|
+
return { ...layer, fundingRail: "tempo" };
|
|
1404
1459
|
}
|
|
1405
|
-
function
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
renameSync2(tmp, CREDITS_FILE);
|
|
1411
|
-
} catch (err) {
|
|
1412
|
-
try {
|
|
1413
|
-
unlinkSync2(tmp);
|
|
1414
|
-
} catch {
|
|
1415
|
-
}
|
|
1416
|
-
throw err;
|
|
1417
|
-
}
|
|
1460
|
+
function migrateTempoWalletKey(config) {
|
|
1461
|
+
const loose = config;
|
|
1462
|
+
if (!loose.mpp || loose.tempo) return config;
|
|
1463
|
+
const { mpp, ...rest } = loose;
|
|
1464
|
+
return { ...rest, tempo: mpp };
|
|
1418
1465
|
}
|
|
1419
|
-
function
|
|
1420
|
-
|
|
1421
|
-
return new DvmError(
|
|
1422
|
-
"credit_store_unreadable",
|
|
1423
|
-
`The prepaid-credit record at ${CREDITS_FILE} exists but could not be opened: ${detail}`,
|
|
1424
|
-
`The file was left untouched. Check that you own it and can read it ('ls -l ${CREDITS_FILE}'), then retry \u2014 'chmod 600 ${CREDITS_FILE}' fixes the common case. Do not delete it: it may hold DVM-signed drain receipts for a refund that is still owed.`,
|
|
1425
|
-
{ credits_file: CREDITS_FILE, cause: detail },
|
|
1426
|
-
`Your local record of prepaid credits could not be opened \u2014 this looks like a file-permissions problem on this machine, not damage to the record. Nothing was changed or deleted, so any signed proof of a refund you are owed is still there.`
|
|
1427
|
-
);
|
|
1466
|
+
function migrateConfig(config) {
|
|
1467
|
+
return migrateCreditConfig(migrateTempoWalletKey(config));
|
|
1428
1468
|
}
|
|
1429
|
-
function
|
|
1430
|
-
const
|
|
1431
|
-
|
|
1432
|
-
let moved = false;
|
|
1433
|
-
try {
|
|
1434
|
-
renameSync2(CREDITS_FILE, quarantined);
|
|
1435
|
-
moved = true;
|
|
1436
|
-
} catch {
|
|
1437
|
-
}
|
|
1438
|
-
const kept = moved ? quarantined : CREDITS_FILE;
|
|
1439
|
-
return new DvmError(
|
|
1440
|
-
"credit_store_corrupt",
|
|
1441
|
-
moved ? `The prepaid-credit record at ${CREDITS_FILE} is damaged and could not be parsed; it was moved aside to ${quarantined}.` : `The prepaid-credit record at ${CREDITS_FILE} is damaged and could not be parsed, and could not be moved aside.`,
|
|
1442
|
-
`Do not delete ${kept} \u2014 it may hold DVM-signed drain receipts for a refund that is still owed. Balances themselves live on the DVM, so 'dvm credit balance <dvm>' re-reads them.`,
|
|
1443
|
-
{
|
|
1444
|
-
credits_file: CREDITS_FILE,
|
|
1445
|
-
...moved ? { quarantined_path: quarantined } : {},
|
|
1446
|
-
cause: cause instanceof Error ? cause.message : String(cause)
|
|
1447
|
-
},
|
|
1448
|
-
`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
|
-
);
|
|
1469
|
+
function loadConfig() {
|
|
1470
|
+
const raw = readRawConfig();
|
|
1471
|
+
return raw === null ? null : migrateConfig(raw);
|
|
1450
1472
|
}
|
|
1451
|
-
function
|
|
1452
|
-
|
|
1453
|
-
const
|
|
1454
|
-
const
|
|
1455
|
-
const
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1473
|
+
async function updateConfig(update) {
|
|
1474
|
+
return withConfigLock(async () => {
|
|
1475
|
+
const raw = readRawConfig();
|
|
1476
|
+
const snapshot = raw === null ? {} : structuredClone(raw);
|
|
1477
|
+
const base = raw === null ? null : migrateConfig(structuredClone(raw));
|
|
1478
|
+
const handed = raw === null ? null : deepFreezeConfig(migrateConfig(structuredClone(raw)));
|
|
1479
|
+
const next = applyConfigPatch(base ?? {}, await runPatch(update, handed));
|
|
1480
|
+
const changed = changedConfigPaths(snapshot, next);
|
|
1481
|
+
if (changed.length === 0) {
|
|
1482
|
+
return {
|
|
1483
|
+
config: base ?? {},
|
|
1484
|
+
previous: base,
|
|
1485
|
+
changed: [],
|
|
1486
|
+
written: false,
|
|
1487
|
+
backup: describeConfigBackup()
|
|
1488
|
+
};
|
|
1465
1489
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1490
|
+
const removals = protectedRemovals(base ?? {}, next);
|
|
1491
|
+
const unauthorized = removals.filter((target) => update.authorize?.covers(target) !== true);
|
|
1492
|
+
if (unauthorized.length > 0) throw protectedRemovalError(unauthorized, update.reason);
|
|
1493
|
+
assertValidConfig(
|
|
1494
|
+
next,
|
|
1495
|
+
changed.map((path) => path.split(".")[0])
|
|
1496
|
+
);
|
|
1497
|
+
const superseded = supersededProtectedSecrets(base ?? {}, next);
|
|
1498
|
+
const stripped = [
|
|
1499
|
+
.../* @__PURE__ */ new Set([...removals, ...superseded, ...update.sanitizeBackup ?? []])
|
|
1500
|
+
].filter(isSecretProtectedTarget);
|
|
1501
|
+
const sweeping = update.sweepSlots ?? CONFIG_SNAPSHOT_SLOTS;
|
|
1502
|
+
const swept = [];
|
|
1503
|
+
if (raw !== null) {
|
|
1504
|
+
if (stripped.length > 0) {
|
|
1505
|
+
writeConfigBackup(withoutProtectedTargets(raw, stripped));
|
|
1506
|
+
swept.push("backup");
|
|
1507
|
+
} else {
|
|
1508
|
+
retainConfigBackup();
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
if (stripped.length > 0 && sweeping.includes("pinned")) {
|
|
1512
|
+
if (sweepPinnedSnapshot(stripped, base ?? {}, update.reason)) swept.push("pinned");
|
|
1513
|
+
}
|
|
1514
|
+
const backup = describeConfigBackup();
|
|
1515
|
+
writeConfigFile(next);
|
|
1516
|
+
appendConfigAudit({
|
|
1517
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1518
|
+
event: "update",
|
|
1519
|
+
reason: update.reason,
|
|
1520
|
+
fields: changed,
|
|
1521
|
+
...removals.length > 0 ? { removed: removals } : {},
|
|
1522
|
+
...superseded.length > 0 ? { superseded } : {},
|
|
1523
|
+
...swept.length > 0 ? { sweptSlots: swept } : {},
|
|
1524
|
+
backup: backup !== null,
|
|
1525
|
+
pid: process.pid
|
|
1526
|
+
});
|
|
1527
|
+
return { config: next, previous: base, changed, written: true, backup };
|
|
1468
1528
|
});
|
|
1469
1529
|
}
|
|
1470
|
-
|
|
1471
|
-
var DEFAULT_LOCK_RETRIES = 600;
|
|
1472
|
-
var DEFAULT_LOCK_RETRY_INTERVAL_MS = 25;
|
|
1473
|
-
var lockDepth = 0;
|
|
1474
|
-
function acquireCreditLock(opts) {
|
|
1530
|
+
async function withConfigLock(fn) {
|
|
1475
1531
|
ensureConfigDir();
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
if (err.code !== "ELOCKED") {
|
|
1491
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1492
|
-
const configDir = dirname(lockPath);
|
|
1493
|
-
throw new DvmError(
|
|
1494
|
-
"credit_store_lock_failed",
|
|
1495
|
-
`Could not create the prepaid-credit lock at ${lockPath}: ${detail}`,
|
|
1496
|
-
`This is not contention \u2014 nothing is holding the lock. Check that ${configDir} exists and is writable by you ('ls -ld ${configDir}'), then retry.`,
|
|
1497
|
-
{
|
|
1498
|
-
lock_file: lockPath,
|
|
1499
|
-
config_dir: configDir,
|
|
1500
|
-
underlying: detail,
|
|
1501
|
-
errno_code: err.code
|
|
1502
|
-
},
|
|
1503
|
-
`The dvm CLI could not create the small lock file it uses to keep two commands from writing your credit record at once. Its folder looks unwritable \u2014 nothing about your credits is wrong, and nothing was changed.`
|
|
1504
|
-
);
|
|
1505
|
-
}
|
|
1506
|
-
if (attempt === retries) break;
|
|
1507
|
-
sleepSync(interval);
|
|
1508
|
-
}
|
|
1532
|
+
let release;
|
|
1533
|
+
try {
|
|
1534
|
+
release = await lockfile2.lock(CONFIG_FILE, {
|
|
1535
|
+
lockfilePath: `${CONFIG_FILE}.lock`,
|
|
1536
|
+
realpath: false,
|
|
1537
|
+
stale: 3e4,
|
|
1538
|
+
retries: { retries: 10, factor: 1, minTimeout: 100, maxTimeout: 100 }
|
|
1539
|
+
});
|
|
1540
|
+
} catch {
|
|
1541
|
+
throw new DvmError(
|
|
1542
|
+
"config_locked",
|
|
1543
|
+
"Another dvm process is updating the local configuration.",
|
|
1544
|
+
"Wait a moment and retry."
|
|
1545
|
+
);
|
|
1509
1546
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1547
|
+
try {
|
|
1548
|
+
return await fn();
|
|
1549
|
+
} finally {
|
|
1550
|
+
await release().catch(() => void 0);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
async function runPatch(update, current) {
|
|
1554
|
+
try {
|
|
1555
|
+
return await update.patch(current);
|
|
1556
|
+
} catch (err) {
|
|
1557
|
+
if (err instanceof TypeError && /read only|not extensible|frozen|cannot delete property|cannot add property/i.test(
|
|
1558
|
+
err.message
|
|
1559
|
+
)) {
|
|
1560
|
+
throw new DvmError(
|
|
1561
|
+
"config_patch_invalid",
|
|
1562
|
+
`The '${update.reason}' change tried to modify the configuration it was handed.`,
|
|
1563
|
+
"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."
|
|
1564
|
+
);
|
|
1517
1565
|
}
|
|
1518
|
-
|
|
1566
|
+
throw err;
|
|
1567
|
+
}
|
|
1519
1568
|
}
|
|
1520
|
-
function
|
|
1521
|
-
if (
|
|
1522
|
-
|
|
1569
|
+
function sweepPinnedSnapshot(targets, departing, reason) {
|
|
1570
|
+
if (describeConfigBackup("pinned") === null) return false;
|
|
1571
|
+
const pinned = readConfigBackup("pinned");
|
|
1572
|
+
if (pinned === null) {
|
|
1573
|
+
return retireSlot(`${reason}:unreadable_pin`, "pinned");
|
|
1523
1574
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1575
|
+
const migrated = migrateConfig(structuredClone(pinned));
|
|
1576
|
+
const matching = targets.filter(
|
|
1577
|
+
(target) => protectedValuesMatch(
|
|
1578
|
+
target,
|
|
1579
|
+
protectedTargetValue(departing, target),
|
|
1580
|
+
protectedTargetValue(migrated, target)
|
|
1581
|
+
)
|
|
1528
1582
|
);
|
|
1583
|
+
if (matching.length === 0) return false;
|
|
1584
|
+
const sanitized = withoutProtectedTargets(pinned, matching);
|
|
1585
|
+
const changed = changedConfigPaths(pinned, sanitized);
|
|
1586
|
+
if (changed.length === 0) return false;
|
|
1587
|
+
writeConfigBackup(sanitized, "pinned");
|
|
1588
|
+
appendConfigAudit({
|
|
1589
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1590
|
+
event: "backup_sanitized",
|
|
1591
|
+
reason,
|
|
1592
|
+
fields: changed,
|
|
1593
|
+
removed: matching,
|
|
1594
|
+
slot: "pinned",
|
|
1595
|
+
backup: describeConfigBackup() !== null,
|
|
1596
|
+
pid: process.pid
|
|
1597
|
+
});
|
|
1598
|
+
return true;
|
|
1529
1599
|
}
|
|
1530
|
-
function
|
|
1531
|
-
|
|
1600
|
+
function retireSlot(reason, slot) {
|
|
1601
|
+
if (!removeConfigBackup(slot)) return false;
|
|
1602
|
+
appendConfigAudit({
|
|
1603
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1604
|
+
event: "backup_retired",
|
|
1605
|
+
reason,
|
|
1606
|
+
fields: [],
|
|
1607
|
+
slot,
|
|
1608
|
+
backup: describeConfigBackup() !== null,
|
|
1609
|
+
pid: process.pid
|
|
1610
|
+
});
|
|
1611
|
+
return true;
|
|
1532
1612
|
}
|
|
1533
1613
|
|
|
1534
1614
|
export {
|
|
@@ -1546,12 +1626,17 @@ export {
|
|
|
1546
1626
|
parseCreditMenu,
|
|
1547
1627
|
normalizeCreditEndpoint,
|
|
1548
1628
|
fundingReachableAt,
|
|
1629
|
+
pendingDrainFor,
|
|
1630
|
+
loadCredit,
|
|
1549
1631
|
listCredits,
|
|
1550
1632
|
listPendingFundings,
|
|
1551
1633
|
loadPendingFunding,
|
|
1552
1634
|
recordPendingFunding,
|
|
1553
1635
|
clearPendingFunding,
|
|
1554
1636
|
recordFundResponse,
|
|
1637
|
+
ensureProvisionalCredit,
|
|
1555
1638
|
loadCreditX402Binding,
|
|
1556
|
-
recordCreditX402Instrument
|
|
1639
|
+
recordCreditX402Instrument,
|
|
1640
|
+
recordPendingDrain,
|
|
1641
|
+
clearPendingDrain
|
|
1557
1642
|
};
|