thumbgate 1.28.0 → 1.28.1
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/.claude-plugin/plugin.json +2 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +96 -122
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +18 -33
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +70 -70
- package/config/gates/default.json +2 -2
- package/config/github-about.json +2 -5
- package/config/post-deploy-marketing-pages.json +1 -1
- package/hooks/hooks.json +38 -0
- package/package.json +18 -8
- package/public/chatgpt-app.html +2 -2
- package/public/codex-enterprise.html +1 -1
- package/public/codex-plugin.html +1 -1
- package/public/diagnostic.html +23 -1
- package/public/guide.html +1 -1
- package/public/index.html +172 -201
- package/public/numbers.html +2 -2
- package/public/partner-intake.html +198 -0
- package/public/pricing.html +51 -14
- package/scripts/billing.js +815 -137
- package/scripts/checkout-attribution-reference.js +85 -0
- package/scripts/claude-feedback-sync.js +23 -6
- package/scripts/cli-feedback.js +19 -10
- package/scripts/feedback-loop.js +290 -1
- package/scripts/feedback-quality.js +25 -26
- package/scripts/feedback-sanitizer.js +151 -3
- package/scripts/gates-engine.js +128 -49
- package/scripts/hosted-config.js +9 -2
- package/scripts/mailer/resend-mailer.js +11 -5
- package/scripts/secret-scanner.js +363 -68
- package/scripts/self-protection.js +21 -36
- package/src/api/server.js +121 -12
- package/public/assets/brand/github-social-preview.png +0 -0
package/scripts/billing.js
CHANGED
|
@@ -51,7 +51,11 @@ const {
|
|
|
51
51
|
} = require('./analytics-window');
|
|
52
52
|
const { ensureParentDir } = require('./fs-utils');
|
|
53
53
|
const mailer = require('./mailer');
|
|
54
|
-
const { recordCheckoutFunnelEvent } = require('./plausible-server-events');
|
|
54
|
+
const { plausibleEvent, recordCheckoutFunnelEvent } = require('./plausible-server-events');
|
|
55
|
+
const { parseCheckoutReference } = require('./checkout-attribution-reference');
|
|
56
|
+
const { redactSecrets } = require('./secret-redaction');
|
|
57
|
+
|
|
58
|
+
const diagnosticEmailInFlight = new Map();
|
|
55
59
|
|
|
56
60
|
function loadWorkflowSprintIntakeModule() {
|
|
57
61
|
const modulePath = path.resolve(__dirname, 'workflow-sprint-intake.js');
|
|
@@ -91,12 +95,17 @@ const CONFIG = {
|
|
|
91
95
|
get TRIAL_EMAIL_LEDGER_PATH() {
|
|
92
96
|
return process.env._TEST_TRIAL_EMAIL_LEDGER_PATH || process.env.THUMBGATE_TRIAL_EMAIL_LEDGER_PATH || path.join(getFeedbackPaths().FEEDBACK_DIR, 'trial-emails.jsonl');
|
|
93
97
|
},
|
|
98
|
+
get ORDER_EMAIL_LEDGER_PATH() {
|
|
99
|
+
return process.env._TEST_ORDER_EMAIL_LEDGER_PATH || process.env.THUMBGATE_ORDER_EMAIL_LEDGER_PATH || path.join(getFeedbackPaths().FEEDBACK_DIR, 'order-emails.jsonl');
|
|
100
|
+
},
|
|
94
101
|
RESEND_API_KEY: process.env.RESEND_API_KEY || process.env.THUMBGATE_RESEND_API_KEY || '',
|
|
95
102
|
TRIAL_EMAIL_FROM: process.env.THUMBGATE_TRIAL_EMAIL_FROM || process.env.RESEND_FROM_EMAIL || process.env.RESEND_FROM || 'onboarding@resend.dev',
|
|
96
103
|
TRIAL_EMAIL_REPLY_TO: process.env.THUMBGATE_TRIAL_EMAIL_REPLY_TO || 'igor.ganapolsky@gmail.com',
|
|
97
104
|
CREDIT_PACKS: {}
|
|
98
105
|
};
|
|
99
106
|
|
|
107
|
+
const DEFAULT_DIAGNOSTIC_PAYMENT_LINK_ID = 'plink_1TsO6lGGBpd520QYsFToXuRC';
|
|
108
|
+
|
|
100
109
|
function resolveLegacyBillingPath(fileName) {
|
|
101
110
|
return resolveFallbackArtifactPath(fileName, {
|
|
102
111
|
feedbackDir: getFeedbackPaths().FEEDBACK_DIR,
|
|
@@ -486,8 +495,8 @@ async function verifyActiveProductForPlan(stripe, planId) {
|
|
|
486
495
|
// session.create fires with inline product_data. Safe path.
|
|
487
496
|
return;
|
|
488
497
|
}
|
|
489
|
-
const
|
|
490
|
-
if (!
|
|
498
|
+
const hasActive = matching.some((p) => p.active === true);
|
|
499
|
+
if (!hasActive) {
|
|
491
500
|
const archived = matching[0];
|
|
492
501
|
throw new Error(
|
|
493
502
|
`Refusing to create checkout session: Stripe product named "${expectedName}" ` +
|
|
@@ -682,7 +691,8 @@ function buildTrialActivationEmail({ customerEmail, apiKey, sessionId, planId, a
|
|
|
682
691
|
|
|
683
692
|
function sendResendEmail(message) {
|
|
684
693
|
return new Promise((resolve, reject) => {
|
|
685
|
-
const
|
|
694
|
+
const { idempotencyKey, ...payload } = message;
|
|
695
|
+
const body = JSON.stringify(payload);
|
|
686
696
|
const req = https.request({
|
|
687
697
|
hostname: 'api.resend.com',
|
|
688
698
|
path: '/emails',
|
|
@@ -691,6 +701,7 @@ function sendResendEmail(message) {
|
|
|
691
701
|
Authorization: `Bearer ${CONFIG.RESEND_API_KEY}`,
|
|
692
702
|
'Content-Type': 'application/json',
|
|
693
703
|
'Content-Length': Buffer.byteLength(body),
|
|
704
|
+
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
|
|
694
705
|
},
|
|
695
706
|
timeout: 10000,
|
|
696
707
|
}, (res) => {
|
|
@@ -730,6 +741,7 @@ async function sendTrialActivationEmail(params = {}, options = {}) {
|
|
|
730
741
|
: null;
|
|
731
742
|
const transport = options.transport || sendResendEmail;
|
|
732
743
|
const planId = normalizeText(params.planId);
|
|
744
|
+
const idempotencyKey = `trial-${crypto.createHash('sha256').update(`${sessionId}:${customerEmail}`).digest('hex')}`;
|
|
733
745
|
|
|
734
746
|
if (!customerEmail) {
|
|
735
747
|
return { status: 'skipped', reason: 'missing_customer_email' };
|
|
@@ -780,6 +792,7 @@ async function sendTrialActivationEmail(params = {}, options = {}) {
|
|
|
780
792
|
customerId: params.customerId,
|
|
781
793
|
customerName: params.customerName,
|
|
782
794
|
trialEndAt: params.trialEndAt,
|
|
795
|
+
idempotencyKey,
|
|
783
796
|
});
|
|
784
797
|
if (!response || response.sent !== true) {
|
|
785
798
|
const rawReason = normalizeText(response && response.reason) || 'provider_error';
|
|
@@ -818,6 +831,7 @@ async function sendTrialActivationEmail(params = {}, options = {}) {
|
|
|
818
831
|
planId,
|
|
819
832
|
appOrigin: params.appOrigin,
|
|
820
833
|
});
|
|
834
|
+
message.idempotencyKey = idempotencyKey;
|
|
821
835
|
const response = await transport(message, params);
|
|
822
836
|
providerId = response && response.body ? response.body.id : response && response.id ? response.id : null;
|
|
823
837
|
}
|
|
@@ -868,6 +882,281 @@ function trialEmailToWebhookEmailResult(trialEmail = {}) {
|
|
|
868
882
|
};
|
|
869
883
|
}
|
|
870
884
|
|
|
885
|
+
function isDiagnosticCheckoutSession(session = {}) {
|
|
886
|
+
const configuredIds = String(
|
|
887
|
+
process.env.THUMBGATE_DIAGNOSTIC_PAYMENT_LINK_IDS
|
|
888
|
+
|| process.env.THUMBGATE_DIAGNOSTIC_PAYMENT_LINK_ID
|
|
889
|
+
|| DEFAULT_DIAGNOSTIC_PAYMENT_LINK_ID
|
|
890
|
+
)
|
|
891
|
+
.split(',')
|
|
892
|
+
.map((value) => value.trim())
|
|
893
|
+
.filter((value) => /^plink_[A-Za-z0-9]+$/.test(value));
|
|
894
|
+
const paymentLinkId = typeof session.payment_link === 'string'
|
|
895
|
+
? session.payment_link
|
|
896
|
+
: normalizeText(session.payment_link && session.payment_link.id);
|
|
897
|
+
// Metadata, client_reference_id, and amount are not authorization: public
|
|
898
|
+
// checkout input can set metadata, while other SKUs can share a price. The
|
|
899
|
+
// Stripe-created Payment Link identity is the diagnostic SKU boundary.
|
|
900
|
+
return session.mode === 'payment'
|
|
901
|
+
&& configuredIds.includes(paymentLinkId);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Decide whether a completed Stripe session is allowed to mint an API key.
|
|
906
|
+
*
|
|
907
|
+
* A paid session is not automatically a software entitlement. ThumbGate also
|
|
908
|
+
* sells one-time services through Stripe Payment Links, and those orders must
|
|
909
|
+
* be recorded without receiving Pro access. Subscription mode is Stripe-owned
|
|
910
|
+
* state. Credit packs are accepted only when both the pack id and credit count
|
|
911
|
+
* match the server-side catalog written by createCheckoutSession().
|
|
912
|
+
*/
|
|
913
|
+
function resolveCheckoutProvisioningGrant(session = {}) {
|
|
914
|
+
if (session.mode === 'subscription') {
|
|
915
|
+
return { allowed: true, kind: 'subscription', credits: null };
|
|
916
|
+
}
|
|
917
|
+
if (session.mode !== 'payment') {
|
|
918
|
+
return { allowed: false, kind: 'unknown', credits: null };
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const packId = normalizeText(session.metadata && session.metadata.packId);
|
|
922
|
+
const configuredPack = packId && CONFIG.CREDIT_PACKS[packId];
|
|
923
|
+
const credits = normalizeInteger(session.metadata && session.metadata.credits);
|
|
924
|
+
if (configuredPack && credits === configuredPack.credits) {
|
|
925
|
+
return { allowed: true, kind: 'credit_pack', credits };
|
|
926
|
+
}
|
|
927
|
+
return { allowed: false, kind: 'service_order', credits: null };
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function resolveStripeCustomerLedgerId(session = {}, customerEmail = '') {
|
|
931
|
+
const stripeCustomerId = normalizeText(session.customer);
|
|
932
|
+
if (stripeCustomerId) return stripeCustomerId;
|
|
933
|
+
const normalizedEmail = normalizeEmail(customerEmail);
|
|
934
|
+
const seed = normalizedEmail || normalizeText(session.id) || crypto.randomBytes(16).toString('hex');
|
|
935
|
+
return `guest_${crypto.createHash('sha256').update(seed).digest('hex').slice(0, 24)}`;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function resolveOperatorAlertEmail(env = process.env) {
|
|
939
|
+
return normalizeEmail(
|
|
940
|
+
env.THUMBGATE_OPERATOR_ALERT_EMAIL
|
|
941
|
+
|| env.THUMBGATE_PRO_ACTIVATION_ALERT_EMAIL
|
|
942
|
+
|| env.THUMBGATE_SUPPORT_EMAIL
|
|
943
|
+
|| 'igor.ganapolsky@gmail.com'
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function findOrderEmailRecord({ sessionId, kind } = {}) {
|
|
948
|
+
const normalizedSessionId = normalizeText(sessionId);
|
|
949
|
+
const normalizedKind = normalizeText(kind);
|
|
950
|
+
if (!normalizedSessionId || !normalizedKind) return null;
|
|
951
|
+
return loadJsonlRecords(CONFIG.ORDER_EMAIL_LEDGER_PATH).find((row) => (
|
|
952
|
+
row
|
|
953
|
+
&& row.sessionId === normalizedSessionId
|
|
954
|
+
&& row.kind === normalizedKind
|
|
955
|
+
&& row.status === 'sent'
|
|
956
|
+
)) || null;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function appendOrderEmailRecord(payload = {}) {
|
|
960
|
+
return appendJsonlRecord(CONFIG.ORDER_EMAIL_LEDGER_PATH, {
|
|
961
|
+
timestamp: new Date().toISOString(),
|
|
962
|
+
...payload,
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function getOrderOperationStatePaths(sessionId, operation) {
|
|
967
|
+
const key = crypto.createHash('sha256').update(String(sessionId || '')).digest('hex');
|
|
968
|
+
const safeOperation = String(operation || 'checkout').replace(/[^a-z0-9_-]/gi, '-').toLowerCase();
|
|
969
|
+
const stateDir = path.join(path.dirname(CONFIG.ORDER_EMAIL_LEDGER_PATH), `${safeOperation}-order-state`);
|
|
970
|
+
return {
|
|
971
|
+
stateDir,
|
|
972
|
+
lockPath: path.join(stateDir, `${key}.lock`),
|
|
973
|
+
receiptPath: path.join(stateDir, `${key}.json`),
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function readOrderOperationReceipt(sessionId, operation) {
|
|
978
|
+
const { receiptPath } = getOrderOperationStatePaths(sessionId, operation);
|
|
979
|
+
try {
|
|
980
|
+
return JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
|
|
981
|
+
} catch {
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function claimOrderOperation(sessionId, operation, now = Date.now()) {
|
|
987
|
+
const previous = readOrderOperationReceipt(sessionId, operation);
|
|
988
|
+
if (previous) return { claimed: false, completed: true, receipt: previous };
|
|
989
|
+
const paths = getOrderOperationStatePaths(sessionId, operation);
|
|
990
|
+
fs.mkdirSync(paths.stateDir, { recursive: true });
|
|
991
|
+
try {
|
|
992
|
+
fs.mkdirSync(paths.lockPath);
|
|
993
|
+
fs.writeFileSync(path.join(paths.lockPath, 'owner.json'), JSON.stringify({ acquiredAt: now }));
|
|
994
|
+
return { claimed: true, completed: false, ...paths };
|
|
995
|
+
} catch (error) {
|
|
996
|
+
if (!error || error.code !== 'EEXIST') throw error;
|
|
997
|
+
try {
|
|
998
|
+
const ageMs = now - fs.statSync(paths.lockPath).mtimeMs;
|
|
999
|
+
if (ageMs > 5 * 60 * 1000) {
|
|
1000
|
+
fs.rmSync(paths.lockPath, { recursive: true, force: true });
|
|
1001
|
+
return claimOrderOperation(sessionId, operation, now);
|
|
1002
|
+
}
|
|
1003
|
+
} catch {
|
|
1004
|
+
return claimOrderOperation(sessionId, operation, now);
|
|
1005
|
+
}
|
|
1006
|
+
return { claimed: false, completed: false, ...paths };
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function releaseOrderOperation(claim) {
|
|
1011
|
+
if (claim && claim.claimed && claim.lockPath) {
|
|
1012
|
+
fs.rmSync(claim.lockPath, { recursive: true, force: true });
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function completeOrderOperation(claim, payload = {}) {
|
|
1017
|
+
if (!claim || !claim.claimed) return;
|
|
1018
|
+
const tmpPath = `${claim.receiptPath}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
1019
|
+
fs.writeFileSync(tmpPath, JSON.stringify({
|
|
1020
|
+
completedAt: new Date().toISOString(),
|
|
1021
|
+
...payload,
|
|
1022
|
+
}));
|
|
1023
|
+
fs.renameSync(tmpPath, claim.receiptPath);
|
|
1024
|
+
releaseOrderOperation(claim);
|
|
1025
|
+
claim.claimed = false;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
async function sendDiagnosticOrderEmails({
|
|
1029
|
+
session = {},
|
|
1030
|
+
customerEmail,
|
|
1031
|
+
customerName,
|
|
1032
|
+
attribution = {},
|
|
1033
|
+
env = process.env,
|
|
1034
|
+
} = {}) {
|
|
1035
|
+
const injectedMailer = module.exports && module.exports._mailer;
|
|
1036
|
+
const sendEmail = injectedMailer && typeof injectedMailer.sendEmail === 'function'
|
|
1037
|
+
? injectedMailer.sendEmail
|
|
1038
|
+
: null;
|
|
1039
|
+
const sessionId = normalizeText(session.id);
|
|
1040
|
+
const operatorEmail = resolveOperatorAlertEmail(env);
|
|
1041
|
+
const normalizedCustomerEmail = normalizeEmail(customerEmail);
|
|
1042
|
+
const safeCustomerName = redactSecrets(normalizeText(customerName) || '');
|
|
1043
|
+
const amount = Number(session.amount_total);
|
|
1044
|
+
const amountLabel = Number.isFinite(amount) ? `$${(amount / 100).toFixed(2)}` : 'unknown';
|
|
1045
|
+
|
|
1046
|
+
async function sendOnce({ kind, to, subject, text }) {
|
|
1047
|
+
if (!to) return { sent: false, reason: 'missing_recipient', kind };
|
|
1048
|
+
const previous = findOrderEmailRecord({ sessionId, kind });
|
|
1049
|
+
if (previous) return { sent: true, status: 'already_sent', providerId: previous.providerId || null };
|
|
1050
|
+
if (!sendEmail) return { sent: false, reason: 'missing_mailer' };
|
|
1051
|
+
const operationKey = `${sessionId}:${kind}`;
|
|
1052
|
+
if (diagnosticEmailInFlight.has(operationKey)) {
|
|
1053
|
+
return diagnosticEmailInFlight.get(operationKey);
|
|
1054
|
+
}
|
|
1055
|
+
const operation = (async () => {
|
|
1056
|
+
try {
|
|
1057
|
+
const idempotencyKey = `diagnostic-${crypto.createHash('sha256').update(operationKey).digest('hex')}`;
|
|
1058
|
+
const response = await sendEmail({ to, subject, text, idempotencyKey });
|
|
1059
|
+
const sent = response && response.sent === true;
|
|
1060
|
+
appendOrderEmailRecord({
|
|
1061
|
+
sessionId,
|
|
1062
|
+
kind,
|
|
1063
|
+
status: sent ? 'sent' : 'skipped',
|
|
1064
|
+
reason: sent ? null : (response && response.reason) || 'provider_error',
|
|
1065
|
+
providerId: response && (response.id || response.providerId) || null,
|
|
1066
|
+
idempotencyKeyHash: crypto.createHash('sha256').update(idempotencyKey).digest('hex').slice(0, 16),
|
|
1067
|
+
});
|
|
1068
|
+
return response || { sent: false, reason: 'provider_error' };
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
appendOrderEmailRecord({
|
|
1071
|
+
sessionId,
|
|
1072
|
+
kind,
|
|
1073
|
+
status: 'failed',
|
|
1074
|
+
reason: 'exception',
|
|
1075
|
+
error: error && error.message ? error.message : String(error),
|
|
1076
|
+
});
|
|
1077
|
+
return {
|
|
1078
|
+
sent: false,
|
|
1079
|
+
reason: 'exception',
|
|
1080
|
+
error: error && error.message ? error.message : String(error),
|
|
1081
|
+
};
|
|
1082
|
+
} finally {
|
|
1083
|
+
diagnosticEmailInFlight.delete(operationKey);
|
|
1084
|
+
}
|
|
1085
|
+
})();
|
|
1086
|
+
diagnosticEmailInFlight.set(operationKey, operation);
|
|
1087
|
+
return operation;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const buyerText = [
|
|
1091
|
+
safeCustomerName ? `Hi ${safeCustomerName},` : 'Hi there,',
|
|
1092
|
+
'',
|
|
1093
|
+
`We received your ${amountLabel} ThumbGate Workflow Hardening Diagnostic order.`,
|
|
1094
|
+
'The diagnostic covers one AI-agent workflow and is delivered within two business days.',
|
|
1095
|
+
'Reply with the repository or product URL plus the repeated failure trace if you did not submit them before checkout.',
|
|
1096
|
+
'',
|
|
1097
|
+
`Order reference: ${sessionId || 'unknown'}`,
|
|
1098
|
+
'Questions: reply to this email.',
|
|
1099
|
+
].join('\n');
|
|
1100
|
+
const operatorText = [
|
|
1101
|
+
'Paid ThumbGate diagnostic received.',
|
|
1102
|
+
'',
|
|
1103
|
+
`Amount: ${amountLabel} ${(session.currency || 'usd').toUpperCase()}`,
|
|
1104
|
+
`Stripe session: ${sessionId || 'unknown'}`,
|
|
1105
|
+
`Customer: ${safeCustomerName || 'not provided'}`,
|
|
1106
|
+
`Email: ${normalizedCustomerEmail || 'not provided'}`,
|
|
1107
|
+
`Source: ${redactSecrets(normalizeText(attribution.source) || 'unknown')}`,
|
|
1108
|
+
`Campaign: ${redactSecrets(normalizeText(attribution.campaign) || 'unknown')}`,
|
|
1109
|
+
`Client reference: ${redactSecrets(normalizeText(session.client_reference_id) || 'not provided')}`,
|
|
1110
|
+
'',
|
|
1111
|
+
'Start the two-business-day fulfillment clock and match the buyer to any captured workflow intake.',
|
|
1112
|
+
].join('\n');
|
|
1113
|
+
|
|
1114
|
+
const [buyer, operator] = await Promise.all([
|
|
1115
|
+
sendOnce({
|
|
1116
|
+
kind: 'diagnostic_buyer_receipt',
|
|
1117
|
+
to: normalizedCustomerEmail,
|
|
1118
|
+
subject: 'ThumbGate diagnostic order received',
|
|
1119
|
+
text: buyerText,
|
|
1120
|
+
}),
|
|
1121
|
+
sendOnce({
|
|
1122
|
+
kind: 'diagnostic_operator_alert',
|
|
1123
|
+
to: operatorEmail,
|
|
1124
|
+
subject: `Paid ThumbGate diagnostic: ${amountLabel}`,
|
|
1125
|
+
text: operatorText,
|
|
1126
|
+
}),
|
|
1127
|
+
]);
|
|
1128
|
+
|
|
1129
|
+
return { buyer, operator };
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function isRetryableDiagnosticDelivery(result = {}) {
|
|
1133
|
+
if (result && result.sent === true) return false;
|
|
1134
|
+
const reason = normalizeText(result && result.reason);
|
|
1135
|
+
if (reason === 'api_error') {
|
|
1136
|
+
const status = Number(result && result.status);
|
|
1137
|
+
if (!Number.isFinite(status)) return true;
|
|
1138
|
+
if (status !== 400 && status !== 422) return true;
|
|
1139
|
+
const bodyText = typeof result.body === 'string'
|
|
1140
|
+
? result.body
|
|
1141
|
+
: JSON.stringify(result.body || {});
|
|
1142
|
+
const normalizedBody = bodyText.toLowerCase();
|
|
1143
|
+
const recipientOnlyFailure = (
|
|
1144
|
+
normalizedBody.includes('invalid_to_address')
|
|
1145
|
+
|| normalizedBody.includes('invalid recipient')
|
|
1146
|
+
|| normalizedBody.includes('recipient address is invalid')
|
|
1147
|
+
) && !(
|
|
1148
|
+
normalizedBody.includes('invalid_from_address')
|
|
1149
|
+
|| normalizedBody.includes('sender')
|
|
1150
|
+
|| normalizedBody.includes('domain')
|
|
1151
|
+
);
|
|
1152
|
+
return !recipientOnlyFailure;
|
|
1153
|
+
}
|
|
1154
|
+
if (reason === 'missing_recipient') {
|
|
1155
|
+
return result.kind === 'diagnostic_operator_alert';
|
|
1156
|
+
}
|
|
1157
|
+
return ['no_api_key', 'no_fetch', 'exception', 'provider_error', 'missing_mailer'].includes(reason);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
871
1160
|
function normalizeCurrency(value) {
|
|
872
1161
|
const text = normalizeText(value);
|
|
873
1162
|
return text ? text.toUpperCase() : null;
|
|
@@ -2518,16 +2807,72 @@ function loadKeyStore() {
|
|
|
2518
2807
|
const primary = CONFIG.API_KEYS_PATH;
|
|
2519
2808
|
const legacy = resolveLegacyBillingPath('api-keys.json');
|
|
2520
2809
|
const target = (IS_TEST || fs.existsSync(primary)) ? primary : legacy;
|
|
2521
|
-
if (!fs.existsSync(target)) return { keys: {} };
|
|
2810
|
+
if (!fs.existsSync(target)) return { keys: {}, canceledSubscriptions: {}, customerRevocations: {} };
|
|
2522
2811
|
const parsed = JSON.parse(fs.readFileSync(target, 'utf-8'));
|
|
2523
|
-
|
|
2524
|
-
|
|
2812
|
+
if (!parsed || typeof parsed.keys !== 'object') {
|
|
2813
|
+
return { keys: {}, canceledSubscriptions: {}, customerRevocations: {} };
|
|
2814
|
+
}
|
|
2815
|
+
parsed.canceledSubscriptions = parsed.canceledSubscriptions && typeof parsed.canceledSubscriptions === 'object'
|
|
2816
|
+
? parsed.canceledSubscriptions
|
|
2817
|
+
: {};
|
|
2818
|
+
parsed.customerRevocations = parsed.customerRevocations && typeof parsed.customerRevocations === 'object'
|
|
2819
|
+
? parsed.customerRevocations
|
|
2820
|
+
: {};
|
|
2821
|
+
return parsed;
|
|
2822
|
+
} catch { return { keys: {}, canceledSubscriptions: {}, customerRevocations: {} }; }
|
|
2525
2823
|
}
|
|
2526
2824
|
|
|
2527
2825
|
function saveKeyStore(store) {
|
|
2528
2826
|
const target = CONFIG.API_KEYS_PATH;
|
|
2529
2827
|
ensureParentDir(target);
|
|
2530
|
-
|
|
2828
|
+
const tmpPath = `${target}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
2829
|
+
fs.writeFileSync(tmpPath, JSON.stringify(store, null, 2), 'utf-8');
|
|
2830
|
+
fs.renameSync(tmpPath, target);
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
function sleepSync(milliseconds) {
|
|
2834
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
function acquireDirectoryLock(lockPath, options = {}) {
|
|
2838
|
+
const timeoutMs = normalizeInteger(options.timeoutMs) || 10000;
|
|
2839
|
+
const staleMs = normalizeInteger(options.staleMs) || 30000;
|
|
2840
|
+
const startedAt = Date.now();
|
|
2841
|
+
ensureParentDir(lockPath);
|
|
2842
|
+
while (true) {
|
|
2843
|
+
try {
|
|
2844
|
+
fs.mkdirSync(lockPath);
|
|
2845
|
+
fs.writeFileSync(path.join(lockPath, 'owner.json'), JSON.stringify({
|
|
2846
|
+
acquiredAt: Date.now(),
|
|
2847
|
+
pid: process.pid,
|
|
2848
|
+
}));
|
|
2849
|
+
return;
|
|
2850
|
+
} catch (error) {
|
|
2851
|
+
if (!error || error.code !== 'EEXIST') throw error;
|
|
2852
|
+
try {
|
|
2853
|
+
if (Date.now() - fs.statSync(lockPath).mtimeMs > staleMs) {
|
|
2854
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
2855
|
+
continue;
|
|
2856
|
+
}
|
|
2857
|
+
} catch {
|
|
2858
|
+
continue;
|
|
2859
|
+
}
|
|
2860
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
2861
|
+
throw new Error(`Timed out waiting for billing state lock: ${path.basename(lockPath)}`);
|
|
2862
|
+
}
|
|
2863
|
+
sleepSync(20);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
function withKeyStoreLock(mutator) {
|
|
2869
|
+
const lockPath = `${CONFIG.API_KEYS_PATH}.lock`;
|
|
2870
|
+
acquireDirectoryLock(lockPath);
|
|
2871
|
+
try {
|
|
2872
|
+
return mutator();
|
|
2873
|
+
} finally {
|
|
2874
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
2875
|
+
}
|
|
2531
2876
|
}
|
|
2532
2877
|
|
|
2533
2878
|
// ---------------------------------------------------------------------------
|
|
@@ -2564,6 +2909,7 @@ async function createCheckoutSession({ successUrl, cancelUrl, customerEmail, ins
|
|
|
2564
2909
|
customer_email: localCustomerEmail,
|
|
2565
2910
|
customer_details: localCustomerEmail ? { email: localCustomerEmail } : null,
|
|
2566
2911
|
metadata: { ...checkoutMetadata, packId: pack ? pack.id : null, credits: pack ? pack.credits : null },
|
|
2912
|
+
mode: pack ? 'payment' : 'subscription',
|
|
2567
2913
|
payment_status: 'paid',
|
|
2568
2914
|
status: 'complete'
|
|
2569
2915
|
};
|
|
@@ -2687,6 +3033,7 @@ function buildCheckoutSessionPayload({ successUrl, cancelUrl, customerEmail, che
|
|
|
2687
3033
|
payment_method_collection: 'always',
|
|
2688
3034
|
...(shouldStartTrial ? { subscription_data: { trial_period_days: trialPeriodDays } } : {}),
|
|
2689
3035
|
}),
|
|
3036
|
+
...(pack ? { customer_creation: 'always' } : {}),
|
|
2690
3037
|
};
|
|
2691
3038
|
|
|
2692
3039
|
const normalizedCustomerEmail = normalizeText(customerEmail);
|
|
@@ -2701,25 +3048,43 @@ async function getCheckoutSessionStatus(sessionId) {
|
|
|
2701
3048
|
const store = loadLocalCheckoutSessions();
|
|
2702
3049
|
const session = store.sessions[sessionId];
|
|
2703
3050
|
if (!session) return { found: false };
|
|
2704
|
-
const
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
3051
|
+
const diagnosticOrder = isDiagnosticCheckoutSession(session);
|
|
3052
|
+
const provisioningGrant = resolveCheckoutProvisioningGrant(session);
|
|
3053
|
+
const provisioned = provisioningGrant.allowed
|
|
3054
|
+
? provisionApiKey(session.customer, {
|
|
3055
|
+
installId: session.metadata?.installId,
|
|
3056
|
+
credits: provisioningGrant.credits,
|
|
3057
|
+
orderId: session.id,
|
|
3058
|
+
subscriptionId: session.subscription,
|
|
3059
|
+
orderCreatedAt: session.created,
|
|
3060
|
+
enforceCancellationHistory: provisioningGrant.kind === 'subscription',
|
|
3061
|
+
entitlementKind: provisioningGrant.kind,
|
|
3062
|
+
source: 'local_checkout_lookup'
|
|
3063
|
+
})
|
|
3064
|
+
: null;
|
|
2709
3065
|
const customerEmail = session.customer_details?.email || session.customer_email || '';
|
|
2710
3066
|
const customerName = session.customer_details?.name || null;
|
|
2711
3067
|
const trialEndAt = computeTrialEndAt(session);
|
|
2712
|
-
const trialEmail =
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
3068
|
+
const trialEmail = !provisioned || !provisioned.key
|
|
3069
|
+
? {
|
|
3070
|
+
status: 'skipped',
|
|
3071
|
+
reason: diagnosticOrder
|
|
3072
|
+
? 'diagnostic_order'
|
|
3073
|
+
: provisioned && provisioned.entitlementActive === false
|
|
3074
|
+
? 'entitlement_inactive'
|
|
3075
|
+
: 'non_entitlement_order',
|
|
3076
|
+
}
|
|
3077
|
+
: await sendTrialActivationEmail({
|
|
3078
|
+
sessionId,
|
|
3079
|
+
customerId: session.customer,
|
|
3080
|
+
customerEmail,
|
|
3081
|
+
customerName,
|
|
3082
|
+
trialEndAt,
|
|
3083
|
+
apiKey: provisioned.key,
|
|
3084
|
+
planId: session.metadata?.planId || session.metadata?.packId || null,
|
|
3085
|
+
appOrigin: process.env.THUMBGATE_PUBLIC_APP_ORIGIN,
|
|
3086
|
+
source: 'local_checkout_lookup',
|
|
3087
|
+
});
|
|
2723
3088
|
return {
|
|
2724
3089
|
found: true,
|
|
2725
3090
|
localMode: true,
|
|
@@ -2737,10 +3102,11 @@ async function getCheckoutSessionStatus(sessionId) {
|
|
|
2737
3102
|
ctaId: session.metadata?.ctaId || null,
|
|
2738
3103
|
ctaPlacement: session.metadata?.ctaPlacement || null,
|
|
2739
3104
|
planId: session.metadata?.planId || session.metadata?.packId || null,
|
|
3105
|
+
offerKind: diagnosticOrder ? 'workflow_hardening_diagnostic' : null,
|
|
2740
3106
|
landingPath: session.metadata?.landingPath || null,
|
|
2741
3107
|
referrerHost: session.metadata?.referrerHost || null,
|
|
2742
|
-
apiKey: provisioned.key,
|
|
2743
|
-
remainingCredits: provisioned.remainingCredits,
|
|
3108
|
+
apiKey: provisioned ? provisioned.key : null,
|
|
3109
|
+
remainingCredits: provisioned ? provisioned.remainingCredits : null,
|
|
2744
3110
|
trialEmail,
|
|
2745
3111
|
};
|
|
2746
3112
|
}
|
|
@@ -2748,28 +3114,53 @@ async function getCheckoutSessionStatus(sessionId) {
|
|
|
2748
3114
|
try {
|
|
2749
3115
|
const stripe = getStripeClient();
|
|
2750
3116
|
const session = await stripe.checkout.sessions.retrieve(sessionId);
|
|
2751
|
-
const
|
|
3117
|
+
const diagnosticOrder = isDiagnosticCheckoutSession(session);
|
|
3118
|
+
const provisioningGrant = resolveCheckoutProvisioningGrant(session);
|
|
3119
|
+
const isPaid = session.payment_status === 'paid'
|
|
3120
|
+
|| (!diagnosticOrder
|
|
3121
|
+
&& session.mode === 'subscription'
|
|
3122
|
+
&& session.payment_status === 'no_payment_required');
|
|
2752
3123
|
const traceId = session.metadata?.traceId || null;
|
|
2753
3124
|
|
|
2754
3125
|
if (!isPaid) return { found: true, localMode: false, sessionId, paid: false, paymentStatus: session.payment_status, status: session.status };
|
|
2755
3126
|
|
|
2756
3127
|
const installId = session.metadata?.installId || null;
|
|
2757
|
-
const credits = session.metadata?.credits ? parseInt(session.metadata.credits, 10) : null;
|
|
2758
|
-
const provisioned = provisionApiKey(session.customer, { installId, credits, source: 'stripe_checkout_session_lookup' });
|
|
2759
3128
|
const customerEmail = session.customer_details?.email || session.customer_email || '';
|
|
3129
|
+
const customerId = resolveStripeCustomerLedgerId(session, customerEmail);
|
|
3130
|
+
const provisioned = provisioningGrant.allowed
|
|
3131
|
+
? provisionApiKey(customerId, {
|
|
3132
|
+
installId,
|
|
3133
|
+
credits: provisioningGrant.credits,
|
|
3134
|
+
orderId: session.id,
|
|
3135
|
+
subscriptionId: session.subscription,
|
|
3136
|
+
orderCreatedAt: session.created,
|
|
3137
|
+
enforceCancellationHistory: provisioningGrant.kind === 'subscription',
|
|
3138
|
+
entitlementKind: provisioningGrant.kind,
|
|
3139
|
+
source: 'stripe_checkout_session_lookup',
|
|
3140
|
+
})
|
|
3141
|
+
: null;
|
|
2760
3142
|
const customerName = session.customer_details?.name || null;
|
|
2761
3143
|
const trialEndAt = computeTrialEndAt(session);
|
|
2762
|
-
const trialEmail =
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
3144
|
+
const trialEmail = !provisioned || !provisioned.key
|
|
3145
|
+
? {
|
|
3146
|
+
status: 'skipped',
|
|
3147
|
+
reason: diagnosticOrder
|
|
3148
|
+
? 'diagnostic_order'
|
|
3149
|
+
: provisioned && provisioned.entitlementActive === false
|
|
3150
|
+
? 'entitlement_inactive'
|
|
3151
|
+
: 'non_entitlement_order',
|
|
3152
|
+
}
|
|
3153
|
+
: await sendTrialActivationEmail({
|
|
3154
|
+
sessionId,
|
|
3155
|
+
customerId,
|
|
3156
|
+
customerEmail,
|
|
3157
|
+
customerName,
|
|
3158
|
+
trialEndAt,
|
|
3159
|
+
apiKey: provisioned.key,
|
|
3160
|
+
planId: session.metadata?.planId || session.metadata?.packId || null,
|
|
3161
|
+
appOrigin: process.env.THUMBGATE_PUBLIC_APP_ORIGIN,
|
|
3162
|
+
source: 'stripe_checkout_session_lookup',
|
|
3163
|
+
});
|
|
2773
3164
|
|
|
2774
3165
|
return {
|
|
2775
3166
|
found: true,
|
|
@@ -2777,7 +3168,7 @@ async function getCheckoutSessionStatus(sessionId) {
|
|
|
2777
3168
|
sessionId,
|
|
2778
3169
|
paid: true,
|
|
2779
3170
|
paymentStatus: session.payment_status,
|
|
2780
|
-
customerId
|
|
3171
|
+
customerId,
|
|
2781
3172
|
customerEmail,
|
|
2782
3173
|
installId,
|
|
2783
3174
|
traceId,
|
|
@@ -2787,10 +3178,11 @@ async function getCheckoutSessionStatus(sessionId) {
|
|
|
2787
3178
|
ctaId: session.metadata?.ctaId || null,
|
|
2788
3179
|
ctaPlacement: session.metadata?.ctaPlacement || null,
|
|
2789
3180
|
planId: session.metadata?.planId || session.metadata?.packId || null,
|
|
3181
|
+
offerKind: diagnosticOrder ? 'workflow_hardening_diagnostic' : null,
|
|
2790
3182
|
landingPath: session.metadata?.landingPath || null,
|
|
2791
3183
|
referrerHost: session.metadata?.referrerHost || null,
|
|
2792
|
-
apiKey: provisioned.key,
|
|
2793
|
-
remainingCredits: provisioned.remainingCredits,
|
|
3184
|
+
apiKey: provisioned ? provisioned.key : null,
|
|
3185
|
+
remainingCredits: provisioned ? provisioned.remainingCredits : null,
|
|
2794
3186
|
trialEmail,
|
|
2795
3187
|
};
|
|
2796
3188
|
} catch {
|
|
@@ -2800,58 +3192,150 @@ async function getCheckoutSessionStatus(sessionId) {
|
|
|
2800
3192
|
|
|
2801
3193
|
function provisionApiKey(customerId, opts = {}) {
|
|
2802
3194
|
if (!customerId || typeof customerId !== 'string') throw new Error('customerId is required');
|
|
2803
|
-
|
|
2804
|
-
|
|
3195
|
+
return withKeyStoreLock(() => {
|
|
3196
|
+
const store = loadKeyStore();
|
|
3197
|
+
const orderId = normalizeText(opts.orderId);
|
|
3198
|
+
const subscriptionId = normalizeText(opts.subscriptionId);
|
|
3199
|
+
const orderCreatedAt = normalizeInteger(opts.orderCreatedAt);
|
|
3200
|
+
const enforceCancellationHistory = opts.enforceCancellationHistory === true;
|
|
3201
|
+
|
|
3202
|
+
if (orderId) {
|
|
3203
|
+
const fulfilled = Object.entries(store.keys)
|
|
3204
|
+
.filter(([, metadata]) => (
|
|
3205
|
+
Array.isArray(metadata.fulfilledOrderIds)
|
|
3206
|
+
&& metadata.fulfilledOrderIds.includes(orderId)
|
|
3207
|
+
))
|
|
3208
|
+
.sort((left, right) => Number(Boolean(right[1].active)) - Number(Boolean(left[1].active)))[0];
|
|
3209
|
+
if (fulfilled) {
|
|
3210
|
+
const [key, metadata] = fulfilled;
|
|
3211
|
+
return {
|
|
3212
|
+
key: metadata.active ? key : null,
|
|
3213
|
+
customerId,
|
|
3214
|
+
createdAt: metadata.createdAt,
|
|
3215
|
+
installId: metadata.installId || null,
|
|
3216
|
+
reused: true,
|
|
3217
|
+
orderAlreadyFulfilled: true,
|
|
3218
|
+
entitlementActive: Boolean(metadata.active),
|
|
3219
|
+
remainingCredits: metadata.remainingCredits,
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
2805
3223
|
|
|
2806
|
-
|
|
3224
|
+
const customerKeys = Object.entries(store.keys)
|
|
3225
|
+
.filter(([, metadata]) => metadata.customerId === customerId);
|
|
3226
|
+
const activeCustomerKey = customerKeys.find(([, metadata]) => metadata.active);
|
|
3227
|
+
const latestDisabledKey = customerKeys
|
|
3228
|
+
.filter(([, metadata]) => !metadata.active && metadata.disabledAt)
|
|
3229
|
+
.sort((left, right) => Date.parse(right[1].disabledAt) - Date.parse(left[1].disabledAt))[0];
|
|
3230
|
+
const canceledSubscription = subscriptionId && store.canceledSubscriptions[subscriptionId];
|
|
3231
|
+
const customerRevocation = store.customerRevocations[customerId];
|
|
3232
|
+
const revocationCreatedAt = normalizeInteger(customerRevocation && customerRevocation.eventCreatedAt);
|
|
3233
|
+
const isNewSubscriptionAfterRevocation = Boolean(
|
|
3234
|
+
customerRevocation
|
|
3235
|
+
&& subscriptionId
|
|
3236
|
+
&& subscriptionId !== customerRevocation.subscriptionId
|
|
3237
|
+
&& orderCreatedAt !== null
|
|
3238
|
+
&& revocationCreatedAt !== null
|
|
3239
|
+
&& orderCreatedAt > revocationCreatedAt
|
|
3240
|
+
);
|
|
3241
|
+
const legacyDisabledAt = latestDisabledKey
|
|
3242
|
+
? Math.floor(Date.parse(latestDisabledKey[1].disabledAt) / 1000)
|
|
3243
|
+
: null;
|
|
3244
|
+
const blockedByLegacyCancellation = enforceCancellationHistory
|
|
3245
|
+
&& !activeCustomerKey
|
|
3246
|
+
&& latestDisabledKey
|
|
3247
|
+
&& (orderCreatedAt === null || !Number.isFinite(legacyDisabledAt) || legacyDisabledAt >= orderCreatedAt);
|
|
3248
|
+
const blockedByCustomerRevocation = customerRevocation && !isNewSubscriptionAfterRevocation
|
|
3249
|
+
&& (
|
|
3250
|
+
!subscriptionId
|
|
3251
|
+
|| subscriptionId === customerRevocation.subscriptionId
|
|
3252
|
+
|| orderCreatedAt === null
|
|
3253
|
+
|| revocationCreatedAt === null
|
|
3254
|
+
|| revocationCreatedAt >= orderCreatedAt
|
|
3255
|
+
);
|
|
2807
3256
|
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
3257
|
+
if (canceledSubscription || blockedByCustomerRevocation || blockedByLegacyCancellation) {
|
|
3258
|
+
return {
|
|
3259
|
+
key: null,
|
|
3260
|
+
customerId,
|
|
3261
|
+
reused: true,
|
|
3262
|
+
entitlementActive: false,
|
|
3263
|
+
remainingCredits: null,
|
|
3264
|
+
reason: canceledSubscription ? 'subscription_cancelled' : 'customer_entitlement_revoked',
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
if (isNewSubscriptionAfterRevocation) {
|
|
3268
|
+
delete store.customerRevocations[customerId];
|
|
2814
3269
|
}
|
|
2815
|
-
saveKeyStore(store);
|
|
2816
|
-
return { key, customerId, createdAt: meta.createdAt, installId: meta.installId || null, reused: true, remainingCredits: meta.remainingCredits };
|
|
2817
|
-
}
|
|
2818
3270
|
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
3271
|
+
const creditsToAdd = normalizeInteger(opts.credits);
|
|
3272
|
+
if (activeCustomerKey) {
|
|
3273
|
+
const key = activeCustomerKey[0];
|
|
3274
|
+
const meta = activeCustomerKey[1];
|
|
3275
|
+
if (opts.installId && !meta.installId) meta.installId = opts.installId;
|
|
3276
|
+
if (opts.entitlementKind === 'subscription') {
|
|
3277
|
+
meta.remainingCredits = null;
|
|
3278
|
+
meta.entitlementKind = 'subscription';
|
|
3279
|
+
if (subscriptionId) meta.subscriptionId = subscriptionId;
|
|
3280
|
+
} else if (creditsToAdd !== null && meta.remainingCredits !== null) {
|
|
3281
|
+
meta.remainingCredits = (meta.remainingCredits || 0) + creditsToAdd;
|
|
3282
|
+
}
|
|
3283
|
+
if (orderId) {
|
|
3284
|
+
meta.fulfilledOrderIds = Array.isArray(meta.fulfilledOrderIds)
|
|
3285
|
+
? [...new Set([...meta.fulfilledOrderIds, orderId])]
|
|
3286
|
+
: [orderId];
|
|
3287
|
+
}
|
|
3288
|
+
saveKeyStore(store);
|
|
3289
|
+
return { key, customerId, createdAt: meta.createdAt, installId: meta.installId || null, reused: true, remainingCredits: meta.remainingCredits };
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
const key = `tg_${crypto.randomBytes(16).toString('hex')}`;
|
|
3293
|
+
const createdAt = new Date().toISOString();
|
|
3294
|
+
store.keys[key] = {
|
|
3295
|
+
customerId,
|
|
3296
|
+
active: true,
|
|
3297
|
+
usageCount: 0,
|
|
3298
|
+
createdAt,
|
|
3299
|
+
installId: opts.installId || null,
|
|
3300
|
+
source: opts.source || 'provision',
|
|
3301
|
+
subscriptionId: subscriptionId || null,
|
|
3302
|
+
entitlementKind: opts.entitlementKind || null,
|
|
3303
|
+
remainingCredits: creditsToAdd, // null means unlimited (standard subscription)
|
|
3304
|
+
fulfilledOrderIds: orderId ? [orderId] : [],
|
|
3305
|
+
};
|
|
3306
|
+
saveKeyStore(store);
|
|
3307
|
+
return { key, customerId, createdAt, installId: opts.installId || null, remainingCredits: creditsToAdd };
|
|
3308
|
+
});
|
|
2832
3309
|
}
|
|
2833
3310
|
|
|
2834
3311
|
function rotateApiKey(oldKey) {
|
|
2835
3312
|
if (!oldKey) return { rotated: false, reason: 'missing_old_key' };
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
3313
|
+
return withKeyStoreLock(() => {
|
|
3314
|
+
const store = loadKeyStore();
|
|
3315
|
+
const meta = store.keys[oldKey];
|
|
3316
|
+
if (!meta || !meta.active) return { rotated: false, reason: 'key_not_active' };
|
|
3317
|
+
|
|
3318
|
+
meta.active = false;
|
|
3319
|
+
meta.disabledAt = new Date().toISOString();
|
|
3320
|
+
const fulfilledOrderIds = Array.isArray(meta.fulfilledOrderIds) ? [...meta.fulfilledOrderIds] : [];
|
|
3321
|
+
meta.fulfilledOrderIds = [];
|
|
3322
|
+
const newKey = `tg_${crypto.randomBytes(16).toString('hex')}`;
|
|
3323
|
+
store.keys[newKey] = {
|
|
3324
|
+
customerId: meta.customerId,
|
|
3325
|
+
active: true,
|
|
3326
|
+
usageCount: 0,
|
|
3327
|
+
createdAt: new Date().toISOString(),
|
|
3328
|
+
installId: meta.installId,
|
|
3329
|
+
source: 'rotation',
|
|
3330
|
+
replacedKey: oldKey,
|
|
3331
|
+
subscriptionId: meta.subscriptionId || null,
|
|
3332
|
+
entitlementKind: meta.entitlementKind || null,
|
|
3333
|
+
remainingCredits: meta.remainingCredits,
|
|
3334
|
+
fulfilledOrderIds,
|
|
3335
|
+
};
|
|
3336
|
+
saveKeyStore(store);
|
|
3337
|
+
return { rotated: true, key: newKey, oldKey };
|
|
3338
|
+
});
|
|
2855
3339
|
}
|
|
2856
3340
|
|
|
2857
3341
|
function validateApiKey(key) {
|
|
@@ -2876,32 +3360,47 @@ function validateApiKey(key) {
|
|
|
2876
3360
|
}
|
|
2877
3361
|
|
|
2878
3362
|
function recordUsage(key) {
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
3363
|
+
return withKeyStoreLock(() => {
|
|
3364
|
+
const store = loadKeyStore();
|
|
3365
|
+
const meta = store.keys[key];
|
|
3366
|
+
if (meta && meta.active) {
|
|
3367
|
+
const oldVal = meta.usageCount || 0;
|
|
3368
|
+
meta.usageCount = oldVal + 1;
|
|
3369
|
+
|
|
3370
|
+
if (meta.remainingCredits !== undefined && meta.remainingCredits !== null) {
|
|
3371
|
+
meta.remainingCredits = Math.max(0, meta.remainingCredits - 1);
|
|
3372
|
+
}
|
|
2884
3373
|
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
3374
|
+
if (oldVal === 0) appendFunnelEvent({ stage: 'activation', event: 'api_key_first_usage', installId: meta.installId, evidence: key, metadata: { customerId: meta.customerId } });
|
|
3375
|
+
saveKeyStore(store);
|
|
3376
|
+
return { recorded: true, usageCount: meta.usageCount, remainingCredits: meta.remainingCredits };
|
|
2888
3377
|
}
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
saveKeyStore(store);
|
|
2892
|
-
return { recorded: true, usageCount: meta.usageCount, remainingCredits: meta.remainingCredits };
|
|
2893
|
-
}
|
|
2894
|
-
return { recorded: false };
|
|
3378
|
+
return { recorded: false };
|
|
3379
|
+
});
|
|
2895
3380
|
}
|
|
2896
3381
|
|
|
2897
|
-
function disableCustomerKeys(customerId) {
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
3382
|
+
function disableCustomerKeys(customerId, options = {}) {
|
|
3383
|
+
if (!customerId || typeof customerId !== 'string') return { disabledCount: 0 };
|
|
3384
|
+
return withKeyStoreLock(() => {
|
|
3385
|
+
const store = loadKeyStore();
|
|
3386
|
+
let disabledCount = 0;
|
|
3387
|
+
const disabledAt = new Date().toISOString();
|
|
3388
|
+
for (const meta of Object.values(store.keys)) {
|
|
3389
|
+
if (meta.customerId === customerId && meta.active) {
|
|
3390
|
+
meta.active = false;
|
|
3391
|
+
meta.disabledAt = disabledAt;
|
|
3392
|
+
disabledCount++;
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
const subscriptionId = normalizeText(options.subscriptionId);
|
|
3396
|
+
if (subscriptionId) {
|
|
3397
|
+
const eventCreatedAt = normalizeInteger(options.eventCreatedAt) || Math.floor(Date.now() / 1000);
|
|
3398
|
+
store.canceledSubscriptions[subscriptionId] = { customerId, eventCreatedAt, disabledAt };
|
|
3399
|
+
store.customerRevocations[customerId] = { subscriptionId, eventCreatedAt, disabledAt };
|
|
3400
|
+
}
|
|
3401
|
+
if (disabledCount > 0 || subscriptionId) saveKeyStore(store);
|
|
3402
|
+
return { disabledCount };
|
|
3403
|
+
});
|
|
2905
3404
|
}
|
|
2906
3405
|
|
|
2907
3406
|
function verifyWebhookSignature(rawBody, signature) {
|
|
@@ -2957,34 +3456,93 @@ async function handleWebhook(rawBody, signature) {
|
|
|
2957
3456
|
}
|
|
2958
3457
|
|
|
2959
3458
|
switch (event.type) {
|
|
3459
|
+
case 'checkout.session.async_payment_succeeded':
|
|
2960
3460
|
case 'checkout.session.completed': {
|
|
2961
3461
|
const session = event.data.object;
|
|
2962
|
-
const
|
|
3462
|
+
const diagnosticOrder = isDiagnosticCheckoutSession(session);
|
|
3463
|
+
const provisioningGrant = resolveCheckoutProvisioningGrant(session);
|
|
3464
|
+
const paymentConfirmed = session.payment_status === 'paid'
|
|
3465
|
+
|| (!diagnosticOrder
|
|
3466
|
+
&& session.mode === 'subscription'
|
|
3467
|
+
&& session.payment_status === 'no_payment_required');
|
|
3468
|
+
if (!paymentConfirmed) {
|
|
3469
|
+
return {
|
|
3470
|
+
handled: true,
|
|
3471
|
+
action: 'payment_pending',
|
|
3472
|
+
sessionId: session.id || null,
|
|
3473
|
+
paymentStatus: session.payment_status || 'unknown',
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3476
|
+
const customerEmail = session.customer_details?.email || session.customer_email || '';
|
|
3477
|
+
const customerId = resolveStripeCustomerLedgerId(session, customerEmail);
|
|
2963
3478
|
const installId = session.metadata?.installId;
|
|
2964
3479
|
const traceId = session.metadata?.traceId || null;
|
|
2965
|
-
const credits = session.metadata?.credits ? parseInt(session.metadata.credits, 10) : null;
|
|
2966
3480
|
const packId = session.metadata?.packId || null;
|
|
2967
|
-
const customerEmail = session.customer_details?.email || session.customer_email || '';
|
|
2968
3481
|
const customerName = session.customer_details?.name || null;
|
|
2969
3482
|
const trialEndAt = computeTrialEndAt(session);
|
|
2970
3483
|
|
|
2971
3484
|
const attribution = extractAttribution(session.metadata);
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
3485
|
+
// External Stripe Payment Links (the diagnostic/sprint checkouts) carry no
|
|
3486
|
+
// metadata but preserve client_reference_id. When metadata yields no source,
|
|
3487
|
+
// recover it so marketplace-attributed paid checkouts (e.g. utm_source=aiventyx)
|
|
3488
|
+
// are credited/reported instead of silently landing as source=unknown.
|
|
3489
|
+
if (!attribution.source) {
|
|
3490
|
+
const ref = parseCheckoutReference(session.client_reference_id);
|
|
3491
|
+
if (ref?.source) {
|
|
3492
|
+
attribution.source = ref.source;
|
|
3493
|
+
if (!attribution.acquisitionId) attribution.acquisitionId = ref.acquisitionId;
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
let diagnosticClaim = null;
|
|
3497
|
+
if (diagnosticOrder) {
|
|
3498
|
+
diagnosticClaim = claimOrderOperation(session.id, 'diagnostic');
|
|
3499
|
+
if (diagnosticClaim.completed) {
|
|
3500
|
+
return {
|
|
3501
|
+
handled: true,
|
|
3502
|
+
action: 'diagnostic_order_already_processed',
|
|
3503
|
+
result: null,
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
3506
|
+
if (!diagnosticClaim.claimed) {
|
|
3507
|
+
return {
|
|
3508
|
+
handled: false,
|
|
3509
|
+
retryable: true,
|
|
3510
|
+
reason: 'diagnostic_fulfillment_in_progress',
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
const result = provisioningGrant.allowed
|
|
3515
|
+
? provisionApiKey(customerId, {
|
|
3516
|
+
installId,
|
|
3517
|
+
credits: provisioningGrant.credits,
|
|
3518
|
+
orderId: session.id,
|
|
3519
|
+
subscriptionId: session.subscription,
|
|
3520
|
+
orderCreatedAt: session.created,
|
|
3521
|
+
enforceCancellationHistory: provisioningGrant.kind === 'subscription',
|
|
3522
|
+
entitlementKind: provisioningGrant.kind,
|
|
3523
|
+
source: 'stripe_webhook_checkout_completed'
|
|
3524
|
+
})
|
|
3525
|
+
: null;
|
|
3526
|
+
const trialEmail = !result || !result.key
|
|
3527
|
+
? {
|
|
3528
|
+
status: 'skipped',
|
|
3529
|
+
reason: diagnosticOrder
|
|
3530
|
+
? 'diagnostic_order'
|
|
3531
|
+
: result && result.entitlementActive === false
|
|
3532
|
+
? 'entitlement_inactive'
|
|
3533
|
+
: 'non_entitlement_order',
|
|
3534
|
+
}
|
|
3535
|
+
: await sendTrialActivationEmail({
|
|
3536
|
+
sessionId: session.id,
|
|
3537
|
+
customerId,
|
|
3538
|
+
customerEmail,
|
|
3539
|
+
customerName,
|
|
3540
|
+
trialEndAt,
|
|
3541
|
+
apiKey: result.key,
|
|
3542
|
+
planId: session.metadata?.planId || packId || null,
|
|
3543
|
+
appOrigin: process.env.THUMBGATE_PUBLIC_APP_ORIGIN,
|
|
3544
|
+
source: 'stripe_webhook_checkout_completed',
|
|
3545
|
+
});
|
|
2988
3546
|
const funnelRecord = {
|
|
2989
3547
|
stage: 'paid',
|
|
2990
3548
|
event: 'stripe_checkout_completed',
|
|
@@ -2994,6 +3552,9 @@ async function handleWebhook(rawBody, signature) {
|
|
|
2994
3552
|
sessionId: session.id,
|
|
2995
3553
|
traceId,
|
|
2996
3554
|
packId,
|
|
3555
|
+
offerKind: diagnosticOrder
|
|
3556
|
+
? 'workflow_hardening_diagnostic'
|
|
3557
|
+
: provisioningGrant.allowed ? provisioningGrant.kind : 'service_order',
|
|
2997
3558
|
...extractJourneyFields(session.metadata),
|
|
2998
3559
|
...attribution,
|
|
2999
3560
|
},
|
|
@@ -3009,11 +3570,9 @@ async function handleWebhook(rawBody, signature) {
|
|
|
3009
3570
|
});
|
|
3010
3571
|
}
|
|
3011
3572
|
// Write checkout_paid_confirmed event with amount/currency for funnel analytics
|
|
3012
|
-
|
|
3573
|
+
const paidConfirmationRecord = {
|
|
3013
3574
|
stage: 'paid',
|
|
3014
3575
|
event: 'checkout_paid_confirmed',
|
|
3015
|
-
installId,
|
|
3016
|
-
traceId,
|
|
3017
3576
|
evidence: session.id,
|
|
3018
3577
|
metadata: {
|
|
3019
3578
|
source: 'stripe_webhook_checkout_completed',
|
|
@@ -3022,7 +3581,14 @@ async function handleWebhook(rawBody, signature) {
|
|
|
3022
3581
|
customerId,
|
|
3023
3582
|
...funnelRecord.metadata,
|
|
3024
3583
|
},
|
|
3025
|
-
}
|
|
3584
|
+
};
|
|
3585
|
+
if (!hasFunnelEventMatch(loadFunnelLedger(), paidConfirmationRecord)) {
|
|
3586
|
+
appendFunnelEvent({
|
|
3587
|
+
...paidConfirmationRecord,
|
|
3588
|
+
installId,
|
|
3589
|
+
traceId,
|
|
3590
|
+
});
|
|
3591
|
+
}
|
|
3026
3592
|
const revenueRecord = {
|
|
3027
3593
|
provider: 'stripe',
|
|
3028
3594
|
event: 'stripe_checkout_completed',
|
|
@@ -3035,10 +3601,25 @@ async function handleWebhook(rawBody, signature) {
|
|
|
3035
3601
|
mode: session.mode || null,
|
|
3036
3602
|
paymentStatus: session.payment_status || null,
|
|
3037
3603
|
packId,
|
|
3604
|
+
offerKind: diagnosticOrder
|
|
3605
|
+
? 'workflow_hardening_diagnostic'
|
|
3606
|
+
: provisioningGrant.allowed ? provisioningGrant.kind : 'service_order',
|
|
3038
3607
|
},
|
|
3039
3608
|
};
|
|
3040
|
-
|
|
3041
|
-
|
|
3609
|
+
const revenueClaim = claimOrderOperation(session.id, 'revenue');
|
|
3610
|
+
if (!revenueClaim.claimed && !revenueClaim.completed) {
|
|
3611
|
+
if (diagnosticOrder) releaseOrderOperation(diagnosticClaim);
|
|
3612
|
+
return {
|
|
3613
|
+
handled: false,
|
|
3614
|
+
retryable: true,
|
|
3615
|
+
reason: 'payment_record_in_progress',
|
|
3616
|
+
};
|
|
3617
|
+
}
|
|
3618
|
+
const isNewRevenueRecord = revenueClaim.claimed
|
|
3619
|
+
&& !hasRevenueEventMatch(loadRevenueLedger(), revenueRecord);
|
|
3620
|
+
let revenueWriteResult = { written: true, reason: 'already_recorded' };
|
|
3621
|
+
if (isNewRevenueRecord) {
|
|
3622
|
+
revenueWriteResult = appendRevenueEvent({
|
|
3042
3623
|
...revenueRecord,
|
|
3043
3624
|
installId,
|
|
3044
3625
|
traceId,
|
|
@@ -3050,10 +3631,25 @@ async function handleWebhook(rawBody, signature) {
|
|
|
3050
3631
|
attribution,
|
|
3051
3632
|
});
|
|
3052
3633
|
}
|
|
3634
|
+
if (revenueWriteResult.written !== true) {
|
|
3635
|
+
releaseOrderOperation(revenueClaim);
|
|
3636
|
+
if (diagnosticOrder) releaseOrderOperation(diagnosticClaim);
|
|
3637
|
+
return {
|
|
3638
|
+
handled: false,
|
|
3639
|
+
retryable: true,
|
|
3640
|
+
reason: 'payment_record_retry_required',
|
|
3641
|
+
};
|
|
3642
|
+
}
|
|
3643
|
+
if (revenueClaim.claimed) {
|
|
3644
|
+
completeOrderOperation(revenueClaim, {
|
|
3645
|
+
sessionId: session.id,
|
|
3646
|
+
action: isNewRevenueRecord ? 'revenue_recorded' : 'revenue_already_recorded',
|
|
3647
|
+
});
|
|
3648
|
+
}
|
|
3053
3649
|
// Fire Plausible purchase event so the funnel poller can measure
|
|
3054
3650
|
// end-to-end conversion: visitor → CTA → checkout → email → Stripe → purchase.
|
|
3055
3651
|
// Fire-and-forget (never blocks the webhook response).
|
|
3056
|
-
|
|
3652
|
+
const purchaseEventOptions = {
|
|
3057
3653
|
page: '/success',
|
|
3058
3654
|
props: {
|
|
3059
3655
|
sessionId: session.id,
|
|
@@ -3064,7 +3660,69 @@ async function handleWebhook(rawBody, signature) {
|
|
|
3064
3660
|
currency: session.currency || '',
|
|
3065
3661
|
...attribution,
|
|
3066
3662
|
},
|
|
3067
|
-
}
|
|
3663
|
+
};
|
|
3664
|
+
if (isNewRevenueRecord) {
|
|
3665
|
+
if (diagnosticOrder) {
|
|
3666
|
+
void plausibleEvent('Workflow Diagnostic Purchase Completed', purchaseEventOptions);
|
|
3667
|
+
} else {
|
|
3668
|
+
void recordCheckoutFunnelEvent('purchase', purchaseEventOptions);
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
if (diagnosticOrder) {
|
|
3673
|
+
const diagnosticEmails = await sendDiagnosticOrderEmails({
|
|
3674
|
+
session,
|
|
3675
|
+
customerEmail,
|
|
3676
|
+
customerName,
|
|
3677
|
+
attribution,
|
|
3678
|
+
});
|
|
3679
|
+
const deliveryComplete = diagnosticEmails.buyer.sent && diagnosticEmails.operator.sent;
|
|
3680
|
+
const deliveryRetryable = [diagnosticEmails.buyer, diagnosticEmails.operator]
|
|
3681
|
+
.some(isRetryableDiagnosticDelivery);
|
|
3682
|
+
if (!deliveryComplete && deliveryRetryable) {
|
|
3683
|
+
releaseOrderOperation(diagnosticClaim);
|
|
3684
|
+
return {
|
|
3685
|
+
handled: false,
|
|
3686
|
+
retryable: true,
|
|
3687
|
+
paymentRecorded: true,
|
|
3688
|
+
reason: 'diagnostic_fulfillment_retry_required',
|
|
3689
|
+
delivery: {
|
|
3690
|
+
buyer: diagnosticEmails.buyer.reason || diagnosticEmails.buyer.status || 'not_sent',
|
|
3691
|
+
operator: diagnosticEmails.operator.reason || diagnosticEmails.operator.status || 'not_sent',
|
|
3692
|
+
},
|
|
3693
|
+
};
|
|
3694
|
+
}
|
|
3695
|
+
completeOrderOperation(diagnosticClaim, {
|
|
3696
|
+
sessionId: session.id,
|
|
3697
|
+
action: 'diagnostic_order_recorded',
|
|
3698
|
+
fulfillment: deliveryComplete ? 'complete' : 'attention_required',
|
|
3699
|
+
});
|
|
3700
|
+
return {
|
|
3701
|
+
handled: true,
|
|
3702
|
+
action: 'diagnostic_order_recorded',
|
|
3703
|
+
result: null,
|
|
3704
|
+
diagnosticEmails,
|
|
3705
|
+
fulfillment: deliveryComplete ? 'complete' : 'attention_required',
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
if (!provisioningGrant.allowed) {
|
|
3710
|
+
return {
|
|
3711
|
+
handled: true,
|
|
3712
|
+
action: 'paid_service_order_recorded',
|
|
3713
|
+
result: null,
|
|
3714
|
+
trialEmail,
|
|
3715
|
+
};
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
if (result && result.entitlementActive === false) {
|
|
3719
|
+
return {
|
|
3720
|
+
handled: true,
|
|
3721
|
+
action: 'checkout_entitlement_inactive',
|
|
3722
|
+
result,
|
|
3723
|
+
trialEmail,
|
|
3724
|
+
};
|
|
3725
|
+
}
|
|
3068
3726
|
|
|
3069
3727
|
return {
|
|
3070
3728
|
handled: true,
|
|
@@ -3074,9 +3732,25 @@ async function handleWebhook(rawBody, signature) {
|
|
|
3074
3732
|
email: trialEmailToWebhookEmailResult(trialEmail),
|
|
3075
3733
|
};
|
|
3076
3734
|
}
|
|
3735
|
+
case 'checkout.session.async_payment_failed': {
|
|
3736
|
+
const session = event.data.object;
|
|
3737
|
+
return {
|
|
3738
|
+
handled: true,
|
|
3739
|
+
action: 'payment_failed',
|
|
3740
|
+
sessionId: session.id || null,
|
|
3741
|
+
paymentStatus: session.payment_status || 'unpaid',
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3077
3744
|
case 'customer.subscription.deleted': {
|
|
3078
3745
|
const sub = event.data.object;
|
|
3079
|
-
return {
|
|
3746
|
+
return {
|
|
3747
|
+
handled: true,
|
|
3748
|
+
action: 'disabled_customer_keys',
|
|
3749
|
+
result: disableCustomerKeys(sub.customer, {
|
|
3750
|
+
subscriptionId: sub.id,
|
|
3751
|
+
eventCreatedAt: event.created || sub.canceled_at || sub.ended_at,
|
|
3752
|
+
}),
|
|
3753
|
+
};
|
|
3080
3754
|
}
|
|
3081
3755
|
default: return { handled: false, reason: `unhandled_event_type:${event.type}` };
|
|
3082
3756
|
}
|
|
@@ -3217,6 +3891,9 @@ module.exports = {
|
|
|
3217
3891
|
_buildCheckoutSessionPayload: buildCheckoutSessionPayload,
|
|
3218
3892
|
_buildTrialActivationEmail: buildTrialActivationEmail,
|
|
3219
3893
|
_sendTrialActivationEmail: sendTrialActivationEmail,
|
|
3894
|
+
_isDiagnosticCheckoutSession: isDiagnosticCheckoutSession,
|
|
3895
|
+
_resolveCheckoutProvisioningGrant: resolveCheckoutProvisioningGrant,
|
|
3896
|
+
_sendDiagnosticOrderEmails: sendDiagnosticOrderEmails,
|
|
3220
3897
|
_resolveSubscriptionCheckoutSelection: resolveSubscriptionCheckoutSelection,
|
|
3221
3898
|
_verifyActiveProductForPlan: verifyActiveProductForPlan,
|
|
3222
3899
|
_API_KEYS_PATH: () => CONFIG.API_KEYS_PATH,
|
|
@@ -3224,6 +3901,7 @@ module.exports = {
|
|
|
3224
3901
|
_REVENUE_LEDGER_PATH: () => CONFIG.REVENUE_LEDGER_PATH,
|
|
3225
3902
|
_LOCAL_CHECKOUT_SESSIONS_PATH: () => CONFIG.LOCAL_CHECKOUT_SESSIONS_PATH,
|
|
3226
3903
|
_TRIAL_EMAIL_LEDGER_PATH: () => CONFIG.TRIAL_EMAIL_LEDGER_PATH,
|
|
3904
|
+
_ORDER_EMAIL_LEDGER_PATH: () => CONFIG.ORDER_EMAIL_LEDGER_PATH,
|
|
3227
3905
|
_LOCAL_MODE: () => LOCAL_MODE(),
|
|
3228
3906
|
_withTimeout: withTimeout,
|
|
3229
3907
|
// Default to the real Resend-backed mailer so production webhooks send the
|