@hasna/economy 0.5.11 → 0.5.12

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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.12
4
+
5
+ ### Patch Changes
6
+
7
+ - Avoid recreating local recovery records when a restarted hosted sync encounters
8
+ an already retired batch with a verified authority anchor and exact server
9
+ receipt. Preserve pending-command reconciliation and fail closed when receipt
10
+ or principal validation fails.
11
+
3
12
  ## 0.5.11
4
13
 
5
14
  ### Patch Changes
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  LOCAL_STORAGE_OPT_IN_KEYS
4
- } from "./index-b36bq51x.js";
4
+ } from "./index-vavbzb1s.js";
5
5
  import {
6
6
  assessCostFromDb,
7
7
  getIngestState,
@@ -4,7 +4,7 @@ import {
4
4
  } from "./index-w3bvv0ns.js";
5
5
  import {
6
6
  getStore
7
- } from "./index-v35tm3xg.js";
7
+ } from "./index-mn8ybzv7.js";
8
8
  import {
9
9
  formatReportedUsd,
10
10
  formatSummaryCoverage,
@@ -15,7 +15,7 @@ import {
15
15
  requireCurrentBillingDiffSummary,
16
16
  requireCurrentRangeStats,
17
17
  requireCurrentSavingsReport
18
- } from "./index-b36bq51x.js";
18
+ } from "./index-vavbzb1s.js";
19
19
  import {
20
20
  deleteBudget,
21
21
  deleteGoal,
@@ -6,7 +6,7 @@ import {
6
6
  } from "./index-gtjqy0kd.js";
7
7
  import {
8
8
  syncAll
9
- } from "./index-mgnzgf1m.js";
9
+ } from "./index-a5gdrfby.js";
10
10
  import {
11
11
  MAX_INGEST_BYTES,
12
12
  MAX_INGEST_ROWS,
@@ -60,7 +60,7 @@ async function recalculateZeroCostRequests(db) {
60
60
  }
61
61
 
62
62
  // src/lib/cloud-ingest.ts
63
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
63
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
64
64
  import { dirname, join as join4 } from "path";
65
65
 
66
66
  // src/lib/ingest-batches.ts
@@ -931,30 +931,35 @@ async function postIngest(cloud, body) {
931
931
  const refused = journal.refusal(batch);
932
932
  if (refused)
933
933
  throw new IngestRefused(refused.status, refused.code, refused.reason);
934
- await maintenance.admit(batch);
935
934
  const known = await journal.lookupReceipt(cloud, batch);
936
- const fresh = journal.stage(batch);
937
- const sendFresh = async () => {
938
- let response;
939
- try {
940
- response = await cloud.client.transport.post("/ingest", batch, { retry: false });
941
- } catch (error) {
942
- const refusal = error instanceof HasnaHttpError2 ? definitiveIngestRefusal(error.status, error.body) : null;
943
- if (refusal) {
944
- journal.recordRefusal(batch, refusal);
945
- throw new IngestRefused(refusal.status, refusal.code, refusal.reason);
935
+ let payload;
936
+ if (known && existsSync2(join4(journal.directory, ".authority.json"))) {
937
+ payload = existsSync2(journal.file(batch.idempotency_key, "intent")) ? journal.accept(batch, known) : known;
938
+ } else {
939
+ await maintenance.admit(batch);
940
+ const fresh = journal.stage(batch);
941
+ const sendFresh = async () => {
942
+ let response;
943
+ try {
944
+ response = await cloud.client.transport.post("/ingest", batch, { retry: false });
945
+ } catch (error) {
946
+ const refusal = error instanceof HasnaHttpError2 ? definitiveIngestRefusal(error.status, error.body) : null;
947
+ if (refusal) {
948
+ journal.recordRefusal(batch, refusal);
949
+ throw new IngestRefused(refusal.status, refusal.code, refusal.reason);
950
+ }
951
+ throw error;
946
952
  }
947
- throw error;
948
- }
949
- return journal.accept(batch, response);
950
- };
951
- const payload = known ? journal.accept(batch, known) : fresh ? await sendFresh() : await journal.reconcileCommand(cloud, batch);
953
+ return journal.accept(batch, response);
954
+ };
955
+ payload = known ? journal.accept(batch, known) : fresh ? await sendFresh() : await journal.reconcileCommand(cloud, batch);
956
+ }
952
957
  await verifyJournalAuthority(journal, cloud);
953
958
  if (!payload)
954
959
  throw new PendingIngestError([batch.idempotency_key]);
955
- verifyIngestReceipt(payload, batch);
956
- const counts = payload["ingested"];
957
- if (!counts || typeof counts !== "object" || Array.isArray(counts) || typeof payload["total"] !== "number" || !Number.isSafeInteger(payload["total"]) || payload["total"] < 0)
960
+ const verified = verifyIngestReceipt(payload, batch);
961
+ const counts = verified["ingested"];
962
+ if (!counts || typeof counts !== "object" || Array.isArray(counts) || typeof verified["total"] !== "number" || !Number.isSafeInteger(verified["total"]) || verified["total"] < 0)
958
963
  throw new Error("Invalid ingest receipt");
959
964
  let sum = 0;
960
965
  for (const [table, count] of Object.entries(counts)) {
@@ -963,16 +968,16 @@ async function postIngest(cloud, body) {
963
968
  sum += count;
964
969
  ingested[table] = (ingested[table] ?? 0) + count;
965
970
  }
966
- if (sum !== payload["total"])
971
+ if (sum !== verified["total"])
967
972
  throw new Error("Invalid ingest receipt");
968
973
  total += sum;
969
- const operation = payload["operation_key"];
974
+ const operation = verified["operation_key"];
970
975
  if (operation !== undefined && (typeof operation !== "string" || !/^[a-f0-9]{64}$/.test(operation)))
971
976
  throw new Error("Invalid ingest receipt");
972
977
  completed.push({
973
978
  idempotency_key: batch.idempotency_key,
974
979
  ...typeof operation === "string" ? { operation_key: operation } : {},
975
- replayed: payload["replayed"] === true
980
+ replayed: verified["replayed"] === true
976
981
  });
977
982
  } catch (error) {
978
983
  if (error instanceof PendingIngestError || error instanceof JournalMaintenanceError || error instanceof IngestRefused)
@@ -86,7 +86,7 @@ function comparisonValueChange(left, right) {
86
86
  // package.json
87
87
  var package_default = {
88
88
  name: "@hasna/economy",
89
- version: "0.5.11",
89
+ version: "0.5.12",
90
90
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
91
91
  repository: {
92
92
  type: "git",
@@ -5,7 +5,7 @@ import {
5
5
  } from "./index-w3bvv0ns.js";
6
6
  import {
7
7
  syncAll
8
- } from "./index-mgnzgf1m.js";
8
+ } from "./index-a5gdrfby.js";
9
9
  import {
10
10
  buildBrief,
11
11
  packageMetadata,
@@ -16,7 +16,7 @@ import {
16
16
  queryProjectDetail,
17
17
  queryRangeStats,
18
18
  querySavingsSummary
19
- } from "./index-b36bq51x.js";
19
+ } from "./index-vavbzb1s.js";
20
20
  import {
21
21
  AGENTS,
22
22
  INGEST_SCHEMA_SQL,
@@ -6,10 +6,10 @@ import {
6
6
  printStatusLine,
7
7
  printWaybarJson,
8
8
  runTui
9
- } from "./index-akxt4n8x.js";
9
+ } from "./index-bx0wbqyr.js";
10
10
  import"./index-w3bvv0ns.js";
11
- import"./index-v35tm3xg.js";
12
- import"./index-b36bq51x.js";
11
+ import"./index-mn8ybzv7.js";
12
+ import"./index-vavbzb1s.js";
13
13
  import"./index-zgvmrqyc.js";
14
14
  import"./index-p35cyhc4.js";
15
15
  export {
@@ -2,19 +2,19 @@
2
2
  import {
3
3
  syncAllToCloud,
4
4
  syncHasFailures
5
- } from "./index-yv28hyk7.js";
5
+ } from "./index-pgt6fhkb.js";
6
6
  import"./index-gtjqy0kd.js";
7
7
  import {
8
8
  agentPaths,
9
9
  syncAll
10
- } from "./index-mgnzgf1m.js";
10
+ } from "./index-a5gdrfby.js";
11
11
  import {
12
12
  getStore,
13
13
  isCloudStore
14
- } from "./index-v35tm3xg.js";
14
+ } from "./index-mn8ybzv7.js";
15
15
  import {
16
16
  economyCloudStorage
17
- } from "./index-b36bq51x.js";
17
+ } from "./index-vavbzb1s.js";
18
18
  import {
19
19
  formatSummaryValue
20
20
  } from "./index-zgvmrqyc.js";
package/dist/cli/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import {
4
4
  buildStatusLine,
5
5
  gatherStatusData
6
- } from "../chunks/index-akxt4n8x.js";
6
+ } from "../chunks/index-bx0wbqyr.js";
7
7
  import"../chunks/index-w3bvv0ns.js";
8
8
  import {
9
9
  JournalMaintenanceError,
@@ -17,7 +17,7 @@ import {
17
17
  syncAllToCloud,
18
18
  syncHasFailures,
19
19
  syncOutcomeLines
20
- } from "../chunks/index-yv28hyk7.js";
20
+ } from "../chunks/index-pgt6fhkb.js";
21
21
  import {
22
22
  syncAnthropicBilling,
23
23
  syncGeminiBilling,
@@ -26,11 +26,11 @@ import {
26
26
  import {
27
27
  agentPaths,
28
28
  syncAll
29
- } from "../chunks/index-mgnzgf1m.js";
29
+ } from "../chunks/index-a5gdrfby.js";
30
30
  import {
31
31
  getStore,
32
32
  isCloudStore
33
- } from "../chunks/index-v35tm3xg.js";
33
+ } from "../chunks/index-mn8ybzv7.js";
34
34
  import {
35
35
  billingDriftCheck,
36
36
  comparisonValueChange,
@@ -38,7 +38,7 @@ import {
38
38
  economyTransportReport,
39
39
  formatComparisonValue,
40
40
  packageMetadata
41
- } from "../chunks/index-b36bq51x.js";
41
+ } from "../chunks/index-vavbzb1s.js";
42
42
  import {
43
43
  AGENTS,
44
44
  dedupeRequests,
@@ -1365,23 +1365,23 @@ function registerExtendedCommands(program) {
1365
1365
  console.log();
1366
1366
  });
1367
1367
  program.command("tui").description("Terminal status dashboard (Linux/SSH friendly)").option("--watch", "Live refresh").option("--interval <seconds>", "Refresh interval", "30").action(async (opts) => {
1368
- const { runTui } = await import("../chunks/tui-hxjczmv5.js");
1368
+ const { runTui } = await import("../chunks/tui-bmf8rp0p.js");
1369
1369
  await runTui({
1370
1370
  watch: Boolean(opts.watch),
1371
1371
  interval: Number(opts.interval ?? 30)
1372
1372
  });
1373
1373
  });
1374
1374
  program.command("waybar").description("Print waybar-compatible JSON status line").action(async () => {
1375
- const { printWaybarJson } = await import("../chunks/tui-hxjczmv5.js");
1375
+ const { printWaybarJson } = await import("../chunks/tui-bmf8rp0p.js");
1376
1376
  await printWaybarJson();
1377
1377
  });
1378
1378
  const barCmd = program.command("bar").description("Status bar helpers (Linux)");
1379
1379
  barCmd.command("tui").description("Alias for economy tui").option("--watch", "Live refresh").action(async (opts) => {
1380
- const { runTui } = await import("../chunks/tui-hxjczmv5.js");
1380
+ const { runTui } = await import("../chunks/tui-bmf8rp0p.js");
1381
1381
  await runTui({ watch: Boolean(opts.watch) });
1382
1382
  });
1383
1383
  barCmd.command("waybar").description("Alias for economy waybar").action(async () => {
1384
- const { printWaybarJson } = await import("../chunks/tui-hxjczmv5.js");
1384
+ const { printWaybarJson } = await import("../chunks/tui-bmf8rp0p.js");
1385
1385
  await printWaybarJson();
1386
1386
  });
1387
1387
  program.command("estimate").description("Estimate cost for token counts").requiredOption("--model <model>", "Model name").option("--input <n>", "Input tokens", "0").option("--output <n>", "Output tokens", "0").action(async (opts) => {
@@ -2207,7 +2207,7 @@ program.command("accounts [period]").description("List account usage by email ad
2207
2207
  console.log();
2208
2208
  });
2209
2209
  program.command("watch").description("Live stream of incoming costs").option("--interval <seconds>", "Poll interval in seconds", "10").option("--daemon", "Watch agent data directories and sync on change").option("--sync-interval <seconds>", "Minimum collection interval and fallback scan interval for --daemon", "60").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--notify <amount>", "Fire macOS notification when cumulative cost crosses this USD threshold").option("--no-notifications", "Disable desktop notifications, including high-cost notices").action(async (opts) => {
2210
- const { watchCosts } = await import("../chunks/watch-ewed2srf.js");
2210
+ const { watchCosts } = await import("../chunks/watch-msg91cnj.js");
2211
2211
  await watchCosts({
2212
2212
  interval: parsePositiveCliInteger(opts.interval ?? "10", "--interval"),
2213
2213
  daemon: Boolean(opts.daemon),
@@ -2445,7 +2445,7 @@ pricingCmd.command("remove <model>").description("Remove pricing for a model").a
2445
2445
  });
2446
2446
  program.command("serve").description("Start the REST API server").option("-p, --port <port>", "Port", "3456").action(async (opts) => {
2447
2447
  const port = parseCliPort(opts.port ?? "3456", "--port");
2448
- const { startServer } = await import("../chunks/serve-kk3n0p7j.js");
2448
+ const { startServer } = await import("../chunks/serve-462ch8yc.js");
2449
2449
  startServer(port);
2450
2450
  });
2451
2451
  program.command("mcp").description("Show MCP server install commands").option("--claude", "Install into Claude Code").option("--codex", "Install into Codex").option("--gemini", "Install into Gemini CLI").option("--all", "Install into all agents").action(async (opts) => {
package/dist/index.js CHANGED
@@ -111,7 +111,7 @@ import { randomUUID } from "crypto";
111
111
  // package.json
112
112
  var package_default = {
113
113
  name: "@hasna/economy",
114
- version: "0.5.11",
114
+ version: "0.5.12",
115
115
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
116
116
  repository: {
117
117
  type: "git",
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-ingest.d.ts","sourceRoot":"","sources":["../../src/lib/cloud-ingest.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cloud-ingest.d.ts","sourceRoot":"","sources":["../../src/lib/cloud-ingest.ts"],"names":[],"mappings":"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"}
package/dist/mcp/index.js CHANGED
@@ -86,7 +86,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
86
86
  // package.json
87
87
  var package_default = {
88
88
  name: "@hasna/economy",
89
- version: "0.5.11",
89
+ version: "0.5.12",
90
90
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
91
91
  repository: {
92
92
  type: "git",
@@ -3737,7 +3737,7 @@ async function syncAll(db, opts = {}) {
3737
3737
  }
3738
3738
 
3739
3739
  // src/lib/cloud-ingest.ts
3740
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync11, renameSync, writeFileSync as writeFileSync2 } from "fs";
3740
+ import { existsSync as existsSync12, mkdirSync as mkdirSync2, readFileSync as readFileSync11, renameSync, writeFileSync as writeFileSync2 } from "fs";
3741
3741
  import { dirname, join as join14 } from "path";
3742
3742
 
3743
3743
  // src/lib/sync-maintenance.ts
@@ -3900,30 +3900,35 @@ async function postIngest(cloud, body) {
3900
3900
  const refused = journal.refusal(batch);
3901
3901
  if (refused)
3902
3902
  throw new IngestRefused(refused.status, refused.code, refused.reason);
3903
- await maintenance.admit(batch);
3904
3903
  const known = await journal.lookupReceipt(cloud, batch);
3905
- const fresh = journal.stage(batch);
3906
- const sendFresh = async () => {
3907
- let response;
3908
- try {
3909
- response = await cloud.client.transport.post("/ingest", batch, { retry: false });
3910
- } catch (error) {
3911
- const refusal = error instanceof HasnaHttpError2 ? definitiveIngestRefusal(error.status, error.body) : null;
3912
- if (refusal) {
3913
- journal.recordRefusal(batch, refusal);
3914
- throw new IngestRefused(refusal.status, refusal.code, refusal.reason);
3904
+ let payload;
3905
+ if (known && existsSync12(join14(journal.directory, ".authority.json"))) {
3906
+ payload = existsSync12(journal.file(batch.idempotency_key, "intent")) ? journal.accept(batch, known) : known;
3907
+ } else {
3908
+ await maintenance.admit(batch);
3909
+ const fresh = journal.stage(batch);
3910
+ const sendFresh = async () => {
3911
+ let response;
3912
+ try {
3913
+ response = await cloud.client.transport.post("/ingest", batch, { retry: false });
3914
+ } catch (error) {
3915
+ const refusal = error instanceof HasnaHttpError2 ? definitiveIngestRefusal(error.status, error.body) : null;
3916
+ if (refusal) {
3917
+ journal.recordRefusal(batch, refusal);
3918
+ throw new IngestRefused(refusal.status, refusal.code, refusal.reason);
3919
+ }
3920
+ throw error;
3915
3921
  }
3916
- throw error;
3917
- }
3918
- return journal.accept(batch, response);
3919
- };
3920
- const payload = known ? journal.accept(batch, known) : fresh ? await sendFresh() : await journal.reconcileCommand(cloud, batch);
3922
+ return journal.accept(batch, response);
3923
+ };
3924
+ payload = known ? journal.accept(batch, known) : fresh ? await sendFresh() : await journal.reconcileCommand(cloud, batch);
3925
+ }
3921
3926
  await verifyJournalAuthority(journal, cloud);
3922
3927
  if (!payload)
3923
3928
  throw new PendingIngestError([batch.idempotency_key]);
3924
- verifyIngestReceipt(payload, batch);
3925
- const counts = payload["ingested"];
3926
- if (!counts || typeof counts !== "object" || Array.isArray(counts) || typeof payload["total"] !== "number" || !Number.isSafeInteger(payload["total"]) || payload["total"] < 0)
3929
+ const verified = verifyIngestReceipt(payload, batch);
3930
+ const counts = verified["ingested"];
3931
+ if (!counts || typeof counts !== "object" || Array.isArray(counts) || typeof verified["total"] !== "number" || !Number.isSafeInteger(verified["total"]) || verified["total"] < 0)
3927
3932
  throw new Error("Invalid ingest receipt");
3928
3933
  let sum = 0;
3929
3934
  for (const [table, count] of Object.entries(counts)) {
@@ -3932,16 +3937,16 @@ async function postIngest(cloud, body) {
3932
3937
  sum += count;
3933
3938
  ingested[table] = (ingested[table] ?? 0) + count;
3934
3939
  }
3935
- if (sum !== payload["total"])
3940
+ if (sum !== verified["total"])
3936
3941
  throw new Error("Invalid ingest receipt");
3937
3942
  total += sum;
3938
- const operation = payload["operation_key"];
3943
+ const operation = verified["operation_key"];
3939
3944
  if (operation !== undefined && (typeof operation !== "string" || !/^[a-f0-9]{64}$/.test(operation)))
3940
3945
  throw new Error("Invalid ingest receipt");
3941
3946
  completed.push({
3942
3947
  idempotency_key: batch.idempotency_key,
3943
3948
  ...typeof operation === "string" ? { operation_key: operation } : {},
3944
- replayed: payload["replayed"] === true
3949
+ replayed: verified["replayed"] === true
3945
3950
  });
3946
3951
  } catch (error) {
3947
3952
  if (error instanceof PendingIngestError || error instanceof JournalMaintenanceError || error instanceof IngestRefused)
@@ -2370,6 +2370,8 @@ async function ingestOtelRows(db, rows) {
2370
2370
  // src/lib/cloud-ingest.ts
2371
2371
  init_database();
2372
2372
  init_pricing();
2373
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
2374
+ import { dirname, join as join11 } from "path";
2373
2375
 
2374
2376
  // src/ingest/collection-outcome.ts
2375
2377
  import { AsyncLocalStorage } from "async_hooks";
@@ -3457,30 +3459,35 @@ async function postIngest(cloud, body) {
3457
3459
  const refused = journal.refusal(batch);
3458
3460
  if (refused)
3459
3461
  throw new IngestRefused(refused.status, refused.code, refused.reason);
3460
- await maintenance.admit(batch);
3461
3462
  const known = await journal.lookupReceipt(cloud, batch);
3462
- const fresh = journal.stage(batch);
3463
- const sendFresh = async () => {
3464
- let response;
3465
- try {
3466
- response = await cloud.client.transport.post("/ingest", batch, { retry: false });
3467
- } catch (error) {
3468
- const refusal = error instanceof HasnaHttpError2 ? definitiveIngestRefusal(error.status, error.body) : null;
3469
- if (refusal) {
3470
- journal.recordRefusal(batch, refusal);
3471
- throw new IngestRefused(refusal.status, refusal.code, refusal.reason);
3463
+ let payload;
3464
+ if (known && existsSync3(join11(journal.directory, ".authority.json"))) {
3465
+ payload = existsSync3(journal.file(batch.idempotency_key, "intent")) ? journal.accept(batch, known) : known;
3466
+ } else {
3467
+ await maintenance.admit(batch);
3468
+ const fresh = journal.stage(batch);
3469
+ const sendFresh = async () => {
3470
+ let response;
3471
+ try {
3472
+ response = await cloud.client.transport.post("/ingest", batch, { retry: false });
3473
+ } catch (error) {
3474
+ const refusal = error instanceof HasnaHttpError2 ? definitiveIngestRefusal(error.status, error.body) : null;
3475
+ if (refusal) {
3476
+ journal.recordRefusal(batch, refusal);
3477
+ throw new IngestRefused(refusal.status, refusal.code, refusal.reason);
3478
+ }
3479
+ throw error;
3472
3480
  }
3473
- throw error;
3474
- }
3475
- return journal.accept(batch, response);
3476
- };
3477
- const payload = known ? journal.accept(batch, known) : fresh ? await sendFresh() : await journal.reconcileCommand(cloud, batch);
3481
+ return journal.accept(batch, response);
3482
+ };
3483
+ payload = known ? journal.accept(batch, known) : fresh ? await sendFresh() : await journal.reconcileCommand(cloud, batch);
3484
+ }
3478
3485
  await verifyJournalAuthority(journal, cloud);
3479
3486
  if (!payload)
3480
3487
  throw new PendingIngestError([batch.idempotency_key]);
3481
- verifyIngestReceipt(payload, batch);
3482
- const counts = payload["ingested"];
3483
- if (!counts || typeof counts !== "object" || Array.isArray(counts) || typeof payload["total"] !== "number" || !Number.isSafeInteger(payload["total"]) || payload["total"] < 0)
3488
+ const verified = verifyIngestReceipt(payload, batch);
3489
+ const counts = verified["ingested"];
3490
+ if (!counts || typeof counts !== "object" || Array.isArray(counts) || typeof verified["total"] !== "number" || !Number.isSafeInteger(verified["total"]) || verified["total"] < 0)
3484
3491
  throw new Error("Invalid ingest receipt");
3485
3492
  let sum = 0;
3486
3493
  for (const [table, count] of Object.entries(counts)) {
@@ -3489,16 +3496,16 @@ async function postIngest(cloud, body) {
3489
3496
  sum += count;
3490
3497
  ingested[table] = (ingested[table] ?? 0) + count;
3491
3498
  }
3492
- if (sum !== payload["total"])
3499
+ if (sum !== verified["total"])
3493
3500
  throw new Error("Invalid ingest receipt");
3494
3501
  total += sum;
3495
- const operation = payload["operation_key"];
3502
+ const operation = verified["operation_key"];
3496
3503
  if (operation !== undefined && (typeof operation !== "string" || !/^[a-f0-9]{64}$/.test(operation)))
3497
3504
  throw new Error("Invalid ingest receipt");
3498
3505
  completed.push({
3499
3506
  idempotency_key: batch.idempotency_key,
3500
3507
  ...typeof operation === "string" ? { operation_key: operation } : {},
3501
- replayed: payload["replayed"] === true
3508
+ replayed: verified["replayed"] === true
3502
3509
  });
3503
3510
  } catch (error) {
3504
3511
  if (error instanceof PendingIngestError || error instanceof JournalMaintenanceError || error instanceof IngestRefused)
@@ -3523,7 +3530,7 @@ async function pushIngestRows(cloud, scratch) {
3523
3530
  // package.json
3524
3531
  var package_default = {
3525
3532
  name: "@hasna/economy",
3526
- version: "0.5.11",
3533
+ version: "0.5.12",
3527
3534
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
3528
3535
  repository: {
3529
3536
  type: "git",
@@ -5578,7 +5578,7 @@ function isAuthorizedRequest(req, path) {
5578
5578
  // package.json
5579
5579
  var package_default = {
5580
5580
  name: "@hasna/economy",
5581
- version: "0.5.11",
5581
+ version: "0.5.12",
5582
5582
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
5583
5583
  repository: {
5584
5584
  type: "git",
@@ -7454,7 +7454,7 @@ import { randomUUID } from "crypto";
7454
7454
  // package.json
7455
7455
  var package_default = {
7456
7456
  name: "@hasna/economy",
7457
- version: "0.5.11",
7457
+ version: "0.5.12",
7458
7458
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
7459
7459
  repository: {
7460
7460
  type: "git",
@@ -107,6 +107,14 @@ an eligible original principal; successful authentication as another principal
107
107
  is insufficient. The same-key path checks hosted history before staging or
108
108
  sending a new command.
109
109
 
110
+ When a scan retries an already retired batch, the verified authority anchor and
111
+ exact hosted receipt are sufficient: no new local intent, capacity admission or
112
+ Trash capture is needed. Existing local records still reconcile normally. An
113
+ empty journal without an anchor establishes one through the ordinary durable
114
+ record path, even if the server already knows the first batch. Failed lookups,
115
+ changed principals and unresolved commands still stop ingestion; they never
116
+ permit a replacement POST.
117
+
110
118
  These are client source contracts. Publication, fleet readiness, actual hosted
111
119
  capture/restore and independent release review require their own acceptance
112
120
  evidence.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.5.11",
3
+ "version": "0.5.12",
4
4
  "description": "AI coding cost tracker — CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
5
5
  "repository": {
6
6
  "type": "git",