taximeter 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +116 -12
- package/dist/{chunk-DJPLFDD7.js → chunk-Q2M76JNT.js} +261 -68
- package/dist/cli/index.js +607 -95
- package/dist/index.d.ts +27 -12
- package/dist/index.js +1 -1
- package/dist/ui/assets/index-_UDg88nw.js +101 -0
- package/dist/ui/index.html +1 -1
- package/docs/SDK.md +20 -9
- package/package.json +1 -1
- package/dist/ui/assets/index-DSVhxCXF.js +0 -101
|
@@ -65,17 +65,23 @@ var blockedBodySchema = z.object({
|
|
|
65
65
|
reason: z.string(),
|
|
66
66
|
budget: amountSchema.nullable(),
|
|
67
67
|
spent: integerStringSchema,
|
|
68
|
-
remaining: amountSchema.nullable()
|
|
68
|
+
remaining: amountSchema.nullable(),
|
|
69
|
+
fix: z.string().optional()
|
|
69
70
|
});
|
|
70
71
|
|
|
71
72
|
// src/config.ts
|
|
72
73
|
import { z as z2 } from "zod";
|
|
73
|
-
var
|
|
74
|
-
amount: amountSchema,
|
|
74
|
+
var budgetFieldsSchema = z2.strictObject({
|
|
75
|
+
amount: amountSchema.optional(),
|
|
76
|
+
maxPayments: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional(),
|
|
75
77
|
asset: labelSchema,
|
|
76
78
|
network: labelSchema.optional(),
|
|
77
79
|
window: z2.enum(["1h", "24h", "7d", "30d"]).optional()
|
|
78
80
|
});
|
|
81
|
+
var budgetSchema = budgetFieldsSchema.refine(
|
|
82
|
+
(budget) => budget.amount !== void 0 || budget.maxPayments !== void 0,
|
|
83
|
+
{ message: "A budget must set amount or maxPayments", path: ["amount"] }
|
|
84
|
+
);
|
|
79
85
|
var policySchema = z2.strictObject({
|
|
80
86
|
allowHosts: z2.array(labelSchema).default([]),
|
|
81
87
|
denyHosts: z2.array(labelSchema).default([]),
|
|
@@ -101,9 +107,9 @@ var configSchema = z2.strictObject({
|
|
|
101
107
|
});
|
|
102
108
|
var configPatchSchema = z2.strictObject({
|
|
103
109
|
budgets: z2.strictObject({
|
|
104
|
-
perTask:
|
|
105
|
-
perAgent:
|
|
106
|
-
global:
|
|
110
|
+
perTask: budgetFieldsSchema.partial().nullable().optional(),
|
|
111
|
+
perAgent: budgetFieldsSchema.partial().nullable().optional(),
|
|
112
|
+
global: budgetFieldsSchema.partial().nullable().optional()
|
|
107
113
|
}).optional(),
|
|
108
114
|
policy: z2.strictObject({
|
|
109
115
|
allowHosts: z2.array(labelSchema).optional(),
|
|
@@ -127,8 +133,17 @@ function parseConfig(...layers) {
|
|
|
127
133
|
const budgets = { ...current.budgets };
|
|
128
134
|
for (const key of ["perTask", "perAgent", "global"]) {
|
|
129
135
|
const next = layer.budgets?.[key];
|
|
130
|
-
if (next
|
|
131
|
-
|
|
136
|
+
if (next === void 0) continue;
|
|
137
|
+
if (next === null) {
|
|
138
|
+
budgets[key] = null;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const parsed = budgetSchema.safeParse({ ...budgets[key], ...next });
|
|
142
|
+
if (!parsed.success)
|
|
143
|
+
throw new z2.ZodError(
|
|
144
|
+
parsed.error.issues.map((issue) => ({ ...issue, path: ["budgets", key, ...issue.path] }))
|
|
145
|
+
);
|
|
146
|
+
budgets[key] = parsed.data;
|
|
132
147
|
}
|
|
133
148
|
current = configSchema.parse({
|
|
134
149
|
...current,
|
|
@@ -158,8 +173,9 @@ function readConfig(path) {
|
|
|
158
173
|
if (!existsSync(path)) return {};
|
|
159
174
|
try {
|
|
160
175
|
return configPatchSchema.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
161
|
-
} catch {
|
|
162
|
-
|
|
176
|
+
} catch (error) {
|
|
177
|
+
const detail = error instanceof z3.ZodError ? error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ") : "expected valid JSON";
|
|
178
|
+
throw new Error(`Invalid Taximeter configuration: ${path}: ${detail}`);
|
|
163
179
|
}
|
|
164
180
|
}
|
|
165
181
|
var portInputSchema = z3.string().regex(/^[0-9]{1,5}$/).pipe(z3.coerce.number().int().min(0).max(65535));
|
|
@@ -168,7 +184,14 @@ var environmentSchema = z3.object({
|
|
|
168
184
|
TAXIMETER_PORT: portInputSchema.optional(),
|
|
169
185
|
TAXIMETER_DASHBOARD_PORT: portInputSchema.optional()
|
|
170
186
|
});
|
|
171
|
-
function
|
|
187
|
+
function leafKeys(value, prefix = "") {
|
|
188
|
+
if (value === void 0) return [];
|
|
189
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return [prefix];
|
|
190
|
+
return Object.entries(value).flatMap(
|
|
191
|
+
([key, child]) => leafKeys(child, prefix ? `${prefix}.${key}` : key)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
function loadConfigDetails(flags = {}, options = {}) {
|
|
172
195
|
const cwd = options.cwd ?? process.cwd();
|
|
173
196
|
const home = options.home ?? homedir();
|
|
174
197
|
const env = environmentSchema.parse(options.env ?? process.env);
|
|
@@ -179,16 +202,78 @@ function loadConfig(flags = {}, options = {}) {
|
|
|
179
202
|
...env.TAXIMETER_DASHBOARD_PORT !== void 0 ? { dashboard: env.TAXIMETER_DASHBOARD_PORT } : {}
|
|
180
203
|
}
|
|
181
204
|
};
|
|
182
|
-
|
|
205
|
+
const paths = {
|
|
206
|
+
home: expandPath(resolve(home, ".taximeter", "config.json"), home, cwd),
|
|
207
|
+
cwd: expandPath(resolve(cwd, "taximeter.config.json"), home, cwd),
|
|
208
|
+
explicit: options.configFile ? expandPath(options.configFile, home, cwd) : void 0
|
|
209
|
+
};
|
|
210
|
+
const overridden = (path) => Object.hasOwn(options.layerOverrides ?? {}, path);
|
|
211
|
+
const fileValue = (path) => !path ? {} : overridden(path) ? configPatchSchema.parse(options.layerOverrides?.[path]) : readConfig(path);
|
|
212
|
+
if (paths.explicit && !existsSync(paths.explicit) && !overridden(paths.explicit))
|
|
183
213
|
throw new Error(`Configuration file not found: ${options.configFile}`);
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
214
|
+
const values = {
|
|
215
|
+
defaults: parseConfig(),
|
|
216
|
+
home: fileValue(paths.home),
|
|
217
|
+
cwd: fileValue(paths.cwd),
|
|
218
|
+
explicit: fileValue(paths.explicit),
|
|
188
219
|
environment,
|
|
189
|
-
flags
|
|
220
|
+
flags: configPatchSchema.parse(flags)
|
|
221
|
+
};
|
|
222
|
+
const config = parseConfig(
|
|
223
|
+
values.home,
|
|
224
|
+
values.cwd,
|
|
225
|
+
values.explicit,
|
|
226
|
+
values.environment,
|
|
227
|
+
values.flags
|
|
190
228
|
);
|
|
191
|
-
|
|
229
|
+
const names = ["defaults", "home", "cwd", "explicit", "environment", "flags"];
|
|
230
|
+
const labels = {
|
|
231
|
+
flags: "flags",
|
|
232
|
+
environment: "environment",
|
|
233
|
+
explicit: "--config",
|
|
234
|
+
cwd: "cwd",
|
|
235
|
+
home: "home",
|
|
236
|
+
defaults: "defaults"
|
|
237
|
+
};
|
|
238
|
+
const owners = /* @__PURE__ */ new Map();
|
|
239
|
+
for (const name of names) {
|
|
240
|
+
for (const key of leafKeys(values[name])) {
|
|
241
|
+
for (const owned of owners.keys()) {
|
|
242
|
+
if (owned === key || owned.startsWith(`${key}.`) || key.startsWith(`${owned}.`))
|
|
243
|
+
owners.delete(owned);
|
|
244
|
+
}
|
|
245
|
+
owners.set(key, name);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const layers = names.map((name) => {
|
|
249
|
+
const path = name === "home" || name === "cwd" || name === "explicit" ? paths[name] : void 0;
|
|
250
|
+
const keys = leafKeys(values[name]);
|
|
251
|
+
const contributedKeys = keys.filter((key) => owners.get(key) === name);
|
|
252
|
+
return {
|
|
253
|
+
name,
|
|
254
|
+
label: labels[name] ?? name,
|
|
255
|
+
...path ? { path } : {},
|
|
256
|
+
exists: path ? existsSync(path) : name === "defaults" || keys.length > 0,
|
|
257
|
+
supplied: path ? existsSync(path) || overridden(path) : name === "defaults" || keys.length > 0,
|
|
258
|
+
contributed: contributedKeys.length > 0,
|
|
259
|
+
keys,
|
|
260
|
+
contributedKeys,
|
|
261
|
+
overriddenKeys: keys.filter((key) => !contributedKeys.includes(key)),
|
|
262
|
+
...name === "environment" ? {
|
|
263
|
+
environmentVariables: Object.keys(env).filter(
|
|
264
|
+
(key) => env[key] !== void 0
|
|
265
|
+
)
|
|
266
|
+
} : {}
|
|
267
|
+
};
|
|
268
|
+
}).reverse();
|
|
269
|
+
return {
|
|
270
|
+
config: { ...config, db: expandPath(config.db, home, cwd) },
|
|
271
|
+
layers,
|
|
272
|
+
defaultWritePath: existsSync(paths.cwd) ? paths.cwd : paths.home
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function loadConfig(flags = {}, options = {}) {
|
|
276
|
+
return loadConfigDetails(flags, options).config;
|
|
192
277
|
}
|
|
193
278
|
|
|
194
279
|
// src/ledger/derive.ts
|
|
@@ -289,14 +374,59 @@ var usdc = {
|
|
|
289
374
|
function assetMetadata(network, asset) {
|
|
290
375
|
return usdc[network] === asset.toLowerCase() ? { decimals: 6, decimalsKnown: true, assetSymbol: "USDC" } : { decimals: 0, decimalsKnown: false };
|
|
291
376
|
}
|
|
377
|
+
function knownAssets() {
|
|
378
|
+
return Object.entries(usdc).map(([network, asset]) => ({
|
|
379
|
+
network,
|
|
380
|
+
asset,
|
|
381
|
+
...assetMetadata(network, asset)
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
292
384
|
function matchesAsset(selector, payment) {
|
|
293
385
|
return selector.toLowerCase() === payment.asset.toLowerCase() || selector === "USDC" && assetMetadata(payment.network, payment.asset).assetSymbol === "USDC";
|
|
294
386
|
}
|
|
295
387
|
|
|
296
388
|
// src/policy/index.ts
|
|
297
389
|
var windows = { "1h": 36e5, "24h": 864e5, "7d": 6048e5, "30d": 2592e6 };
|
|
298
|
-
|
|
390
|
+
var amountReasons = {
|
|
391
|
+
perTask: "per_task_budget",
|
|
392
|
+
perAgent: "per_agent_budget",
|
|
393
|
+
global: "global_budget"
|
|
394
|
+
};
|
|
395
|
+
var countReasons = {
|
|
396
|
+
perTask: "per_task_payment_count",
|
|
397
|
+
perAgent: "per_agent_payment_count",
|
|
398
|
+
global: "global_payment_count"
|
|
399
|
+
};
|
|
400
|
+
var fixedRemedies = {
|
|
401
|
+
host_denied: 'taximeter config set policy.denyHosts "[]"',
|
|
402
|
+
host_not_allowed: 'taximeter config set policy.allowHosts "[]"',
|
|
403
|
+
recipient_not_allowed: 'taximeter config set policy.allowPayTo "[]"',
|
|
404
|
+
unknown_asset: "taximeter config set policy.unknownAsset allow"
|
|
405
|
+
};
|
|
406
|
+
var limitKeys = {
|
|
407
|
+
max_single_payment: "policy.maxSinglePayment",
|
|
408
|
+
per_task_budget: "budgets.perTask.amount",
|
|
409
|
+
per_agent_budget: "budgets.perAgent.amount",
|
|
410
|
+
global_budget: "budgets.global.amount",
|
|
411
|
+
per_task_payment_count: "budgets.perTask.maxPayments",
|
|
412
|
+
per_agent_payment_count: "budgets.perAgent.maxPayments",
|
|
413
|
+
global_payment_count: "budgets.global.maxPayments"
|
|
414
|
+
};
|
|
415
|
+
function deny(reason, budget = null, spent = "0", increment = 0n) {
|
|
299
416
|
const difference = budget === null ? null : BigInt(budget) - BigInt(spent);
|
|
417
|
+
let fix;
|
|
418
|
+
if (reason in fixedRemedies) {
|
|
419
|
+
fix = fixedRemedies[reason];
|
|
420
|
+
} else {
|
|
421
|
+
const key = limitKeys[reason];
|
|
422
|
+
const minimum = BigInt(budget) + 1n;
|
|
423
|
+
const needed = BigInt(spent) + increment;
|
|
424
|
+
const next = needed > minimum ? needed : minimum;
|
|
425
|
+
const countLimit = key.endsWith(".maxPayments");
|
|
426
|
+
const representable = countLimit ? next <= BigInt(Number.MAX_SAFE_INTEGER) : next.toString().length <= 78;
|
|
427
|
+
const target = key.startsWith("budgets.") ? key.slice(0, key.lastIndexOf(".")) : key;
|
|
428
|
+
fix = representable ? `taximeter config set ${key} ${next}` : `taximeter config set ${target} null`;
|
|
429
|
+
}
|
|
300
430
|
return {
|
|
301
431
|
allowed: false,
|
|
302
432
|
body: {
|
|
@@ -304,13 +434,15 @@ function deny(reason, budget = null, spent = "0") {
|
|
|
304
434
|
reason,
|
|
305
435
|
budget,
|
|
306
436
|
spent,
|
|
307
|
-
remaining: difference === null ? null : (difference < 0n ? 0n : difference).toString()
|
|
437
|
+
remaining: difference === null ? null : (difference < 0n ? 0n : difference).toString(),
|
|
438
|
+
fix
|
|
308
439
|
}
|
|
309
440
|
};
|
|
310
441
|
}
|
|
311
|
-
function
|
|
442
|
+
function totalsForBudget(proposed, events, budget, scope, now) {
|
|
312
443
|
const start = budget.window ? now - windows[budget.window] : -Infinity;
|
|
313
444
|
let spent = 0n;
|
|
445
|
+
let count = 0n;
|
|
314
446
|
for (const event of deriveEvents(events)) {
|
|
315
447
|
if (!countsAsSpend(event) || assetKey(event) !== assetKey(proposed)) continue;
|
|
316
448
|
const ts = Date.parse(event.attemptedAt ?? event.ts);
|
|
@@ -318,21 +450,28 @@ function spentForBudget(proposed, events, budget, scope, now) {
|
|
|
318
450
|
if (scope === "perTask" && event.taskId !== proposed.taskId) continue;
|
|
319
451
|
if (scope === "perAgent" && event.agentId !== proposed.agentId) continue;
|
|
320
452
|
spent += BigInt(event.amount);
|
|
453
|
+
count += 1n;
|
|
321
454
|
}
|
|
322
|
-
return spent.toString();
|
|
455
|
+
return { amount: spent.toString(), count: count.toString() };
|
|
323
456
|
}
|
|
324
457
|
function evaluate(proposed, events, config, now) {
|
|
458
|
+
const spent = { perTask: "0", perAgent: "0", global: "0" };
|
|
459
|
+
const counts = { perTask: "0", perAgent: "0", global: "0" };
|
|
460
|
+
for (const scope of ["perTask", "perAgent", "global"]) {
|
|
461
|
+
const budget = config.budgets[scope];
|
|
462
|
+
if (!budget) continue;
|
|
463
|
+
const total = totalsForBudget(proposed, events, budget, scope, now);
|
|
464
|
+
spent[scope] = total.amount;
|
|
465
|
+
counts[scope] = total.count;
|
|
466
|
+
}
|
|
325
467
|
return evaluateWithState(
|
|
326
468
|
proposed,
|
|
327
469
|
{
|
|
328
470
|
reserved: events.find(
|
|
329
471
|
(event) => event.paymentKey === proposed.paymentKey && countsAsSpend(event)
|
|
330
472
|
),
|
|
331
|
-
spent
|
|
332
|
-
|
|
333
|
-
perAgent: config.budgets.perAgent ? spentForBudget(proposed, events, config.budgets.perAgent, "perAgent", now) : "0",
|
|
334
|
-
global: config.budgets.global ? spentForBudget(proposed, events, config.budgets.global, "global", now) : "0"
|
|
335
|
-
}
|
|
473
|
+
spent,
|
|
474
|
+
counts
|
|
336
475
|
},
|
|
337
476
|
config,
|
|
338
477
|
now
|
|
@@ -349,7 +488,7 @@ function evaluateWithState(proposed, state, config, now) {
|
|
|
349
488
|
if (policy.unknownAsset === "deny" && !assetMetadata(proposed.network, proposed.asset).decimalsKnown)
|
|
350
489
|
return deny("unknown_asset");
|
|
351
490
|
if (policy.maxSinglePayment !== null && matchesAsset(policy.maxSingleAsset, proposed) && BigInt(proposed.amount) > BigInt(policy.maxSinglePayment))
|
|
352
|
-
return deny("max_single_payment", policy.maxSinglePayment);
|
|
491
|
+
return deny("max_single_payment", policy.maxSinglePayment, "0", BigInt(proposed.amount));
|
|
353
492
|
const reserved = state.reserved && countsAsSpend(state.reserved) ? state.reserved : void 0;
|
|
354
493
|
for (const scope of ["perTask", "perAgent", "global"]) {
|
|
355
494
|
const budget = config.budgets[scope];
|
|
@@ -358,10 +497,14 @@ function evaluateWithState(proposed, state, config, now) {
|
|
|
358
497
|
const spent = state.spent[scope];
|
|
359
498
|
const reservedTime = reserved ? Date.parse(reserved.attemptedAt ?? reserved.ts) : -Infinity;
|
|
360
499
|
const inWindow = reservedTime <= now && (!budget.window || reservedTime >= now - windows[budget.window]);
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
return deny(
|
|
500
|
+
const countIncrement = reserved && (reserved.settlementStatus === "confirmed" || inWindow) ? 0n : 1n;
|
|
501
|
+
const increment = countIncrement * BigInt(proposed.amount);
|
|
502
|
+
if (budget.amount !== void 0 && BigInt(spent) + increment > BigInt(budget.amount)) {
|
|
503
|
+
return deny(amountReasons[scope], budget.amount, spent, increment);
|
|
504
|
+
}
|
|
505
|
+
const count = state.counts[scope];
|
|
506
|
+
if (budget.maxPayments !== void 0 && BigInt(count) + countIncrement > BigInt(budget.maxPayments)) {
|
|
507
|
+
return deny(countReasons[scope], budget.maxPayments.toString(), count, countIncrement);
|
|
365
508
|
}
|
|
366
509
|
}
|
|
367
510
|
return { allowed: true };
|
|
@@ -641,10 +784,27 @@ var X402Rail = class {
|
|
|
641
784
|
|
|
642
785
|
// src/core.ts
|
|
643
786
|
import { v7 as v72 } from "uuid";
|
|
787
|
+
|
|
788
|
+
// src/notices.ts
|
|
789
|
+
import { Console } from "console";
|
|
790
|
+
var console = new Console({ stdout: process.stdout, stderr: process.stderr, ignoreErrors: true });
|
|
791
|
+
var shown = /* @__PURE__ */ new Set();
|
|
792
|
+
function blockedNotice(body) {
|
|
793
|
+
if (!body.fix || shown.has(body.reason)) return;
|
|
794
|
+
shown.add(body.reason);
|
|
795
|
+
try {
|
|
796
|
+
console.error(
|
|
797
|
+
`Taximeter blocked (${body.reason}). To change this policy: ${body.fix}. Restart taximeter after editing.`
|
|
798
|
+
);
|
|
799
|
+
} catch {
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/core.ts
|
|
644
804
|
var Meter = class {
|
|
645
805
|
constructor(ledger, config) {
|
|
646
806
|
this.ledger = ledger;
|
|
647
|
-
this.config =
|
|
807
|
+
this.config = configSchema.parse(config);
|
|
648
808
|
this.rail = new X402Rail({
|
|
649
809
|
diagnostic: (code, resource, message) => this.diagnose(code, resource, message)
|
|
650
810
|
});
|
|
@@ -665,7 +825,7 @@ var Meter = class {
|
|
|
665
825
|
const proposed = this.rail.parse(request);
|
|
666
826
|
if (!proposed) return {};
|
|
667
827
|
try {
|
|
668
|
-
|
|
828
|
+
const intake = this.ledger.transaction(() => {
|
|
669
829
|
const now = Date.now();
|
|
670
830
|
const state = this.ledger.policyState(proposed, this.config.budgets, now);
|
|
671
831
|
const decision = evaluateWithState(proposed, state, this.config, now);
|
|
@@ -695,6 +855,8 @@ var Meter = class {
|
|
|
695
855
|
this.ledger.appendOutcome(attempt);
|
|
696
856
|
return { payment, attempt };
|
|
697
857
|
});
|
|
858
|
+
if (intake.body) blockedNotice(intake.body);
|
|
859
|
+
return intake;
|
|
698
860
|
} catch {
|
|
699
861
|
this.diagnose(
|
|
700
862
|
"storage_failed",
|
|
@@ -822,12 +984,15 @@ var __default = "CREATE TABLE schema_version (version INTEGER PRIMARY KEY);\nINS
|
|
|
822
984
|
// migrations/002.sql
|
|
823
985
|
var __default2 = "CREATE TABLE cache_cursors (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n eventRowid INTEGER NOT NULL CHECK (eventRowid >= 0),\n outcomeRowid INTEGER NOT NULL CHECK (outcomeRowid >= 0)\n);\nINSERT INTO cache_cursors VALUES (1, 0, 0);\n\nCREATE TABLE cache_payments (\n paymentId TEXT PRIMARY KEY REFERENCES events(id),\n paymentKey TEXT NOT NULL UNIQUE,\n payload TEXT NOT NULL CHECK (json_valid(payload)),\n maxAttemptedAt TEXT\n);\n\nCREATE TABLE cache_attempts (\n paymentId TEXT NOT NULL REFERENCES cache_payments(paymentId),\n attemptId TEXT NOT NULL,\n firstTs TEXT NOT NULL,\n firstId TEXT NOT NULL,\n winnerTs TEXT NOT NULL,\n winnerId TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN ('unknown', 'confirmed', 'failed')),\n payload TEXT NOT NULL CHECK (json_valid(payload)),\n PRIMARY KEY (paymentId, attemptId)\n);\nCREATE INDEX cache_attempt_selection ON cache_attempts (\n paymentId, status, firstTs, firstId COLLATE NOCASE, firstId DESC\n);\n\nCREATE TABLE cache_prefix (\n partitionKey TEXT NOT NULL,\n prefix TEXT NOT NULL,\n amount TEXT NOT NULL CHECK (amount <> '' AND amount NOT GLOB '*[^0-9]*'),\n PRIMARY KEY (partitionKey, prefix)\n) WITHOUT ROWID;\n\nINSERT INTO schema_version VALUES (2);\n";
|
|
824
986
|
|
|
987
|
+
// migrations/003.sql
|
|
988
|
+
var __default3 = "ALTER TABLE cache_prefix ADD COLUMN count TEXT NOT NULL DEFAULT '0'\n CHECK (count <> '' AND count NOT GLOB '*[^0-9]*');\n\nINSERT INTO schema_version VALUES (3);\n";
|
|
989
|
+
|
|
825
990
|
// src/ledger/backup.ts
|
|
826
991
|
import { mkdtempSync, renameSync, writeFileSync } from "fs";
|
|
827
992
|
import { join as join2, resolve as resolve2 } from "path";
|
|
828
993
|
import Database from "better-sqlite3";
|
|
829
|
-
function backupLedger(path) {
|
|
830
|
-
const directory = mkdtempSync(`${resolve2(path)}.backup-
|
|
994
|
+
function backupLedger(path, version3) {
|
|
995
|
+
const directory = mkdtempSync(`${resolve2(path)}.backup-v${version3}-`);
|
|
831
996
|
const partial = join2(directory, "ledger.partial.db");
|
|
832
997
|
const destination = join2(directory, "ledger.db");
|
|
833
998
|
writeFileSync(partial, "", { flag: "wx", mode: 384 });
|
|
@@ -868,7 +1033,8 @@ var attemptRowSchema = z7.object({
|
|
|
868
1033
|
});
|
|
869
1034
|
var prefixRowSchema = z7.object({
|
|
870
1035
|
prefix: z7.string().regex(/^[0-9a-f]{0,16}$/),
|
|
871
|
-
amount: integerStringSchema
|
|
1036
|
+
amount: integerStringSchema,
|
|
1037
|
+
count: integerStringSchema
|
|
872
1038
|
});
|
|
873
1039
|
var scopeSchema = z7.enum(["perTask", "perAgent", "global"]);
|
|
874
1040
|
var windowSchema = z7.enum(["1h", "24h", "7d", "30d"]);
|
|
@@ -968,17 +1134,18 @@ var LedgerCache = class {
|
|
|
968
1134
|
const row = payloadSchema.optional().parse(this.statement("SELECT payload FROM cache_payments WHERE paymentKey = ?").get(key));
|
|
969
1135
|
return row ? paymentEventSchema.parse(JSON.parse(row.payload)) : void 0;
|
|
970
1136
|
}
|
|
971
|
-
|
|
1137
|
+
totals(proposed, inputScope, inputWindow, now) {
|
|
972
1138
|
const payment = paymentEventSchema.parse(proposed);
|
|
973
1139
|
const scope = scopeSchema.parse(inputScope);
|
|
974
1140
|
const window = windowSchema.optional().parse(inputWindow);
|
|
975
1141
|
const end = timeSchema.parse(now);
|
|
976
1142
|
const key = partition(payment, scope);
|
|
977
1143
|
const upper = this.prefixTotal(key, end);
|
|
978
|
-
const lower = window ? this.prefixTotal(key, end - windows2[window] - 1) : 0n;
|
|
979
|
-
const
|
|
980
|
-
|
|
981
|
-
|
|
1144
|
+
const lower = window ? this.prefixTotal(key, end - windows2[window] - 1) : { amount: 0n, count: 0n };
|
|
1145
|
+
const amount = upper.amount - lower.amount;
|
|
1146
|
+
const count = upper.count - lower.count;
|
|
1147
|
+
if (amount < 0n || count < 0n) throw new Error("Cached budget total is inconsistent");
|
|
1148
|
+
return { amount: amount.toString(), count: count.toString() };
|
|
982
1149
|
}
|
|
983
1150
|
applyOutcome(outcome) {
|
|
984
1151
|
const row = paymentRowSchema.optional().parse(
|
|
@@ -1065,14 +1232,16 @@ var LedgerCache = class {
|
|
|
1065
1232
|
const add = (payment, sign) => {
|
|
1066
1233
|
if (!countsAsSpend(payment)) return;
|
|
1067
1234
|
const amount = BigInt(payment.amount) * sign;
|
|
1068
|
-
if (amount === 0n) return;
|
|
1069
1235
|
const timestamp = timeKey(Date.parse(payment.attemptedAt ?? payment.ts));
|
|
1070
1236
|
for (const scope of ["perTask", "perAgent", "global"]) {
|
|
1071
1237
|
const key = partition(payment, scope);
|
|
1072
1238
|
const nodes = changes.get(key) ?? /* @__PURE__ */ new Map();
|
|
1073
1239
|
for (let length = 0; length <= timestamp.length; length += 1) {
|
|
1074
1240
|
const prefix = timestamp.slice(0, length);
|
|
1075
|
-
|
|
1241
|
+
const contribution = nodes.get(prefix) ?? { amount: 0n, count: 0n };
|
|
1242
|
+
contribution.amount += amount;
|
|
1243
|
+
contribution.count += sign;
|
|
1244
|
+
nodes.set(prefix, contribution);
|
|
1076
1245
|
}
|
|
1077
1246
|
changes.set(key, nodes);
|
|
1078
1247
|
}
|
|
@@ -1080,27 +1249,29 @@ var LedgerCache = class {
|
|
|
1080
1249
|
if (previous) add(previous, -1n);
|
|
1081
1250
|
add(current, 1n);
|
|
1082
1251
|
for (const [key, nodes] of changes) {
|
|
1083
|
-
const pending = [...nodes].filter(([,
|
|
1252
|
+
const pending = [...nodes].filter(([, value]) => value.amount !== 0n || value.count !== 0n);
|
|
1084
1253
|
if (pending.length === 0) continue;
|
|
1085
1254
|
const existing = /* @__PURE__ */ new Map();
|
|
1086
1255
|
for (const input of this.statement(
|
|
1087
|
-
"SELECT prefix, amount FROM cache_prefix WHERE partitionKey = ? AND prefix IN (SELECT value FROM json_each(?))"
|
|
1256
|
+
"SELECT prefix, amount, count FROM cache_prefix WHERE partitionKey = ? AND prefix IN (SELECT value FROM json_each(?))"
|
|
1088
1257
|
).iterate(key, JSON.stringify(pending.map(([prefix]) => prefix)))) {
|
|
1089
1258
|
const row = prefixRowSchema.parse(input);
|
|
1090
|
-
existing.set(row.prefix, BigInt(row.amount));
|
|
1259
|
+
existing.set(row.prefix, { amount: BigInt(row.amount), count: BigInt(row.count) });
|
|
1091
1260
|
}
|
|
1092
1261
|
for (const [prefix, delta] of pending) {
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
1262
|
+
const before = existing.get(prefix) ?? { amount: 0n, count: 0n };
|
|
1263
|
+
const amount = before.amount + delta.amount;
|
|
1264
|
+
const count = before.count + delta.count;
|
|
1265
|
+
if (amount < 0n || count < 0n) throw new Error("Cached contribution underflow");
|
|
1266
|
+
if (amount === 0n && count === 0n) {
|
|
1096
1267
|
this.statement("DELETE FROM cache_prefix WHERE partitionKey = ? AND prefix = ?").run(
|
|
1097
1268
|
key,
|
|
1098
1269
|
prefix
|
|
1099
1270
|
);
|
|
1100
1271
|
} else {
|
|
1101
1272
|
this.statement(
|
|
1102
|
-
"INSERT INTO cache_prefix (partitionKey, prefix, amount) VALUES (?, ?, ?) ON CONFLICT(partitionKey, prefix) DO UPDATE SET amount = excluded.amount"
|
|
1103
|
-
).run(key, prefix,
|
|
1273
|
+
"INSERT INTO cache_prefix (partitionKey, prefix, amount, count) VALUES (?, ?, ?, ?) ON CONFLICT(partitionKey, prefix) DO UPDATE SET amount = excluded.amount, count = excluded.count"
|
|
1274
|
+
).run(key, prefix, amount.toString(), count.toString());
|
|
1104
1275
|
}
|
|
1105
1276
|
}
|
|
1106
1277
|
}
|
|
@@ -1117,11 +1288,13 @@ var LedgerCache = class {
|
|
|
1117
1288
|
}
|
|
1118
1289
|
}
|
|
1119
1290
|
prefixes.push(timestamp);
|
|
1120
|
-
|
|
1291
|
+
const sum = { amount: 0n, count: 0n };
|
|
1121
1292
|
for (const input of this.statement(
|
|
1122
|
-
"SELECT prefix, amount FROM cache_prefix WHERE partitionKey = ? AND prefix IN (SELECT value FROM json_each(?))"
|
|
1293
|
+
"SELECT prefix, amount, count FROM cache_prefix WHERE partitionKey = ? AND prefix IN (SELECT value FROM json_each(?))"
|
|
1123
1294
|
).iterate(key, JSON.stringify(prefixes))) {
|
|
1124
|
-
|
|
1295
|
+
const row = prefixRowSchema.parse(input);
|
|
1296
|
+
sum.amount += BigInt(row.amount);
|
|
1297
|
+
sum.count += BigInt(row.count);
|
|
1125
1298
|
}
|
|
1126
1299
|
return sum;
|
|
1127
1300
|
}
|
|
@@ -1129,6 +1302,7 @@ var LedgerCache = class {
|
|
|
1129
1302
|
|
|
1130
1303
|
// src/ledger/store.ts
|
|
1131
1304
|
var rowSchema = z8.object({ payload: z8.string() });
|
|
1305
|
+
var countRowSchema = z8.object({ count: z8.number().int().nonnegative() });
|
|
1132
1306
|
var Ledger = class {
|
|
1133
1307
|
database;
|
|
1134
1308
|
cache;
|
|
@@ -1145,20 +1319,22 @@ var Ledger = class {
|
|
|
1145
1319
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'schema_version'"
|
|
1146
1320
|
).get();
|
|
1147
1321
|
if (!table) this.database.exec(__default);
|
|
1148
|
-
const version3 = z8.object({ version: z8.union([z8.literal(1), z8.literal(2)]) }).parse(
|
|
1322
|
+
const version3 = z8.object({ version: z8.union([z8.literal(1), z8.literal(2), z8.literal(3)]) }).parse(
|
|
1149
1323
|
this.database.prepare("SELECT MAX(version) AS version FROM schema_version").get()
|
|
1150
1324
|
);
|
|
1151
|
-
if (version3.version
|
|
1325
|
+
if (version3.version !== 3) {
|
|
1152
1326
|
if (table && path !== ":memory:") {
|
|
1153
1327
|
process.stderr.write("Migrating ledger\u2026\n");
|
|
1154
|
-
const backup = backupLedger(path);
|
|
1328
|
+
const backup = backupLedger(path, version3.version);
|
|
1155
1329
|
process.stderr.write(`Ledger backup saved: ${backup}
|
|
1156
1330
|
`);
|
|
1157
1331
|
}
|
|
1158
|
-
this.database.exec(__default2);
|
|
1332
|
+
if (version3.version === 1) this.database.exec(__default2);
|
|
1333
|
+
this.database.exec(__default3);
|
|
1159
1334
|
}
|
|
1160
1335
|
const cache = new LedgerCache(this.database);
|
|
1161
|
-
cache.
|
|
1336
|
+
if (version3.version === 2) cache.rebuild();
|
|
1337
|
+
else cache.synchronize();
|
|
1162
1338
|
return cache;
|
|
1163
1339
|
}).immediate();
|
|
1164
1340
|
} catch (error) {
|
|
@@ -1201,12 +1377,22 @@ var Ledger = class {
|
|
|
1201
1377
|
policyState(proposed, budgets, now) {
|
|
1202
1378
|
return this.transaction(() => {
|
|
1203
1379
|
this.cache.synchronize();
|
|
1380
|
+
const totals2 = {
|
|
1381
|
+
perTask: budgets.perTask ? this.cache.totals(proposed, "perTask", budgets.perTask.window, now) : { amount: "0", count: "0" },
|
|
1382
|
+
perAgent: budgets.perAgent ? this.cache.totals(proposed, "perAgent", budgets.perAgent.window, now) : { amount: "0", count: "0" },
|
|
1383
|
+
global: budgets.global ? this.cache.totals(proposed, "global", budgets.global.window, now) : { amount: "0", count: "0" }
|
|
1384
|
+
};
|
|
1204
1385
|
return {
|
|
1205
1386
|
reserved: this.cache.reservation(proposed.paymentKey),
|
|
1206
1387
|
spent: {
|
|
1207
|
-
perTask:
|
|
1208
|
-
perAgent:
|
|
1209
|
-
global:
|
|
1388
|
+
perTask: totals2.perTask.amount,
|
|
1389
|
+
perAgent: totals2.perAgent.amount,
|
|
1390
|
+
global: totals2.global.amount
|
|
1391
|
+
},
|
|
1392
|
+
counts: {
|
|
1393
|
+
perTask: totals2.perTask.count,
|
|
1394
|
+
perAgent: totals2.perAgent.count,
|
|
1395
|
+
global: totals2.global.count
|
|
1210
1396
|
}
|
|
1211
1397
|
};
|
|
1212
1398
|
});
|
|
@@ -1215,6 +1401,9 @@ var Ledger = class {
|
|
|
1215
1401
|
rebuildCache() {
|
|
1216
1402
|
this.transaction(() => this.cache.rebuild());
|
|
1217
1403
|
}
|
|
1404
|
+
eventCount() {
|
|
1405
|
+
return countRowSchema.parse(this.database.prepare("SELECT COUNT(*) AS count FROM events").get()).count;
|
|
1406
|
+
}
|
|
1218
1407
|
events() {
|
|
1219
1408
|
return this.database.prepare("SELECT payload FROM events ORDER BY ts, id").all().map((row) => paymentEventSchema.parse(JSON.parse(rowSchema.parse(row).payload)));
|
|
1220
1409
|
}
|
|
@@ -1243,7 +1432,7 @@ var Ledger = class {
|
|
|
1243
1432
|
};
|
|
1244
1433
|
|
|
1245
1434
|
// src/proxy/index.ts
|
|
1246
|
-
import { Console } from "console";
|
|
1435
|
+
import { Console as Console2 } from "console";
|
|
1247
1436
|
import {
|
|
1248
1437
|
createServer,
|
|
1249
1438
|
request as httpRequest,
|
|
@@ -1258,7 +1447,7 @@ var incomingHeadersSchema = z9.record(
|
|
|
1258
1447
|
);
|
|
1259
1448
|
var sockets = /* @__PURE__ */ new WeakMap();
|
|
1260
1449
|
var tlsUnmeteredMessage = "Encrypted CONNECT tunnel forwarded without payment visibility. Use the SDK or explicit upstream mode to meter HTTPS.";
|
|
1261
|
-
var notices = new
|
|
1450
|
+
var notices = new Console2({ stdout: process.stdout, stderr: process.stderr, ignoreErrors: true });
|
|
1262
1451
|
var tlsNoticeShown = false;
|
|
1263
1452
|
function normalizedHeaders(input) {
|
|
1264
1453
|
const result = {};
|
|
@@ -1315,7 +1504,7 @@ function json(response, status, body) {
|
|
|
1315
1504
|
response.end(JSON.stringify(body));
|
|
1316
1505
|
}
|
|
1317
1506
|
function createProxy(options) {
|
|
1318
|
-
const config =
|
|
1507
|
+
const config = configSchema.parse(options.config);
|
|
1319
1508
|
const meter = new Meter(options.ledger, config);
|
|
1320
1509
|
const server = createServer({ requestTimeout: 0 }, (request, response) => {
|
|
1321
1510
|
let target;
|
|
@@ -1512,13 +1701,14 @@ async function closeProxy(server) {
|
|
|
1512
1701
|
import { z as z10 } from "zod";
|
|
1513
1702
|
|
|
1514
1703
|
// package.json
|
|
1515
|
-
var version = "0.
|
|
1704
|
+
var version = "0.3.0";
|
|
1516
1705
|
|
|
1517
1706
|
// src/version.ts
|
|
1518
1707
|
var version2 = z10.string().regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/).parse(version);
|
|
1519
1708
|
|
|
1520
1709
|
export {
|
|
1521
1710
|
integerStringSchema,
|
|
1711
|
+
amountSchema,
|
|
1522
1712
|
httpUrlSchema,
|
|
1523
1713
|
labelSchema,
|
|
1524
1714
|
paymentEventSchema,
|
|
@@ -1528,14 +1718,17 @@ export {
|
|
|
1528
1718
|
parseConfig,
|
|
1529
1719
|
expandPath,
|
|
1530
1720
|
portInputSchema,
|
|
1721
|
+
loadConfigDetails,
|
|
1531
1722
|
loadConfig,
|
|
1723
|
+
assetMetadata,
|
|
1724
|
+
knownAssets,
|
|
1532
1725
|
matchesAsset,
|
|
1533
1726
|
totalSchema,
|
|
1534
1727
|
deriveEvents,
|
|
1535
1728
|
countsAsSpend,
|
|
1536
1729
|
totals,
|
|
1537
1730
|
formatAmount,
|
|
1538
|
-
|
|
1731
|
+
totalsForBudget,
|
|
1539
1732
|
evaluate,
|
|
1540
1733
|
X402Rail,
|
|
1541
1734
|
Meter,
|