git-paid 2.1.5 → 2.1.7

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/lib/pseo.js CHANGED
@@ -68,19 +68,19 @@ function renderTopicSpecificSection(pageSlug, cleanName, domain) {
68
68
  <td><strong>Free Audit &amp; Preview</strong></td>
69
69
  <td><strong>$0</strong></td>
70
70
  <td>Any public URL</td>
71
- <td>8-point SEO &amp; AI crawler readiness scan, copy-paste <code>robots.txt</code> / <code>llms.txt</code> / JSON-LD snippets, and 5-directory preview.</td>
71
+ <td>8-point SEO &amp; AI crawler readiness scan, copy-paste <code>robots.txt</code> / <code>llms.txt</code> / JSON-LD snippets, Auto-Fill Bookmarklet, and 5-directory preview.</td>
72
72
  </tr>
73
73
  <tr>
74
74
  <td><strong>Launch Pass</strong> <span class="pill emerald" style="margin-left:6px;">Recommended</span></td>
75
75
  <td><strong>$29</strong> (one-time)</td>
76
76
  <td>1 project domain</td>
77
- <td>All 85 prefilled directory launch links + CSV export, 25 static HTML SEO route generators (<code>npx git-paid fix</code>), dedicated <code>&lt;domain&gt;-bot@postbuild.dev</code> verification inbox (30-day retention), and CLI/MCP key.</td>
77
+ <td>All 85 prefilled directory launch links + CSV export, 1-click <code>.zip</code> Launch Bundle with 25 static HTML SEO routes (<code>npx git-paid fix</code>), dedicated <code>&lt;domain&gt;-bot@postbuild.dev</code> verification inbox (30-day retention), and CLI/MCP key.</td>
78
78
  </tr>
79
79
  <tr>
80
- <td><strong>Studio</strong></td>
81
- <td><strong>$49 / month</strong></td>
82
- <td>Multi-project</td>
83
- <td>Everything in Launch Pass across multiple domains, IndexNow &amp; WebSub batch pinging, and Stripe Customer Portal billing management.</td>
80
+ <td><strong>Done-For-You &amp; Studio</strong></td>
81
+ <td><strong>Waitlist</strong></td>
82
+ <td>Multi-project / Concierge</td>
83
+ <td>Done-for-you manual &amp; CAPTCHA directory submissions, weekly AI search visibility checks, and Buyer Intent Radar alerts (contact <code>support@postbuild.dev</code>).</td>
84
84
  </tr>
85
85
  </tbody>
86
86
  </table>
@@ -91,39 +91,39 @@ function renderTopicSpecificSection(pageSlug, cleanName, domain) {
91
91
  if (pageSlug.includes("roi-case-study")) {
92
92
  return `
93
93
  <section class="card" style="margin-bottom:28px;">
94
- <h2>Dogfooding Case Study: Launching <code>www.postbuild.dev</code> with <code>npx git-paid</code></h2>
94
+ <h2>Live Self-Audit Walkthrough: How <code>www.postbuild.dev</code> Uses <code>npx git-paid</code></h2>
95
95
  <p style="font-size:14px;color:var(--muted);margin-top:4px;">
96
- We built and launched <code>www.postbuild.dev</code> using our own CLI and MCP server. Here is the exact baseline vs. post-build output measured on our own repository:
96
+ Every deliverable generated for <code>www.postbuild.dev</code> is publicly deployed and verifiable via live HTTP inspection:
97
97
  </p>
98
- <div class="table-wrap" role="region" aria-label="Dogfooding launch metrics" tabindex="0">
98
+ <div class="table-wrap" role="region" aria-label="Live self-audit verification" tabindex="0">
99
99
  <table>
100
100
  <thead>
101
101
  <tr>
102
102
  <th>Deliverable</th>
103
- <th>Before <code>npx git-paid fix</code></th>
104
- <th>After <code>npx git-paid fix</code></th>
103
+ <th>Without Postbuild</th>
104
+ <th>Live on <code>www.postbuild.dev</code> (Verifiable Now)</th>
105
105
  </tr>
106
106
  </thead>
107
107
  <tbody>
108
108
  <tr>
109
- <td><strong>Technical Readiness Score</strong></td>
110
- <td>38 / 100 (missing <code>llms.txt</code>, <code>sitemap.xml</code>, and AI crawler rules)</td>
111
- <td>100 / 100 (all 8 SEO, JSON-LD, OpenGraph, and telemetry checks passing)</td>
109
+ <td><strong>8-Point Technical Audit</strong></td>
110
+ <td>Manual inspection of headers, schema, and crawler rules</td>
111
+ <td><strong>100 / 100</strong> across all 8 checks (run <code>npx git-paid https://www.postbuild.dev</code> to verify live)</td>
112
112
  </tr>
113
113
  <tr>
114
114
  <td><strong>Static SEO Landing Pages</strong></td>
115
- <td>1 route (<code>/</code> only)</td>
116
- <td>26 routes indexed in <code>sitemap.xml</code> (<code>/</code> + 25 pre-rendered guides &amp; comparisons)</td>
115
+ <td>Single-page app root (<code>/</code>) only</td>
116
+ <td>26 routes indexed in <a href="/sitemap.xml"><code>/sitemap.xml</code></a> (<code>/</code> + 25 pre-rendered static HTML guides)</td>
117
117
  </tr>
118
118
  <tr>
119
- <td><strong>Directory Submission Prep</strong></td>
120
- <td>Manual spreadsheet tracking</td>
121
- <td>85 prefilled submission URLs with UTM parameters + dedicated <code>postbuild-bot@postbuild.dev</code> inbox</td>
119
+ <td><strong>AI Search Crawler Manifest</strong></td>
120
+ <td>No <code>llms.txt</code> or explicit AI bot rules</td>
121
+ <td>Live <a href="/llms.txt"><code>/llms.txt</code></a> and <a href="/robots.txt"><code>/robots.txt</code></a> allowing <code>OAI-SearchBot</code>, <code>PerplexityBot</code>, and <code>ClaudeBot</code></td>
122
122
  </tr>
123
123
  <tr>
124
- <td><strong>Setup Time</strong></td>
125
- <td>~28 hours estimated manual work</td>
126
- <td>Under 5 minutes via CLI + Vercel deploy</td>
124
+ <td><strong>Directory Launch Queue</strong></td>
125
+ <td>Manual spreadsheet copy-pasting &amp; personal inbox clutter</td>
126
+ <td>85 curated directories ranked by DR with prefilled UTM links + dedicated <code>postbuild-bot@postbuild.dev</code> inbox</td>
127
127
  </tr>
128
128
  </tbody>
129
129
  </table>
@@ -151,7 +151,7 @@ function renderTopicSpecificSection(pageSlug, cleanName, domain) {
151
151
  <tbody>
152
152
  <tr>
153
153
  <td><strong>Manual Spreadsheets</strong></td>
154
- <td>$0 (14–28 hrs founder time)</td>
154
+ <td>$0 (manual form entry)</td>
155
155
  <td>Clutters personal email</td>
156
156
  <td>Manual authoring required</td>
157
157
  </tr>
@@ -192,22 +192,28 @@ export function renderSinglePseoPage(page, allPages = [], options = {}) {
192
192
  const relatedPages = allPages.filter((p) => p.path !== page.path).slice(0, 8);
193
193
  const topicSection = renderTopicSpecificSection(pageSlug, cleanName, domain);
194
194
 
195
+ const softwareAppNode = {
196
+ "@type": "SoftwareApplication",
197
+ name: cleanName,
198
+ url: origin,
199
+ applicationCategory: "DeveloperApplication",
200
+ operatingSystem: "Web, macOS, Windows, Linux",
201
+ description: metaDescription,
202
+ ...(domain === "postbuild.dev"
203
+ ? {
204
+ offers: {
205
+ "@type": "Offer",
206
+ price: "29.00",
207
+ priceCurrency: "USD"
208
+ }
209
+ }
210
+ : {})
211
+ };
212
+
195
213
  const jsonLd = {
196
214
  "@context": "https://schema.org",
197
215
  "@graph": [
198
- {
199
- "@type": "SoftwareApplication",
200
- name: cleanName,
201
- url: origin,
202
- applicationCategory: "DeveloperApplication",
203
- operatingSystem: "Web, macOS, Windows, Linux",
204
- description: metaDescription,
205
- offers: {
206
- "@type": "Offer",
207
- price: "29.00",
208
- priceCurrency: "USD"
209
- }
210
- },
216
+ softwareAppNode,
211
217
  {
212
218
  "@type": "BreadcrumbList",
213
219
  itemListElement: [
@@ -257,7 +263,7 @@ export function renderSinglePseoPage(page, allPages = [], options = {}) {
257
263
  <meta name="twitter:title" content="${escapeHtml(fullTitle)}" />
258
264
  <meta name="twitter:description" content="${escapeHtml(metaDescription)}" />
259
265
  <link rel="icon" type="image/svg+xml" href="/brand/postbuild-icon.svg" />
260
- <script type="application/ld+json">${JSON.stringify(jsonLd)}</script>
266
+ <script type="application/ld+json">${JSON.stringify(jsonLd).replace(/</g, "\\u003c")}</script>
261
267
  <script>
262
268
  window.si = window.si || function () { (window.siq = window.siq || []).push(arguments); };
263
269
  </script>
@@ -632,7 +638,7 @@ export function renderSinglePseoPage(page, allPages = [], options = {}) {
632
638
 
633
639
  <footer class="site-footer">
634
640
  <div>
635
- <strong>postbuild.dev</strong> (<code>git-paid</code> v2.1.5) — Post-build SEO, directory launch kit &amp; verification inbox.
641
+ <strong>postbuild.dev</strong> (<code>git-paid</code> v2.1.6) — Post-build SEO, directory launch kit &amp; verification inbox.
636
642
  </div>
637
643
  <div style="display:flex;gap:14px;flex-wrap:wrap;">
638
644
  <a href="/">Home &amp; Audit</a>
@@ -657,19 +663,213 @@ export function renderSinglePseoPage(page, allPages = [], options = {}) {
657
663
  </html>`;
658
664
  }
659
665
 
666
+ export function renderBuyerStarterPseoPage(page, allPages = [], options = {}) {
667
+ const cleanDomain = String(options.domain || "example.com")
668
+ .toLowerCase()
669
+ .trim()
670
+ .replace(/^https?:\/\//, "")
671
+ .replace(/^www\./, "")
672
+ .split("/")[0];
673
+ const cleanOrig = String(options.origin || `https://${cleanDomain}`).replace(/\/$/, "");
674
+ const prodName = String(options.cleanName || cleanDomain).trim();
675
+ const canonicalUrl = `${cleanOrig}${page.path}`;
676
+ const cat = page.category || page.volume || "Guide";
677
+ const cleanHeading = String(page.keyword || humanizeSlug(page.path, prodName).title);
678
+ const pageTitle = cleanHeading.toLowerCase().includes(prodName.toLowerCase())
679
+ ? cleanHeading
680
+ : `${cleanHeading} | ${prodName}`;
681
+
682
+ const rawTagline = String(options.tagline || "").trim();
683
+ const tagline =
684
+ rawTagline &&
685
+ rawTagline.toLowerCase() !== prodName.toLowerCase() &&
686
+ !rawTagline.toLowerCase().includes(`(${cleanDomain})`)
687
+ ? rawTagline
688
+ : "";
689
+
690
+ const shortPitch = String(options.shortPitch || options.description || "").trim();
691
+ const summaryText =
692
+ shortPitch ||
693
+ `${prodName} is available at ${cleanOrig}. Customize this starter draft with your specific product workflows, screenshots, and customer examples before publishing.`;
694
+
695
+ const cats =
696
+ Array.isArray(options.categories) && options.categories.length > 0
697
+ ? options.categories.join(", ")
698
+ : "Web Application";
699
+
700
+ const metaDesc = `${cleanHeading}. ${summaryText}`.slice(0, 158).trim();
701
+ const ogImgUrl = options.ogImage || `${cleanOrig}/og-card.svg`;
702
+ const related = (allPages || []).filter((p) => p.path !== page.path).slice(0, 5);
703
+ const headingTopic = cleanHeading
704
+ .replace(new RegExp(`^${prodName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[—–:-]?\\s*`, "i"), "")
705
+ .trim() || cleanHeading;
706
+
707
+ const hasFaqSchema = String(page.schemaType || page.kd || "").includes("FAQPage");
708
+ const faqItems = [
709
+ {
710
+ q: `What is covered in this ${headingTopic} guide?`,
711
+ a: `${summaryText}`
712
+ },
713
+ {
714
+ q: `Where can I access ${prodName}?`,
715
+ a: `Visit ${cleanOrig} to explore ${prodName} directly and review current product documentation.`
716
+ }
717
+ ];
718
+
719
+ const graph = [
720
+ {
721
+ "@type": "SoftwareApplication",
722
+ name: prodName,
723
+ url: cleanOrig,
724
+ applicationCategory: "BusinessApplication",
725
+ operatingSystem: "Web",
726
+ description: summaryText
727
+ },
728
+ {
729
+ "@type": "BreadcrumbList",
730
+ itemListElement: [
731
+ { "@type": "ListItem", position: 1, name: prodName, item: cleanOrig },
732
+ { "@type": "ListItem", position: 2, name: cat, item: canonicalUrl }
733
+ ]
734
+ }
735
+ ];
736
+ if (hasFaqSchema) {
737
+ graph.push({
738
+ "@type": "FAQPage",
739
+ mainEntity: faqItems.map((f) => ({
740
+ "@type": "Question",
741
+ name: f.q,
742
+ acceptedAnswer: { "@type": "Answer", text: f.a }
743
+ }))
744
+ });
745
+ }
746
+ const ldJson = JSON.stringify({ "@context": "https://schema.org", "@graph": graph }).replace(/</g, "\\u003c");
747
+
748
+ let bodySectionHtml = "";
749
+ if (cat === "Comparison" || cat === "Walkthrough") {
750
+ bodySectionHtml = ` <section class="card">
751
+ <h2>Evaluation Criteria: ${escapeHtml(headingTopic)}</h2>
752
+ <p>Use the worksheet below when comparing <strong>${escapeHtml(prodName)}</strong> against existing internal workflows:</p>
753
+ <table class="cmp-table">
754
+ <thead><tr><th>Criterion</th><th>${escapeHtml(prodName)} (${escapeHtml(cleanDomain)})</th><th>Current / Baseline Process</th></tr></thead>
755
+ <tbody>
756
+ <tr><td><strong>Primary Scope</strong></td><td>${escapeHtml(tagline || prodName)}</td><td>Document current team baseline</td></tr>
757
+ <tr><td><strong>Overview</strong></td><td>${escapeHtml(summaryText)}</td><td>Record current steps and handoffs</td></tr>
758
+ <tr><td><strong>Direct Access</strong></td><td><a href="${escapeHtml(cleanOrig)}">${escapeHtml(cleanOrig)}</a></td><td>Internal docs / spreadsheets</td></tr>
759
+ </tbody>
760
+ </table>
761
+ </section>`;
762
+ } else if (cat === "Calculator") {
763
+ bodySectionHtml = ` <section class="card">
764
+ <h2>Workflow Time Worksheet — ${escapeHtml(headingTopic)}</h2>
765
+ <p>Enter your team's monthly frequency and average minutes per task to estimate total monthly time spent on this workflow:</p>
766
+ <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:18px;margin-top:12px;">
767
+ <label style="display:block;font-weight:600;margin-bottom:6px;">Monthly runs: <span id="runsVal">10 runs/mo</span></label>
768
+ <input id="runsInput" type="range" min="1" max="60" value="10" style="width:100%;margin-bottom:12px;" oninput="var r=parseInt(this.value,10)||1;var m=parseInt(document.getElementById('minsInput').value,10)||15;document.getElementById('runsVal').textContent=r+' runs/mo';document.getElementById('hrsTotal').textContent=((r*m)/60).toFixed(1)+' hrs/mo';" />
769
+ <label style="display:block;font-weight:600;margin-bottom:6px;">Minutes per run: <span id="minsVal">30 mins/run</span></label>
770
+ <input id="minsInput" type="range" min="5" max="120" step="5" value="30" style="width:100%;" oninput="var m=parseInt(this.value,10)||15;var r=parseInt(document.getElementById('runsInput').value,10)||1;document.getElementById('minsVal').textContent=m+' mins/run';document.getElementById('hrsTotal').textContent=((r*m)/60).toFixed(1)+' hrs/mo';" />
771
+ <div style="margin-top:14px;background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:12px;">
772
+ <div style="font-size:12px;color:#64748b;">Estimated Monthly Workflow Time (Runs &times; Minutes)</div>
773
+ <div id="hrsTotal" style="font-size:22px;font-weight:800;color:#047857;">5.0 hrs/mo</div>
774
+ </div>
775
+ </div>
776
+ </section>`;
777
+ } else {
778
+ bodySectionHtml = ` <section class="card">
779
+ <h2>Key Notes: ${escapeHtml(headingTopic)}</h2>
780
+ <ul style="padding-left:20px;margin:12px 0;">
781
+ <li style="margin-bottom:10px;"><strong>Objective:</strong> Define how ${escapeHtml(prodName)} fits into your ${escapeHtml(headingTopic.toLowerCase())} workflow.</li>
782
+ <li style="margin-bottom:10px;"><strong>Live Environment:</strong> Visit <a href="${escapeHtml(cleanOrig)}">${escapeHtml(cleanOrig)}</a> to inspect current features and setup options.</li>
783
+ <li style="margin-bottom:10px;"><strong>Customization Note:</strong> Replace this starter section with your product's specific steps, screenshots, or code examples before publishing.</li>
784
+ </ul>
785
+ </section>`;
786
+ }
787
+
788
+ return `<!DOCTYPE html>
789
+ <!-- Starter SEO draft for ${escapeHtml(cleanDomain)} (${escapeHtml(page.path)}). Review and customize with your product details before publishing to production. -->
790
+ <html lang="en">
791
+ <head>
792
+ <meta charset="UTF-8" />
793
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
794
+ <title>${escapeHtml(pageTitle)}</title>
795
+ <meta name="description" content="${escapeHtml(metaDesc)}" />
796
+ <link rel="canonical" href="${escapeHtml(canonicalUrl)}" />
797
+ <meta property="og:type" content="article" />
798
+ <meta property="og:title" content="${escapeHtml(pageTitle)}" />
799
+ <meta property="og:description" content="${escapeHtml(metaDesc)}" />
800
+ <meta property="og:url" content="${escapeHtml(canonicalUrl)}" />
801
+ <meta property="og:image" content="${escapeHtml(ogImgUrl)}" />
802
+ <meta name="twitter:card" content="summary_large_image" />
803
+ <meta name="twitter:title" content="${escapeHtml(pageTitle)}" />
804
+ <meta name="twitter:description" content="${escapeHtml(metaDesc)}" />
805
+ <meta name="twitter:image" content="${escapeHtml(ogImgUrl)}" />
806
+ <script type="application/ld+json">${ldJson}</script>
807
+ <style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;max-width:880px;margin:0 auto;padding:36px 20px 64px;line-height:1.65;color:#0f172a;background:#f8fafc}.card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:26px;margin-bottom:20px}.cta{display:inline-block;background:#047857;color:#fff;padding:10px 18px;border-radius:8px;text-decoration:none;font-weight:600}.cmp-table{width:100%;border-collapse:collapse;margin-top:14px;font-size:14px}.cmp-table th,.cmp-table td{border:1px solid #e2e8f0;padding:10px 12px;text-align:left}.cmp-table th{background:#f1f5f9;font-weight:700}.faq-item{border-top:1px solid #e2e8f0;padding-top:14px;margin-top:14px}</style>
808
+ </head>
809
+ <body>
810
+ <header style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;padding-bottom:14px;border-bottom:1px solid #e2e8f0;">
811
+ <nav aria-label="Breadcrumb" style="font-size:13px;color:#64748b;"><a href="${escapeHtml(cleanOrig)}" style="font-weight:700;color:#0f172a;text-decoration:none;">${escapeHtml(prodName)}</a> / <span>${escapeHtml(cat)}</span></nav>
812
+ <a class="cta" href="${escapeHtml(cleanOrig)}" style="font-size:13px;padding:7px 14px;">Open ${escapeHtml(prodName)} &rarr;</a>
813
+ </header>
814
+ <main>
815
+ <article class="card">
816
+ <div style="font-size:12px;font-weight:700;color:#047857;text-transform:uppercase;letter-spacing:0.04em;">${escapeHtml(cat)} &middot; ${escapeHtml(cats)}</div>
817
+ <h1 style="margin:8px 0 12px;font-size:28px;line-height:1.25;">${escapeHtml(cleanHeading)}</h1>
818
+ ${tagline ? `<p style="font-size:16px;color:#334155;margin-bottom:14px;"><strong>${escapeHtml(prodName)}</strong> &mdash; ${escapeHtml(tagline)}</p>` : ""}
819
+ <p style="color:#475569;">${escapeHtml(summaryText)}</p>
820
+ <p style="margin-top:18px;"><a class="cta" href="${escapeHtml(cleanOrig)}">Explore ${escapeHtml(prodName)} on ${escapeHtml(cleanDomain)} &rarr;</a></p>
821
+ </article>
822
+ ${bodySectionHtml}
823
+ ${
824
+ hasFaqSchema
825
+ ? ` <section class="card">
826
+ <h2>Frequently Asked Questions</h2>
827
+ ${faqItems.map((f) => ` <div class="faq-item"><h3 style="margin:0 0 6px;font-size:16px;">${escapeHtml(f.q)}</h3><p style="margin:0;color:#475569;font-size:14px;">${escapeHtml(f.a)}</p></div>`).join("\n")}
828
+ </section>`
829
+ : ""
830
+ }
831
+ <section class="card">
832
+ <h2>Related ${escapeHtml(prodName)} Guides</h2>
833
+ <ul style="padding-left:20px;margin:10px 0;">
834
+ ${related.map((r) => ` <li style="margin-bottom:6px;"><a href="${escapeHtml(r.path)}" style="color:#0284c7;text-decoration:none;font-weight:500;">${escapeHtml(r.keyword || r.path)}</a> <span style="color:#64748b;font-size:12px;">(${escapeHtml(r.category || r.volume || "Guide")})</span></li>`).join("\n")}
835
+ </ul>
836
+ </section>
837
+ </main>
838
+ </body>
839
+ </html>`;
840
+ }
841
+
660
842
  /**
661
- * Writes all 25 Programmatic SEO HTML files into targetBaseDir so cleanUrls (/use-cases/...) resolve with 200 OK.
843
+ * Writes Programmatic SEO HTML files into targetBaseDir so cleanUrls (/use-cases/...) resolve with 200 OK.
844
+ * Uses the buyer-safe starter template for all customer domains, and only uses postbuild.dev's own marketing
845
+ * template when building postbuild.dev itself.
662
846
  */
663
847
  export function writePseoHtmlFiles(pseoPages = [], targetBaseDir, options = {}) {
848
+ const cleanDomain = String(options.domain || "postbuild.dev")
849
+ .toLowerCase()
850
+ .trim()
851
+ .replace(/^https?:\/\//, "")
852
+ .replace(/^www\./, "")
853
+ .split("/")[0];
854
+ const isPostbuildSelf = cleanDomain === "postbuild.dev";
664
855
  const written = [];
665
856
  for (const page of pseoPages) {
666
857
  const cleanRel = String(page.path || "").replace(/^\/+/, "");
667
858
  if (!cleanRel) continue;
668
- const outFile = path.join(targetBaseDir, `${cleanRel}.html`);
669
- fs.mkdirSync(path.dirname(outFile), { recursive: true });
670
- const html = renderSinglePseoPage(page, pseoPages, options);
671
- fs.writeFileSync(outFile, html, "utf8");
672
- written.push(outFile);
859
+ const html = isPostbuildSelf
860
+ ? renderSinglePseoPage(page, pseoPages, options)
861
+ : renderBuyerStarterPseoPage(page, pseoPages, options);
862
+ const indexOutFile = path.join(targetBaseDir, cleanRel, "index.html");
863
+ fs.mkdirSync(path.dirname(indexOutFile), { recursive: true });
864
+ fs.writeFileSync(indexOutFile, html, "utf8");
865
+ written.push(indexOutFile);
866
+
867
+ if (isPostbuildSelf) {
868
+ const flatOutFile = path.join(targetBaseDir, `${cleanRel}.html`);
869
+ fs.mkdirSync(path.dirname(flatOutFile), { recursive: true });
870
+ fs.writeFileSync(flatOutFile, html, "utf8");
871
+ }
673
872
  }
674
873
  return written;
675
874
  }
875
+
package/lib/safe-fetch.js CHANGED
@@ -145,7 +145,7 @@ export async function assertSafeUrl(rawUrl, options = {}) {
145
145
  return parsed;
146
146
  }
147
147
 
148
- async function readResponseWithCap(response, maxBytes = MAX_RESPONSE_BYTES) {
148
+ async function readResponseWithCap(response, maxBytes = MAX_RESPONSE_BYTES, truncateOnMaxBytes = false) {
149
149
  if (!response.body) {
150
150
  return Buffer.alloc(0);
151
151
  }
@@ -157,11 +157,19 @@ async function readResponseWithCap(response, maxBytes = MAX_RESPONSE_BYTES) {
157
157
  const { done, value } = await reader.read();
158
158
  if (done) break;
159
159
  const buf = Buffer.from(value);
160
- total += buf.length;
161
- if (total > maxBytes) {
162
- await reader.cancel();
160
+ if (total + buf.length > maxBytes) {
161
+ if (truncateOnMaxBytes) {
162
+ const remaining = Math.max(0, maxBytes - total);
163
+ if (remaining > 0) {
164
+ chunks.push(buf.subarray(0, remaining));
165
+ }
166
+ await reader.cancel().catch(() => {});
167
+ break;
168
+ }
169
+ await reader.cancel().catch(() => {});
163
170
  throw new Error(`Response exceeded maximum size limit of ${maxBytes} bytes`);
164
171
  }
172
+ total += buf.length;
165
173
  chunks.push(buf);
166
174
  }
167
175
 
@@ -176,6 +184,7 @@ export async function safeFetch(rawUrl, options = {}) {
176
184
  timeoutMs = DEFAULT_TIMEOUT_MS,
177
185
  maxRedirects = MAX_REDIRECTS,
178
186
  maxBytes = MAX_RESPONSE_BYTES,
187
+ truncateOnMaxBytes = false,
179
188
  allowedDomains = null,
180
189
  lookupFn
181
190
  } = options;
@@ -220,7 +229,7 @@ export async function safeFetch(rawUrl, options = {}) {
220
229
  continue;
221
230
  }
222
231
 
223
- const bodyBuffer = await readResponseWithCap(resp, maxBytes);
232
+ const bodyBuffer = await readResponseWithCap(resp, maxBytes, truncateOnMaxBytes);
224
233
  return {
225
234
  ok: resp.ok,
226
235
  status: resp.status,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-paid",
3
- "version": "2.1.5",
3
+ "version": "2.1.7",
4
4
  "description": "git push ships your code. git paid sells it. Autonomous SEO, Paywall, MCP & Directory Launch Agent (www.postbuild.dev).",
5
5
  "type": "module",
6
6
  "main": "bin/git-paid.js",
@@ -13,6 +13,7 @@
13
13
  "lib/directories.js",
14
14
  "lib/engine.js",
15
15
  "lib/fixer.js",
16
+ "lib/guard.js",
16
17
  "lib/inbox.js",
17
18
  "lib/mcp.js",
18
19
  "lib/pseo.js",