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 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) or Studio ($49/mo) license.`);
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 / Studio license");
46
- console.log(" git paid fix [url] [PRO] Write 25 pSEO HTML pages, robots.txt, sitemap.xml & llms.txt");
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 & 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.5)");
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.5 (Stateful Delta Compaction)\n`);
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: "postbuild.dev" });
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 inboxAddr = args.find((a, i) => i > 0 && !a.startsWith("--")) || "postbuild-bot@postbuild.dev";
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, { autoVerify: !args.includes("--no-verify") });
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: isFixMode || args.includes("--force")
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) or Studio ($49/mo) license (pb_live_...) is required to unlock 25 Programmatic SEO HTML pages, 85-directory dispatch, and @postbuild.dev inbox verification."
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: 28px; border: 1px solid #e2e8f0; border-radius: 14px; color: #0f172a;">
529
- <h2 style="margin: 0 0 8px; font-size: 20px;">postbuild · Account Verification</h2>
530
- <p style="color: #475569; font-size: 14px; line-height: 1.6;">Enter this 6-digit verification code on <strong>www.postbuild.dev</strong> within 10 minutes to issue your license key and manage your billing:</p>
531
- <div style="background: #ecfdf5; border: 1px solid #6ee7b7; color: #047857; font-family: monospace; font-size: 28px; font-weight: 800; letter-spacing: 6px; text-align: center; padding: 14px; border-radius: 10px; margin: 18px 0;">${otpCode}</div>
532
- <p style="color: #64748b; font-size: 12px; line-height: 1.5;">For security, your <code>pb_live_</code> license key is issued only after you enter this code.</p>
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 safeDomain = escapeHtml(targetDomain || "postbuild.dev");
560
- const inbox = escapeHtml(getBotInboxForDomain(targetDomain, email));
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 postbuild (${planId === "studio_monthly" ? "Studio" : "Launch Pass"}) License Key for ${targetDomain}`,
573
- html: `<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif; max-width: 540px; margin: 0 auto; padding: 28px; border: 1px solid #e2e8f0; border-radius: 14px; color: #0f172a;">
574
- <h2 style="margin: 0 0 8px; font-size: 20px;">postbuild · License Activated</h2>
575
- <p style="color: #475569; font-size: 14px; line-height: 1.6;">Your <strong>${planId === "studio_monthly" ? "postbuild Studio" : "postbuild Launch Pass"}</strong> for <strong>${safeDomain}</strong> is active.</p>
576
- <p style="color: #475569; font-size: 13px; margin-bottom: 6px;"><strong>Dedicated Verification Inbox:</strong> <code>${inbox}</code></p>
577
- <p style="color: #475569; font-size: 13px; margin-bottom: 6px;"><strong>CLI &amp; Web License Key:</strong></p>
578
- <pre style="background: #f8fafc; border: 1px solid #e2e8f0; padding: 12px; border-radius: 8px; font-size: 11px; overflow-x: auto;">${safeToken}</pre>
579
- <p style="color: #475569; font-size: 13px; margin-top: 12px;">Authenticate your CLI with: <code>npx git-paid login ${safeToken}</code></p>
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 &amp; 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 &amp; Download .zip Bundle &rarr;</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 &amp; 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
  };