refactor(persistence): migrate bridge events file to canonical atomic-write util (DC-104) [glm-grade=A]
writeEvents() in scripts/stripe-license-bridge.js drops its private tmp+writeFileSync+rename copy (no fsync, Date.now() tmp names) and delegates to src/utils/atomic-write.js atomicWriteJSON (exclusive-create tmp, fsync, rename, parent-dir fsync, 0600). stripe-events.json is the Stripe webhook idempotency log - a torn write silently drops event-ids, so a Stripe retry re-runs delivery (duplicate license email; combined with a torn fulfillment record, a duplicate key mint). readEvents is JSON.parse-only, so the canonical serializer's trailing-newline drop is unobservable. Sole writer confirmed by grep. +2 DC-104 pins: full recordEvent->eventSeen dedupe cycle (0600/complete JSON/zero-leftovers) and 8-step back-to-back mutation parse-complete check. 121 suites / 2778 tests green. Judge: GLM-5.3 round-1 A (deleg_e8e1f9d0), URN urn:ump:frwh34wtlsunsymok6pzyujyehq6ikpvadypmsg7ndk7jjvzrp5a
This commit is contained in:
@@ -422,3 +422,63 @@ describe('DC-103 fulfillment-store on canonical atomic-write (real fs)', () => {
|
|||||||
expect(leftovers).toEqual([]);
|
expect(leftovers).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// DC-104: stripe-license-bridge events file (Stripe webhook idempotency
|
||||||
|
// log) migrated off its private tmp+writeFileSync+rename copy onto the
|
||||||
|
// canonical writer. A torn stripe-events.json silently drops event-ids —
|
||||||
|
// the next Stripe retry then re-runs delivery (duplicate license email /
|
||||||
|
// duplicate key mint when combined with a torn fulfillment record).
|
||||||
|
// Pins: 0600 on create, complete JSON after every recordEvent mutation,
|
||||||
|
// no temp leftovers, and the full read-modify-write dedupe cycle through
|
||||||
|
// the bridge's exported functions. (The ignored-type / unpaid-status
|
||||||
|
// write classes route through the same writeEvents and are driven
|
||||||
|
// end-to-end in __tests__/billing/stripe-license-bridge.test.js.)
|
||||||
|
describe('DC-104 bridge events file on canonical atomic-write (real fs)', () => {
|
||||||
|
let dir;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dc104-events-'));
|
||||||
|
process.env.STRIPE_BRIDGE_EVENTS_FILE = path.join(dir, 'stripe-events.json');
|
||||||
|
jest.resetModules();
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
delete process.env.STRIPE_BRIDGE_EVENTS_FILE;
|
||||||
|
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('recordEvent → eventSeen dedupe cycle lands at 0600, complete JSON, no temp leftovers', () => {
|
||||||
|
// Env is captured at require time — resetModules above makes this
|
||||||
|
// require see the fresh STRIPE_BRIDGE_EVENTS_FILE.
|
||||||
|
const bridge = require('../scripts/stripe-license-bridge');
|
||||||
|
|
||||||
|
const first = bridge.recordEvent('evt_dc104_a', { ignoredType: 'product.updated' });
|
||||||
|
expect(first).toBe(true); // new event recorded
|
||||||
|
expect(bridge.eventSeen('evt_dc104_a')).toBe(true);
|
||||||
|
expect(bridge.eventSeen('evt_dc104_unknown')).toBe(false);
|
||||||
|
|
||||||
|
const dup = bridge.recordEvent('evt_dc104_a', { ignoredType: 'product.updated' });
|
||||||
|
expect(dup).toBe(false); // idempotent — already present
|
||||||
|
|
||||||
|
const file = path.join(dir, 'stripe-events.json');
|
||||||
|
const st = fs.statSync(file);
|
||||||
|
expect(st.mode & 0o777).toBe(0o600); // canonical writer default
|
||||||
|
|
||||||
|
// complete JSON carrying the event — a torn write would fail parse here
|
||||||
|
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||||
|
expect(data.events['evt_dc104_a'].ignoredType).toBe('product.updated');
|
||||||
|
|
||||||
|
const leftovers = fs.readdirSync(dir).filter((f) => f !== 'stripe-events.json');
|
||||||
|
expect(leftovers).toEqual([]); // no tmp survivors
|
||||||
|
});
|
||||||
|
|
||||||
|
test('back-to-back recordEvent writes parse complete after every mutation', () => {
|
||||||
|
const bridge = require('../scripts/stripe-license-bridge');
|
||||||
|
for (let i = 0; i < 8; i += 1) {
|
||||||
|
const ok = bridge.recordEvent(`evt_dc104_seq_${i}`, { ignoredType: 'product.updated', seq: i });
|
||||||
|
expect(ok).toBe(true);
|
||||||
|
const file = path.join(dir, 'stripe-events.json');
|
||||||
|
const data = JSON.parse(fs.readFileSync(file, 'utf8')); // throws on torn write
|
||||||
|
expect(Object.keys(data.events)).toHaveLength(i + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { generateCodes, loadSecret } = require('../license-keygen');
|
const { generateCodes, loadSecret } = require('../license-keygen');
|
||||||
const platformPaths = require('../platform-paths');
|
const platformPaths = require('../platform-paths');
|
||||||
|
const { atomicWriteJSON } = require('../src/utils/atomic-write');
|
||||||
const catalog = require('../src/billing/catalog');
|
const catalog = require('../src/billing/catalog');
|
||||||
const invoice = require('../src/billing/invoice');
|
const invoice = require('../src/billing/invoice');
|
||||||
const { createFulfillmentStore, DELIVERY_LEASE_MS } = require('../src/billing/fulfillment-store');
|
const { createFulfillmentStore, DELIVERY_LEASE_MS } = require('../src/billing/fulfillment-store');
|
||||||
@@ -220,10 +221,11 @@ function readEvents() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function writeEvents(state) {
|
function writeEvents(state) {
|
||||||
// Atomic write: tmp + rename.
|
// Canonical atomic writer (DC-099/DC-104): exclusive-create tmp + fsync +
|
||||||
const tmp = `${EVENTS_FILE}.tmp.${process.pid}.${Date.now()}`;
|
// rename + parent-dir fsync, 0600. Replaces the private tmp+rename copy —
|
||||||
fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
|
// a torn stripe-events.json silently drops event-ids, which makes a
|
||||||
fs.renameSync(tmp, EVENTS_FILE);
|
// Stripe retry re-run delivery (duplicate license email / duplicate key).
|
||||||
|
atomicWriteJSON(EVENTS_FILE, state, { mode: 0o600 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function recordEvent(eventId, meta) {
|
function recordEvent(eventId, meta) {
|
||||||
|
|||||||
Reference in New Issue
Block a user