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/bin/git-paid.js +25 -12
- package/lib/auth.js +289 -29
- package/lib/directories.js +179 -123
- package/lib/engine.js +97 -53
- package/lib/fixer.js +129 -50
- package/lib/guard.js +402 -0
- package/lib/inbox.js +56 -10
- package/lib/mcp.js +35 -11
- package/lib/pseo.js +244 -44
- package/lib/safe-fetch.js +14 -5
- 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
|
|
46
|
-
console.log(" git paid fix [url] [PRO] Write
|
|
48
|
+
console.log(" git paid login <pb_live_key> Authenticate CLI & MCP with your Launch Pass license");
|
|
49
|
+
console.log(" git paid fix [url] [PRO] Write starter pSEO pages (.postbuild/pseo), robots.txt, sitemap.xml & llms.txt");
|
|
47
50
|
console.log(" git paid submit [url] [PRO] Headless 85-directory form submitter + OTP auto-verifier");
|
|
48
|
-
console.log(" git paid inbox [bot-email] [PRO] Check
|
|
49
|
-
console.log(" git paid mcp Start Postbuild MCP Server (postbuild-mcp v2.1.
|
|
51
|
+
console.log(" git paid inbox [bot-email] [PRO] Check <domain-tld>-bot@postbuild.dev inbox (pass --verify to auto-click links)");
|
|
52
|
+
console.log(" git paid mcp Start Postbuild MCP Server (postbuild-mcp v2.1.7)");
|
|
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.7 (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,17 +203,25 @@ 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");
|
|
212
215
|
}
|
|
213
|
-
const res = await autoVerifyInboxLinks(inboxAddr, {
|
|
216
|
+
const res = await autoVerifyInboxLinks(inboxAddr, {
|
|
217
|
+
autoVerify: args.includes("--verify"),
|
|
218
|
+
bearerToken: license.token
|
|
219
|
+
});
|
|
220
|
+
if (res.requiresDomainVerification) {
|
|
221
|
+
console.log(` [!] Domain ownership verification required for ${license.domain || inboxAddr}:`);
|
|
222
|
+
console.log(` ${res.error || "Verify ownership on https://www.postbuild.dev/?tab=account to unlock your inbox."}\n`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
214
225
|
console.log(
|
|
215
226
|
` [*] Verification Inbox: ${res.inboxAddress} (${res.totalMessages} messages, ${res.verifiedCount} auto-verified)\n`
|
|
216
227
|
);
|
|
@@ -274,7 +285,9 @@ async function runCli() {
|
|
|
274
285
|
|
|
275
286
|
const fixResult = applyLocalSeoFixes(report, {
|
|
276
287
|
cwd: process.cwd(),
|
|
277
|
-
overwritePublic:
|
|
288
|
+
overwritePublic: args.includes("--force"),
|
|
289
|
+
publishPseoToPublic: args.includes("--publish-pages"),
|
|
290
|
+
verificationToken: license.verificationToken || ""
|
|
278
291
|
});
|
|
279
292
|
|
|
280
293
|
console.log(`\n --- Local Repository Auto-Fix (${fixResult.framework} — Licensed to ${license.email}) ---`);
|
package/lib/auth.js
CHANGED
|
@@ -10,19 +10,178 @@ const otpChallengeState = new Map(); // cid -> { attempts: number, used: boolean
|
|
|
10
10
|
const memoryEntitlements = new Map(); // key (jti or email) -> { jti, email, customerId, plan, domain, inbox, status, expiresAt }
|
|
11
11
|
const processedWebhookEvents = new Set(); // stripe_event_id idempotency set
|
|
12
12
|
|
|
13
|
+
export const FREE_EMAIL_PROVIDERS = new Set([
|
|
14
|
+
"gmail.com",
|
|
15
|
+
"googlemail.com",
|
|
16
|
+
"outlook.com",
|
|
17
|
+
"outlook.de",
|
|
18
|
+
"outlook.fr",
|
|
19
|
+
"outlook.es",
|
|
20
|
+
"outlook.co.uk",
|
|
21
|
+
"hotmail.com",
|
|
22
|
+
"hotmail.co.uk",
|
|
23
|
+
"hotmail.fr",
|
|
24
|
+
"hotmail.de",
|
|
25
|
+
"hotmail.es",
|
|
26
|
+
"hotmail.it",
|
|
27
|
+
"live.com",
|
|
28
|
+
"live.co.uk",
|
|
29
|
+
"live.fr",
|
|
30
|
+
"live.de",
|
|
31
|
+
"msn.com",
|
|
32
|
+
"yahoo.com",
|
|
33
|
+
"yahoo.co.uk",
|
|
34
|
+
"yahoo.ca",
|
|
35
|
+
"yahoo.co.jp",
|
|
36
|
+
"yahoo.fr",
|
|
37
|
+
"yahoo.de",
|
|
38
|
+
"yahoo.es",
|
|
39
|
+
"yahoo.it",
|
|
40
|
+
"yahoo.com.au",
|
|
41
|
+
"yahoo.com.br",
|
|
42
|
+
"yahoo.co.in",
|
|
43
|
+
"ymail.com",
|
|
44
|
+
"icloud.com",
|
|
45
|
+
"me.com",
|
|
46
|
+
"mac.com",
|
|
47
|
+
"aol.com",
|
|
48
|
+
"proton.me",
|
|
49
|
+
"protonmail.com",
|
|
50
|
+
"protonmail.ch",
|
|
51
|
+
"pm.me",
|
|
52
|
+
"zoho.com",
|
|
53
|
+
"gmx.com",
|
|
54
|
+
"gmx.de",
|
|
55
|
+
"gmx.net",
|
|
56
|
+
"gmx.at",
|
|
57
|
+
"gmx.ch",
|
|
58
|
+
"web.de",
|
|
59
|
+
"t-online.de",
|
|
60
|
+
"freenet.de",
|
|
61
|
+
"mail.com",
|
|
62
|
+
"fastmail.com",
|
|
63
|
+
"hey.com",
|
|
64
|
+
"yandex.com",
|
|
65
|
+
"yandex.ru",
|
|
66
|
+
"duck.com",
|
|
67
|
+
"tuta.com",
|
|
68
|
+
"tutanota.com",
|
|
69
|
+
"qq.com",
|
|
70
|
+
"163.com",
|
|
71
|
+
"126.com",
|
|
72
|
+
"sina.com",
|
|
73
|
+
"sohu.com",
|
|
74
|
+
"foxmail.com",
|
|
75
|
+
"naver.com",
|
|
76
|
+
"daum.net",
|
|
77
|
+
"hanmail.net",
|
|
78
|
+
"mail.ru",
|
|
79
|
+
"bk.ru",
|
|
80
|
+
"inbox.ru",
|
|
81
|
+
"list.ru",
|
|
82
|
+
"rambler.ru",
|
|
83
|
+
"seznam.cz",
|
|
84
|
+
"wp.pl",
|
|
85
|
+
"onet.pl",
|
|
86
|
+
"interia.pl",
|
|
87
|
+
"o2.pl",
|
|
88
|
+
"libero.it",
|
|
89
|
+
"virgilio.it",
|
|
90
|
+
"alice.it",
|
|
91
|
+
"orange.fr",
|
|
92
|
+
"wanadoo.fr",
|
|
93
|
+
"free.fr",
|
|
94
|
+
"sfr.fr",
|
|
95
|
+
"laposte.net"
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
export function isFreeEmailDomain(emailDomain = "") {
|
|
99
|
+
const clean = String(emailDomain || "").toLowerCase().trim();
|
|
100
|
+
if (!clean) return true;
|
|
101
|
+
if (FREE_EMAIL_PROVIDERS.has(clean)) return true;
|
|
102
|
+
if (/^(gmail|googlemail|yahoo|ymail|hotmail|outlook|live|msn|icloud|aol|proton|protonmail|zoho|gmx|yandex|tuta|tutanota|fastmail)\.[a-z.]{2,6}$/i.test(clean)) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
13
108
|
export function requireAuthSecret() {
|
|
14
109
|
const explicit = (process.env.POSTBUILD_AUTH_SECRET || process.env.POSTBUILD_SECRET || "").trim();
|
|
15
110
|
if (explicit.length >= 32) {
|
|
16
111
|
return explicit;
|
|
17
112
|
}
|
|
18
|
-
// Safe domain-separated derivation on Vercel if POSTBUILD_AUTH_SECRET has not been added to project env yet
|
|
19
|
-
const resendSeed = (process.env.RESEND_API_KEY || "").trim();
|
|
20
|
-
if (process.env.VERCEL && resendSeed.length >= 24 && !process.env.POSTBUILD_STRICT_SECRET_TEST) {
|
|
21
|
-
return crypto.createHmac("sha256", resendSeed).update("postbuild:auth:v2:hmac-signing-key").digest("hex");
|
|
22
|
-
}
|
|
23
113
|
throw new Error("POSTBUILD_SECRET or POSTBUILD_AUTH_SECRET is missing or too short (minimum 32 characters required)");
|
|
24
114
|
}
|
|
25
115
|
|
|
116
|
+
export function getDomainVerificationToken(domain = "", email = "") {
|
|
117
|
+
const cleanDomain = String(domain || "")
|
|
118
|
+
.toLowerCase()
|
|
119
|
+
.trim()
|
|
120
|
+
.replace(/^https?:\/\//, "")
|
|
121
|
+
.replace(/^www\./, "")
|
|
122
|
+
.split("/")[0]
|
|
123
|
+
.split(":")[0];
|
|
124
|
+
const cleanEmail = String(email || "").toLowerCase().trim();
|
|
125
|
+
const verifyKey = (process.env.POSTBUILD_VERIFY_SECRET || "").trim();
|
|
126
|
+
const secret = verifyKey.length >= 16 ? verifyKey : requireAuthSecret();
|
|
127
|
+
const digest = crypto
|
|
128
|
+
.createHmac("sha256", secret)
|
|
129
|
+
.update(`domain-verify:${cleanDomain}:${cleanEmail}`)
|
|
130
|
+
.digest("hex")
|
|
131
|
+
.slice(0, 32);
|
|
132
|
+
return `postbuild-verify=${digest}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function getCandidateVerificationTokens(domain = "", email = "") {
|
|
136
|
+
const cleanDomain = String(domain || "")
|
|
137
|
+
.toLowerCase()
|
|
138
|
+
.trim()
|
|
139
|
+
.replace(/^https?:\/\//, "")
|
|
140
|
+
.replace(/^www\./, "")
|
|
141
|
+
.split("/")[0]
|
|
142
|
+
.split(":")[0];
|
|
143
|
+
const cleanEmail = String(email || "").toLowerCase().trim();
|
|
144
|
+
const secrets = [
|
|
145
|
+
(process.env.POSTBUILD_VERIFY_SECRET || "").trim(),
|
|
146
|
+
(() => {
|
|
147
|
+
try {
|
|
148
|
+
return requireAuthSecret();
|
|
149
|
+
} catch {
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
})()
|
|
153
|
+
].filter((s) => s.length >= 16);
|
|
154
|
+
const tokens = new Set([getDomainVerificationToken(cleanDomain, cleanEmail)]);
|
|
155
|
+
for (const s of secrets) {
|
|
156
|
+
const digest = crypto
|
|
157
|
+
.createHmac("sha256", s)
|
|
158
|
+
.update(`domain-verify:${cleanDomain}:${cleanEmail}`)
|
|
159
|
+
.digest("hex")
|
|
160
|
+
.slice(0, 32);
|
|
161
|
+
tokens.add(`postbuild-verify=${digest}`);
|
|
162
|
+
}
|
|
163
|
+
return Array.from(tokens);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function isSameDomainVerifiedEmail(email = "", domain = "", emailVerified = false) {
|
|
167
|
+
const cleanEmail = String(email || "").toLowerCase().trim();
|
|
168
|
+
const cleanDomain = String(domain || "")
|
|
169
|
+
.toLowerCase()
|
|
170
|
+
.trim()
|
|
171
|
+
.replace(/^https?:\/\//, "")
|
|
172
|
+
.replace(/^www\./, "")
|
|
173
|
+
.split("/")[0]
|
|
174
|
+
.split(":")[0];
|
|
175
|
+
if (!cleanEmail || !cleanDomain || !cleanDomain.includes(".")) return false;
|
|
176
|
+
if (getFounderEmailsSet().has(cleanEmail) && cleanDomain === "postbuild.dev") {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
if (!emailVerified) return false;
|
|
180
|
+
const emailDomain = cleanEmail.includes("@") ? cleanEmail.split("@")[1] : "";
|
|
181
|
+
if (!emailDomain || isFreeEmailDomain(emailDomain)) return false;
|
|
182
|
+
return cleanDomain === emailDomain || cleanDomain.endsWith(`.${emailDomain}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
26
185
|
function timingSafeStringEqual(a, b) {
|
|
27
186
|
if (typeof a !== "string" || typeof b !== "string") return false;
|
|
28
187
|
const bufA = Buffer.from(a, "utf8");
|
|
@@ -207,18 +366,44 @@ export function loadLocalCredentials() {
|
|
|
207
366
|
|
|
208
367
|
export async function verifyCliLicense({ domain = "postbuild.dev", token = "" } = {}) {
|
|
209
368
|
const candidateToken = (token || "").trim() || loadLocalCredentials()?.token || "";
|
|
369
|
+
const cleanReqDomain = String(domain || "")
|
|
370
|
+
.toLowerCase()
|
|
371
|
+
.trim()
|
|
372
|
+
.replace(/^https?:\/\//, "")
|
|
373
|
+
.replace(/^(www\.)+/, "")
|
|
374
|
+
.split("/")[0]
|
|
375
|
+
.split(":")[0];
|
|
210
376
|
|
|
211
377
|
if (candidateToken && candidateToken.startsWith("pb_live_")) {
|
|
212
378
|
// 1. If running on the server (where signing secret is present), verify signature & revocation directly
|
|
213
379
|
try {
|
|
214
380
|
const localCheck = verifyLicenseToken(candidateToken);
|
|
215
381
|
if (localCheck.valid) {
|
|
382
|
+
const licDomain = String(localCheck.domain || "")
|
|
383
|
+
.toLowerCase()
|
|
384
|
+
.trim()
|
|
385
|
+
.replace(/^https?:\/\//, "")
|
|
386
|
+
.replace(/^(www\.)+/, "")
|
|
387
|
+
.split("/")[0]
|
|
388
|
+
.split(":")[0];
|
|
389
|
+
if (cleanReqDomain && licDomain && licDomain !== "*" && cleanReqDomain !== licDomain) {
|
|
390
|
+
return {
|
|
391
|
+
valid: false,
|
|
392
|
+
reason: "DOMAIN_MISMATCH",
|
|
393
|
+
licensedDomain: licDomain,
|
|
394
|
+
requestedDomain: cleanReqDomain,
|
|
395
|
+
checkoutUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain)}`,
|
|
396
|
+
webUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain)}`,
|
|
397
|
+
message: `Your Launch Pass license key is scoped to ${licDomain} and does not cover ${cleanReqDomain}.`
|
|
398
|
+
};
|
|
399
|
+
}
|
|
216
400
|
return {
|
|
217
401
|
valid: true,
|
|
218
402
|
email: localCheck.email,
|
|
219
403
|
plan: localCheck.plan,
|
|
220
404
|
domain: localCheck.domain,
|
|
221
405
|
inbox: localCheck.inbox,
|
|
406
|
+
verificationToken: getDomainVerificationToken(localCheck.domain, localCheck.email),
|
|
222
407
|
jti: localCheck.jti,
|
|
223
408
|
token: candidateToken
|
|
224
409
|
};
|
|
@@ -226,8 +411,8 @@ export async function verifyCliLicense({ domain = "postbuild.dev", token = "" }
|
|
|
226
411
|
return {
|
|
227
412
|
valid: false,
|
|
228
413
|
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)}`,
|
|
414
|
+
checkoutUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain || domain)}`,
|
|
415
|
+
webUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain || domain)}`,
|
|
231
416
|
message: localCheck.error || "Invalid or revoked pb_live_ license key."
|
|
232
417
|
};
|
|
233
418
|
} catch {
|
|
@@ -245,12 +430,31 @@ export async function verifyCliLicense({ domain = "postbuild.dev", token = "" }
|
|
|
245
430
|
if (resp.ok) {
|
|
246
431
|
const data = await resp.json();
|
|
247
432
|
if (data && data.ok && data.authenticated) {
|
|
433
|
+
const licDomain = String(data.domain || "")
|
|
434
|
+
.toLowerCase()
|
|
435
|
+
.trim()
|
|
436
|
+
.replace(/^https?:\/\//, "")
|
|
437
|
+
.replace(/^(www\.)+/, "")
|
|
438
|
+
.split("/")[0]
|
|
439
|
+
.split(":")[0];
|
|
440
|
+
if (cleanReqDomain && licDomain && licDomain !== "*" && cleanReqDomain !== licDomain) {
|
|
441
|
+
return {
|
|
442
|
+
valid: false,
|
|
443
|
+
reason: "DOMAIN_MISMATCH",
|
|
444
|
+
licensedDomain: licDomain,
|
|
445
|
+
requestedDomain: cleanReqDomain,
|
|
446
|
+
checkoutUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain)}`,
|
|
447
|
+
webUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain)}`,
|
|
448
|
+
message: `Your Launch Pass license key is scoped to ${licDomain} and does not cover ${cleanReqDomain}.`
|
|
449
|
+
};
|
|
450
|
+
}
|
|
248
451
|
return {
|
|
249
452
|
valid: true,
|
|
250
453
|
email: data.email,
|
|
251
454
|
plan: data.plan,
|
|
252
455
|
domain: data.domain,
|
|
253
456
|
inbox: data.inbox,
|
|
457
|
+
verificationToken: data.verificationToken || "",
|
|
254
458
|
token: candidateToken
|
|
255
459
|
};
|
|
256
460
|
}
|
|
@@ -262,10 +466,10 @@ export async function verifyCliLicense({ domain = "postbuild.dev", token = "" }
|
|
|
262
466
|
return {
|
|
263
467
|
valid: false,
|
|
264
468
|
reason: "PRO_LICENSE_REQUIRED",
|
|
265
|
-
checkoutUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(domain)}`,
|
|
266
|
-
webUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(domain)}`,
|
|
469
|
+
checkoutUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain || domain)}`,
|
|
470
|
+
webUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(cleanReqDomain || domain)}`,
|
|
267
471
|
message:
|
|
268
|
-
"A verified postbuild Launch Pass ($29)
|
|
472
|
+
"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
473
|
};
|
|
270
474
|
}
|
|
271
475
|
|
|
@@ -277,6 +481,7 @@ export function signLicenseToken({
|
|
|
277
481
|
domain = "postbuild.dev",
|
|
278
482
|
jti = "",
|
|
279
483
|
customerId = null,
|
|
484
|
+
emailVerified = true,
|
|
280
485
|
ttlSeconds = 365 * 24 * 3600
|
|
281
486
|
}) {
|
|
282
487
|
const secret = requireAuthSecret();
|
|
@@ -310,6 +515,7 @@ export function signLicenseToken({
|
|
|
310
515
|
plan,
|
|
311
516
|
domain: cleanDomain,
|
|
312
517
|
inbox,
|
|
518
|
+
emailVerified: Boolean(emailVerified),
|
|
313
519
|
iat: now,
|
|
314
520
|
exp
|
|
315
521
|
};
|
|
@@ -360,7 +566,7 @@ export function verifyLicenseToken(token) {
|
|
|
360
566
|
if (!payload.inbox && payload.domain) {
|
|
361
567
|
payload.inbox = getBotInboxForDomain(payload.domain, payload.email);
|
|
362
568
|
}
|
|
363
|
-
return { valid: true, ...payload };
|
|
569
|
+
return { valid: true, ...payload, emailVerified: Boolean(payload.emailVerified) };
|
|
364
570
|
} catch {
|
|
365
571
|
return { valid: false };
|
|
366
572
|
}
|
|
@@ -523,13 +729,17 @@ async function sendLoginOtpViaResend(email, otpCode) {
|
|
|
523
729
|
},
|
|
524
730
|
body: JSON.stringify({
|
|
525
731
|
from: "postbuild <login@postbuild.dev>",
|
|
732
|
+
reply_to: "support@postbuild.dev",
|
|
526
733
|
to: recipients,
|
|
527
734
|
subject: `${otpCode} is your postbuild verification code`,
|
|
528
|
-
html: `<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif; max-width: 520px; margin: 0 auto; padding:
|
|
529
|
-
<
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
<
|
|
735
|
+
html: `<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif; max-width: 520px; margin: 0 auto; padding: 24px; border: 1px solid #e2e8f0; border-radius: 14px; background: #ffffff; color: #0f172a;">
|
|
736
|
+
<div style="margin-bottom: 16px;">
|
|
737
|
+
<span style="display: inline-block; background: #ecfdf5; color: #047857; border: 1px solid #a7f3d0; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; padding: 3px 8px; border-radius: 999px;">Security Code</span>
|
|
738
|
+
</div>
|
|
739
|
+
<h2 style="margin: 0 0 8px; font-size: 20px; font-weight: 700; color: #0f172a;">Verify your postbuild sign-in</h2>
|
|
740
|
+
<p style="color: #475569; font-size: 14px; line-height: 1.6; margin: 0 0 18px;">Enter this 6-digit verification code on <strong>postbuild.dev</strong> within 10 minutes to access your license key, verification inbox, and receipts:</p>
|
|
741
|
+
<div style="background: #ecfdf5; border: 1px solid #6ee7b7; color: #047857; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 28px; font-weight: 800; letter-spacing: 6px; text-align: center; padding: 14px; border-radius: 10px; margin: 0 0 18px;">${otpCode}</div>
|
|
742
|
+
<p style="color: #64748b; font-size: 12px; line-height: 1.5; margin: 0;">If you did not request this code, you can safely ignore this email.</p>
|
|
533
743
|
</div>`
|
|
534
744
|
})
|
|
535
745
|
});
|
|
@@ -556,9 +766,16 @@ function escapeHtml(str) {
|
|
|
556
766
|
export async function sendLicenseFulfillmentEmail({ email, licenseToken, planId, targetDomain }) {
|
|
557
767
|
const resendKey = (process.env.RESEND_API_KEY || "").trim();
|
|
558
768
|
if (!resendKey || !email) return { sent: false };
|
|
559
|
-
const
|
|
560
|
-
const
|
|
769
|
+
const rawDomain = targetDomain || "postbuild.dev";
|
|
770
|
+
const safeDomain = escapeHtml(rawDomain);
|
|
771
|
+
const inbox = escapeHtml(getBotInboxForDomain(rawDomain, email));
|
|
561
772
|
const safeToken = escapeHtml(licenseToken);
|
|
773
|
+
const planName = planId === "studio_monthly" ? "postbuild Studio" : "postbuild Launch Pass";
|
|
774
|
+
const badgeLabel = planId === "studio_monthly" ? "Studio Active" : "Launch Pass Active";
|
|
775
|
+
const autoVerifiedByEmail = isSameDomainVerifiedEmail(email, rawDomain, true);
|
|
776
|
+
const inboxStatusNote = autoVerifiedByEmail
|
|
777
|
+
? "Confirm your email on Account tab (or use key below) to open"
|
|
778
|
+
: "Opens after domain check on Account tab";
|
|
562
779
|
try {
|
|
563
780
|
const resp = await fetch("https://api.resend.com/emails", {
|
|
564
781
|
method: "POST",
|
|
@@ -568,15 +785,49 @@ export async function sendLicenseFulfillmentEmail({ email, licenseToken, planId,
|
|
|
568
785
|
},
|
|
569
786
|
body: JSON.stringify({
|
|
570
787
|
from: "postbuild <login@postbuild.dev>",
|
|
788
|
+
reply_to: "support@postbuild.dev",
|
|
571
789
|
to: [email],
|
|
572
|
-
subject: `Your
|
|
573
|
-
html: `<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif; max-width: 540px; margin: 0 auto; padding:
|
|
574
|
-
<
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
<
|
|
578
|
-
<
|
|
579
|
-
|
|
790
|
+
subject: `Your ${planName} for ${rawDomain} is active`,
|
|
791
|
+
html: `<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif; max-width: 540px; margin: 0 auto; padding: 24px; border: 1px solid #e2e8f0; border-radius: 14px; background: #ffffff; color: #0f172a;">
|
|
792
|
+
<div style="margin-bottom: 14px;">
|
|
793
|
+
<span style="display: inline-block; background: #ecfdf5; color: #047857; border: 1px solid #a7f3d0; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; padding: 3px 9px; border-radius: 999px;">${escapeHtml(badgeLabel)}</span>
|
|
794
|
+
</div>
|
|
795
|
+
<h2 style="margin: 0 0 8px; font-size: 20px; font-weight: 700; color: #0f172a;">Your ${escapeHtml(planName)} for ${safeDomain} is ready</h2>
|
|
796
|
+
<p style="color: #475569; font-size: 14px; line-height: 1.6; margin: 0 0 18px;">All 85 startup & AI directories, your SEO Launch Bundle (<code>.zip</code>), and your verification inbox (<code>${inbox}</code>) are included.${autoVerifiedByEmail ? ` Confirm your <strong>@${safeDomain}</strong> email once on your Account tab (or sign in with the license key below) to open <code>${inbox}</code>.` : ` Because your checkout email is not on <strong>@${safeDomain}</strong>, complete the domain ownership check on your Account tab (and keep the verification file or meta tag in place) before submitting to directories so you can read incoming confirmation codes.`}</p>
|
|
797
|
+
|
|
798
|
+
<div style="margin: 0 0 20px;">
|
|
799
|
+
<a href="https://www.postbuild.dev/?tab=account" style="display: inline-block; background: #047857; color: #ffffff; text-decoration: none; font-weight: 600; font-size: 14px; padding: 11px 18px; border-radius: 8px;">Open Account & Download .zip Bundle →</a>
|
|
800
|
+
</div>
|
|
801
|
+
|
|
802
|
+
<table role="presentation" style="width: 100%; border-collapse: collapse; margin: 0 0 18px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px;">
|
|
803
|
+
<tr>
|
|
804
|
+
<td style="padding: 10px 14px; border-bottom: 1px solid #e2e8f0; font-size: 12px; color: #64748b; font-weight: 600;">Licensed Domain</td>
|
|
805
|
+
<td style="padding: 10px 14px; border-bottom: 1px solid #e2e8f0; font-size: 13px; color: #0f172a; font-weight: 700; text-align: right;">${safeDomain}</td>
|
|
806
|
+
</tr>
|
|
807
|
+
<tr>
|
|
808
|
+
<td style="padding: 10px 14px; font-size: 12px; color: #64748b; font-weight: 600;">Verification Inbox</td>
|
|
809
|
+
<td style="padding: 10px 14px; font-size: 12px; color: #047857; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 600; text-align: right; word-break: break-all;">${inbox}<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif; font-size: 11px; color: #64748b; font-weight: 500; margin-top: 2px; word-break: normal; overflow-wrap: break-word;">${escapeHtml(inboxStatusNote)}</div></td>
|
|
810
|
+
</tr>
|
|
811
|
+
</table>
|
|
812
|
+
|
|
813
|
+
<div style="border-top: 1px solid #e2e8f0; padding-top: 16px; margin-bottom: 18px;">
|
|
814
|
+
<div style="font-size: 13px; font-weight: 700; color: #0f172a; margin-bottom: 8px;">3-Step Quickstart</div>
|
|
815
|
+
<ol style="margin: 0; padding-left: 18px; color: #475569; font-size: 13px; line-height: 1.6;">
|
|
816
|
+
<li style="margin-bottom: 6px;"><strong>Deploy Your .zip Launch Bundle:</strong> Click <em>Download .zip</em> on your Account tab (includes <code>START_HERE.md</code>, <code>public/.well-known/postbuild-verify.txt</code>, starter HTML SEO pages, and <code>directories-85.csv</code>) and deploy to <code>${safeDomain}</code>.</li>
|
|
817
|
+
<li style="margin-bottom: 6px;"><strong>Verify Domain Ownership:</strong> ${autoVerifiedByEmail ? `Confirm your <code>@${safeDomain}</code> email once on your Account tab (or paste the license key below) to open <code>${inbox}</code>.` : `Click <em>Verify Domain Now</em> on your Account tab after deploying your bundle (or adding the meta tag / DNS TXT record) and keep it in place to unlock <code>${inbox}</code>.`}</li>
|
|
818
|
+
<li><strong>Submit to 85 Directories:</strong> Launch prefilled directory links from the Dispatch tab or <code>directories-85.csv</code> using <code>${inbox}</code> — 6-digit OTPs and confirmation links arrive in your Account Inbox.</li>
|
|
819
|
+
</ol>
|
|
820
|
+
</div>
|
|
821
|
+
|
|
822
|
+
<div style="margin: 0 0 18px;">
|
|
823
|
+
<div style="font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: #64748b; margin-bottom: 4px;">CLI & MCP License Key</div>
|
|
824
|
+
<div style="font-size: 12px; color: #64748b; margin-bottom: 6px;">Most buyers never need this — you can sign in with your email at <strong>postbuild.dev</strong>, or copy this key for <code>npx git-paid login</code>:</div>
|
|
825
|
+
<div style="background: #f8fafc; border: 1px solid #cbd5e1; padding: 12px; border-radius: 8px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; line-height: 1.5; color: #0f172a; word-break: break-all;">${safeToken}</div>
|
|
826
|
+
</div>
|
|
827
|
+
|
|
828
|
+
<div style="border-top: 1px solid #e2e8f0; padding-top: 12px; font-size: 12px; color: #64748b; line-height: 1.5;">
|
|
829
|
+
Covered by our 14-day refund guarantee. Questions? Reply to this email or contact <a href="mailto:support@postbuild.dev" style="color: #047857; text-decoration: none; font-weight: 600;">support@postbuild.dev</a>.
|
|
830
|
+
</div>
|
|
580
831
|
</div>`
|
|
581
832
|
})
|
|
582
833
|
});
|
|
@@ -662,10 +913,13 @@ export async function authenticateUser({ email, token, sessionId, otpCode, chall
|
|
|
662
913
|
ok: true,
|
|
663
914
|
authenticated: true,
|
|
664
915
|
email: verified.email,
|
|
916
|
+
emailVerified: Boolean(verified.emailVerified),
|
|
917
|
+
domainVerified: isSameDomainVerifiedEmail(verified.email, verified.domain, verified.emailVerified),
|
|
665
918
|
customerId: verified.customerId || null,
|
|
666
919
|
plan: verified.plan,
|
|
667
920
|
domain: verified.domain,
|
|
668
921
|
inbox: verified.inbox,
|
|
922
|
+
verificationToken: getDomainVerificationToken(verified.domain, verified.email),
|
|
669
923
|
isFounder: founderEmails.has(String(verified.email || "").toLowerCase().trim()),
|
|
670
924
|
token
|
|
671
925
|
};
|
|
@@ -686,15 +940,18 @@ export async function authenticateUser({ email, token, sessionId, otpCode, chall
|
|
|
686
940
|
const plan = existingEnt?.plan || check.plan || "launch_pass";
|
|
687
941
|
const domain = existingEnt?.domain || check.domain || "postbuild.dev";
|
|
688
942
|
const customerId = existingEnt?.customerId || check.customerId || null;
|
|
689
|
-
const issued = signLicenseToken({ email: check.email, plan, domain, customerId });
|
|
943
|
+
const issued = signLicenseToken({ email: check.email, plan, domain, customerId, emailVerified: true });
|
|
690
944
|
return {
|
|
691
945
|
ok: true,
|
|
692
946
|
authenticated: true,
|
|
693
947
|
email: check.email,
|
|
948
|
+
emailVerified: true,
|
|
949
|
+
domainVerified: isSameDomainVerifiedEmail(check.email, domain, true),
|
|
694
950
|
customerId,
|
|
695
951
|
plan,
|
|
696
952
|
domain,
|
|
697
953
|
inbox: getBotInboxForDomain(domain, check.email),
|
|
954
|
+
verificationToken: getDomainVerificationToken(domain, check.email),
|
|
698
955
|
isFounder: founderEmails.has(String(check.email || "").toLowerCase().trim()),
|
|
699
956
|
token: issued
|
|
700
957
|
};
|
|
@@ -706,7 +963,7 @@ export async function authenticateUser({ email, token, sessionId, otpCode, chall
|
|
|
706
963
|
stripe = stripeMod.getStripeClient();
|
|
707
964
|
} catch {}
|
|
708
965
|
|
|
709
|
-
// 3. Stripe Checkout return session_id verification
|
|
966
|
+
// 3. Stripe Checkout return session_id verification (checkout email is NOT verified by Stripe)
|
|
710
967
|
if (sessionId && stripe) {
|
|
711
968
|
const session = await stripe.checkout.sessions.retrieve(sessionId, { expand: ["customer"] });
|
|
712
969
|
if (session && session.payment_status !== "unpaid") {
|
|
@@ -734,15 +991,18 @@ export async function authenticateUser({ email, token, sessionId, otpCode, chall
|
|
|
734
991
|
if (custStatus !== "active" || isEntitlementRevoked({ email: custEmail, customerId, plan })) {
|
|
735
992
|
return { ok: false, authenticated: false, error: "This license entitlement has been revoked or refunded." };
|
|
736
993
|
}
|
|
737
|
-
const newToken = signLicenseToken({ email: custEmail, plan, domain, customerId });
|
|
994
|
+
const newToken = signLicenseToken({ email: custEmail, plan, domain, customerId, emailVerified: false });
|
|
738
995
|
return {
|
|
739
996
|
ok: true,
|
|
740
997
|
authenticated: true,
|
|
741
998
|
email: custEmail,
|
|
999
|
+
emailVerified: false,
|
|
1000
|
+
domainVerified: false,
|
|
742
1001
|
customerId,
|
|
743
1002
|
plan,
|
|
744
1003
|
domain,
|
|
745
1004
|
inbox: getBotInboxForDomain(domain, custEmail),
|
|
1005
|
+
verificationToken: getDomainVerificationToken(domain, custEmail),
|
|
746
1006
|
isFounder: false,
|
|
747
1007
|
token: newToken
|
|
748
1008
|
};
|