Files
dashcaddy/dashcaddy-api/src/billing/fulfillment-store.js
T
Hermes 6f8fac142f
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s
refactor(persistence): migrate fulfillment-store to canonical atomic-write util (DC-103) [glm-grade=A]
writeState() drops its private tmp+writeFileSync+rename copy (no fsync,
Date.now() tmp names, best-effort chmod) and delegates to
src/utils/atomic-write.js atomicWriteJSON (exclusive-create tmp, fsync,
rename, parent-dir fsync, 0600). A torn stripe-fulfillments.json could
previously make a webhook retry mint a SECOND valid license key for an
order that already has one. Both consumers (routes/billing.js,
scripts/stripe-license-bridge.js) JSON.parse only - trailing-newline
drop in the canonical serializer is harmless. Unused crypto require
removed. +2 DC-103 pins: full lifecycle 0600/complete/zero-leftovers,
and dual-instance interleaved writes (bridge+API file-IPC) with no tmp
collisions. 121 suites / 2776 tests green.

Judge: GLM-5.3 round-1 A (deleg_bd49a97b), URN urn:ump:layk7h326sqymcsh6tiusrs2sdbqdcx6ygvcuwwoxqn4rf7ycoua
2026-08-23 01:21:28 -07:00

175 lines
7.0 KiB
JavaScript

'use strict';
/**
* Durable Stripe fulfillment state.
*
* The bridge and the API share this file through the host data mount. A
* generated license is persisted before email delivery so a webhook retry can
* resend the same key instead of minting a second valid key.
*/
const fs = require('fs');
const path = require('path');
const platformPaths = require('../../platform-paths');
const { atomicWriteJSON } = require('../utils/atomic-write');
const DELIVERY_LEASE_MS = 5 * 60 * 1000;
function createFulfillmentStore(options = {}) {
const filePath = options.filePath
|| process.env.STRIPE_FULFILLMENT_FILE
|| path.join(platformPaths.dataDir, 'stripe-fulfillments.json');
let queue = Promise.resolve();
function emptyState() {
return { version: 1, byEventId: {}, bySessionId: {} };
}
function readState() {
try {
if (!fs.existsSync(filePath)) return emptyState();
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
if (!parsed || typeof parsed !== 'object'
|| !parsed.byEventId || typeof parsed.byEventId !== 'object'
|| !parsed.bySessionId || typeof parsed.bySessionId !== 'object') {
throw new Error('fulfillment state has an invalid shape');
}
return parsed;
} catch (error) {
if (error && error.code === 'ENOENT') return emptyState();
throw new Error(`Stripe fulfillment state unavailable: ${error.message}`);
}
}
function writeState(state) {
const dir = path.dirname(filePath);
fs.mkdirSync(dir, { recursive: true });
// Canonical atomic-write (DC-099): fsync-before-rename + exclusive-create
// tmp + dir fsync. A crash mid-write can no longer leave a torn
// stripe-fulfillments.json — which would have forced the bridge to
// re-mint a duplicate license key on the next webhook retry.
atomicWriteJSON(filePath, state);
}
function mutate(mutator) {
const run = queue.then(async () => {
const state = readState();
const result = await mutator(state);
if (result && result.changed) writeState(state);
return result;
});
queue = run.catch(() => {});
return run;
}
function readBySession(sessionId) {
if (!sessionId) return null;
const record = readState().bySessionId[sessionId];
return record ? { ...record } : null;
}
function readByEvent(eventId) {
if (!eventId) return null;
const record = readState().byEventId[eventId];
return record ? { ...record } : null;
}
async function claim({ eventId, sessionId, productId, durationDays, email }) {
if (!eventId || !sessionId) throw new Error('eventId and sessionId are required');
return mutate((state) => {
const existing = state.bySessionId[sessionId] || state.byEventId[eventId];
const now = Date.now();
if (existing) {
if (existing.status === 'generating' && existing.leaseUntil > now && existing.claimToken !== eventId) {
return { changed: false, claimed: false, busy: true, record: { ...existing } };
}
if (existing.status === 'generating' && existing.leaseUntil <= now) {
existing.claimToken = eventId;
existing.leaseUntil = now + DELIVERY_LEASE_MS;
state.byEventId[eventId] = existing;
return { changed: true, claimed: true, busy: false, record: { ...existing } };
}
return { changed: false, claimed: false, busy: false, record: { ...existing } };
}
const record = {
eventId, sessionId, productId, durationDays, email,
status: 'generating', claimToken: eventId, leaseUntil: now + DELIVERY_LEASE_MS,
createdAt: new Date(now).toISOString(), updatedAt: new Date(now).toISOString(),
};
state.byEventId[eventId] = record;
state.bySessionId[sessionId] = record;
return { changed: true, claimed: true, busy: false, record: { ...record } };
});
}
async function saveLicense({ eventId, sessionId, code, codeId }) {
return mutate((state) => {
const record = state.bySessionId[sessionId] || state.byEventId[eventId];
if (!record || record.claimToken !== eventId) return { changed: false, saved: false, record: record ? { ...record } : null };
record.code = code;
record.codeId = codeId;
record.status = 'pending_email';
record.leaseUntil = 0;
record.updatedAt = new Date().toISOString();
state.byEventId[record.eventId] = record;
state.byEventId[eventId] = record;
state.bySessionId[record.sessionId] = record;
return { changed: true, saved: true, record: { ...record } };
});
}
async function claimDelivery({ sessionId, ownerToken }) {
return mutate((state) => {
const record = state.bySessionId[sessionId];
if (!record || !record.code) return { changed: false, claimed: false, record: record ? { ...record } : null };
const now = Date.now();
if (record.status === 'delivered') return { changed: false, claimed: false, record: { ...record } };
if (record.status === 'delivering' && record.leaseUntil > now && record.leaseOwner !== ownerToken) {
return { changed: false, claimed: false, busy: true, record: { ...record } };
}
record.status = 'delivering';
record.leaseOwner = ownerToken;
record.leaseUntil = now + DELIVERY_LEASE_MS;
record.updatedAt = new Date(now).toISOString();
state.byEventId[record.eventId] = record;
state.bySessionId[sessionId] = record;
return { changed: true, claimed: true, busy: false, record: { ...record } };
});
}
async function markDelivered({ sessionId, ownerToken, deliveredVia }) {
return mutate((state) => {
const record = state.bySessionId[sessionId];
if (!record || record.leaseOwner !== ownerToken) return { changed: false, saved: false };
record.status = 'delivered';
record.deliveredVia = deliveredVia;
record.deliveredAt = new Date().toISOString();
record.lastError = null;
record.leaseUntil = 0;
record.leaseOwner = null;
record.updatedAt = new Date().toISOString();
state.byEventId[record.eventId] = record;
state.bySessionId[sessionId] = record;
return { changed: true, saved: true, record: { ...record } };
});
}
async function markDeliveryFailed({ sessionId, ownerToken, error }) {
return mutate((state) => {
const record = state.bySessionId[sessionId];
if (!record || record.leaseOwner !== ownerToken) return { changed: false, saved: false };
record.status = 'pending_email';
record.lastError = String(error || 'email delivery failed').slice(0, 500);
record.leaseUntil = 0;
record.leaseOwner = null;
record.updatedAt = new Date().toISOString();
state.byEventId[record.eventId] = record;
state.bySessionId[sessionId] = record;
return { changed: true, saved: true, record: { ...record } };
});
}
return { filePath, readBySession, readByEvent, claim, saveLicense, claimDelivery, markDelivered, markDeliveryFailed };
}
module.exports = { createFulfillmentStore, DELIVERY_LEASE_MS };