thumbgate 1.28.4 → 1.29.2

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.
Files changed (87) hide show
  1. package/.claude/commands/dashboard.md +11 -1
  2. package/.claude/commands/thumbgate-dashboard.md +23 -8
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/llms.txt +18 -10
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +66 -3
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/forge/forge.yaml +3 -3
  9. package/adapters/mcp/server-stdio.js +88 -2
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bin/cli.js +8 -8
  12. package/bin/postinstall.js +4 -13
  13. package/commands/dashboard.md +11 -1
  14. package/commands/thumbgate-dashboard.md +23 -8
  15. package/config/agent-outcome-monitor-thresholds.json +63 -0
  16. package/config/evals/agent-outcomes-baseline.json +17 -0
  17. package/config/evals/agent-outcomes-golden.json +412 -0
  18. package/config/evals/prompt-eval-baseline.json +23 -0
  19. package/config/github-about.json +5 -4
  20. package/config/post-deploy-marketing-pages.json +6 -6
  21. package/config/schemas/task-outcome-receipt.schema.json +296 -0
  22. package/docs/integrations/grafana/README.md +109 -0
  23. package/docs/integrations/grafana/thumbgate-revenue-evidence-dashboard.json +1930 -0
  24. package/openapi/openapi.yaml +475 -5
  25. package/package.json +75 -22
  26. package/public/agent-manager.html +10 -11
  27. package/public/agents-cost-savings.html +2 -2
  28. package/public/assets/brand/thumbgate-logo-transparent.svg +6 -11
  29. package/public/assets/brand/thumbgate-mark-inline-v3.svg +11 -10
  30. package/public/assets/brand/thumbgate-mark.svg +10 -11
  31. package/public/blog/inside-your-boundary.html +114 -0
  32. package/public/blog/process-over-outcome-gates.html +119 -0
  33. package/public/blog.html +296 -402
  34. package/public/brand/thumbgate-mark.svg +5 -9
  35. package/public/codex-enterprise.html +2 -2
  36. package/public/compare.html +12 -3
  37. package/public/diagnostic.html +79 -29
  38. package/public/guide.html +4 -4
  39. package/public/index.html +1090 -2098
  40. package/public/install.html +3 -3
  41. package/public/js/buyer-intent.js +33 -18
  42. package/public/numbers.html +2 -2
  43. package/public/pricing.html +268 -408
  44. package/public/pro.html +4 -4
  45. package/scripts/agent-outcome-eval.js +130 -0
  46. package/scripts/agent-outcome-monitor.js +261 -0
  47. package/scripts/agent-reasoning-traces.js +8 -9
  48. package/scripts/async-job-runner.js +107 -13
  49. package/scripts/billing.js +456 -126
  50. package/scripts/buyer-paths.js +102 -0
  51. package/scripts/cli-feedback.js +2 -2
  52. package/scripts/commercial-offer.js +18 -10
  53. package/scripts/durability/step.js +121 -12
  54. package/scripts/external-customer-audit.js +881 -0
  55. package/scripts/feedback-loop.js +26 -0
  56. package/scripts/gates-engine.js +554 -19
  57. package/scripts/grafana-revenue-evidence.js +856 -0
  58. package/scripts/human-escalation.js +265 -0
  59. package/scripts/hybrid-feedback-context.js +93 -50
  60. package/scripts/jsonl-window.js +89 -0
  61. package/scripts/judge-reward-function.js +30 -18
  62. package/scripts/lesson-embedding-index.js +3 -7
  63. package/scripts/meta-agent-loop.js +20 -2
  64. package/scripts/observability-env.js +139 -0
  65. package/scripts/observability-setup.js +55 -0
  66. package/scripts/plausible-domain-config.js +4 -0
  67. package/scripts/prompt-eval.js +81 -4
  68. package/scripts/provider-live-evidence.js +1290 -0
  69. package/scripts/provider-payment-reconciler.js +442 -0
  70. package/scripts/provider-revenue-evidence.js +249 -0
  71. package/scripts/rate-limiter.js +1 -5
  72. package/scripts/revenue-action-eligibility.js +414 -0
  73. package/scripts/revenue-evidence-remediation.js +694 -0
  74. package/scripts/revenue-offer-system.js +709 -0
  75. package/scripts/sales-pipeline.js +1117 -0
  76. package/scripts/schedule-manager.js +249 -0
  77. package/scripts/seo-gsd.js +8 -4
  78. package/scripts/stripe-credentials.js +37 -0
  79. package/scripts/stripe-revenue-catalog-audit.js +363 -0
  80. package/scripts/stripe-revenue-catalog.js +164 -0
  81. package/scripts/task-outcomes.js +425 -0
  82. package/scripts/telemetry-analytics.js +23 -3
  83. package/scripts/tool-contract-validator.js +287 -59
  84. package/scripts/tool-registry.js +143 -0
  85. package/scripts/vector-store.js +83 -7
  86. package/scripts/workflow-intake-queue.js +483 -0
  87. package/src/api/server.js +647 -118
@@ -1,17 +1,16 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * billing.js — Stripe billing integration using official Stripe SDK.
4
- */
5
-
6
2
  'use strict';
7
3
 
8
4
  const STRIPE_TIMEOUT_MS = 5000;
9
5
  const STRIPE_RECONCILIATION_SUMMARY_TIMEOUT_MS = 3500;
10
6
  function withTimeout(promise, ms = STRIPE_TIMEOUT_MS) {
11
- return Promise.race([
12
- promise,
13
- new Promise((_, reject) => setTimeout(() => reject(new Error(`Stripe API timeout after ${ms}ms`)), ms)),
14
- ]);
7
+ return new Promise((resolve, reject) => {
8
+ const timeout = setTimeout(() => reject(new Error(`Stripe API timeout after ${ms}ms`)), ms);
9
+ Promise.resolve(promise).then(
10
+ (value) => { clearTimeout(timeout); resolve(value); },
11
+ (error) => { clearTimeout(timeout); reject(error); },
12
+ );
13
+ });
15
14
  }
16
15
 
17
16
  const fs = require('fs');
@@ -63,14 +62,14 @@ function loadWorkflowSprintIntakeModule() {
63
62
  return require(modulePath);
64
63
  }
65
64
 
66
- // ---------------------------------------------------------------------------
67
- // Config
68
- // ---------------------------------------------------------------------------
69
-
70
65
  const CONFIG = {
71
66
  STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY || '',
72
67
  STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET || '',
73
68
  GITHUB_MARKETPLACE_WEBHOOK_SECRET: process.env.GITHUB_MARKETPLACE_WEBHOOK_SECRET || '',
69
+ PAYPAL_CLIENT_ID: process.env.THUMBGATE_PAYPAL_CLIENT_ID || process.env.PAYPAL_CLIENT_ID || '',
70
+ PAYPAL_CLIENT_SECRET: process.env.THUMBGATE_PAYPAL_CLIENT_SECRET || process.env.PAYPAL_CLIENT_SECRET || '',
71
+ PAYPAL_WEBHOOK_ID: process.env.THUMBGATE_PAYPAL_WEBHOOK_ID || '',
72
+ PAYPAL_API_BASE_URL: process.env.THUMBGATE_PAYPAL_API_BASE_URL || 'https://api-m.paypal.com',
74
73
  GITHUB_MARKETPLACE_PLAN_PRICES_JSON: process.env.THUMBGATE_GITHUB_MARKETPLACE_PLAN_PRICES_JSON || '',
75
74
  STRIPE_PRICE_ID: process.env.STRIPE_PRICE_ID || PRO_MONTHLY_PRICE_ID,
76
75
  STRIPE_PRICE_ID_PRO_MONTHLY: process.env.STRIPE_PRICE_ID_PRO_MONTHLY || PRO_MONTHLY_PRICE_ID,
@@ -86,6 +85,16 @@ const CONFIG = {
86
85
  get REVENUE_LEDGER_PATH() {
87
86
  return process.env._TEST_REVENUE_LEDGER_PATH || process.env.THUMBGATE_REVENUE_LEDGER_PATH || path.join(getFeedbackPaths().FEEDBACK_DIR, 'revenue-events.jsonl');
88
87
  },
88
+ get GITHUB_MARKETPLACE_WEBHOOK_LEDGER_PATH() {
89
+ return process.env._TEST_GITHUB_MARKETPLACE_WEBHOOK_LEDGER_PATH ||
90
+ process.env.THUMBGATE_GITHUB_MARKETPLACE_WEBHOOK_LEDGER_PATH ||
91
+ path.join(getFeedbackPaths().FEEDBACK_DIR, 'github-marketplace-webhook-deliveries.jsonl');
92
+ },
93
+ get PAYPAL_WEBHOOK_LEDGER_PATH() {
94
+ return process.env._TEST_PAYPAL_WEBHOOK_LEDGER_PATH ||
95
+ process.env.THUMBGATE_PAYPAL_WEBHOOK_LEDGER_PATH ||
96
+ path.join(getFeedbackPaths().FEEDBACK_DIR, 'paypal-webhook-deliveries.jsonl');
97
+ },
89
98
  get LOCAL_CHECKOUT_SESSIONS_PATH() {
90
99
  return process.env._TEST_LOCAL_CHECKOUT_SESSIONS_PATH || path.join(getFeedbackPaths().FEEDBACK_DIR, 'local-checkout-sessions.json');
91
100
  },
@@ -144,6 +153,8 @@ const IS_TEST = !!(
144
153
  process.env._TEST_API_KEYS_PATH ||
145
154
  process.env._TEST_FUNNEL_LEDGER_PATH ||
146
155
  process.env._TEST_REVENUE_LEDGER_PATH ||
156
+ process.env._TEST_GITHUB_MARKETPLACE_WEBHOOK_LEDGER_PATH ||
157
+ process.env._TEST_PAYPAL_WEBHOOK_LEDGER_PATH ||
147
158
  process.env._TEST_LOCAL_CHECKOUT_SESSIONS_PATH ||
148
159
  process.env.NODE_ENV === 'test'
149
160
  );
@@ -152,6 +163,10 @@ function allowUnsignedStripeWebhooks() {
152
163
  return IS_TEST && process.env.THUMBGATE_ALLOW_UNSIGNED_STRIPE_WEBHOOKS === '1';
153
164
  }
154
165
 
166
+ function allowUnsignedGithubWebhooks() {
167
+ return IS_TEST && process.env.THUMBGATE_ALLOW_UNSIGNED_GITHUB_WEBHOOKS === '1';
168
+ }
169
+
155
170
  function shouldMergeLegacyBillingData() {
156
171
  return process.env._TEST_INCLUDE_LEGACY_BILLING_DATA === '1'
157
172
  || process.env.THUMBGATE_INCLUDE_LEGACY_BILLING_DATA === '1';
@@ -170,10 +185,6 @@ function safeCompareHex(expectedHex, actualHex) {
170
185
  }
171
186
  }
172
187
 
173
- // ---------------------------------------------------------------------------
174
- // Internal helpers
175
- // ---------------------------------------------------------------------------
176
-
177
188
  function sanitizeMetadata(metadata) {
178
189
  if (!metadata || typeof metadata !== 'object') return {};
179
190
  try {
@@ -458,41 +469,17 @@ function buildCheckoutProductData({ name, description, appOrigin, planId }) {
458
469
  };
459
470
  }
460
471
 
461
- /**
462
- * Verify an ACTIVE Stripe product exists for the given plan name before
463
- * we let buildSubscriptionPriceData create inline price_data under it.
464
- *
465
- * Stripe matches product_data by `name`. If only an archived product
466
- * matches, new prices created via that path inherit active=false and the
467
- * generated checkout URL renders "Something went wrong / The page you
468
- * were looking for could not be found." for the buyer. Stripe Dashboard
469
- * shows the session as `open` with no email captured — looks like the
470
- * buyer abandoned, but they were never given a working page.
471
- *
472
- * Failing here surfaces the misconfiguration at the first checkout
473
- * attempt instead of silently breaking every buyer for days.
474
- *
475
- * Verified incident: ThumbGate#2188 (May 2026) — 20 sessions abandoned in
476
- * 7 days, all because the only product named "ThumbGate Pro" matching the
477
- * inline product_data was archived (prod_UXxOHAfbDsPyRb), while an active
478
- * product with the same name existed (prod_UW82THPxfNvwKT) that should
479
- * have been used instead.
480
- */
481
472
  async function verifyActiveProductForPlan(stripe, planId) {
482
473
  const expectedName = planId === 'team' ? 'ThumbGate Team' : 'ThumbGate Pro';
483
474
  let products;
484
475
  try {
485
476
  products = await stripe.products.list({ limit: 100 });
486
477
  } catch (err) {
487
- // Network/transient failures shouldn't block checkout creation.
488
- // The original session.create call will surface real Stripe errors.
489
478
  return;
490
479
  }
491
480
  const matching = (products && products.data ? products.data : [])
492
481
  .filter((p) => p && p.name === expectedName);
493
482
  if (matching.length === 0) {
494
- // No product with this name exists; Stripe will create a new one when
495
- // session.create fires with inline product_data. Safe path.
496
483
  return;
497
484
  }
498
485
  const hasActive = matching.some((p) => p.active === true);
@@ -566,14 +553,6 @@ function appendTrialEmailRecord(payload) {
566
553
  });
567
554
  }
568
555
 
569
- /**
570
- * Resolve the trial expiry date for a Stripe checkout session.
571
- *
572
- * Prefers an explicit `subscription.trial_end` unix timestamp when the session
573
- * embeds one (subscriptions with trial_period_days populate it). Falls back to
574
- * the session's `expires_at`, and finally to now + 7 days. Always returns a
575
- * Date; never throws.
576
- */
577
556
  function computeTrialEndAt(session) {
578
557
  const TRIAL_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
579
558
  if (session && session.subscription && typeof session.subscription === 'object') {
@@ -796,10 +775,6 @@ async function sendTrialActivationEmail(params = {}, options = {}) {
796
775
  });
797
776
  if (!response || response.sent !== true) {
798
777
  const rawReason = normalizeText(response && response.reason) || 'provider_error';
799
- // Normalize the mailer module's `no_api_key` to billing.js's legacy
800
- // `missing_resend_api_key` reason so downstream consumers (dashboards,
801
- // tests, support tooling) see a stable vocabulary regardless of which
802
- // transport produced the skip.
803
778
  const reason = rawReason === 'no_api_key' ? 'missing_resend_api_key' : rawReason;
804
779
  const isSkipped = reason === 'missing_resend_api_key';
805
780
  const previousSkipped = isSkipped
@@ -894,22 +869,10 @@ function isDiagnosticCheckoutSession(session = {}) {
894
869
  const paymentLinkId = typeof session.payment_link === 'string'
895
870
  ? session.payment_link
896
871
  : 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
872
  return session.mode === 'payment'
901
873
  && configuredIds.includes(paymentLinkId);
902
874
  }
903
875
 
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
876
  function resolveCheckoutProvisioningGrant(session = {}) {
914
877
  if (session.mode === 'subscription') {
915
878
  return { allowed: true, kind: 'subscription', credits: null };
@@ -1334,7 +1297,14 @@ function resolveRevenueEventKey(entry = {}) {
1334
1297
  );
1335
1298
  }
1336
1299
 
1337
- function isQualifiedWorkflowSprintLead(entry = {}) {
1300
+ const QUALIFIED_WORKFLOW_SPRINT_STATUSES = new Set([
1301
+ 'qualified',
1302
+ 'named_pilot',
1303
+ 'proof_backed_run',
1304
+ 'paid_team',
1305
+ ]);
1306
+
1307
+ function isCompleteWorkflowSprintIntake(entry = {}) {
1338
1308
  return Boolean(
1339
1309
  normalizeText(entry.contact && entry.contact.email) &&
1340
1310
  normalizeText(entry.qualification && entry.qualification.workflow) &&
@@ -1344,6 +1314,13 @@ function isQualifiedWorkflowSprintLead(entry = {}) {
1344
1314
  );
1345
1315
  }
1346
1316
 
1317
+ function isQualifiedWorkflowSprintLead(entry = {}, workflowSprintIntake = null) {
1318
+ return isCompleteWorkflowSprintIntake(entry)
1319
+ && typeof workflowSprintIntake?.isEvidenceBasedQualificationReview === 'function'
1320
+ && workflowSprintIntake.isEvidenceBasedQualificationReview(entry.qualificationReview)
1321
+ && QUALIFIED_WORKFLOW_SPRINT_STATUSES.has(normalizeText(entry.status));
1322
+ }
1323
+
1347
1324
  function isOperatorGeneratedAcquisitionEntry(entry = {}) {
1348
1325
  const metadata = sanitizeMetadata(entry.metadata);
1349
1326
  const attribution = extractAttribution({
@@ -1508,8 +1485,6 @@ function repairGithubMarketplaceRevenueLedger(options = {}) {
1508
1485
  const resolvedAt = new Date().toISOString();
1509
1486
  const repairs = [];
1510
1487
 
1511
- // Pass 1: in-place repair of rows already in revenue-events.jsonl that have
1512
- // unknown amounts but resolvable plan metadata.
1513
1488
  const updatedRows = rows.map((entry) => {
1514
1489
  const result = resolveGithubMarketplaceRevenueEntry(entry, {
1515
1490
  annotate: true,
@@ -1532,12 +1507,6 @@ function repairGithubMarketplaceRevenueLedger(options = {}) {
1532
1507
  return result.entry;
1533
1508
  });
1534
1509
 
1535
- // Pass 2: append rows for funnel-derived paid github_marketplace events that
1536
- // never landed in the revenue ledger. The webhook handler at handleGithubWebhook
1537
- // skips appendRevenueEvent when hasRevenueEventMatch is true, so duplicates from
1538
- // a re-run are already prevented; the only way an order gets here is if the
1539
- // revenue write was skipped at webhook time (e.g. funnel pre-existed, planPricing
1540
- // had unknown amount at the time, or the row was created via a different path).
1541
1510
  const funnelRecords = loadFunnelLedger().filter(
1542
1511
  (e) =>
1543
1512
  e &&
@@ -1554,10 +1523,6 @@ function repairGithubMarketplaceRevenueLedger(options = {}) {
1554
1523
  annotate: true,
1555
1524
  resolvedAt,
1556
1525
  });
1557
- // Skip funnel-derived rows that still have unknown amounts after resolving —
1558
- // we don't want to permanently bake amountKnown:false rows when there's no
1559
- // pricing data to attach. Better to leave them off-disk and keep the
1560
- // read-time merge in loadResolvedRevenueEvents covering them.
1561
1526
  if (!resolved.changed || !resolved.entry.amountKnown) continue;
1562
1527
 
1563
1528
  const persisted = {
@@ -2404,11 +2369,14 @@ function getBusinessAnalytics(options = {}) {
2404
2369
  const workflowSprintLeadByCreator = {};
2405
2370
  const workflowSprintLeadByCommunity = {};
2406
2371
  const workflowSprintLeadByRuntime = {};
2372
+ const completeWorkflowSprintIntakeBySource = {};
2373
+ const completeWorkflowSprintIntakeByCreator = {};
2407
2374
  const qualifiedWorkflowSprintLeadBySource = {};
2408
2375
  const qualifiedWorkflowSprintLeadByCreator = {};
2409
2376
  let workflowSprintLeadLatest = null;
2410
2377
  let workflowSprintLeadLatestAt = null;
2411
2378
  let workflowSprintLeadContactable = 0;
2379
+ let completeWorkflowSprintIntakeCount = 0;
2412
2380
  let qualifiedWorkflowSprintLeadCount = 0;
2413
2381
  const newsletterBySource = {};
2414
2382
  const newsletterByCampaign = {};
@@ -2435,7 +2403,15 @@ function getBusinessAnalytics(options = {}) {
2435
2403
  if (entry.contact?.email) {
2436
2404
  workflowSprintLeadContactable += 1;
2437
2405
  }
2438
- if (isQualifiedWorkflowSprintLead(entry)) {
2406
+ if (isCompleteWorkflowSprintIntake(entry)) {
2407
+ completeWorkflowSprintIntakeCount += 1;
2408
+ incrementCounter(
2409
+ completeWorkflowSprintIntakeBySource,
2410
+ resolveAttributionSource(attribution, 'workflow_sprint_intake')
2411
+ );
2412
+ incrementCounter(completeWorkflowSprintIntakeByCreator, attribution.creator);
2413
+ }
2414
+ if (isQualifiedWorkflowSprintLead(entry, workflowSprintIntake)) {
2439
2415
  qualifiedWorkflowSprintLeadCount += 1;
2440
2416
  incrementCounter(
2441
2417
  qualifiedWorkflowSprintLeadBySource,
@@ -2614,6 +2590,11 @@ function getBusinessAnalytics(options = {}) {
2614
2590
  latestLeadAt: workflowSprintLeadLatestAt,
2615
2591
  latestLead: workflowSprintLeadLatest,
2616
2592
  },
2593
+ completeWorkflowSprintIntakes: {
2594
+ total: completeWorkflowSprintIntakeCount,
2595
+ bySource: completeWorkflowSprintIntakeBySource,
2596
+ byCreator: completeWorkflowSprintIntakeByCreator,
2597
+ },
2617
2598
  qualifiedWorkflowSprintLeads: {
2618
2599
  total: qualifiedWorkflowSprintLeadCount,
2619
2600
  bySource: qualifiedWorkflowSprintLeadBySource,
@@ -2778,30 +2759,49 @@ function getBillingSummary(options = {}) {
2778
2759
  };
2779
2760
  }
2780
2761
 
2781
- async function getBillingSummaryLive(options = {}) {
2762
+ function buildBillingSummaryFailure(error) {
2763
+ const isTimeout = error && error.message && error.message.includes('Stripe API timeout');
2764
+ return {
2765
+ error: isTimeout ? 'stripe_timeout' : 'billing_summary_error',
2766
+ message: error && error.message ? error.message : 'Unknown error',
2767
+ revenue: { total: 0, mrr: 0, events: [] },
2768
+ usage: { totalUsage: 0, bySource: {}, activeBySource: {} },
2769
+ customers: [],
2770
+ };
2771
+ }
2772
+
2773
+ async function getBillingSummariesLive(optionsByWindow = {}, {
2774
+ listStripeReconciledRevenueEventsFn = listStripeReconciledRevenueEvents,
2775
+ } = {}) {
2776
+ const entries = Object.entries(optionsByWindow || {});
2777
+ if (entries.length === 0) return {};
2778
+
2782
2779
  try {
2783
- const reconciliationTimeoutMs = normalizeInteger(options.stripeReconciliationTimeoutMs)
2780
+ const reconciliationTimeoutMs = entries
2781
+ .map(([, options]) => normalizeInteger(options && options.stripeReconciliationTimeoutMs))
2782
+ .find((value) => value !== null)
2784
2783
  || STRIPE_RECONCILIATION_SUMMARY_TIMEOUT_MS;
2785
2784
  const extraRevenueEvents = await withTimeout(
2786
- listStripeReconciledRevenueEvents(),
2785
+ listStripeReconciledRevenueEventsFn(),
2787
2786
  reconciliationTimeoutMs
2788
2787
  ).catch(() => []);
2789
- return getBillingSummary({
2790
- ...options,
2791
- extraRevenueEvents,
2792
- });
2788
+ return Object.fromEntries(entries.map(([key, options]) => [
2789
+ key,
2790
+ getBillingSummary({
2791
+ ...(options || {}),
2792
+ extraRevenueEvents,
2793
+ }),
2794
+ ]));
2793
2795
  } catch (err) {
2794
- const isTimeout = err && err.message && err.message.includes('Stripe API timeout');
2795
- return {
2796
- error: isTimeout ? 'stripe_timeout' : 'billing_summary_error',
2797
- message: err && err.message ? err.message : 'Unknown error',
2798
- revenue: { total: 0, mrr: 0, events: [] },
2799
- usage: { totalUsage: 0, bySource: {}, activeBySource: {} },
2800
- customers: [],
2801
- };
2796
+ return Object.fromEntries(entries.map(([key]) => [key, buildBillingSummaryFailure(err)]));
2802
2797
  }
2803
2798
  }
2804
2799
 
2800
+ async function getBillingSummaryLive(options = {}, runtime = {}) {
2801
+ const summaries = await getBillingSummariesLive({ summary: options }, runtime);
2802
+ return summaries.summary;
2803
+ }
2804
+
2805
2805
  function loadKeyStore() {
2806
2806
  try {
2807
2807
  const primary = CONFIG.API_KEYS_PATH;
@@ -2875,10 +2875,6 @@ function withKeyStoreLock(mutator) {
2875
2875
  }
2876
2876
  }
2877
2877
 
2878
- // ---------------------------------------------------------------------------
2879
- // Core Exports
2880
- // ---------------------------------------------------------------------------
2881
-
2882
2878
  async function createCheckoutSession({ successUrl, cancelUrl, customerEmail, installId, traceId, packId = null, metadata = {}, appOrigin } = {}) {
2883
2879
  const resolvedTraceId = traceId || metadata.traceId || createTraceId('checkout');
2884
2880
  const baseCheckoutMetadata = sanitizeMetadata({
@@ -2928,13 +2924,6 @@ async function createCheckoutSession({ successUrl, cancelUrl, customerEmail, ins
2928
2924
 
2929
2925
  const stripe = getStripeClient();
2930
2926
 
2931
- // Defensive guard against ThumbGate#2188:
2932
- // When buildSubscriptionPriceData passes inline `product_data` to Stripe,
2933
- // Stripe name-matches existing products. If the only existing product with
2934
- // that name is ARCHIVED (active=false), the new price inherits active=false
2935
- // and every Stripe checkout page renders "page not found" for the buyer.
2936
- // That bug burnt 20+ silent abandoned sessions in May 2026. Fail fast
2937
- // instead of letting the broken page ship.
2938
2927
  if (!packId) {
2939
2928
  await verifyActiveProductForPlan(stripe, checkoutSelection.planId);
2940
2929
  }
@@ -3344,7 +3333,6 @@ function validateApiKey(key) {
3344
3333
  const meta = store.keys[key];
3345
3334
  if (!meta || !meta.active) return { valid: false };
3346
3335
 
3347
- // Check if credits are exhausted
3348
3336
  if (meta.remainingCredits !== undefined && meta.remainingCredits !== null && meta.remainingCredits <= 0) {
3349
3337
  return { valid: false, reason: 'credits_exhausted' };
3350
3338
  }
@@ -3407,7 +3395,6 @@ function verifyWebhookSignature(rawBody, signature) {
3407
3395
  if (!CONFIG.STRIPE_WEBHOOK_SECRET) return allowUnsignedStripeWebhooks();
3408
3396
  if (!signature || !rawBody) return false;
3409
3397
 
3410
- // Stripe signature format: t=<timestamp>,v1=<hmac>,...
3411
3398
  const parts = { v1: [] };
3412
3399
  for (const part of signature.split(',')) {
3413
3400
  const [k, v] = part.split('=');
@@ -3421,7 +3408,6 @@ function verifyWebhookSignature(rawBody, signature) {
3421
3408
 
3422
3409
  if (!parts.t || !Array.isArray(parts.v1) || parts.v1.length === 0) return false;
3423
3410
 
3424
- // Timestamp tolerance: +/- 5 minutes
3425
3411
  const timestamp = parseInt(parts.t, 10);
3426
3412
  const now = Math.floor(Date.now() / 1000);
3427
3413
  if (isNaN(timestamp) || Math.abs(now - timestamp) > 300) return false;
@@ -3447,8 +3433,6 @@ async function handleWebhook(rawBody, signature) {
3447
3433
  const stripe = getStripeClient();
3448
3434
  event = stripe.webhooks.constructEvent(rawBody, signature, CONFIG.STRIPE_WEBHOOK_SECRET);
3449
3435
  } else {
3450
- // No webhook secret configured — signature was already checked by verifyWebhookSignature
3451
- // (which is also lenient when no secret). Parse the raw body directly.
3452
3436
  event = JSON.parse(rawBody.toString('utf-8'));
3453
3437
  }
3454
3438
  } catch (err) {
@@ -3482,10 +3466,6 @@ async function handleWebhook(rawBody, signature) {
3482
3466
  const trialEndAt = computeTrialEndAt(session);
3483
3467
 
3484
3468
  const attribution = extractAttribution(session.metadata);
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
3469
  if (!attribution.source) {
3490
3470
  const ref = parseCheckoutReference(session.client_reference_id);
3491
3471
  if (ref?.source) {
@@ -3569,7 +3549,6 @@ async function handleWebhook(rawBody, signature) {
3569
3549
  metadata: funnelRecord.metadata,
3570
3550
  });
3571
3551
  }
3572
- // Write checkout_paid_confirmed event with amount/currency for funnel analytics
3573
3552
  const paidConfirmationRecord = {
3574
3553
  stage: 'paid',
3575
3554
  event: 'checkout_paid_confirmed',
@@ -3646,9 +3625,6 @@ async function handleWebhook(rawBody, signature) {
3646
3625
  action: isNewRevenueRecord ? 'revenue_recorded' : 'revenue_already_recorded',
3647
3626
  });
3648
3627
  }
3649
- // Fire Plausible purchase event so the funnel poller can measure
3650
- // end-to-end conversion: visitor → CTA → checkout → email → Stripe → purchase.
3651
- // Fire-and-forget (never blocks the webhook response).
3652
3628
  const purchaseEventOptions = {
3653
3629
  page: '/success',
3654
3630
  props: {
@@ -3757,7 +3733,7 @@ async function handleWebhook(rawBody, signature) {
3757
3733
  }
3758
3734
 
3759
3735
  function verifyGithubWebhookSignature(rawBody, signature) {
3760
- if (!CONFIG.GITHUB_MARKETPLACE_WEBHOOK_SECRET) return true;
3736
+ if (!CONFIG.GITHUB_MARKETPLACE_WEBHOOK_SECRET) return allowUnsignedGithubWebhooks();
3761
3737
  if (!signature || !rawBody) return false;
3762
3738
  const expected = crypto.createHmac('sha256', CONFIG.GITHUB_MARKETPLACE_WEBHOOK_SECRET).update(rawBody).digest('hex');
3763
3739
  const digest = Buffer.from(`sha256=${expected}`, 'utf8');
@@ -3765,7 +3741,359 @@ function verifyGithubWebhookSignature(rawBody, signature) {
3765
3741
  return checksum.length === digest.length && crypto.timingSafeEqual(digest, checksum);
3766
3742
  }
3767
3743
 
3768
- function buildGithubMarketplaceRevenueMetadata(marketplacePurchase = {}, marketplaceOrderId, planPricing = {}) {
3744
+ const PAYPAL_REVENUE_WEBHOOK_EVENT_TYPES = new Set([
3745
+ 'PAYMENT.CAPTURE.COMPLETED',
3746
+ 'PAYMENT.CAPTURE.REFUNDED',
3747
+ 'PAYMENT.CAPTURE.REVERSED',
3748
+ ]);
3749
+ const PAYPAL_WEBHOOK_REPLAY_WINDOW_MS = 4 * 24 * 60 * 60 * 1000;
3750
+ const PAYPAL_WEBHOOK_FUTURE_SKEW_MS = 5 * 60 * 1000;
3751
+ const PAYPAL_API_HOSTS = new Set(['api-m.paypal.com', 'api-m.sandbox.paypal.com']);
3752
+ const PAYPAL_WEBHOOK_MAX_BYTES = 1024 * 1024;
3753
+
3754
+ function normalizePayPalTransmissionHeaders(headers = {}) {
3755
+ const entries = Object.fromEntries(Object.entries(headers || {}).map(([key, value]) => [
3756
+ String(key).trim().toLowerCase(),
3757
+ Array.isArray(value) ? value[0] : value,
3758
+ ]));
3759
+ const read = (name) => normalizeText(entries[name]);
3760
+ return {
3761
+ authAlgo: read('paypal-auth-algo'),
3762
+ certUrl: read('paypal-cert-url'),
3763
+ transmissionId: read('paypal-transmission-id'),
3764
+ transmissionSig: read('paypal-transmission-sig'),
3765
+ transmissionTime: read('paypal-transmission-time'),
3766
+ };
3767
+ }
3768
+
3769
+ function resolvePayPalWebhookVerifierConfig(overrides = {}) {
3770
+ const clientId = normalizeText(overrides.clientId || CONFIG.PAYPAL_CLIENT_ID);
3771
+ const clientSecret = normalizeText(overrides.clientSecret || CONFIG.PAYPAL_CLIENT_SECRET);
3772
+ const webhookId = normalizeText(overrides.webhookId || CONFIG.PAYPAL_WEBHOOK_ID);
3773
+ const apiBaseUrl = normalizeText(overrides.apiBaseUrl || CONFIG.PAYPAL_API_BASE_URL);
3774
+ if (!clientId || !clientSecret || !webhookId) {
3775
+ return {
3776
+ configured: false,
3777
+ reason: 'paypal_webhook_verification_not_configured',
3778
+ };
3779
+ }
3780
+ let parsedBaseUrl;
3781
+ try {
3782
+ parsedBaseUrl = new URL(apiBaseUrl);
3783
+ } catch {
3784
+ return { configured: false, reason: 'invalid_paypal_api_base_url' };
3785
+ }
3786
+ if (parsedBaseUrl.protocol !== 'https:' || !PAYPAL_API_HOSTS.has(parsedBaseUrl.hostname)) {
3787
+ return { configured: false, reason: 'untrusted_paypal_api_base_url' };
3788
+ }
3789
+ if (!/^[A-Za-z0-9]+$/.test(webhookId) || webhookId.length > 50) {
3790
+ return { configured: false, reason: 'invalid_paypal_webhook_id' };
3791
+ }
3792
+ return {
3793
+ configured: true,
3794
+ clientId,
3795
+ clientSecret,
3796
+ webhookId,
3797
+ apiBaseUrl: parsedBaseUrl.toString(),
3798
+ };
3799
+ }
3800
+
3801
+ async function readPayPalJsonResponse(response) {
3802
+ try {
3803
+ return await response.json();
3804
+ } catch {
3805
+ return null;
3806
+ }
3807
+ }
3808
+
3809
+ async function verifyPayPalWebhookSignature({
3810
+ rawBody,
3811
+ headers,
3812
+ fetchImpl = globalThis.fetch,
3813
+ now = new Date().toISOString(),
3814
+ ...configOverrides
3815
+ } = {}) {
3816
+ const config = resolvePayPalWebhookVerifierConfig(configOverrides);
3817
+ if (!config.configured) return { verified: false, reason: config.reason };
3818
+ if (typeof fetchImpl !== 'function') return { verified: false, reason: 'paypal_verification_fetch_unavailable' };
3819
+ const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody || '');
3820
+ if (body.length === 0) return { verified: false, reason: 'missing_paypal_webhook_body' };
3821
+ if (body.length > PAYPAL_WEBHOOK_MAX_BYTES) return { verified: false, reason: 'paypal_webhook_body_too_large' };
3822
+
3823
+ let event;
3824
+ try {
3825
+ event = JSON.parse(body.toString('utf8'));
3826
+ } catch {
3827
+ return { verified: false, reason: 'invalid_paypal_webhook_json' };
3828
+ }
3829
+ const eventId = normalizeText(event && event.id);
3830
+ const eventType = normalizeText(event && event.event_type);
3831
+ const eventCreatedAt = normalizeText(event && event.create_time);
3832
+ const eventCreatedMs = new Date(eventCreatedAt || '').getTime();
3833
+ if (!eventId || eventId.length > 128 || !Number.isFinite(eventCreatedMs) ||
3834
+ !event.resource || typeof event.resource !== 'object' || Array.isArray(event.resource) ||
3835
+ !PAYPAL_REVENUE_WEBHOOK_EVENT_TYPES.has(eventType)) {
3836
+ return { verified: false, reason: 'invalid_or_unsupported_paypal_webhook_event' };
3837
+ }
3838
+
3839
+ const transmission = normalizePayPalTransmissionHeaders(headers);
3840
+ if (Object.values(transmission).some((value) => !value)) {
3841
+ return { verified: false, reason: 'missing_paypal_transmission_headers' };
3842
+ }
3843
+ let certUrl;
3844
+ try {
3845
+ certUrl = new URL(transmission.certUrl);
3846
+ } catch {
3847
+ return { verified: false, reason: 'invalid_paypal_cert_url' };
3848
+ }
3849
+ if (certUrl.protocol !== 'https:') return { verified: false, reason: 'invalid_paypal_cert_url' };
3850
+ const nowMs = new Date(now).getTime();
3851
+ const transmissionMs = new Date(transmission.transmissionTime).getTime();
3852
+ if (!Number.isFinite(nowMs) || !Number.isFinite(transmissionMs) ||
3853
+ transmissionMs > nowMs + PAYPAL_WEBHOOK_FUTURE_SKEW_MS ||
3854
+ nowMs - transmissionMs > PAYPAL_WEBHOOK_REPLAY_WINDOW_MS) {
3855
+ return { verified: false, reason: 'stale_or_future_paypal_transmission' };
3856
+ }
3857
+
3858
+ let tokenResponse;
3859
+ try {
3860
+ tokenResponse = await withTimeout(fetchImpl(new URL('/v1/oauth2/token', config.apiBaseUrl), {
3861
+ method: 'POST',
3862
+ headers: {
3863
+ Authorization: `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')}`,
3864
+ Accept: 'application/json',
3865
+ 'Content-Type': 'application/x-www-form-urlencoded',
3866
+ },
3867
+ body: 'grant_type=client_credentials',
3868
+ }));
3869
+ } catch {
3870
+ return { verified: false, reason: 'paypal_oauth_request_failed' };
3871
+ }
3872
+ const tokenPayload = await readPayPalJsonResponse(tokenResponse);
3873
+ if (!tokenResponse.ok || !normalizeText(tokenPayload && tokenPayload.access_token)) {
3874
+ return { verified: false, reason: 'paypal_oauth_rejected' };
3875
+ }
3876
+
3877
+ let verificationResponse;
3878
+ try {
3879
+ verificationResponse = await withTimeout(fetchImpl(new URL('/v1/notifications/verify-webhook-signature', config.apiBaseUrl), {
3880
+ method: 'POST',
3881
+ headers: {
3882
+ Authorization: `Bearer ${tokenPayload.access_token}`,
3883
+ Accept: 'application/json',
3884
+ 'Content-Type': 'application/json',
3885
+ },
3886
+ body: JSON.stringify({
3887
+ auth_algo: transmission.authAlgo,
3888
+ cert_url: transmission.certUrl,
3889
+ transmission_id: transmission.transmissionId,
3890
+ transmission_sig: transmission.transmissionSig,
3891
+ transmission_time: transmission.transmissionTime,
3892
+ webhook_id: config.webhookId,
3893
+ webhook_event: event,
3894
+ }),
3895
+ }));
3896
+ } catch {
3897
+ return { verified: false, reason: 'paypal_signature_verification_request_failed' };
3898
+ }
3899
+ const verificationPayload = await readPayPalJsonResponse(verificationResponse);
3900
+ const verificationStatus = (normalizeText(verificationPayload && verificationPayload.verification_status) || '').toUpperCase();
3901
+ if (!verificationResponse.ok || verificationStatus !== 'SUCCESS') {
3902
+ return { verified: false, reason: 'invalid_paypal_webhook_signature' };
3903
+ }
3904
+ const paypalDebugId = normalizeText(verificationResponse.headers && verificationResponse.headers.get
3905
+ ? verificationResponse.headers.get('paypal-debug-id')
3906
+ : null);
3907
+ return {
3908
+ verified: true,
3909
+ reason: null,
3910
+ event,
3911
+ eventId,
3912
+ eventType,
3913
+ eventCreatedAt,
3914
+ transmission,
3915
+ verificationStatus,
3916
+ paypalDebugId: paypalDebugId || null,
3917
+ webhookId: config.webhookId,
3918
+ };
3919
+ }
3920
+
3921
+ function readPayPalWebhookLedgerStrict() {
3922
+ const ledgerPath = CONFIG.PAYPAL_WEBHOOK_LEDGER_PATH;
3923
+ try {
3924
+ if (!fs.existsSync(ledgerPath)) return { ok: true, rows: [] };
3925
+ const rows = [];
3926
+ for (const [index, line] of fs.readFileSync(ledgerPath, 'utf8').split('\n').entries()) {
3927
+ if (!line.trim()) continue;
3928
+ const row = JSON.parse(line);
3929
+ if (!row || typeof row !== 'object' || Array.isArray(row)) {
3930
+ return { ok: false, rows: [], reason: `invalid_row_${index}` };
3931
+ }
3932
+ rows.push(row);
3933
+ }
3934
+ return { ok: true, rows };
3935
+ } catch {
3936
+ return { ok: false, rows: [], reason: 'read_failed' };
3937
+ }
3938
+ }
3939
+
3940
+ function loadPayPalWebhookLedger() {
3941
+ const ledger = readPayPalWebhookLedgerStrict();
3942
+ return ledger.ok ? ledger.rows : [];
3943
+ }
3944
+
3945
+ async function recordPayPalWebhookDelivery({
3946
+ rawBody,
3947
+ headers,
3948
+ receivedAt = new Date().toISOString(),
3949
+ fetchImpl = globalThis.fetch,
3950
+ now = receivedAt,
3951
+ ...configOverrides
3952
+ } = {}) {
3953
+ const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody || '');
3954
+ const verification = await verifyPayPalWebhookSignature({
3955
+ rawBody: body,
3956
+ headers,
3957
+ fetchImpl,
3958
+ now,
3959
+ ...configOverrides,
3960
+ });
3961
+ if (!verification.verified) return { verified: false, recorded: false, reason: verification.reason };
3962
+
3963
+ const payloadSha256 = `sha256:${crypto.createHash('sha256').update(body).digest('hex')}`;
3964
+ const lockPath = `${CONFIG.PAYPAL_WEBHOOK_LEDGER_PATH}.lock`;
3965
+ let lockAcquired = false;
3966
+ try {
3967
+ acquireDirectoryLock(lockPath);
3968
+ lockAcquired = true;
3969
+ const ledger = readPayPalWebhookLedgerStrict();
3970
+ if (!ledger.ok) {
3971
+ return { verified: true, recorded: false, reason: 'paypal_webhook_ledger_unreadable' };
3972
+ }
3973
+ const eventId = normalizeText(verification.eventId);
3974
+ const transmissionId = normalizeText(verification.transmission.transmissionId);
3975
+ const existing = ledger.rows.find((entry) => (
3976
+ normalizeText(entry.eventId) === eventId ||
3977
+ normalizeText(entry.transmissionId) === transmissionId
3978
+ ));
3979
+ if (existing) {
3980
+ if (normalizeText(existing.payloadSha256) !== payloadSha256 ||
3981
+ normalizeText(existing.eventId) !== eventId) {
3982
+ return { verified: false, recorded: false, reason: 'paypal_webhook_delivery_collision' };
3983
+ }
3984
+ return {
3985
+ verified: true, recorded: true, duplicate: true,
3986
+ eventId: verification.eventId, eventType: verification.eventType, payloadSha256,
3987
+ };
3988
+ }
3989
+ const write = appendJsonlRecord(CONFIG.PAYPAL_WEBHOOK_LEDGER_PATH, {
3990
+ schemaVersion: 1,
3991
+ provider: 'paypal',
3992
+ receivedAt,
3993
+ eventId: verification.eventId,
3994
+ eventType: verification.eventType,
3995
+ eventCreatedAt: verification.eventCreatedAt,
3996
+ webhookId: verification.webhookId,
3997
+ transmissionId: verification.transmission.transmissionId,
3998
+ transmissionTime: verification.transmission.transmissionTime,
3999
+ authAlgo: verification.transmission.authAlgo,
4000
+ certUrl: verification.transmission.certUrl,
4001
+ transmissionSig: verification.transmission.transmissionSig,
4002
+ verificationStatus: verification.verificationStatus,
4003
+ verificationSource: 'paypal_verify_webhook_signature_api',
4004
+ paypalDebugId: verification.paypalDebugId,
4005
+ payloadSha256,
4006
+ rawBodyBase64: body.toString('base64'),
4007
+ });
4008
+ if (!write.written) return { verified: true, recorded: false, reason: 'paypal_webhook_ledger_write_failed' };
4009
+ } catch {
4010
+ return { verified: true, recorded: false, reason: 'paypal_webhook_ledger_lock_failed' };
4011
+ } finally {
4012
+ if (lockAcquired) {
4013
+ try { fs.rmSync(lockPath, { recursive: true, force: true }); } catch { /* retry via provider */ }
4014
+ }
4015
+ }
4016
+ return {
4017
+ verified: true,
4018
+ recorded: true,
4019
+ duplicate: false,
4020
+ eventId: verification.eventId,
4021
+ eventType: verification.eventType,
4022
+ payloadSha256,
4023
+ };
4024
+ }
4025
+
4026
+ function loadGithubMarketplaceWebhookLedger() {
4027
+ return loadJsonlRecords(CONFIG.GITHUB_MARKETPLACE_WEBHOOK_LEDGER_PATH);
4028
+ }
4029
+
4030
+ function recordGithubMarketplaceWebhookDelivery({
4031
+ rawBody,
4032
+ signature,
4033
+ deliveryId,
4034
+ eventName,
4035
+ receivedAt = new Date().toISOString(),
4036
+ } = {}) {
4037
+ const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody || '');
4038
+ const normalizedDeliveryId = normalizeText(deliveryId);
4039
+ const normalizedEventName = normalizeText(eventName);
4040
+ if (!normalizedDeliveryId || normalizedEventName !== 'marketplace_purchase') {
4041
+ return { verified: false, recorded: false, reason: 'missing_or_invalid_github_delivery_headers' };
4042
+ }
4043
+ if (!verifyGithubWebhookSignature(body, signature)) {
4044
+ return { verified: false, recorded: false, reason: 'invalid_github_webhook_signature' };
4045
+ }
4046
+
4047
+ let event;
4048
+ try {
4049
+ event = JSON.parse(body.toString('utf8'));
4050
+ } catch {
4051
+ return { verified: false, recorded: false, reason: 'invalid_github_webhook_json' };
4052
+ }
4053
+ if (!event || !event.action || !event.marketplace_purchase) {
4054
+ return { verified: false, recorded: false, reason: 'invalid_github_marketplace_payload' };
4055
+ }
4056
+
4057
+ const payloadSha256 = `sha256:${crypto.createHash('sha256').update(body).digest('hex')}`;
4058
+ const existing = loadGithubMarketplaceWebhookLedger()
4059
+ .find((entry) => normalizeText(entry.deliveryId) === normalizedDeliveryId);
4060
+ if (existing) {
4061
+ if (normalizeText(existing.payloadSha256) !== payloadSha256) {
4062
+ return { verified: false, recorded: false, reason: 'github_delivery_id_collision' };
4063
+ }
4064
+ return {
4065
+ verified: true,
4066
+ recorded: true,
4067
+ duplicate: true,
4068
+ deliveryId: normalizedDeliveryId,
4069
+ eventName: normalizedEventName,
4070
+ payloadSha256,
4071
+ };
4072
+ }
4073
+
4074
+ const write = appendJsonlRecord(CONFIG.GITHUB_MARKETPLACE_WEBHOOK_LEDGER_PATH, {
4075
+ schemaVersion: 1,
4076
+ receivedAt,
4077
+ deliveryId: normalizedDeliveryId,
4078
+ eventName: normalizedEventName,
4079
+ signature: normalizeText(signature),
4080
+ payloadSha256,
4081
+ rawBodyBase64: body.toString('base64'),
4082
+ });
4083
+ if (!write.written) {
4084
+ return { verified: true, recorded: false, reason: 'github_webhook_ledger_write_failed' };
4085
+ }
4086
+ return {
4087
+ verified: true,
4088
+ recorded: true,
4089
+ duplicate: false,
4090
+ deliveryId: normalizedDeliveryId,
4091
+ eventName: normalizedEventName,
4092
+ payloadSha256,
4093
+ };
4094
+ }
4095
+
4096
+ function buildGithubMarketplaceRevenueMetadata(marketplacePurchase = {}, marketplaceOrderId, planPricing = {}, provenance = {}) {
3769
4097
  const plan = marketplacePurchase && typeof marketplacePurchase.plan === 'object'
3770
4098
  ? marketplacePurchase.plan
3771
4099
  : {};
@@ -3781,17 +4109,21 @@ function buildGithubMarketplaceRevenueMetadata(marketplacePurchase = {}, marketp
3781
4109
  monthlyPriceInCents: normalizeInteger(plan.monthly_price_in_cents ?? plan.monthlyPriceInCents),
3782
4110
  yearlyPriceInCents: normalizeInteger(plan.yearly_price_in_cents ?? plan.yearlyPriceInCents),
3783
4111
  githubMarketplaceAmountSource: normalizeText(planPricing.pricingSource),
4112
+ githubWebhookSignatureVerified: provenance.verified === true,
4113
+ githubDeliveryId: normalizeText(provenance.deliveryId),
4114
+ githubEventName: normalizeText(provenance.eventName),
4115
+ githubPayloadSha256: normalizeText(provenance.payloadSha256),
3784
4116
  };
3785
4117
  }
3786
4118
 
3787
- function handleGithubWebhook(event) {
4119
+ function handleGithubWebhook(event, provenance = {}) {
3788
4120
  if (!event) return { handled: false, reason: 'missing_payload_data' };
3789
4121
  const { action, marketplace_purchase: mp } = event;
3790
4122
  if (!action || !mp || !mp.account?.id) return { handled: false, reason: 'missing_payload_data' };
3791
4123
  const customerId = `github_${String(mp.account.type).toLowerCase()}_${mp.account.id}`;
3792
4124
  const marketplaceOrderId = normalizeText(mp.id) || `github_marketplace_${String(mp.account.id)}_${String(mp.plan?.id || 'unknown')}`;
3793
4125
  const planPricing = resolveGithubPlanPricing(mp.plan?.id, mp);
3794
- const githubMetadata = buildGithubMarketplaceRevenueMetadata(mp, marketplaceOrderId, planPricing);
4126
+ const githubMetadata = buildGithubMarketplaceRevenueMetadata(mp, marketplaceOrderId, planPricing, provenance);
3795
4127
  switch (action) {
3796
4128
  case 'purchased': {
3797
4129
  const result = provisionApiKey(customerId, { source: 'github_marketplace_purchased' });
@@ -3887,7 +4219,7 @@ function handleGithubWebhook(event) {
3887
4219
  }
3888
4220
 
3889
4221
  module.exports = {
3890
- CONFIG, createCheckoutSession, getCheckoutSessionStatus, provisionApiKey, rotateApiKey, validateApiKey, recordUsage, disableCustomerKeys, handleWebhook, verifyWebhookSignature, verifyGithubWebhookSignature, handleGithubWebhook, loadKeyStore, appendFunnelEvent, appendRevenueEvent, loadFunnelLedger, loadRevenueLedger, loadNewsletterSubscribers, loadResolvedRevenueEvents, getFunnelAnalytics, getBusinessAnalytics, getBillingSummary, getBillingSummaryLive, listStripeReconciledRevenueEvents, repairGithubMarketplaceRevenueLedger,
4222
+ CONFIG, createCheckoutSession, getCheckoutSessionStatus, provisionApiKey, rotateApiKey, validateApiKey, recordUsage, disableCustomerKeys, handleWebhook, verifyWebhookSignature, verifyGithubWebhookSignature, verifyPayPalWebhookSignature, recordGithubMarketplaceWebhookDelivery, recordPayPalWebhookDelivery, handleGithubWebhook, loadKeyStore, appendFunnelEvent, appendRevenueEvent, loadFunnelLedger, loadRevenueLedger, loadGithubMarketplaceWebhookLedger, loadPayPalWebhookLedger, loadNewsletterSubscribers, loadResolvedRevenueEvents, getFunnelAnalytics, getBusinessAnalytics, getBillingSummary, getBillingSummaryLive, getBillingSummariesLive, listStripeReconciledRevenueEvents, repairGithubMarketplaceRevenueLedger,
3891
4223
  _buildCheckoutSessionPayload: buildCheckoutSessionPayload,
3892
4224
  _buildTrialActivationEmail: buildTrialActivationEmail,
3893
4225
  _sendTrialActivationEmail: sendTrialActivationEmail,
@@ -3899,14 +4231,12 @@ module.exports = {
3899
4231
  _API_KEYS_PATH: () => CONFIG.API_KEYS_PATH,
3900
4232
  _FUNNEL_LEDGER_PATH: () => CONFIG.FUNNEL_LEDGER_PATH,
3901
4233
  _REVENUE_LEDGER_PATH: () => CONFIG.REVENUE_LEDGER_PATH,
4234
+ _GITHUB_MARKETPLACE_WEBHOOK_LEDGER_PATH: () => CONFIG.GITHUB_MARKETPLACE_WEBHOOK_LEDGER_PATH,
4235
+ _PAYPAL_WEBHOOK_LEDGER_PATH: () => CONFIG.PAYPAL_WEBHOOK_LEDGER_PATH,
3902
4236
  _LOCAL_CHECKOUT_SESSIONS_PATH: () => CONFIG.LOCAL_CHECKOUT_SESSIONS_PATH,
3903
4237
  _TRIAL_EMAIL_LEDGER_PATH: () => CONFIG.TRIAL_EMAIL_LEDGER_PATH,
3904
4238
  _ORDER_EMAIL_LEDGER_PATH: () => CONFIG.ORDER_EMAIL_LEDGER_PATH,
3905
4239
  _LOCAL_MODE: () => LOCAL_MODE(),
3906
4240
  _withTimeout: withTimeout,
3907
- // Default to the real Resend-backed mailer so production webhooks send the
3908
- // marketing-grade trial-welcome template. Tests overwrite this with a stub
3909
- // (freshBilling() re-requires the module so the default is restored between
3910
- // tests — see tests/billing-webhook-email.test.js).
3911
4241
  _mailer: mailer,
3912
4242
  };