stripe-no-webhooks 0.0.5 → 0.0.8
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/bin/cli.js +318 -147
- package/dist/BillingConfig-n6VbfqGY.d.mts +23 -0
- package/dist/BillingConfig-n6VbfqGY.d.ts +23 -0
- package/dist/client.d.mts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +41 -20
- package/dist/index.mjs +41 -20
- package/package.json +1 -1
- package/src/templates/billing.config.ts +21 -18
package/bin/cli.js
CHANGED
|
@@ -413,131 +413,151 @@ function findMatchingBrace(content, startIndex) {
|
|
|
413
413
|
return -1;
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
function
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (
|
|
420
|
-
return
|
|
416
|
+
function getMode(stripeKey) {
|
|
417
|
+
if (stripeKey.includes("_test_")) {
|
|
418
|
+
return "test";
|
|
419
|
+
} else if (stripeKey.includes("_live_")) {
|
|
420
|
+
return "production";
|
|
421
|
+
} else {
|
|
422
|
+
throw new Error("Invalid Stripe key");
|
|
421
423
|
}
|
|
424
|
+
}
|
|
422
425
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
426
|
+
function tsObjectToJson(tsContent) {
|
|
427
|
+
// Remove single-line comments
|
|
428
|
+
let json = tsContent.replace(/\/\/.*$/gm, "");
|
|
429
|
+
// Remove multi-line comments
|
|
430
|
+
json = json.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
431
|
+
// Quote unquoted keys (word characters followed by colon)
|
|
432
|
+
json = json.replace(/(\s*)(\w+)(\s*:)/g, '$1"$2"$3');
|
|
433
|
+
// Remove trailing commas before ] or }
|
|
434
|
+
json = json.replace(/,(\s*[}\]])/g, "$1");
|
|
435
|
+
return json;
|
|
436
|
+
}
|
|
429
437
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (depth === 0) planStart = i;
|
|
438
|
-
depth++;
|
|
439
|
-
} else if (char === "}") {
|
|
440
|
-
depth--;
|
|
441
|
-
if (depth === 0 && planStart !== -1) {
|
|
442
|
-
const planRaw = plansContent.substring(planStart, i + 1);
|
|
443
|
-
const plan = parsePlanObject(planRaw);
|
|
444
|
-
if (plan.name) {
|
|
445
|
-
plans.push({
|
|
446
|
-
plan,
|
|
447
|
-
raw: planRaw,
|
|
448
|
-
startIndex:
|
|
449
|
-
plansStartMatch.index + plansStartMatch[0].length + planStart,
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
planStart = -1;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
438
|
+
function extractBillingConfigObject(content) {
|
|
439
|
+
// Find the start of the config object
|
|
440
|
+
const configStartMatch = content.match(
|
|
441
|
+
/const\s+billingConfig\s*:\s*BillingConfig\s*=\s*\{/
|
|
442
|
+
);
|
|
443
|
+
if (!configStartMatch) {
|
|
444
|
+
return null;
|
|
455
445
|
}
|
|
456
446
|
|
|
457
|
-
|
|
447
|
+
const objectStart = configStartMatch.index + configStartMatch[0].length - 1;
|
|
448
|
+
const objectEnd = findMatchingBrace(content, objectStart);
|
|
449
|
+
if (objectEnd === -1) return null;
|
|
450
|
+
|
|
451
|
+
const rawObject = content.substring(objectStart, objectEnd + 1);
|
|
452
|
+
return {
|
|
453
|
+
raw: rawObject,
|
|
454
|
+
start: objectStart,
|
|
455
|
+
end: objectEnd + 1,
|
|
456
|
+
};
|
|
458
457
|
}
|
|
459
458
|
|
|
460
|
-
function
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const idMatch = planContent.match(/^\s*\{\s*id\s*:\s*["']([^"']+)["']/);
|
|
465
|
-
if (idMatch) plan.id = idMatch[1];
|
|
466
|
-
|
|
467
|
-
// Also try to find id not at start
|
|
468
|
-
if (!plan.id) {
|
|
469
|
-
const idMatch2 = planContent.match(/[,{]\s*id\s*:\s*["']([^"']+)["']/);
|
|
470
|
-
if (idMatch2) plan.id = idMatch2[1];
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Extract name
|
|
474
|
-
const nameMatch = planContent.match(/name\s*:\s*["']([^"']+)["']/);
|
|
475
|
-
if (nameMatch) plan.name = nameMatch[1];
|
|
476
|
-
|
|
477
|
-
// Extract description
|
|
478
|
-
const descMatch = planContent.match(/description\s*:\s*["']([^"']+)["']/);
|
|
479
|
-
if (descMatch) plan.description = descMatch[1];
|
|
480
|
-
|
|
481
|
-
// Extract price array
|
|
482
|
-
const priceStartMatch = planContent.match(/price\s*:\s*\[/);
|
|
483
|
-
if (priceStartMatch) {
|
|
484
|
-
const priceStart = priceStartMatch.index + priceStartMatch[0].length - 1;
|
|
485
|
-
const priceEnd = findMatchingBrace(planContent, priceStart);
|
|
486
|
-
if (priceEnd !== -1) {
|
|
487
|
-
const priceArrayContent = planContent.substring(priceStart + 1, priceEnd);
|
|
488
|
-
plan.prices = parsePriceArray(priceArrayContent);
|
|
489
|
-
plan.priceArrayStart = priceStart;
|
|
490
|
-
plan.priceArrayEnd = priceEnd;
|
|
491
|
-
}
|
|
459
|
+
function parseBillingConfig(content, mode) {
|
|
460
|
+
const extracted = extractBillingConfigObject(content);
|
|
461
|
+
if (!extracted) {
|
|
462
|
+
return { config: null, plans: [] };
|
|
492
463
|
}
|
|
493
464
|
|
|
494
|
-
|
|
465
|
+
// Convert to JSON and parse
|
|
466
|
+
const jsonString = tsObjectToJson(extracted.raw);
|
|
467
|
+
let config;
|
|
468
|
+
try {
|
|
469
|
+
config = JSON.parse(jsonString);
|
|
470
|
+
} catch (e) {
|
|
471
|
+
console.error("Failed to parse billing config as JSON:", e.message);
|
|
472
|
+
return { config: null, plans: [] };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Get plans for the specified mode
|
|
476
|
+
const modeConfig = config[mode];
|
|
477
|
+
if (!modeConfig || !modeConfig.plans || modeConfig.plans.length === 0) {
|
|
478
|
+
return { config, plans: [], extracted };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Return parsed plans with their indices for updating
|
|
482
|
+
const plans = modeConfig.plans.map((plan, index) => ({
|
|
483
|
+
plan,
|
|
484
|
+
index,
|
|
485
|
+
}));
|
|
486
|
+
|
|
487
|
+
return { config, plans, extracted };
|
|
495
488
|
}
|
|
496
489
|
|
|
497
|
-
function
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
for (let i = 0; i < priceArrayContent.length; i++) {
|
|
503
|
-
const char = priceArrayContent[i];
|
|
504
|
-
if (char === "{") {
|
|
505
|
-
if (depth === 0) priceStart = i;
|
|
506
|
-
depth++;
|
|
507
|
-
} else if (char === "}") {
|
|
508
|
-
depth--;
|
|
509
|
-
if (depth === 0 && priceStart !== -1) {
|
|
510
|
-
const priceRaw = priceArrayContent.substring(priceStart, i + 1);
|
|
511
|
-
const price = parsePriceObject(priceRaw);
|
|
512
|
-
prices.push({ price, raw: priceRaw, localStart: priceStart });
|
|
513
|
-
priceStart = -1;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
490
|
+
function reorderWithIdFirst(obj) {
|
|
491
|
+
// Reorder object so 'id' is the first property if it exists
|
|
492
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
493
|
+
return obj;
|
|
516
494
|
}
|
|
517
495
|
|
|
518
|
-
|
|
496
|
+
const { id, ...rest } = obj;
|
|
497
|
+
if (id !== undefined) {
|
|
498
|
+
return { id, ...rest };
|
|
499
|
+
}
|
|
500
|
+
return obj;
|
|
519
501
|
}
|
|
520
502
|
|
|
521
|
-
function
|
|
522
|
-
const
|
|
503
|
+
function toTsObjectLiteral(value, indent = 0) {
|
|
504
|
+
const spaces = " ".repeat(indent);
|
|
505
|
+
const childSpaces = " ".repeat(indent + 1);
|
|
523
506
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
507
|
+
if (value === null || value === undefined) {
|
|
508
|
+
return String(value);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (typeof value === "string") {
|
|
512
|
+
return `"${value}"`;
|
|
513
|
+
}
|
|
527
514
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
515
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
516
|
+
return String(value);
|
|
517
|
+
}
|
|
531
518
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
519
|
+
if (Array.isArray(value)) {
|
|
520
|
+
if (value.length === 0) {
|
|
521
|
+
return "[]";
|
|
522
|
+
}
|
|
523
|
+
const items = value.map((item) => toTsObjectLiteral(item, indent + 1));
|
|
524
|
+
return `[\n${childSpaces}${items.join(`,\n${childSpaces}`)},\n${spaces}]`;
|
|
525
|
+
}
|
|
535
526
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
527
|
+
if (typeof value === "object") {
|
|
528
|
+
const entries = Object.entries(value);
|
|
529
|
+
if (entries.length === 0) {
|
|
530
|
+
return "{}";
|
|
531
|
+
}
|
|
532
|
+
const props = entries.map(
|
|
533
|
+
([key, val]) => `${key}: ${toTsObjectLiteral(val, indent + 1)}`
|
|
534
|
+
);
|
|
535
|
+
return `{\n${childSpaces}${props.join(`,\n${childSpaces}`)},\n${spaces}}`;
|
|
536
|
+
}
|
|
539
537
|
|
|
540
|
-
return
|
|
538
|
+
return String(value);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function formatConfigToTs(config) {
|
|
542
|
+
// Reorder plans and prices so 'id' is always first
|
|
543
|
+
const reorderedConfig = {};
|
|
544
|
+
|
|
545
|
+
for (const mode of ["test", "production"]) {
|
|
546
|
+
if (config[mode]) {
|
|
547
|
+
reorderedConfig[mode] = {
|
|
548
|
+
plans: (config[mode].plans || []).map((plan) => {
|
|
549
|
+
const reorderedPlan = reorderWithIdFirst(plan);
|
|
550
|
+
if (reorderedPlan.price) {
|
|
551
|
+
reorderedPlan.price = reorderedPlan.price.map(reorderWithIdFirst);
|
|
552
|
+
}
|
|
553
|
+
return reorderedPlan;
|
|
554
|
+
}),
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Convert to TypeScript object literal format (unquoted keys)
|
|
560
|
+
return toTsObjectLiteral(reorderedConfig, 0);
|
|
541
561
|
}
|
|
542
562
|
|
|
543
563
|
async function sync() {
|
|
@@ -572,28 +592,176 @@ async function sync() {
|
|
|
572
592
|
process.exit(1);
|
|
573
593
|
}
|
|
574
594
|
|
|
595
|
+
// Determine mode based on Stripe key
|
|
596
|
+
let mode;
|
|
597
|
+
try {
|
|
598
|
+
mode = getMode(stripeSecretKey);
|
|
599
|
+
} catch (e) {
|
|
600
|
+
console.error("❌", e.message);
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
|
|
575
604
|
const stripe = new Stripe(stripeSecretKey);
|
|
576
605
|
|
|
577
|
-
console.log(
|
|
606
|
+
console.log(`\n🔄 Syncing billing plans with Stripe (${mode} mode)...\n`);
|
|
578
607
|
|
|
579
608
|
let content = fs.readFileSync(billingConfigPath, "utf8");
|
|
580
|
-
const
|
|
609
|
+
const { config, plans, extracted } = parseBillingConfig(content, mode);
|
|
581
610
|
|
|
582
|
-
if (
|
|
583
|
-
console.
|
|
584
|
-
|
|
585
|
-
|
|
611
|
+
if (!config) {
|
|
612
|
+
console.error("❌ Failed to parse billing.config.ts");
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Ensure the mode config exists with plans array
|
|
617
|
+
if (!config[mode]) {
|
|
618
|
+
config[mode] = { plans: [] };
|
|
619
|
+
}
|
|
620
|
+
if (!config[mode].plans) {
|
|
621
|
+
config[mode].plans = [];
|
|
586
622
|
}
|
|
587
623
|
|
|
588
|
-
let
|
|
624
|
+
let configModified = false;
|
|
625
|
+
let productsPulled = 0;
|
|
626
|
+
let pricesPulled = 0;
|
|
589
627
|
let productsCreated = 0;
|
|
590
628
|
let pricesCreated = 0;
|
|
591
629
|
let skippedProducts = 0;
|
|
592
630
|
let skippedPrices = 0;
|
|
593
631
|
|
|
594
|
-
|
|
632
|
+
// === PULL: Fetch products and prices from Stripe and add missing ones ===
|
|
633
|
+
console.log("📥 Pulling products from Stripe...\n");
|
|
634
|
+
|
|
635
|
+
try {
|
|
636
|
+
// Fetch all active products from Stripe
|
|
637
|
+
const stripeProducts = await stripe.products.list({
|
|
638
|
+
active: true,
|
|
639
|
+
limit: 100,
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// Fetch all active prices from Stripe
|
|
643
|
+
const stripePrices = await stripe.prices.list({ active: true, limit: 100 });
|
|
644
|
+
|
|
645
|
+
// Build a map of price by product
|
|
646
|
+
const pricesByProduct = {};
|
|
647
|
+
for (const price of stripePrices.data) {
|
|
648
|
+
const productId =
|
|
649
|
+
typeof price.product === "string" ? price.product : price.product.id;
|
|
650
|
+
if (!pricesByProduct[productId]) {
|
|
651
|
+
pricesByProduct[productId] = [];
|
|
652
|
+
}
|
|
653
|
+
pricesByProduct[productId].push(price);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Get existing product IDs in config
|
|
657
|
+
const existingProductIds = new Set(
|
|
658
|
+
config[mode].plans.filter((p) => p.id).map((p) => p.id)
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
// Get existing price IDs in config
|
|
662
|
+
const existingPriceIds = new Set();
|
|
663
|
+
for (const plan of config[mode].plans) {
|
|
664
|
+
if (plan.price) {
|
|
665
|
+
for (const price of plan.price) {
|
|
666
|
+
if (price.id) {
|
|
667
|
+
existingPriceIds.add(price.id);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Add missing products and their prices
|
|
674
|
+
for (const product of stripeProducts.data) {
|
|
675
|
+
if (existingProductIds.has(product.id)) {
|
|
676
|
+
// Product exists, but check if any prices are missing
|
|
677
|
+
const planIndex = config[mode].plans.findIndex(
|
|
678
|
+
(p) => p.id === product.id
|
|
679
|
+
);
|
|
680
|
+
const plan = config[mode].plans[planIndex];
|
|
681
|
+
const productPrices = pricesByProduct[product.id] || [];
|
|
682
|
+
|
|
683
|
+
for (const stripePrice of productPrices) {
|
|
684
|
+
if (!existingPriceIds.has(stripePrice.id)) {
|
|
685
|
+
// Add missing price to existing plan
|
|
686
|
+
const newPrice = {
|
|
687
|
+
id: stripePrice.id,
|
|
688
|
+
amount: stripePrice.unit_amount,
|
|
689
|
+
currency: stripePrice.currency,
|
|
690
|
+
interval: stripePrice.recurring?.interval || "one_time",
|
|
691
|
+
};
|
|
692
|
+
if (!plan.price) {
|
|
693
|
+
plan.price = [];
|
|
694
|
+
}
|
|
695
|
+
plan.price.push(newPrice);
|
|
696
|
+
existingPriceIds.add(stripePrice.id);
|
|
697
|
+
pricesPulled++;
|
|
698
|
+
configModified = true;
|
|
699
|
+
console.log(
|
|
700
|
+
` 📥 Added price ${stripePrice.unit_amount / 100} ${
|
|
701
|
+
stripePrice.currency
|
|
702
|
+
}/${newPrice.interval} to "${product.name}"`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Product doesn't exist in config, add it
|
|
710
|
+
const productPrices = pricesByProduct[product.id] || [];
|
|
711
|
+
const newPlan = {
|
|
712
|
+
id: product.id,
|
|
713
|
+
name: product.name,
|
|
714
|
+
description: product.description || undefined,
|
|
715
|
+
price: productPrices.map((p) => ({
|
|
716
|
+
id: p.id,
|
|
717
|
+
amount: p.unit_amount,
|
|
718
|
+
currency: p.currency,
|
|
719
|
+
interval: p.recurring?.interval || "one_time",
|
|
720
|
+
})),
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
// Remove undefined description
|
|
724
|
+
if (!newPlan.description) {
|
|
725
|
+
delete newPlan.description;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
config[mode].plans.push(newPlan);
|
|
729
|
+
productsPulled++;
|
|
730
|
+
pricesPulled += productPrices.length;
|
|
731
|
+
configModified = true;
|
|
732
|
+
|
|
733
|
+
console.log(`📥 Added product "${product.name}" (${product.id})`);
|
|
734
|
+
for (const price of newPlan.price) {
|
|
735
|
+
console.log(
|
|
736
|
+
` 📥 Added price ${price.amount / 100} ${price.currency}/${
|
|
737
|
+
price.interval
|
|
738
|
+
}`
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (productsPulled === 0 && pricesPulled === 0) {
|
|
744
|
+
console.log(" No new products or prices to pull from Stripe.\n");
|
|
745
|
+
} else {
|
|
746
|
+
console.log("");
|
|
747
|
+
}
|
|
748
|
+
} catch (error) {
|
|
749
|
+
console.error("❌ Failed to fetch products from Stripe:", error.message);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// === PUSH: Create products and prices in Stripe from config ===
|
|
753
|
+
console.log("📤 Pushing new plans to Stripe...\n");
|
|
754
|
+
|
|
755
|
+
// Re-get plans after potential modifications
|
|
756
|
+
const currentPlans = config[mode].plans || [];
|
|
757
|
+
|
|
758
|
+
if (currentPlans.length === 0) {
|
|
759
|
+
console.log(` No plans in billing.config.ts for ${mode} mode.\n`);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
for (let index = 0; index < currentPlans.length; index++) {
|
|
763
|
+
const plan = currentPlans[index];
|
|
595
764
|
let productId = plan.id;
|
|
596
|
-
let updatedPlanRaw = raw;
|
|
597
765
|
|
|
598
766
|
// Create product if needed
|
|
599
767
|
if (!productId) {
|
|
@@ -608,12 +776,10 @@ async function sync() {
|
|
|
608
776
|
productId = product.id;
|
|
609
777
|
console.log(`✅ Created product "${plan.name}" (${productId})`);
|
|
610
778
|
|
|
611
|
-
//
|
|
612
|
-
|
|
613
|
-
/\{/,
|
|
614
|
-
`{\n id: "${productId}",`
|
|
615
|
-
);
|
|
779
|
+
// Update the config object with the new product id
|
|
780
|
+
config[mode].plans[index].id = productId;
|
|
616
781
|
productsCreated++;
|
|
782
|
+
configModified = true;
|
|
617
783
|
} catch (error) {
|
|
618
784
|
console.error(
|
|
619
785
|
`❌ Failed to create product "${plan.name}":`,
|
|
@@ -622,17 +788,15 @@ async function sync() {
|
|
|
622
788
|
continue;
|
|
623
789
|
}
|
|
624
790
|
} else {
|
|
625
|
-
console.log(`⏭️ Product "${plan.name}" already exists (${productId})`);
|
|
626
791
|
skippedProducts++;
|
|
627
792
|
}
|
|
628
793
|
|
|
629
794
|
// Create prices if needed
|
|
630
|
-
if (plan.
|
|
631
|
-
for (
|
|
795
|
+
if (plan.price && plan.price.length > 0) {
|
|
796
|
+
for (let priceIndex = 0; priceIndex < plan.price.length; priceIndex++) {
|
|
797
|
+
const price = plan.price[priceIndex];
|
|
798
|
+
|
|
632
799
|
if (price.id) {
|
|
633
|
-
console.log(
|
|
634
|
-
` ⏭️ Price ${price.interval}/${price.currency} already exists (${price.id})`
|
|
635
|
-
);
|
|
636
800
|
skippedPrices++;
|
|
637
801
|
continue;
|
|
638
802
|
}
|
|
@@ -644,24 +808,27 @@ async function sync() {
|
|
|
644
808
|
}...`
|
|
645
809
|
);
|
|
646
810
|
|
|
647
|
-
const
|
|
811
|
+
const priceParams = {
|
|
648
812
|
product: productId,
|
|
649
813
|
unit_amount: price.amount,
|
|
650
814
|
currency: price.currency.toLowerCase(),
|
|
651
|
-
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
// Only add recurring for non-one_time intervals
|
|
818
|
+
if (price.interval && price.interval !== "one_time") {
|
|
819
|
+
priceParams.recurring = {
|
|
652
820
|
interval: price.interval,
|
|
653
|
-
}
|
|
654
|
-
}
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const stripePrice = await stripe.prices.create(priceParams);
|
|
655
825
|
|
|
656
826
|
console.log(` ✅ Created price (${stripePrice.id})`);
|
|
657
827
|
|
|
658
|
-
//
|
|
659
|
-
|
|
660
|
-
/\{/,
|
|
661
|
-
`{\n id: "${stripePrice.id}",`
|
|
662
|
-
);
|
|
663
|
-
updatedPlanRaw = updatedPlanRaw.replace(priceRaw, updatedPriceRaw);
|
|
828
|
+
// Update the config object with the new price id
|
|
829
|
+
config[mode].plans[index].price[priceIndex].id = stripePrice.id;
|
|
664
830
|
pricesCreated++;
|
|
831
|
+
configModified = true;
|
|
665
832
|
} catch (error) {
|
|
666
833
|
console.error(
|
|
667
834
|
` ❌ Failed to create price ${price.interval}/${price.currency}:`,
|
|
@@ -670,26 +837,30 @@ async function sync() {
|
|
|
670
837
|
}
|
|
671
838
|
}
|
|
672
839
|
}
|
|
840
|
+
}
|
|
673
841
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
updatedContent = updatedContent.replace(raw, updatedPlanRaw);
|
|
677
|
-
}
|
|
842
|
+
if (productsCreated === 0 && pricesCreated === 0) {
|
|
843
|
+
console.log(" No new products or prices to push to Stripe.\n");
|
|
678
844
|
}
|
|
679
845
|
|
|
680
|
-
// Write updated
|
|
681
|
-
if (
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
846
|
+
// Write updated config back to file
|
|
847
|
+
if (configModified) {
|
|
848
|
+
const newConfigJson = formatConfigToTs(config);
|
|
849
|
+
const newContent =
|
|
850
|
+
content.substring(0, extracted.start) +
|
|
851
|
+
newConfigJson +
|
|
852
|
+
content.substring(extracted.end);
|
|
853
|
+
fs.writeFileSync(billingConfigPath, newContent);
|
|
854
|
+
console.log(`\n📝 Updated billing.config.ts`);
|
|
686
855
|
}
|
|
687
856
|
|
|
688
857
|
console.log(`\n✅ Done!`);
|
|
689
858
|
console.log(
|
|
690
|
-
`
|
|
859
|
+
` Pulled from Stripe: ${productsPulled} product(s), ${pricesPulled} price(s)`
|
|
860
|
+
);
|
|
861
|
+
console.log(
|
|
862
|
+
` Pushed to Stripe: ${productsCreated} product(s), ${pricesCreated} price(s)`
|
|
691
863
|
);
|
|
692
|
-
console.log(` Prices: ${pricesCreated} created, ${skippedPrices} skipped`);
|
|
693
864
|
}
|
|
694
865
|
|
|
695
866
|
async function main() {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type PriceInterval = "month" | "year" | "week" | "one_time";
|
|
2
|
+
type Price = {
|
|
3
|
+
id?: string;
|
|
4
|
+
amount: number;
|
|
5
|
+
currency: string;
|
|
6
|
+
interval: PriceInterval;
|
|
7
|
+
};
|
|
8
|
+
type Plan = {
|
|
9
|
+
id?: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
price: Price[];
|
|
13
|
+
};
|
|
14
|
+
type BillingConfig = {
|
|
15
|
+
test?: {
|
|
16
|
+
plans?: Plan[];
|
|
17
|
+
};
|
|
18
|
+
production?: {
|
|
19
|
+
plans?: Plan[];
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type { BillingConfig as B, PriceInterval as P, Price as a, Plan as b };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type PriceInterval = "month" | "year" | "week" | "one_time";
|
|
2
|
+
type Price = {
|
|
3
|
+
id?: string;
|
|
4
|
+
amount: number;
|
|
5
|
+
currency: string;
|
|
6
|
+
interval: PriceInterval;
|
|
7
|
+
};
|
|
8
|
+
type Plan = {
|
|
9
|
+
id?: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
price: Price[];
|
|
13
|
+
};
|
|
14
|
+
type BillingConfig = {
|
|
15
|
+
test?: {
|
|
16
|
+
plans?: Plan[];
|
|
17
|
+
};
|
|
18
|
+
production?: {
|
|
19
|
+
plans?: Plan[];
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type { BillingConfig as B, PriceInterval as P, Price as a, Plan as b };
|
package/dist/client.d.mts
CHANGED
package/dist/client.d.ts
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Stripe from 'stripe';
|
|
2
|
-
import { B as BillingConfig, P as PriceInterval } from './BillingConfig-
|
|
3
|
-
export { b as Plan, a as Price } from './BillingConfig-
|
|
2
|
+
import { B as BillingConfig, P as PriceInterval } from './BillingConfig-n6VbfqGY.mjs';
|
|
3
|
+
export { b as Plan, a as Price } from './BillingConfig-n6VbfqGY.mjs';
|
|
4
4
|
|
|
5
5
|
interface StripeWebhookCallbacks {
|
|
6
6
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Stripe from 'stripe';
|
|
2
|
-
import { B as BillingConfig, P as PriceInterval } from './BillingConfig-
|
|
3
|
-
export { b as Plan, a as Price } from './BillingConfig-
|
|
2
|
+
import { B as BillingConfig, P as PriceInterval } from './BillingConfig-n6VbfqGY.js';
|
|
3
|
+
export { b as Plan, a as Price } from './BillingConfig-n6VbfqGY.js';
|
|
4
4
|
|
|
5
5
|
interface StripeWebhookCallbacks {
|
|
6
6
|
/**
|
package/dist/index.js
CHANGED
|
@@ -37,6 +37,19 @@ module.exports = __toCommonJS(index_exports);
|
|
|
37
37
|
// src/handler.ts
|
|
38
38
|
var import_stripe_sync_engine = require("@supabase/stripe-sync-engine");
|
|
39
39
|
var import_stripe = __toESM(require("stripe"));
|
|
40
|
+
|
|
41
|
+
// src/utils.ts
|
|
42
|
+
var getMode = (stripeKey) => {
|
|
43
|
+
if (stripeKey.includes("_test_")) {
|
|
44
|
+
return "test";
|
|
45
|
+
} else if (stripeKey.includes("_live_")) {
|
|
46
|
+
return "production";
|
|
47
|
+
} else {
|
|
48
|
+
throw new Error("Invalid Stripe key");
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// src/handler.ts
|
|
40
53
|
function createStripeHandler(config = {}) {
|
|
41
54
|
const {
|
|
42
55
|
stripeSecretKey = process.env.STRIPE_SECRET_KEY,
|
|
@@ -58,19 +71,19 @@ function createStripeHandler(config = {}) {
|
|
|
58
71
|
stripeSecretKey,
|
|
59
72
|
stripeWebhookSecret
|
|
60
73
|
}) : null;
|
|
61
|
-
function resolvePriceId(body) {
|
|
74
|
+
function resolvePriceId(body, mode) {
|
|
62
75
|
if (body.priceId) {
|
|
63
76
|
return body.priceId;
|
|
64
77
|
}
|
|
65
78
|
if (!body.interval) {
|
|
66
79
|
throw new Error("interval is required when using planName or planId");
|
|
67
80
|
}
|
|
68
|
-
if (!billingConfig?.plans) {
|
|
81
|
+
if (!billingConfig?.[mode]?.plans) {
|
|
69
82
|
throw new Error(
|
|
70
83
|
"billingConfig with plans is required when using planName or planId"
|
|
71
84
|
);
|
|
72
85
|
}
|
|
73
|
-
const plan = body.planName ? billingConfig
|
|
86
|
+
const plan = body.planName ? billingConfig[mode]?.plans?.find((p) => p.name === body.planName) : body.planId ? billingConfig[mode]?.plans?.find((p) => p.id === body.planId) : null;
|
|
74
87
|
if (!plan) {
|
|
75
88
|
const identifier = body.planName || body.planId;
|
|
76
89
|
throw new Error(`Plan not found: ${identifier}`);
|
|
@@ -106,7 +119,7 @@ function createStripeHandler(config = {}) {
|
|
|
106
119
|
const origin = request.headers.get("origin") || "";
|
|
107
120
|
const successUrl = body.successUrl || defaultSuccessUrl || `${origin}/success?session_id={CHECKOUT_SESSION_ID}`;
|
|
108
121
|
const cancelUrl = body.cancelUrl || defaultCancelUrl || `${origin}/`;
|
|
109
|
-
const priceId = resolvePriceId(body);
|
|
122
|
+
const priceId = resolvePriceId(body, getMode(stripeSecretKey));
|
|
110
123
|
const mode = await getPriceMode(priceId);
|
|
111
124
|
const sessionParams = {
|
|
112
125
|
line_items: [
|
|
@@ -157,26 +170,34 @@ function createStripeHandler(config = {}) {
|
|
|
157
170
|
async function handleWebhook(request) {
|
|
158
171
|
try {
|
|
159
172
|
const body = await request.text();
|
|
173
|
+
const url = new URL(request.url);
|
|
174
|
+
const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
160
175
|
const signature = request.headers.get("stripe-signature");
|
|
161
|
-
if (!signature) {
|
|
162
|
-
return new Response("Missing stripe-signature header", { status: 400 });
|
|
163
|
-
}
|
|
164
176
|
let event;
|
|
165
|
-
|
|
166
|
-
event =
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
if (isLocalhost) {
|
|
178
|
+
event = JSON.parse(body);
|
|
179
|
+
} else {
|
|
180
|
+
if (!signature) {
|
|
181
|
+
return new Response("Missing stripe-signature header", {
|
|
182
|
+
status: 400
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
event = stripe.webhooks.constructEvent(
|
|
187
|
+
body,
|
|
188
|
+
signature,
|
|
189
|
+
stripeWebhookSecret
|
|
190
|
+
);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
193
|
+
return new Response(
|
|
194
|
+
`Webhook signature verification failed: ${message}`,
|
|
195
|
+
{ status: 400 }
|
|
196
|
+
);
|
|
197
|
+
}
|
|
177
198
|
}
|
|
178
199
|
if (sync) {
|
|
179
|
-
await sync.
|
|
200
|
+
await sync.processEvent(event);
|
|
180
201
|
}
|
|
181
202
|
if (callbacks) {
|
|
182
203
|
await handleCallbacks(event, callbacks);
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
// src/handler.ts
|
|
2
2
|
import { StripeSync } from "@supabase/stripe-sync-engine";
|
|
3
3
|
import Stripe from "stripe";
|
|
4
|
+
|
|
5
|
+
// src/utils.ts
|
|
6
|
+
var getMode = (stripeKey) => {
|
|
7
|
+
if (stripeKey.includes("_test_")) {
|
|
8
|
+
return "test";
|
|
9
|
+
} else if (stripeKey.includes("_live_")) {
|
|
10
|
+
return "production";
|
|
11
|
+
} else {
|
|
12
|
+
throw new Error("Invalid Stripe key");
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/handler.ts
|
|
4
17
|
function createStripeHandler(config = {}) {
|
|
5
18
|
const {
|
|
6
19
|
stripeSecretKey = process.env.STRIPE_SECRET_KEY,
|
|
@@ -22,19 +35,19 @@ function createStripeHandler(config = {}) {
|
|
|
22
35
|
stripeSecretKey,
|
|
23
36
|
stripeWebhookSecret
|
|
24
37
|
}) : null;
|
|
25
|
-
function resolvePriceId(body) {
|
|
38
|
+
function resolvePriceId(body, mode) {
|
|
26
39
|
if (body.priceId) {
|
|
27
40
|
return body.priceId;
|
|
28
41
|
}
|
|
29
42
|
if (!body.interval) {
|
|
30
43
|
throw new Error("interval is required when using planName or planId");
|
|
31
44
|
}
|
|
32
|
-
if (!billingConfig?.plans) {
|
|
45
|
+
if (!billingConfig?.[mode]?.plans) {
|
|
33
46
|
throw new Error(
|
|
34
47
|
"billingConfig with plans is required when using planName or planId"
|
|
35
48
|
);
|
|
36
49
|
}
|
|
37
|
-
const plan = body.planName ? billingConfig
|
|
50
|
+
const plan = body.planName ? billingConfig[mode]?.plans?.find((p) => p.name === body.planName) : body.planId ? billingConfig[mode]?.plans?.find((p) => p.id === body.planId) : null;
|
|
38
51
|
if (!plan) {
|
|
39
52
|
const identifier = body.planName || body.planId;
|
|
40
53
|
throw new Error(`Plan not found: ${identifier}`);
|
|
@@ -70,7 +83,7 @@ function createStripeHandler(config = {}) {
|
|
|
70
83
|
const origin = request.headers.get("origin") || "";
|
|
71
84
|
const successUrl = body.successUrl || defaultSuccessUrl || `${origin}/success?session_id={CHECKOUT_SESSION_ID}`;
|
|
72
85
|
const cancelUrl = body.cancelUrl || defaultCancelUrl || `${origin}/`;
|
|
73
|
-
const priceId = resolvePriceId(body);
|
|
86
|
+
const priceId = resolvePriceId(body, getMode(stripeSecretKey));
|
|
74
87
|
const mode = await getPriceMode(priceId);
|
|
75
88
|
const sessionParams = {
|
|
76
89
|
line_items: [
|
|
@@ -121,26 +134,34 @@ function createStripeHandler(config = {}) {
|
|
|
121
134
|
async function handleWebhook(request) {
|
|
122
135
|
try {
|
|
123
136
|
const body = await request.text();
|
|
137
|
+
const url = new URL(request.url);
|
|
138
|
+
const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
124
139
|
const signature = request.headers.get("stripe-signature");
|
|
125
|
-
if (!signature) {
|
|
126
|
-
return new Response("Missing stripe-signature header", { status: 400 });
|
|
127
|
-
}
|
|
128
140
|
let event;
|
|
129
|
-
|
|
130
|
-
event =
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
if (isLocalhost) {
|
|
142
|
+
event = JSON.parse(body);
|
|
143
|
+
} else {
|
|
144
|
+
if (!signature) {
|
|
145
|
+
return new Response("Missing stripe-signature header", {
|
|
146
|
+
status: 400
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
event = stripe.webhooks.constructEvent(
|
|
151
|
+
body,
|
|
152
|
+
signature,
|
|
153
|
+
stripeWebhookSecret
|
|
154
|
+
);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
157
|
+
return new Response(
|
|
158
|
+
`Webhook signature verification failed: ${message}`,
|
|
159
|
+
{ status: 400 }
|
|
160
|
+
);
|
|
161
|
+
}
|
|
141
162
|
}
|
|
142
163
|
if (sync) {
|
|
143
|
-
await sync.
|
|
164
|
+
await sync.processEvent(event);
|
|
144
165
|
}
|
|
145
166
|
if (callbacks) {
|
|
146
167
|
await handleCallbacks(event, callbacks);
|
package/package.json
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import type { BillingConfig } from "stripe-no-webhooks";
|
|
2
2
|
|
|
3
3
|
const billingConfig: BillingConfig = {
|
|
4
|
-
|
|
4
|
+
test: {
|
|
5
5
|
plans: [
|
|
6
|
-
{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
},
|
|
6
|
+
// {
|
|
7
|
+
// name: "Premium",
|
|
8
|
+
// description: "Access to all features",
|
|
9
|
+
// price: [
|
|
10
|
+
// {
|
|
11
|
+
// amount: 1000, // in cents, 1000 = $10.00
|
|
12
|
+
// currency: "usd",
|
|
13
|
+
// interval: "month",
|
|
14
|
+
// },
|
|
15
|
+
// {
|
|
16
|
+
// amount: 10000, // in cents, 10000 = $100.00
|
|
17
|
+
// currency: "usd",
|
|
18
|
+
// interval: "year",
|
|
19
|
+
// },
|
|
20
|
+
// ],
|
|
21
|
+
// },
|
|
22
22
|
],
|
|
23
|
-
|
|
23
|
+
},
|
|
24
|
+
production: {
|
|
25
|
+
plans: [],
|
|
26
|
+
},
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
export default billingConfig;
|