git-paid 2.1.5 → 2.1.6
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/git-paid.js +11 -8
- package/lib/auth.js +48 -5
- package/lib/engine.js +6 -6
- package/lib/fixer.js +3 -0
- package/lib/guard.js +200 -0
- package/lib/inbox.js +7 -6
- package/lib/mcp.js +20 -10
- package/lib/pseo.js +42 -36
- package/package.json +2 -1
package/bin/git-paid.js
CHANGED
|
@@ -26,7 +26,10 @@ if (subcmd === "mcp" || args.includes("--mcp")) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function printProGateBanner(licenseCheck, featureName) {
|
|
29
|
-
console.log(` [LOCKED] ${featureName} requires an active postbuild Launch Pass ($29)
|
|
29
|
+
console.log(` [LOCKED] ${featureName} requires an active postbuild Launch Pass ($29) license.`);
|
|
30
|
+
if (licenseCheck && licenseCheck.reason === "DOMAIN_MISMATCH") {
|
|
31
|
+
console.log(` [SCOPE] Your active license covers ${licenseCheck.licensedDomain}, not ${licenseCheck.requestedDomain}.`);
|
|
32
|
+
}
|
|
30
33
|
console.log(` [UNLOCK] Instant Stripe Checkout ($29 Launch Pass):`);
|
|
31
34
|
console.log(` ${licenseCheck.checkoutUrl}\n`);
|
|
32
35
|
console.log(` [LOGIN] Already purchased? Authenticate your CLI & MCP server:`);
|
|
@@ -42,11 +45,11 @@ async function runCli() {
|
|
|
42
45
|
if (subcmd === "--help" || subcmd === "-h" || subcmd === "help") {
|
|
43
46
|
console.log(" Usage:");
|
|
44
47
|
console.log(" npx git-paid <url> Run free 8-point SEO/paywall audit & 5-directory preview");
|
|
45
|
-
console.log(" git paid login <pb_live_key> Authenticate CLI & MCP with your Launch Pass
|
|
48
|
+
console.log(" git paid login <pb_live_key> Authenticate CLI & MCP with your Launch Pass license");
|
|
46
49
|
console.log(" git paid fix [url] [PRO] Write 25 pSEO HTML pages, robots.txt, sitemap.xml & llms.txt");
|
|
47
50
|
console.log(" git paid submit [url] [PRO] Headless 85-directory form submitter + OTP auto-verifier");
|
|
48
51
|
console.log(" git paid inbox [bot-email] [PRO] Check & auto-verify <domain-tld>-bot@postbuild.dev (e.g. acme-com-bot@) links & OTPs");
|
|
49
|
-
console.log(" git paid mcp Start Postbuild MCP Server (postbuild-mcp v2.1.
|
|
52
|
+
console.log(" git paid mcp Start Postbuild MCP Server (postbuild-mcp v2.1.6)");
|
|
50
53
|
console.log(" git paid init-agent Scaffold .agent/skills/postbuild/SKILL.md & MCP config");
|
|
51
54
|
console.log(" git paid vault [path] [PRO] Sync live telemetry & Bento dashboard to Obsidian VaultOS\n");
|
|
52
55
|
process.exit(0);
|
|
@@ -150,7 +153,7 @@ async function runCli() {
|
|
|
150
153
|
if (subcmd === "init-agent" || subcmd === "agent" || subcmd === "skill") {
|
|
151
154
|
const res = installAgentSkillFiles({ cwd: process.cwd() });
|
|
152
155
|
console.log(` [+] Scaffolded Agent Skill: ${res.skillMdPath}`);
|
|
153
|
-
console.log(` [+] Powered by: postbuild-mcp@2.1.
|
|
156
|
+
console.log(` [+] Powered by: postbuild-mcp@2.1.6 (Stateful Delta Compaction)\n`);
|
|
154
157
|
console.log(" Add this to your Claude Code / Cursor / Antigravity MCP config:\n");
|
|
155
158
|
console.log(JSON.stringify(res.mcpConfigSnippet, null, 2));
|
|
156
159
|
console.log("");
|
|
@@ -158,7 +161,7 @@ async function runCli() {
|
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
if (subcmd === "vault" || subcmd === "vaultos") {
|
|
161
|
-
const license = await verifyCliLicense({ domain: "
|
|
164
|
+
const license = await verifyCliLicense({ domain: "" });
|
|
162
165
|
if (!license.valid) {
|
|
163
166
|
printProGateBanner(license, "Markdown / Docs & Telemetry Sync");
|
|
164
167
|
process.exit(1);
|
|
@@ -168,7 +171,7 @@ async function runCli() {
|
|
|
168
171
|
args.find((a, i) => i > 0 && !a.startsWith("--") && !/^https?:\/\//i.test(a)) ||
|
|
169
172
|
process.cwd();
|
|
170
173
|
const urlArgs = args.filter((a, i) => i > 0 && /^https?:\/\//i.test(a));
|
|
171
|
-
const targetUrls = urlArgs.length ? urlArgs : ["https://postbuild.dev"];
|
|
174
|
+
const targetUrls = urlArgs.length ? urlArgs : [license.domain ? `https://${license.domain}` : "https://postbuild.dev"];
|
|
172
175
|
|
|
173
176
|
console.log(` [*] Syncing Postbuild MCP Telemetry & Launch Status...`);
|
|
174
177
|
console.log(` [*] Target Directory: ${vaultArg}`);
|
|
@@ -200,12 +203,12 @@ async function runCli() {
|
|
|
200
203
|
}
|
|
201
204
|
|
|
202
205
|
if (subcmd === "inbox") {
|
|
203
|
-
const
|
|
204
|
-
const license = await verifyCliLicense({ domain: "postbuild.dev" });
|
|
206
|
+
const license = await verifyCliLicense({ domain: "" });
|
|
205
207
|
if (!license.valid) {
|
|
206
208
|
printProGateBanner(license, "Dedicated @postbuild.dev Verification Inbox & OTP Auto-Verifier");
|
|
207
209
|
process.exit(1);
|
|
208
210
|
}
|
|
211
|
+
const inboxAddr = args.find((a, i) => i > 0 && !a.startsWith("--")) || license.inbox || "postbuild-bot@postbuild.dev";
|
|
209
212
|
|
|
210
213
|
if (args.includes("--test")) {
|
|
211
214
|
await sendTestVerificationEmail(inboxAddr, "postbuild.dev");
|
package/lib/auth.js
CHANGED
|
@@ -207,12 +207,37 @@ export function loadLocalCredentials() {
|
|
|
207
207
|
|
|
208
208
|
export async function verifyCliLicense({ domain = "postbuild.dev", token = "" } = {}) {
|
|
209
209
|
const candidateToken = (token || "").trim() || loadLocalCredentials()?.token || "";
|
|
210
|
+
const cleanReqDomain = String(domain || "")
|
|
211
|
+
.toLowerCase()
|
|
212
|
+
.trim()
|
|
213
|
+
.replace(/^https?:\/\//, "")
|
|
214
|
+
.replace(/^(www\.)+/, "")
|
|
215
|
+
.split("/")[0]
|
|
216
|
+
.split(":")[0];
|
|
210
217
|
|
|
211
218
|
if (candidateToken && candidateToken.startsWith("pb_live_")) {
|
|
212
219
|
// 1. If running on the server (where signing secret is present), verify signature & revocation directly
|
|
213
220
|
try {
|
|
214
221
|
const localCheck = verifyLicenseToken(candidateToken);
|
|
215
222
|
if (localCheck.valid) {
|
|
223
|
+
const licDomain = String(localCheck.domain || "")
|
|
224
|
+
.toLowerCase()
|
|
225
|
+
.trim()
|
|
226
|
+
.replace(/^https?:\/\//, "")
|
|
227
|
+
.replace(/^(www\.)+/, "")
|
|
228
|
+
.split("/")[0]
|
|
229
|
+
.split(":")[0];
|
|
230
|
+
if (cleanReqDomain && licDomain && licDomain !== "*" && cleanReqDomain !== licDomain) {
|
|
231
|
+
return {
|
|
232
|
+
valid: false,
|
|
233
|
+
reason: "DOMAIN_MISMATCH",
|
|
234
|
+
licensedDomain: licDomain,
|
|
235
|
+
requestedDomain: cleanReqDomain,
|
|
236
|
+
checkoutUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain)}`,
|
|
237
|
+
webUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain)}`,
|
|
238
|
+
message: `Your Launch Pass license key is scoped to ${licDomain} and does not cover ${cleanReqDomain}.`
|
|
239
|
+
};
|
|
240
|
+
}
|
|
216
241
|
return {
|
|
217
242
|
valid: true,
|
|
218
243
|
email: localCheck.email,
|
|
@@ -226,8 +251,8 @@ export async function verifyCliLicense({ domain = "postbuild.dev", token = "" }
|
|
|
226
251
|
return {
|
|
227
252
|
valid: false,
|
|
228
253
|
reason: localCheck.error || "INVALID_OR_REVOKED_LICENSE",
|
|
229
|
-
checkoutUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(domain)}`,
|
|
230
|
-
webUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(domain)}`,
|
|
254
|
+
checkoutUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain || domain)}`,
|
|
255
|
+
webUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain || domain)}`,
|
|
231
256
|
message: localCheck.error || "Invalid or revoked pb_live_ license key."
|
|
232
257
|
};
|
|
233
258
|
} catch {
|
|
@@ -245,6 +270,24 @@ export async function verifyCliLicense({ domain = "postbuild.dev", token = "" }
|
|
|
245
270
|
if (resp.ok) {
|
|
246
271
|
const data = await resp.json();
|
|
247
272
|
if (data && data.ok && data.authenticated) {
|
|
273
|
+
const licDomain = String(data.domain || "")
|
|
274
|
+
.toLowerCase()
|
|
275
|
+
.trim()
|
|
276
|
+
.replace(/^https?:\/\//, "")
|
|
277
|
+
.replace(/^(www\.)+/, "")
|
|
278
|
+
.split("/")[0]
|
|
279
|
+
.split(":")[0];
|
|
280
|
+
if (cleanReqDomain && licDomain && licDomain !== "*" && cleanReqDomain !== licDomain) {
|
|
281
|
+
return {
|
|
282
|
+
valid: false,
|
|
283
|
+
reason: "DOMAIN_MISMATCH",
|
|
284
|
+
licensedDomain: licDomain,
|
|
285
|
+
requestedDomain: cleanReqDomain,
|
|
286
|
+
checkoutUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain)}`,
|
|
287
|
+
webUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain)}`,
|
|
288
|
+
message: `Your Launch Pass license key is scoped to ${licDomain} and does not cover ${cleanReqDomain}.`
|
|
289
|
+
};
|
|
290
|
+
}
|
|
248
291
|
return {
|
|
249
292
|
valid: true,
|
|
250
293
|
email: data.email,
|
|
@@ -262,10 +305,10 @@ export async function verifyCliLicense({ domain = "postbuild.dev", token = "" }
|
|
|
262
305
|
return {
|
|
263
306
|
valid: false,
|
|
264
307
|
reason: "PRO_LICENSE_REQUIRED",
|
|
265
|
-
checkoutUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(domain)}`,
|
|
266
|
-
webUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(domain)}`,
|
|
308
|
+
checkoutUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain || domain)}`,
|
|
309
|
+
webUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain || domain)}`,
|
|
267
310
|
message:
|
|
268
|
-
"A verified postbuild Launch Pass ($29)
|
|
311
|
+
"A verified postbuild Launch Pass ($29) license (pb_live_...) is required to unlock 25 Programmatic SEO HTML pages, 85-directory dispatch, and @postbuild.dev inbox verification."
|
|
269
312
|
};
|
|
270
313
|
}
|
|
271
314
|
|
package/lib/engine.js
CHANGED
|
@@ -384,6 +384,11 @@ Allow: /
|
|
|
384
384
|
|
|
385
385
|
Sitemap: ${origin}/sitemap.xml`;
|
|
386
386
|
|
|
387
|
+
const isPostbuildSelf = origin.includes("postbuild.dev");
|
|
388
|
+
const offersBlock = isPostbuildSelf
|
|
389
|
+
? `,\n "offers": {\n "@type": "Offer",\n "price": "29.00",\n "priceCurrency": "USD"\n }`
|
|
390
|
+
: "";
|
|
391
|
+
|
|
387
392
|
const jsonLd = `<script type="application/ld+json">
|
|
388
393
|
{
|
|
389
394
|
"@context": "https://schema.org",
|
|
@@ -392,12 +397,7 @@ Sitemap: ${origin}/sitemap.xml`;
|
|
|
392
397
|
"url": "${origin}",
|
|
393
398
|
"applicationCategory": "BusinessApplication",
|
|
394
399
|
"operatingSystem": "Web",
|
|
395
|
-
"description": "${description.replace(/"/g, '\\"')}"
|
|
396
|
-
"offers": {
|
|
397
|
-
"@type": "Offer",
|
|
398
|
-
"price": "29.00",
|
|
399
|
-
"priceCurrency": "USD"
|
|
400
|
-
}
|
|
400
|
+
"description": "${description.replace(/"/g, '\\"')}"${offersBlock}
|
|
401
401
|
}
|
|
402
402
|
</script>`;
|
|
403
403
|
|
package/lib/fixer.js
CHANGED
|
@@ -53,6 +53,7 @@ export function buildHeadSeoSnippet(report) {
|
|
|
53
53
|
? `${report.launchKit.brandName || report.launchKit.productName || report.domain} — ${report.launchKit.tagline}`
|
|
54
54
|
: report.meta?.title || report.domain;
|
|
55
55
|
const desc = report.launchKit?.shortPitch || report.meta?.description || "";
|
|
56
|
+
const cleanOrigin = String(report.targetUrl || `https://${report.domain}`).replace(/\/$/, "");
|
|
56
57
|
return `<!-- Generated by postbuild (git paid fix) for ${report.domain} -->
|
|
57
58
|
<title>${title}</title>
|
|
58
59
|
<meta name="description" content="${desc.replace(/"/g, """)}" />
|
|
@@ -64,9 +65,11 @@ export function buildHeadSeoSnippet(report) {
|
|
|
64
65
|
<meta property="og:title" content="${title.replace(/"/g, """)}" />
|
|
65
66
|
<meta property="og:description" content="${desc.replace(/"/g, """)}" />
|
|
66
67
|
<meta property="og:url" content="${report.targetUrl}" />
|
|
68
|
+
<meta property="og:image" content="${cleanOrigin}/og-card.png" />
|
|
67
69
|
<meta name="twitter:card" content="summary_large_image" />
|
|
68
70
|
<meta name="twitter:title" content="${title.replace(/"/g, """)}" />
|
|
69
71
|
<meta name="twitter:description" content="${desc.replace(/"/g, """)}" />
|
|
72
|
+
<meta name="twitter:image" content="${cleanOrigin}/og-card.png" />
|
|
70
73
|
|
|
71
74
|
<!-- Structured Data (JSON-LD) -->
|
|
72
75
|
${report.artifacts?.jsonLd || ""}
|
package/lib/guard.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Authentication, Inbox Ownership, Admin Guard, CORS & Rate Limiting for Postbuild API Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import { verifyLicenseToken, checkStripeCustomerStatusCached, getFounderEmailsSet } from "./auth.js";
|
|
7
|
+
import { getBotInboxForDomain, normalizeInboxInput } from "./inbox.js";
|
|
8
|
+
|
|
9
|
+
const rateLimitBuckets = new Map(); // `${bucket}:${ip}` -> { count: number, resetAt: number }
|
|
10
|
+
|
|
11
|
+
const ALLOWED_ORIGINS = new Set([
|
|
12
|
+
"https://www.postbuild.dev",
|
|
13
|
+
"https://postbuild.dev"
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export function applyCorsHeaders(req, res) {
|
|
17
|
+
const origin = req.headers?.origin || "";
|
|
18
|
+
if (ALLOWED_ORIGINS.has(origin)) {
|
|
19
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
20
|
+
res.setHeader("Vary", "Origin");
|
|
21
|
+
} else {
|
|
22
|
+
res.setHeader("Access-Control-Allow-Origin", "https://www.postbuild.dev");
|
|
23
|
+
}
|
|
24
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
25
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Admin-Secret");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function extractBearerToken(req, body = null) {
|
|
29
|
+
const header = req.headers?.authorization || req.headers?.Authorization || "";
|
|
30
|
+
if (typeof header === "string" && header.startsWith("Bearer ")) {
|
|
31
|
+
return header.slice(7).trim();
|
|
32
|
+
}
|
|
33
|
+
if (body && typeof body === "object" && typeof body.token === "string") {
|
|
34
|
+
return body.token.trim();
|
|
35
|
+
}
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function requireLicense(req, body = null) {
|
|
40
|
+
const token = extractBearerToken(req, body);
|
|
41
|
+
if (!token) return null;
|
|
42
|
+
try {
|
|
43
|
+
const verified = verifyLicenseToken(token);
|
|
44
|
+
return verified && verified.valid && verified.email ? { ...verified, token } : null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function requireLicenseAsync(req, body = null) {
|
|
51
|
+
const verified = requireLicense(req, body);
|
|
52
|
+
if (!verified) return null;
|
|
53
|
+
const remoteStatus = await checkStripeCustomerStatusCached(verified);
|
|
54
|
+
if (remoteStatus !== "active") {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return verified;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isValidProjectDomain(rawDomain = "", allowPostbuildOperator = false) {
|
|
61
|
+
const clean = String(rawDomain || "")
|
|
62
|
+
.toLowerCase()
|
|
63
|
+
.trim()
|
|
64
|
+
.replace(/^https?:\/\//, "")
|
|
65
|
+
.replace(/^(www\.)+/, "")
|
|
66
|
+
.split("/")[0]
|
|
67
|
+
.split("?")[0]
|
|
68
|
+
.split("#")[0]
|
|
69
|
+
.split(":")[0];
|
|
70
|
+
|
|
71
|
+
if (!clean || !clean.includes(".") || clean.length < 4 || clean.length > 253) {
|
|
72
|
+
return { valid: false, domain: "" };
|
|
73
|
+
}
|
|
74
|
+
if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/.test(clean)) {
|
|
75
|
+
return { valid: false, domain: "" };
|
|
76
|
+
}
|
|
77
|
+
const labels = clean.split(".");
|
|
78
|
+
const tld = labels[labels.length - 1];
|
|
79
|
+
if (tld.length < 2 || /^\d+$/.test(tld)) {
|
|
80
|
+
return { valid: false, domain: "" };
|
|
81
|
+
}
|
|
82
|
+
if (
|
|
83
|
+
clean === "example.com" ||
|
|
84
|
+
clean.endsWith(".invalid") ||
|
|
85
|
+
clean.endsWith(".localhost") ||
|
|
86
|
+
clean.endsWith(".local") ||
|
|
87
|
+
clean.endsWith(".internal")
|
|
88
|
+
) {
|
|
89
|
+
return { valid: false, domain: "" };
|
|
90
|
+
}
|
|
91
|
+
if (!allowPostbuildOperator && (clean === "postbuild.dev" || clean.endsWith(".postbuild.dev"))) {
|
|
92
|
+
return { valid: false, domain: "" };
|
|
93
|
+
}
|
|
94
|
+
return { valid: true, domain: clean };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function requireAdmin(req) {
|
|
98
|
+
const givenStr = String(req.headers?.["x-admin-secret"] || "").trim();
|
|
99
|
+
const expectedStr = String(process.env.POSTBUILD_ADMIN_SECRET || "").trim();
|
|
100
|
+
if (expectedStr.length < 32 || givenStr.length !== expectedStr.length) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const given = Buffer.from(givenStr, "utf8");
|
|
104
|
+
const expected = Buffer.from(expectedStr, "utf8");
|
|
105
|
+
return crypto.timingSafeEqual(given, expected);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Verifies that the authenticated license holder owns the requested inbox and domain.
|
|
110
|
+
* Every license is strictly bound to its own verified domain and inbox.
|
|
111
|
+
*/
|
|
112
|
+
export function isInboxOwnedByLicense(verified, requestedInbox = "", requestedDomain = "") {
|
|
113
|
+
if (!verified || !verified.valid) return false;
|
|
114
|
+
|
|
115
|
+
const licenseDomain = String(verified.domain || "")
|
|
116
|
+
.toLowerCase()
|
|
117
|
+
.trim()
|
|
118
|
+
.replace(/^https?:\/\//, "")
|
|
119
|
+
.replace(/^www\./, "")
|
|
120
|
+
.split("/")[0]
|
|
121
|
+
.split(":")[0];
|
|
122
|
+
const licenseEmail = String(verified.email || "").toLowerCase().trim();
|
|
123
|
+
if (!licenseDomain || !licenseDomain.includes(".")) return false;
|
|
124
|
+
|
|
125
|
+
const isPostbuildOperator = getFounderEmailsSet().has(licenseEmail);
|
|
126
|
+
|
|
127
|
+
// Only an explicit operator may access the postbuild.dev inbox
|
|
128
|
+
if (licenseDomain === "postbuild.dev" && !isPostbuildOperator) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (requestedDomain) {
|
|
133
|
+
const cleanReqDomain = String(requestedDomain)
|
|
134
|
+
.toLowerCase()
|
|
135
|
+
.trim()
|
|
136
|
+
.replace(/^https?:\/\//, "")
|
|
137
|
+
.replace(/^www\./, "")
|
|
138
|
+
.split("/")[0]
|
|
139
|
+
.split(":")[0];
|
|
140
|
+
if (!cleanReqDomain || cleanReqDomain !== licenseDomain) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const targetInbox = normalizeInboxInput(requestedInbox || requestedDomain || verified.inbox || licenseDomain);
|
|
146
|
+
if (!targetInbox || targetInbox === "unassigned-bot@postbuild.dev") return false;
|
|
147
|
+
if (targetInbox === "postbuild-bot@postbuild.dev" && !isPostbuildOperator) return false;
|
|
148
|
+
|
|
149
|
+
const expectedInboxWithEmail = getBotInboxForDomain(licenseDomain, licenseEmail);
|
|
150
|
+
const expectedInboxDomainOnly = getBotInboxForDomain(licenseDomain);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
targetInbox === verified.inbox ||
|
|
154
|
+
targetInbox === expectedInboxWithEmail ||
|
|
155
|
+
targetInbox === expectedInboxDomainOnly
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getClientIp(req) {
|
|
160
|
+
const xff = req.headers?.["x-forwarded-for"];
|
|
161
|
+
if (typeof xff === "string" && xff.trim()) {
|
|
162
|
+
return xff.split(",")[0].trim();
|
|
163
|
+
}
|
|
164
|
+
return req.socket?.remoteAddress || "127.0.0.1";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function checkRateLimit(req, bucket = "default", limitPerMinute = 20) {
|
|
168
|
+
if (process.env.POSTBUILD_DISABLE_RATE_LIMIT === "1") {
|
|
169
|
+
return { allowed: true, remaining: limitPerMinute };
|
|
170
|
+
}
|
|
171
|
+
const ip = getClientIp(req);
|
|
172
|
+
const key = `${bucket}:${ip}`;
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
const windowMs = 60 * 1000;
|
|
175
|
+
|
|
176
|
+
let entry = rateLimitBuckets.get(key);
|
|
177
|
+
if (!entry || now >= entry.resetAt) {
|
|
178
|
+
entry = { count: 0, resetAt: now + windowMs };
|
|
179
|
+
rateLimitBuckets.set(key, entry);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
entry.count += 1;
|
|
183
|
+
if (entry.count > limitPerMinute) {
|
|
184
|
+
return {
|
|
185
|
+
allowed: false,
|
|
186
|
+
remaining: 0,
|
|
187
|
+
retryAfterSeconds: Math.max(1, Math.ceil((entry.resetAt - now) / 1000))
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
allowed: true,
|
|
193
|
+
remaining: Math.max(0, limitPerMinute - entry.count),
|
|
194
|
+
retryAfterSeconds: 0
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function resetRateLimitsForTests() {
|
|
199
|
+
rateLimitBuckets.clear();
|
|
200
|
+
}
|
package/lib/inbox.js
CHANGED
|
@@ -232,7 +232,7 @@ async function syncFromResendReceivingApi(targetInbox = "") {
|
|
|
232
232
|
const normalizedTarget = targetInbox.trim().toLowerCase();
|
|
233
233
|
|
|
234
234
|
try {
|
|
235
|
-
const resp = await fetch("https://api.resend.com/emails/receiving?limit=
|
|
235
|
+
const resp = await fetch("https://api.resend.com/emails/receiving?limit=100", {
|
|
236
236
|
headers: { Authorization: `Bearer ${resendApiKey}` },
|
|
237
237
|
signal: AbortSignal.timeout(3500)
|
|
238
238
|
});
|
|
@@ -241,15 +241,16 @@ async function syncFromResendReceivingApi(targetInbox = "") {
|
|
|
241
241
|
const data = await resp.json();
|
|
242
242
|
const items = Array.isArray(data?.data) ? data.data : Array.isArray(data) ? data : [];
|
|
243
243
|
|
|
244
|
-
|
|
245
|
-
const id = item.id || item.email_id;
|
|
246
|
-
if (!id || memoryInboxMessages.has(id)) continue;
|
|
247
|
-
|
|
244
|
+
const matchingItems = items.filter((item) => {
|
|
248
245
|
const recipients = (Array.isArray(item.to) ? item.to : [item.to])
|
|
249
246
|
.filter(Boolean)
|
|
250
247
|
.map((r) => String(r).replace(/^.*<([^>]+)>.*$/, "$1").trim().toLowerCase());
|
|
248
|
+
return recipients.includes(normalizedTarget);
|
|
249
|
+
});
|
|
251
250
|
|
|
252
|
-
|
|
251
|
+
for (const item of matchingItems.slice(0, 15)) {
|
|
252
|
+
const id = item.id || item.email_id;
|
|
253
|
+
if (!id || memoryInboxMessages.has(id)) continue;
|
|
253
254
|
|
|
254
255
|
let fullItem = item;
|
|
255
256
|
if (!item.text && !item.html) {
|
package/lib/mcp.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Postbuild Stateful MCP Server & Obsidian VaultOS Sync Engine
|
|
3
|
-
* Powered by Postbuild MCP Middleware (postbuild-mcp v2.1.
|
|
3
|
+
* Powered by Postbuild MCP Middleware (postbuild-mcp v2.1.6)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import fs from "node:fs";
|
|
@@ -8,6 +8,7 @@ import path from "node:path";
|
|
|
8
8
|
import readline from "node:readline";
|
|
9
9
|
import { scanTargetUrl, searchBuyerIntentRadar, dispatchDirectories, DIRECTORIES } from "./engine.js";
|
|
10
10
|
import { verifyCliLicense } from "./auth.js";
|
|
11
|
+
import { isInboxOwnedByLicense } from "./guard.js";
|
|
11
12
|
import { applyLocalSeoFixes } from "./fixer.js";
|
|
12
13
|
import { listInboxMessages, sendTestVerificationEmail, getBotInboxForDomain, markMessageVerified } from "./inbox.js";
|
|
13
14
|
import { withStatefulMcp, createGenericEntityAdapter, spotlightUntrustedText } from "./stateful-mcp.js";
|
|
@@ -238,7 +239,7 @@ export async function runStatefulMcpSimulation({
|
|
|
238
239
|
});
|
|
239
240
|
|
|
240
241
|
return {
|
|
241
|
-
server: "postbuild-mcp@2.1.
|
|
242
|
+
server: "postbuild-mcp@2.1.6",
|
|
242
243
|
domain: state.domain,
|
|
243
244
|
readinessScore: report.readinessScore,
|
|
244
245
|
botEmail: report.botEmail,
|
|
@@ -317,9 +318,9 @@ export async function syncPostbuildToVault({
|
|
|
317
318
|
|
|
318
319
|
const telemetryPayload = {
|
|
319
320
|
generatedAt: new Date().toISOString(),
|
|
320
|
-
engine: "postbuild-mcp@2.1.
|
|
321
|
+
engine: "postbuild-mcp@2.1.6",
|
|
321
322
|
mcpTelemetry: {
|
|
322
|
-
server: "postbuild-mcp@2.1.
|
|
323
|
+
server: "postbuild-mcp@2.1.6",
|
|
323
324
|
tokenReductionPct: projectsData[0]?.mcpReductionPct || "-75.4%",
|
|
324
325
|
supersetCacheHits: mcpStats.supersetCacheHits,
|
|
325
326
|
deltasEmitted: mcpStats.deltasEmitted,
|
|
@@ -424,7 +425,7 @@ try {
|
|
|
424
425
|
|
|
425
426
|
const projects = pb && pb.projects ? pb.projects : [];
|
|
426
427
|
const radar = pb && pb.radarThreads ? pb.radarThreads : [];
|
|
427
|
-
const mcp = pb && pb.mcpTelemetry ? pb.mcpTelemetry : { server: 'postbuild-mcp@2.1.
|
|
428
|
+
const mcp = pb && pb.mcpTelemetry ? pb.mcpTelemetry : { server: 'postbuild-mcp@2.1.6', tokenReductionPct: '-75.4%', supersetCacheHits: 4 };
|
|
428
429
|
|
|
429
430
|
root.innerHTML = \`
|
|
430
431
|
<div class="meta-bar">
|
|
@@ -865,7 +866,7 @@ export async function executeMcpToolCall(name, args = {}, sessionId = "mcp-stdio
|
|
|
865
866
|
}
|
|
866
867
|
|
|
867
868
|
if (name === "postbuild_inbox") {
|
|
868
|
-
const license = await verifyCliLicense({ domain: "
|
|
869
|
+
const license = await verifyCliLicense({ domain: "", token: authToken });
|
|
869
870
|
if (!license.valid) {
|
|
870
871
|
return {
|
|
871
872
|
isError: true,
|
|
@@ -876,7 +877,16 @@ export async function executeMcpToolCall(name, args = {}, sessionId = "mcp-stdio
|
|
|
876
877
|
};
|
|
877
878
|
}
|
|
878
879
|
|
|
879
|
-
const
|
|
880
|
+
const defaultInbox = license.inbox || getBotInboxForDomain(license.domain || "postbuild.dev", license.email);
|
|
881
|
+
const inboxAddr = args.inbox || defaultInbox;
|
|
882
|
+
if (args.inbox && !isInboxOwnedByLicense({ email: license.email, domain: license.domain, inbox: defaultInbox }, inboxAddr, "")) {
|
|
883
|
+
return {
|
|
884
|
+
isError: true,
|
|
885
|
+
status: "FORBIDDEN_INBOX",
|
|
886
|
+
message: `Your license covers ${defaultInbox}. You cannot read ${inboxAddr}.`
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
880
890
|
const autoVerify = Boolean(args.auto_verify);
|
|
881
891
|
const result = await autoVerifyInboxLinks(inboxAddr, {
|
|
882
892
|
autoVerify,
|
|
@@ -919,7 +929,7 @@ export async function executeMcpToolCall(name, args = {}, sessionId = "mcp-stdio
|
|
|
919
929
|
}
|
|
920
930
|
|
|
921
931
|
if (name === "postbuild_vault_sync") {
|
|
922
|
-
const license = await verifyCliLicense({ domain: "
|
|
932
|
+
const license = await verifyCliLicense({ domain: "", token: authToken });
|
|
923
933
|
if (!license.valid) {
|
|
924
934
|
return {
|
|
925
935
|
isError: true,
|
|
@@ -932,7 +942,7 @@ export async function executeMcpToolCall(name, args = {}, sessionId = "mcp-stdio
|
|
|
932
942
|
|
|
933
943
|
return await syncPostbuildToVault({
|
|
934
944
|
vaultPath: args.vault_path || process.cwd(),
|
|
935
|
-
urls: Array.isArray(args.urls) && args.urls.length ? args.urls : ["https://www.postbuild.dev"],
|
|
945
|
+
urls: Array.isArray(args.urls) && args.urls.length ? args.urls : [license.domain ? `https://${license.domain}` : "https://www.postbuild.dev"],
|
|
936
946
|
sessionId,
|
|
937
947
|
bearerToken: license.token
|
|
938
948
|
});
|
|
@@ -975,7 +985,7 @@ export function startPostbuildMcpServer() {
|
|
|
975
985
|
},
|
|
976
986
|
serverInfo: {
|
|
977
987
|
name: "postbuild-mcp",
|
|
978
|
-
version: "2.1.
|
|
988
|
+
version: "2.1.6"
|
|
979
989
|
}
|
|
980
990
|
}
|
|
981
991
|
});
|
package/lib/pseo.js
CHANGED
|
@@ -68,19 +68,19 @@ function renderTopicSpecificSection(pageSlug, cleanName, domain) {
|
|
|
68
68
|
<td><strong>Free Audit & Preview</strong></td>
|
|
69
69
|
<td><strong>$0</strong></td>
|
|
70
70
|
<td>Any public URL</td>
|
|
71
|
-
<td>8-point SEO & 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 & 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
|
|
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><domain>-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
|
|
82
|
-
<td>Multi-project</td>
|
|
83
|
-
<td>
|
|
80
|
+
<td><strong>Done-For-You & Studio</strong></td>
|
|
81
|
+
<td><strong>Waitlist</strong></td>
|
|
82
|
+
<td>Multi-project / Concierge</td>
|
|
83
|
+
<td>Done-for-you manual & 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>
|
|
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
|
-
|
|
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="
|
|
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>
|
|
104
|
-
<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
|
|
110
|
-
<td>
|
|
111
|
-
<td>100 / 100
|
|
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>
|
|
116
|
-
<td>26 routes indexed in <code
|
|
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>
|
|
120
|
-
<td>
|
|
121
|
-
<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>
|
|
125
|
-
<td
|
|
126
|
-
<td>
|
|
124
|
+
<td><strong>Directory Launch Queue</strong></td>
|
|
125
|
+
<td>Manual spreadsheet copy-pasting & 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 (
|
|
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: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-paid",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.6",
|
|
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",
|