fix(usage): introduce consented v4 without changing historical reports

This commit is contained in:
Paul Nothaft
2026-09-06 21:34:04 +02:00
parent 3cc893126d
commit ef8a52f02c
21 changed files with 5046 additions and 197 deletions
+12 -26
View File
@@ -539,6 +539,9 @@ class UsageService {
return value;
}
async deliver(state) {
// A lost receipt may mean this packet is already stored by the collector.
// Preserve its original schema, date and payload across binary upgrades;
// only re-sign transport metadata, never rebuild under the same packet ID.
const packet = JSON.parse(state.pending_packet);
if (
packet.action !== 'delete' &&
@@ -564,31 +567,14 @@ class UsageService {
});
return null;
}
const identity = {
public_key: state.public_key,
private_key: this.decrypt(state.private_key_encrypted)
};
let envelope;
try {
envelope = signPacket(packet, identity, new Date(this.now()));
} catch (error) {
// A report queued under a catalog this build no longer ships — the
// upgrade replaced a key in the same wire version — fails local
// validation before anything is sent, and retrying cannot repair it.
// Left as it was it blocked every operation behind it for good. A
// report's payload is derived state, so rebuild it from the current
// snapshot in place. The packet ID and sequence are kept: a re-signed
// retry must reuse them so a lost acknowledgement does not duplicate
// data. Reports only — a stale registration, deletion or command is a
// genuine conflict and keeps the handling below.
if (error.code !== 'INVALID_PACKET' || packet.action !== 'report') throw error;
packet.payload = await this.snapshot(packet.schema_version);
state.pending_packet = JSON.stringify(packet);
await this.db('product_usage_state')
.where({ id: 1, status: 'active' })
.update({ pending_packet: state.pending_packet });
envelope = signPacket(packet, identity, new Date(this.now()));
}
const envelope = signPacket(
packet,
{
public_key: state.public_key,
private_key: this.decrypt(state.private_key_encrypted)
},
new Date(this.now())
);
// Last check before anything leaves. The guard at the top of this
// method runs before the binding lookup above, which is asynchronous —
// so a withdrawal that COMPLETED during it would previously still have
@@ -974,7 +960,7 @@ class UsageService {
generated_at: now,
features: expanded,
gallery_layouts: [...layouts].sort(),
...(version === 'usage.v3' ? { inventory: await require('./inventorySnapshot').inventorySnapshot(this.db) } : {})
...(['usage.v3', 'usage.v4'].includes(version) ? { inventory: await require('./inventorySnapshot').inventorySnapshot(this.db) } : {})
};
}