git-paid 2.1.3 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/auth.js CHANGED
@@ -1,282 +1,866 @@
1
- // Zero-DB Auth, Email OTP via Resend & Cryptographic License Verification for postbuild (postbuild.dev)
2
-
3
- import crypto from "node:crypto";
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import os from "node:os";
7
- import { getStripeClient } from "./stripe.js";
8
-
9
- const SECRET =
10
- process.env.POSTBUILD_AUTH_SECRET ||
11
- process.env.STRIPE_WEBHOOK_SECRET ||
12
- process.env.STRIPE_RESTRICTED_KEY ||
13
- process.env.STRIPE_SECRET_KEY ||
14
- "postbuild-dev-secret-key-2026";
15
-
16
- export function loadLocalCredentials() {
17
- if (process.env.POSTBUILD_LICENSE_KEY) {
18
- return { token: process.env.POSTBUILD_LICENSE_KEY.trim(), email: "cli@postbuild.dev", plan: "launch_pass" };
19
- }
20
- try {
21
- const credPath = path.join(os.homedir(), ".postbuild", "credentials.json");
22
- if (fs.existsSync(credPath)) {
23
- const data = JSON.parse(fs.readFileSync(credPath, "utf8"));
24
- if (data && data.token) return data;
25
- }
26
- } catch {}
27
- return null;
28
- }
29
-
30
- export async function verifyCliLicense({ domain = "postbuild.dev" } = {}) {
31
- // Always allow internal server-side execution on Vercel
32
- if (process.env.VERCEL || process.env.STRIPE_RESTRICTED_KEY || process.env.STRIPE_SECRET_KEY) {
33
- return { valid: true, email: "server@postbuild.dev", plan: "studio_monthly" };
34
- }
35
-
36
- const creds = loadLocalCredentials();
37
- if (creds && creds.token && creds.token.startsWith("pb_live_")) {
38
- try {
39
- const resp = await fetch("https://www.postbuild.dev/api/auth", {
40
- method: "POST",
41
- headers: { "Content-Type": "application/json" },
42
- body: JSON.stringify({ token: creds.token }),
43
- signal: AbortSignal.timeout(5000)
44
- });
45
- if (resp.ok) {
46
- const data = await resp.json();
47
- if (data && data.ok && data.authenticated) {
48
- return { valid: true, email: data.email, plan: data.plan, token: creds.token };
49
- }
50
- }
51
- } catch {
52
- // Offline fallback: verify structural format if network is unreachable
53
- const parts = creds.token.slice("pb_live_".length).split(".");
54
- if (parts.length === 2 && parts[0].length > 10 && parts[1].length === 24) {
55
- return { valid: true, email: creds.email || "offline@postbuild.dev", plan: creds.plan || "launch_pass", token: creds.token };
56
- }
57
- }
58
- }
59
-
60
- let checkoutUrl = `https://www.postbuild.dev/?domain=${encodeURIComponent(domain)}`;
61
- try {
62
- const cResp = await fetch(
63
- `https://www.postbuild.dev/api/checkout?plan=launch_pass&domain=${encodeURIComponent(domain)}`,
64
- { signal: AbortSignal.timeout(4500) }
65
- );
66
- if (cResp.ok) {
67
- const cData = await cResp.json();
68
- if (cData && cData.checkoutUrl) checkoutUrl = cData.checkoutUrl;
69
- }
70
- } catch {}
71
-
72
- return {
73
- valid: false,
74
- reason: "PRO_LICENSE_REQUIRED",
75
- checkoutUrl,
76
- webUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(domain)}`,
77
- message:
78
- "A 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 auto-verification."
79
- };
80
- }
81
-
82
- export function signLicenseToken({ email, plan = "launch_pass", domain = "*" }) {
83
- const payload = {
84
- email: (email || "").toLowerCase().trim(),
85
- plan,
86
- domain,
87
- iat: Math.floor(Date.now() / 1000)
88
- };
89
- const base64Payload = Buffer.from(JSON.stringify(payload)).toString("base64url");
90
- const sig = crypto.createHmac("sha256", SECRET).update(base64Payload).digest("base64url").slice(0, 24);
91
- return `pb_live_${base64Payload}.${sig}`;
92
- }
93
-
94
- export function verifyLicenseToken(token) {
95
- if (!token || typeof token !== "string" || !token.startsWith("pb_live_")) {
96
- return { valid: false };
97
- }
98
- const raw = token.slice("pb_live_".length);
99
- const [base64Payload, sig] = raw.split(".");
100
- if (!base64Payload || !sig) return { valid: false };
101
-
102
- const expectedSig = crypto.createHmac("sha256", SECRET).update(base64Payload).digest("base64url").slice(0, 24);
103
- if (sig !== expectedSig) return { valid: false };
104
-
105
- try {
106
- const payload = JSON.parse(Buffer.from(base64Payload, "base64url").toString("utf8"));
107
- return { valid: true, ...payload };
108
- } catch {
109
- return { valid: false };
110
- }
111
- }
112
-
113
- function createSignedOtpChallenge(email, otpCode, plan = "launch_pass") {
114
- const exp = Math.floor(Date.now() / 1000) + 600; // 10 minutes
115
- const data = `${email.toLowerCase().trim()}:${otpCode}:${plan}:${exp}`;
116
- const sig = crypto.createHmac("sha256", SECRET).update(data).digest("base64url").slice(0, 24);
117
- const encoded = Buffer.from(JSON.stringify({ email: email.toLowerCase().trim(), plan, exp })).toString("base64url");
118
- return `${encoded}.${sig}`;
119
- }
120
-
121
- function verifySignedOtpChallenge(challengeToken, otpCode) {
122
- if (!challengeToken || !otpCode) return { valid: false };
123
- const [encoded, sig] = String(challengeToken).split(".");
124
- if (!encoded || !sig) return { valid: false };
125
- try {
126
- const parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
127
- if (!parsed.exp || Math.floor(Date.now() / 1000) > parsed.exp) {
128
- return { valid: false, error: "Verification code expired. Please request a new code." };
129
- }
130
- const data = `${parsed.email}:${String(otpCode).trim()}:${parsed.plan}:${parsed.exp}`;
131
- const expectedSig = crypto.createHmac("sha256", SECRET).update(data).digest("base64url").slice(0, 24);
132
- if (sig !== expectedSig) {
133
- return { valid: false, error: "Invalid 6-digit verification code." };
134
- }
135
- return { valid: true, email: parsed.email, plan: parsed.plan };
136
- } catch {
137
- return { valid: false, error: "Invalid verification challenge." };
138
- }
139
- }
140
-
141
- async function sendLoginOtpViaResend(email, otpCode, licenseToken) {
142
- const resendKey = (process.env.RESEND_API_KEY || "").trim();
143
- if (!resendKey) {
144
- return { sent: false, reason: "missing_resend_key" };
145
- }
146
- try {
147
- const resp = await fetch("https://api.resend.com/emails", {
148
- method: "POST",
149
- headers: {
150
- Authorization: `Bearer ${resendKey}`,
151
- "Content-Type": "application/json"
152
- },
153
- body: JSON.stringify({
154
- from: "postbuild <login@postbuild.dev>",
155
- to: [email],
156
- subject: `${otpCode} is your postbuild verification code`,
157
- 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;">
158
- <h2 style="margin: 0 0 8px; font-size: 20px;">postbuild · Account Verification</h2>
159
- <p style="color: #475569; font-size: 14px; line-height: 1.6;">Enter this 6-digit verification code on <strong>postbuild.dev</strong> to view your Stripe receipts and manage your subscription:</p>
160
- <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>
161
- <p style="color: #475569; font-size: 13px; line-height: 1.6;">Your active CLI License Key (for <code>git paid login</code>):</p>
162
- <pre style="background: #f8fafc; border: 1px solid #e2e8f0; padding: 10px; border-radius: 8px; font-size: 11px; overflow-x: auto;">${licenseToken}</pre>
163
- </div>`
164
- })
165
- });
166
- return { sent: resp.ok };
167
- } catch {
168
- return { sent: false };
169
- }
170
- }
171
-
172
- export async function authenticateUser({ email, token, sessionId, otpCode, challengeToken }) {
173
- // 1. If user passes an existing pb_live_ token, verify cryptographic HMAC signature
174
- if (token && token.startsWith("pb_live_")) {
175
- const verified = verifyLicenseToken(token);
176
- if (verified.valid) {
177
- return {
178
- ok: true,
179
- authenticated: true,
180
- email: verified.email,
181
- plan: verified.plan,
182
- domain: verified.domain,
183
- token
184
- };
185
- }
186
- return { ok: false, authenticated: false, error: "Invalid pb_live_ license key signature." };
187
- }
188
-
189
- // 2. If user is completing Step 2 of Email OTP verification
190
- if (otpCode && challengeToken) {
191
- const check = verifySignedOtpChallenge(challengeToken, otpCode);
192
- if (!check.valid) {
193
- return { ok: false, authenticated: false, error: check.error || "Invalid verification code." };
194
- }
195
- const issued = signLicenseToken({ email: check.email, plan: check.plan, domain: "*" });
196
- return {
197
- ok: true,
198
- authenticated: true,
199
- email: check.email,
200
- plan: check.plan,
201
- domain: "*",
202
- token: issued
203
- };
204
- }
205
-
206
- const stripe = getStripeClient();
207
-
208
- // 3. If user returned from Stripe Checkout with unguessable session_id (cs_...), verify session via StripeClient
209
- if (sessionId && stripe) {
210
- const session = await stripe.checkout.sessions.retrieve(sessionId);
211
- if (session && session.payment_status !== "unpaid") {
212
- const custEmail = (session.customer_details?.email || email || "founder@postbuild.dev").toLowerCase().trim();
213
- const plan = session.metadata?.plan_id || "launch_pass";
214
- const domain = session.client_reference_id || session.metadata?.target_domain || "*";
215
- const newToken = signLicenseToken({ email: custEmail, plan, domain });
216
- return {
217
- ok: true,
218
- authenticated: true,
219
- email: custEmail,
220
- plan,
221
- domain,
222
- token: newToken
223
- };
224
- }
225
- }
226
-
227
- // 4. If user enters an email address, verify they have a record in Stripe and require a 6-digit email OTP code (or their pb_live_ key)
228
- const cleanEmail = (email || "").toLowerCase().trim();
229
- if (!cleanEmail || !cleanEmail.includes("@")) {
230
- return { ok: false, authenticated: false, error: "Enter your email address or pb_live_ license key." };
231
- }
232
-
233
- if (stripe) {
234
- const customers = await stripe.customers.list({ email: cleanEmail, limit: 1 });
235
- let hasPurchase = Array.isArray(customers.data) && customers.data.length > 0;
236
-
237
- if (!hasPurchase) {
238
- const sessions = await stripe.checkout.sessions.list({ limit: 20 });
239
- hasPurchase = sessions.data.some(
240
- (s) =>
241
- s.payment_status !== "unpaid" &&
242
- (s.customer_details?.email || s.customer_email || "").toLowerCase().trim() === cleanEmail
243
- );
244
- }
245
-
246
- if (!hasPurchase) {
247
- return {
248
- ok: false,
249
- authenticated: false,
250
- error: "No active Launch Pass found for this email in Stripe. Purchase a Launch Pass ($29) first."
251
- };
252
- }
253
-
254
- const generatedOtp = String(Math.floor(100000 + Math.random() * 900000));
255
- const challenge = createSignedOtpChallenge(cleanEmail, generatedOtp, "launch_pass");
256
- const licenseForEmail = signLicenseToken({ email: cleanEmail, plan: "launch_pass", domain: "*" });
257
- const emailResult = await sendLoginOtpViaResend(cleanEmail, generatedOtp, licenseForEmail);
258
-
259
- if (emailResult.sent) {
260
- return {
261
- ok: true,
262
- authenticated: false,
263
- requiresOtp: true,
264
- email: cleanEmail,
265
- challengeToken: challenge,
266
- message: `We emailed a 6-digit verification code to ${cleanEmail}. Enter it below to unlock your billing & receipts.`
267
- };
268
- }
269
-
270
- return {
271
- ok: false,
272
- authenticated: false,
273
- error: "For security, email-only lookup is disabled until RESEND_API_KEY is set in Vercel. Please sign in using your pb_live_... license key."
274
- };
275
- }
276
-
277
- return {
278
- ok: false,
279
- authenticated: false,
280
- error: "Please sign in with your pb_live_... license key."
281
- };
282
- }
1
+ // Cryptographic License Verification, Entitlement Ledger & Hardened Email OTP for postbuild (www.postbuild.dev)
2
+
3
+ import crypto from "node:crypto";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import os from "node:os";
7
+ import { getBotInboxForDomain } from "./inbox.js";
8
+
9
+ const otpChallengeState = new Map(); // cid -> { attempts: number, used: boolean, expiresAt: number }
10
+ const memoryEntitlements = new Map(); // key (jti or email) -> { jti, email, customerId, plan, domain, inbox, status, expiresAt }
11
+ const processedWebhookEvents = new Set(); // stripe_event_id idempotency set
12
+
13
+ export function requireAuthSecret() {
14
+ const explicit = (process.env.POSTBUILD_AUTH_SECRET || process.env.POSTBUILD_SECRET || "").trim();
15
+ if (explicit.length >= 32) {
16
+ return explicit;
17
+ }
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
+ throw new Error("POSTBUILD_SECRET or POSTBUILD_AUTH_SECRET is missing or too short (minimum 32 characters required)");
24
+ }
25
+
26
+ function timingSafeStringEqual(a, b) {
27
+ if (typeof a !== "string" || typeof b !== "string") return false;
28
+ const bufA = Buffer.from(a, "utf8");
29
+ const bufB = Buffer.from(b, "utf8");
30
+ if (bufA.length !== bufB.length || bufA.length === 0) return false;
31
+ return crypto.timingSafeEqual(bufA, bufB);
32
+ }
33
+
34
+ // ─── ENTITLEMENT & WEBHOOK IDEMPOTENCY LEDGER ────────────────────────────────
35
+
36
+ export function hasProcessedWebhookEvent(eventId) {
37
+ if (!eventId) return false;
38
+ return processedWebhookEvents.has(eventId);
39
+ }
40
+
41
+ export function recordWebhookEventProcessed(eventId) {
42
+ if (!eventId) return false;
43
+ processedWebhookEvents.add(eventId);
44
+ return true;
45
+ }
46
+
47
+ export function markWebhookEventProcessed(eventId) {
48
+ if (!eventId) return false;
49
+ if (processedWebhookEvents.has(eventId)) {
50
+ return false; // Already processed
51
+ }
52
+ processedWebhookEvents.add(eventId);
53
+ return true;
54
+ }
55
+
56
+ export function upsertEntitlement({
57
+ jti,
58
+ email,
59
+ customerId = null,
60
+ plan = "launch_pass",
61
+ domain = "postbuild.dev",
62
+ status = "active",
63
+ expiresAt = null
64
+ }) {
65
+ const cleanEmail = String(email || "").toLowerCase().trim();
66
+ const cleanDomain = String(domain || "postbuild.dev")
67
+ .toLowerCase()
68
+ .trim()
69
+ .replace(/^https?:\/\//, "")
70
+ .replace(/^www\./, "")
71
+ .split("/")[0] || "postbuild.dev";
72
+ const resolvedJti = jti || crypto.randomUUID();
73
+ const inbox = getBotInboxForDomain(cleanDomain, cleanEmail);
74
+ const record = {
75
+ jti: resolvedJti,
76
+ email: cleanEmail,
77
+ customerId: customerId || null,
78
+ plan,
79
+ domain: cleanDomain,
80
+ inbox,
81
+ status,
82
+ expiresAt: expiresAt || Math.floor(Date.now() / 1000) + 365 * 24 * 3600,
83
+ updatedAt: new Date().toISOString()
84
+ };
85
+
86
+ memoryEntitlements.set(`jti:${resolvedJti}`, record);
87
+ if (cleanEmail) {
88
+ memoryEntitlements.set(`email:${cleanEmail}`, record);
89
+ memoryEntitlements.set(`email:${cleanEmail}:${plan}`, record);
90
+ }
91
+ if (customerId) {
92
+ memoryEntitlements.set(`cust:${customerId}`, record);
93
+ memoryEntitlements.set(`cust:${customerId}:${plan}`, record);
94
+ }
95
+ return record;
96
+ }
97
+
98
+ export function updateEntitlementStatus({
99
+ jti,
100
+ email,
101
+ customerId,
102
+ status = "revoked",
103
+ targetPlan = null,
104
+ allowClearRevoked = false
105
+ }) {
106
+ let updated = false;
107
+ const cleanEmail = email ? String(email).toLowerCase().trim() : "";
108
+
109
+ for (const [, rec] of memoryEntitlements.entries()) {
110
+ const matchesIdentity =
111
+ (jti && rec.jti === jti) ||
112
+ (cleanEmail && rec.email === cleanEmail) ||
113
+ (customerId && rec.customerId === customerId);
114
+ if (!matchesIdentity) continue;
115
+ if (targetPlan && rec.plan && rec.plan !== targetPlan) continue;
116
+
117
+ // Never allow a subscription 'active' event to clear a refund ('revoked') or dispute ('suspended')
118
+ if (
119
+ status === "active" &&
120
+ !allowClearRevoked &&
121
+ (rec.status === "revoked" || rec.status === "suspended")
122
+ ) {
123
+ continue;
124
+ }
125
+
126
+ rec.status = status;
127
+ rec.updatedAt = new Date().toISOString();
128
+ updated = true;
129
+ }
130
+
131
+ if (!updated && (jti || cleanEmail || customerId) && status !== "active") {
132
+ upsertEntitlement({
133
+ jti: jti || `revoked_${Date.now()}`,
134
+ email: cleanEmail,
135
+ customerId,
136
+ plan: targetPlan || "launch_pass",
137
+ status
138
+ });
139
+ }
140
+ return updated;
141
+ }
142
+
143
+ export function getEntitlement({ jti, email, customerId, plan = null } = {}) {
144
+ if (jti && memoryEntitlements.has(`jti:${jti}`)) {
145
+ return memoryEntitlements.get(`jti:${jti}`);
146
+ }
147
+ if (email) {
148
+ const cleanEmail = String(email).toLowerCase().trim();
149
+ if (plan && memoryEntitlements.has(`email:${cleanEmail}:${plan}`)) {
150
+ return memoryEntitlements.get(`email:${cleanEmail}:${plan}`);
151
+ }
152
+ if (memoryEntitlements.has(`email:${cleanEmail}`)) {
153
+ const rec = memoryEntitlements.get(`email:${cleanEmail}`);
154
+ if (!plan || !rec.plan || rec.plan === plan || rec.status === "revoked" || rec.status === "suspended") {
155
+ return rec;
156
+ }
157
+ }
158
+ }
159
+ if (customerId) {
160
+ if (plan && memoryEntitlements.has(`cust:${customerId}:${plan}`)) {
161
+ return memoryEntitlements.get(`cust:${customerId}:${plan}`);
162
+ }
163
+ if (memoryEntitlements.has(`cust:${customerId}`)) {
164
+ const rec = memoryEntitlements.get(`cust:${customerId}`);
165
+ if (!plan || !rec.plan || rec.plan === plan || rec.status === "revoked" || rec.status === "suspended") {
166
+ return rec;
167
+ }
168
+ }
169
+ }
170
+ return null;
171
+ }
172
+
173
+ export function isEntitlementRevoked({ jti, email, customerId, plan = null } = {}) {
174
+ const rec = getEntitlement({ jti, email, customerId, plan });
175
+ if (!rec) return false;
176
+ return rec.status !== "active";
177
+ }
178
+
179
+ const stripeCustomerStatusCache = new Map(); // key -> { status, checkedAt }
180
+
181
+ export function clearAuthStateForTests() {
182
+ otpChallengeState.clear();
183
+ memoryEntitlements.clear();
184
+ processedWebhookEvents.clear();
185
+ stripeCustomerStatusCache.clear();
186
+ }
187
+
188
+ // ─── CLI CREDENTIALS & LICENSE VERIFICATION ──────────────────────────────────
189
+
190
+ export function loadLocalCredentials() {
191
+ if (process.env.POSTBUILD_LICENSE_KEY) {
192
+ return {
193
+ token: process.env.POSTBUILD_LICENSE_KEY.trim(),
194
+ email: "cli@postbuild.dev",
195
+ plan: "launch_pass"
196
+ };
197
+ }
198
+ try {
199
+ const credPath = path.join(os.homedir(), ".postbuild", "credentials.json");
200
+ if (fs.existsSync(credPath)) {
201
+ const data = JSON.parse(fs.readFileSync(credPath, "utf8"));
202
+ if (data && data.token) return data;
203
+ }
204
+ } catch {}
205
+ return null;
206
+ }
207
+
208
+ export async function verifyCliLicense({ domain = "postbuild.dev", token = "" } = {}) {
209
+ const candidateToken = (token || "").trim() || loadLocalCredentials()?.token || "";
210
+
211
+ if (candidateToken && candidateToken.startsWith("pb_live_")) {
212
+ // 1. If running on the server (where signing secret is present), verify signature & revocation directly
213
+ try {
214
+ const localCheck = verifyLicenseToken(candidateToken);
215
+ if (localCheck.valid) {
216
+ return {
217
+ valid: true,
218
+ email: localCheck.email,
219
+ plan: localCheck.plan,
220
+ domain: localCheck.domain,
221
+ inbox: localCheck.inbox,
222
+ jti: localCheck.jti,
223
+ token: candidateToken
224
+ };
225
+ }
226
+ return {
227
+ valid: false,
228
+ 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)}`,
231
+ message: localCheck.error || "Invalid or revoked pb_live_ license key."
232
+ };
233
+ } catch {
234
+ // 2. When running on a user's machine via CLI (without server secret), verify against https://www.postbuild.dev/api/auth
235
+ try {
236
+ const resp = await fetch("https://www.postbuild.dev/api/auth", {
237
+ method: "POST",
238
+ headers: {
239
+ "Content-Type": "application/json",
240
+ Authorization: `Bearer ${candidateToken}`
241
+ },
242
+ body: JSON.stringify({}),
243
+ signal: AbortSignal.timeout(5000)
244
+ });
245
+ if (resp.ok) {
246
+ const data = await resp.json();
247
+ if (data && data.ok && data.authenticated) {
248
+ return {
249
+ valid: true,
250
+ email: data.email,
251
+ plan: data.plan,
252
+ domain: data.domain,
253
+ inbox: data.inbox,
254
+ token: candidateToken
255
+ };
256
+ }
257
+ }
258
+ } catch {}
259
+ }
260
+ }
261
+
262
+ return {
263
+ valid: false,
264
+ reason: "PRO_LICENSE_REQUIRED",
265
+ checkoutUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(domain)}`,
266
+ webUrl: `https://www.postbuild.dev/?domain=${encodeURIComponent(domain)}`,
267
+ 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."
269
+ };
270
+ }
271
+
272
+ // ─── CRYPTOGRAPHIC TOKEN SIGNING & VERIFICATION ──────────────────────────────
273
+
274
+ export function signLicenseToken({
275
+ email,
276
+ plan = "launch_pass",
277
+ domain = "postbuild.dev",
278
+ jti = "",
279
+ customerId = null,
280
+ ttlSeconds = 365 * 24 * 3600
281
+ }) {
282
+ const secret = requireAuthSecret();
283
+ const cleanEmail = (email || "").toLowerCase().trim();
284
+ const cleanDomain =
285
+ String(domain || "postbuild.dev")
286
+ .toLowerCase()
287
+ .trim()
288
+ .replace(/^https?:\/\//, "")
289
+ .replace(/^www\./, "")
290
+ .split("/")[0] || "postbuild.dev";
291
+ const now = Math.floor(Date.now() / 1000);
292
+ const exp = now + ttlSeconds;
293
+ const resolvedJti = jti || crypto.randomUUID();
294
+ const inbox = getBotInboxForDomain(cleanDomain, cleanEmail);
295
+
296
+ upsertEntitlement({
297
+ jti: resolvedJti,
298
+ email: cleanEmail,
299
+ customerId,
300
+ plan,
301
+ domain: cleanDomain,
302
+ status: "active",
303
+ expiresAt: exp
304
+ });
305
+
306
+ const payload = {
307
+ jti: resolvedJti,
308
+ email: cleanEmail,
309
+ customerId: customerId || null,
310
+ plan,
311
+ domain: cleanDomain,
312
+ inbox,
313
+ iat: now,
314
+ exp
315
+ };
316
+ const base64Payload = Buffer.from(JSON.stringify(payload)).toString("base64url");
317
+ const sig = crypto.createHmac("sha256", secret).update(base64Payload).digest("base64url").slice(0, 32);
318
+ return `pb_live_${base64Payload}.${sig}`;
319
+ }
320
+
321
+ export function verifyLicenseToken(token) {
322
+ if (!token || typeof token !== "string" || !token.startsWith("pb_live_")) {
323
+ return { valid: false };
324
+ }
325
+ const secret = requireAuthSecret();
326
+ const raw = token.slice("pb_live_".length);
327
+ const dotIdx = raw.indexOf(".");
328
+ if (dotIdx <= 0) return { valid: false };
329
+
330
+ const base64Payload = raw.slice(0, dotIdx);
331
+ const sig = raw.slice(dotIdx + 1);
332
+ if (!base64Payload || !sig) return { valid: false };
333
+
334
+ const fullHmac = crypto.createHmac("sha256", secret).update(base64Payload).digest("base64url");
335
+ const expectedSig32 = fullHmac.slice(0, 32);
336
+ const expectedSig24 = fullHmac.slice(0, 24);
337
+
338
+ const sigValid =
339
+ timingSafeStringEqual(sig, expectedSig32) || timingSafeStringEqual(sig, expectedSig24);
340
+ if (!sigValid) {
341
+ return { valid: false, error: "Invalid license signature" };
342
+ }
343
+
344
+ try {
345
+ const payload = JSON.parse(Buffer.from(base64Payload, "base64url").toString("utf8"));
346
+ const now = Math.floor(Date.now() / 1000);
347
+ if (payload.exp && now > Number(payload.exp)) {
348
+ return { valid: false, error: "License token has expired" };
349
+ }
350
+ if (
351
+ isEntitlementRevoked({
352
+ jti: payload.jti,
353
+ email: payload.email,
354
+ customerId: payload.customerId,
355
+ plan: payload.plan
356
+ })
357
+ ) {
358
+ return { valid: false, error: "License entitlement has been revoked or refunded" };
359
+ }
360
+ if (!payload.inbox && payload.domain) {
361
+ payload.inbox = getBotInboxForDomain(payload.domain, payload.email);
362
+ }
363
+ return { valid: true, ...payload };
364
+ } catch {
365
+ return { valid: false };
366
+ }
367
+ }
368
+
369
+ // ─── HARDENED EMAIL OTP CHALLENGE (MAX 5 ATTEMPTS, SINGLE-USE, TIMING-SAFE) ─
370
+
371
+ function deriveOtpCipherKey(secret) {
372
+ return crypto.createHmac("sha256", secret).update("postbuild:otp:aes-256-gcm:v2").digest();
373
+ }
374
+
375
+ export function createSignedOtpChallenge(
376
+ email,
377
+ otpCode,
378
+ plan = "launch_pass",
379
+ domain = "postbuild.dev",
380
+ options = {}
381
+ ) {
382
+ const resolvedOptions =
383
+ domain && typeof domain === "object" ? domain : options || {};
384
+ const rawDomain =
385
+ domain && typeof domain === "object" ? "postbuild.dev" : domain;
386
+
387
+ const secret = requireAuthSecret();
388
+ const cid = crypto.randomBytes(12).toString("base64url");
389
+ const exp = Math.floor(Date.now() / 1000) + 600; // 10 minutes
390
+ const cleanEmail = email.toLowerCase().trim();
391
+ const cleanDomain =
392
+ String(rawDomain || "postbuild.dev")
393
+ .toLowerCase()
394
+ .trim()
395
+ .replace(/^https?:\/\//, "")
396
+ .replace(/^www\./, "")
397
+ .split("/")[0] || "postbuild.dev";
398
+ const customerId = resolvedOptions.customerId || "";
399
+
400
+ const payloadObj = {
401
+ cid,
402
+ email: cleanEmail,
403
+ code: String(otpCode).trim(),
404
+ plan,
405
+ domain: cleanDomain,
406
+ customerId,
407
+ exp,
408
+ dummy: Boolean(resolvedOptions.dummy)
409
+ };
410
+
411
+ // Pad plaintext to fixed 320-byte length so real and dummy challenges have identical ciphertext length
412
+ const plaintext = JSON.stringify(payloadObj).padEnd(320, " ");
413
+ const iv = crypto.randomBytes(12);
414
+ const cipher = crypto.createCipheriv("aes-256-gcm", deriveOtpCipherKey(secret), iv);
415
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
416
+ const authTag = cipher.getAuthTag();
417
+
418
+ otpChallengeState.set(cid, {
419
+ attempts: 0,
420
+ used: false,
421
+ expiresAt: exp * 1000
422
+ });
423
+
424
+ return `${iv.toString("base64url")}.${encrypted.toString("base64url")}.${authTag.toString("base64url")}`;
425
+ }
426
+
427
+ export function verifySignedOtpChallenge(challengeToken, otpCode) {
428
+ if (!challengeToken || !otpCode) return { valid: false, error: "Missing verification code." };
429
+ const secret = requireAuthSecret();
430
+ const parts = String(challengeToken).split(".");
431
+ if (parts.length !== 3) return { valid: false, error: "Invalid verification challenge." };
432
+ const [ivB64, encB64, tagB64] = parts;
433
+ if (!ivB64 || !encB64 || !tagB64) return { valid: false, error: "Invalid verification challenge." };
434
+
435
+ try {
436
+ const iv = Buffer.from(ivB64, "base64url");
437
+ const encrypted = Buffer.from(encB64, "base64url");
438
+ const authTag = Buffer.from(tagB64, "base64url");
439
+ if (iv.length !== 12 || authTag.length !== 16) {
440
+ return { valid: false, error: "Invalid verification challenge." };
441
+ }
442
+
443
+ const decipher = crypto.createDecipheriv("aes-256-gcm", deriveOtpCipherKey(secret), iv);
444
+ decipher.setAuthTag(authTag);
445
+ const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8").trim();
446
+ const parsed = JSON.parse(decrypted);
447
+
448
+ const nowSec = Math.floor(Date.now() / 1000);
449
+ if (!parsed.exp || nowSec > parsed.exp) {
450
+ return { valid: false, error: "Verification code expired. Please request a new code." };
451
+ }
452
+
453
+ const cid = parsed.cid || encB64.slice(0, 16);
454
+ let state = otpChallengeState.get(cid);
455
+ if (!state) {
456
+ state = { attempts: 0, used: false, expiresAt: parsed.exp * 1000 };
457
+ otpChallengeState.set(cid, state);
458
+ }
459
+
460
+ if (state.used) {
461
+ return { valid: false, error: "This verification code has already been used." };
462
+ }
463
+
464
+ state.attempts += 1;
465
+ if (state.attempts > 5) {
466
+ return { valid: false, error: "Maximum verification attempts (5) exceeded. Please request a new code." };
467
+ }
468
+
469
+ if (!timingSafeStringEqual(String(otpCode).trim(), String(parsed.code || "").trim())) {
470
+ return { valid: false, error: "Invalid 6-digit verification code." };
471
+ }
472
+
473
+ // Never allow a dummy enumeration-resistance challenge to mint a real license
474
+ if (parsed.dummy === true || parsed.plan === "none") {
475
+ return { valid: false, error: "Invalid 6-digit verification code." };
476
+ }
477
+
478
+ state.used = true;
479
+ return {
480
+ valid: true,
481
+ email: parsed.email,
482
+ plan: parsed.plan || "launch_pass",
483
+ domain: parsed.domain || "postbuild.dev",
484
+ customerId: parsed.customerId || null
485
+ };
486
+ } catch {
487
+ return { valid: false, error: "Invalid verification challenge." };
488
+ }
489
+ }
490
+
491
+ export function getFounderEmailsSet() {
492
+ return new Set(
493
+ [
494
+ "bennett@postbuild.dev",
495
+ (process.env.ADMIN_EMAIL || "").toLowerCase().trim(),
496
+ (process.env.ADMIN_FORWARD_EMAIL || "").toLowerCase().trim(),
497
+ ...(process.env.POSTBUILD_OPERATOR_EMAILS || "")
498
+ .split(",")
499
+ .map((s) => s.toLowerCase().trim())
500
+ ].filter(Boolean)
501
+ );
502
+ }
503
+
504
+ async function sendLoginOtpViaResend(email, otpCode) {
505
+ const resendKey = (process.env.RESEND_API_KEY || "").trim();
506
+ if (!resendKey) {
507
+ return { sent: false, reason: "missing_resend_key" };
508
+ }
509
+ const cleanEmail = String(email || "").toLowerCase().trim();
510
+ const recipients = [cleanEmail];
511
+ if (cleanEmail === "bennett@postbuild.dev") {
512
+ const founderForward = (process.env.ADMIN_FORWARD_EMAIL || process.env.ADMIN_EMAIL || "").toLowerCase().trim();
513
+ if (founderForward && !recipients.includes(founderForward)) {
514
+ recipients.push(founderForward);
515
+ }
516
+ }
517
+ try {
518
+ const resp = await fetch("https://api.resend.com/emails", {
519
+ method: "POST",
520
+ headers: {
521
+ Authorization: `Bearer ${resendKey}`,
522
+ "Content-Type": "application/json"
523
+ },
524
+ body: JSON.stringify({
525
+ from: "postbuild <login@postbuild.dev>",
526
+ to: recipients,
527
+ 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>
533
+ </div>`
534
+ })
535
+ });
536
+ if (!resp.ok) {
537
+ const errText = await resp.text().catch(() => "");
538
+ console.error("[auth/resend] Failed to send OTP email:", resp.status, errText);
539
+ }
540
+ return { sent: resp.ok };
541
+ } catch (err) {
542
+ console.error("[auth/resend] Error sending OTP email:", err?.message || err);
543
+ return { sent: false };
544
+ }
545
+ }
546
+
547
+ function escapeHtml(str) {
548
+ return String(str || "")
549
+ .replace(/&/g, "&amp;")
550
+ .replace(/</g, "&lt;")
551
+ .replace(/>/g, "&gt;")
552
+ .replace(/"/g, "&quot;")
553
+ .replace(/'/g, "&#39;");
554
+ }
555
+
556
+ export async function sendLicenseFulfillmentEmail({ email, licenseToken, planId, targetDomain }) {
557
+ const resendKey = (process.env.RESEND_API_KEY || "").trim();
558
+ if (!resendKey || !email) return { sent: false };
559
+ const safeDomain = escapeHtml(targetDomain || "postbuild.dev");
560
+ const inbox = escapeHtml(getBotInboxForDomain(targetDomain, email));
561
+ const safeToken = escapeHtml(licenseToken);
562
+ try {
563
+ const resp = await fetch("https://api.resend.com/emails", {
564
+ method: "POST",
565
+ headers: {
566
+ Authorization: `Bearer ${resendKey}`,
567
+ "Content-Type": "application/json"
568
+ },
569
+ body: JSON.stringify({
570
+ from: "postbuild <login@postbuild.dev>",
571
+ 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>
580
+ </div>`
581
+ })
582
+ });
583
+ return { sent: resp.ok };
584
+ } catch {
585
+ return { sent: false };
586
+ }
587
+ }
588
+
589
+ export async function checkStripeCustomerStatusCached(verified) {
590
+ if (!verified || !verified.valid) return "active";
591
+ const baseId = verified.customerId || verified.email || "";
592
+ if (!baseId) return "active";
593
+ const cacheKey = `${baseId}:${verified.plan || "launch_pass"}`;
594
+
595
+ const now = Date.now();
596
+ const cached = stripeCustomerStatusCache.get(cacheKey);
597
+ if (cached && now - cached.checkedAt < 60000) {
598
+ return cached.status;
599
+ }
600
+
601
+ let stripe = null;
602
+ try {
603
+ const stripeMod = await import("./stripe.js");
604
+ stripe = stripeMod.getStripeClient();
605
+ } catch {}
606
+ if (!stripe) return "active";
607
+
608
+ try {
609
+ let cust = null;
610
+ if (verified.customerId) {
611
+ cust = await stripe.customers.retrieve(verified.customerId);
612
+ } else if (verified.email) {
613
+ const list = await stripe.customers.list({ email: verified.email, limit: 1 });
614
+ cust = list.data?.[0] || null;
615
+ }
616
+ if (cust && !cust.deleted && cust.metadata?.pb_status) {
617
+ const pbStatus = cust.metadata.pb_status;
618
+ if (pbStatus === "revoked" || pbStatus === "suspended") {
619
+ stripeCustomerStatusCache.set(cacheKey, { status: pbStatus, checkedAt: now });
620
+ updateEntitlementStatus({
621
+ customerId: cust.id,
622
+ email: verified.email,
623
+ status: pbStatus
624
+ });
625
+ return pbStatus;
626
+ }
627
+ if (pbStatus === "cancelled" && verified.plan === "studio_monthly") {
628
+ stripeCustomerStatusCache.set(cacheKey, { status: "cancelled", checkedAt: now });
629
+ updateEntitlementStatus({
630
+ customerId: cust.id,
631
+ email: verified.email,
632
+ status: "cancelled",
633
+ targetPlan: "studio_monthly"
634
+ });
635
+ return "cancelled";
636
+ }
637
+ stripeCustomerStatusCache.set(cacheKey, { status: "active", checkedAt: now });
638
+ } else {
639
+ stripeCustomerStatusCache.set(cacheKey, { status: "active", checkedAt: now });
640
+ }
641
+ } catch {}
642
+
643
+ return "active";
644
+ }
645
+
646
+ export async function authenticateUser({ email, token, sessionId, otpCode, challengeToken }) {
647
+ const founderEmails = getFounderEmailsSet();
648
+
649
+ // 1. Verify existing pb_live_ token via HMAC-SHA256 + revocation check + 60s Stripe Customer pb_status check
650
+ if (token && token.startsWith("pb_live_")) {
651
+ const verified = verifyLicenseToken(token);
652
+ if (verified.valid) {
653
+ const remoteStatus = await checkStripeCustomerStatusCached(verified);
654
+ if (remoteStatus !== "active") {
655
+ return {
656
+ ok: false,
657
+ authenticated: false,
658
+ error: "License entitlement has been revoked, refunded, or cancelled."
659
+ };
660
+ }
661
+ return {
662
+ ok: true,
663
+ authenticated: true,
664
+ email: verified.email,
665
+ customerId: verified.customerId || null,
666
+ plan: verified.plan,
667
+ domain: verified.domain,
668
+ inbox: verified.inbox,
669
+ isFounder: founderEmails.has(String(verified.email || "").toLowerCase().trim()),
670
+ token
671
+ };
672
+ }
673
+ return { ok: false, authenticated: false, error: verified.error || "Invalid pb_live_ license key signature." };
674
+ }
675
+
676
+ // 2. Step 2 of Email OTP verification — issues pb_live_ license ONLY after OTP check passes
677
+ if (otpCode && challengeToken) {
678
+ const check = verifySignedOtpChallenge(challengeToken, otpCode);
679
+ if (!check.valid) {
680
+ return { ok: false, authenticated: false, error: check.error || "Invalid verification code." };
681
+ }
682
+ if (isEntitlementRevoked({ email: check.email, customerId: check.customerId, plan: check.plan })) {
683
+ return { ok: false, authenticated: false, error: "This license entitlement has been revoked or refunded." };
684
+ }
685
+ const existingEnt = getEntitlement({ email: check.email, customerId: check.customerId, plan: check.plan });
686
+ const plan = existingEnt?.plan || check.plan || "launch_pass";
687
+ const domain = existingEnt?.domain || check.domain || "postbuild.dev";
688
+ const customerId = existingEnt?.customerId || check.customerId || null;
689
+ const issued = signLicenseToken({ email: check.email, plan, domain, customerId });
690
+ return {
691
+ ok: true,
692
+ authenticated: true,
693
+ email: check.email,
694
+ customerId,
695
+ plan,
696
+ domain,
697
+ inbox: getBotInboxForDomain(domain, check.email),
698
+ isFounder: founderEmails.has(String(check.email || "").toLowerCase().trim()),
699
+ token: issued
700
+ };
701
+ }
702
+
703
+ let stripe = null;
704
+ try {
705
+ const stripeMod = await import("./stripe.js");
706
+ stripe = stripeMod.getStripeClient();
707
+ } catch {}
708
+
709
+ // 3. Stripe Checkout return session_id verification
710
+ if (sessionId && stripe) {
711
+ const session = await stripe.checkout.sessions.retrieve(sessionId, { expand: ["customer"] });
712
+ if (session && session.payment_status !== "unpaid") {
713
+ const custEmail = (session.customer_details?.email || email || "").toLowerCase().trim();
714
+ // Never allow an unverified checkout email claiming @postbuild.dev or operator status to bypass Email OTP
715
+ if (custEmail.endsWith("@postbuild.dev") || founderEmails.has(custEmail)) {
716
+ return {
717
+ ok: false,
718
+ authenticated: false,
719
+ error: "Operator and @postbuild.dev addresses require 6-digit Email OTP verification to sign in."
720
+ };
721
+ }
722
+ const plan = session.metadata?.plan_id || (session.mode === "subscription" ? "studio_monthly" : "launch_pass");
723
+ const domain = session.client_reference_id || session.metadata?.target_domain || "";
724
+ if (!domain || domain === "postbuild.dev") {
725
+ return {
726
+ ok: false,
727
+ authenticated: false,
728
+ error: "A valid non-reserved project domain is required to issue a license."
729
+ };
730
+ }
731
+ const customerObj = session.customer && typeof session.customer === "object" ? session.customer : null;
732
+ const customerId = typeof session.customer === "string" ? session.customer : customerObj?.id || null;
733
+ const custStatus = customerObj?.metadata?.pb_status || "active";
734
+ if (custStatus !== "active" || isEntitlementRevoked({ email: custEmail, customerId, plan })) {
735
+ return { ok: false, authenticated: false, error: "This license entitlement has been revoked or refunded." };
736
+ }
737
+ const newToken = signLicenseToken({ email: custEmail, plan, domain, customerId });
738
+ return {
739
+ ok: true,
740
+ authenticated: true,
741
+ email: custEmail,
742
+ customerId,
743
+ plan,
744
+ domain,
745
+ inbox: getBotInboxForDomain(domain, custEmail),
746
+ isFounder: false,
747
+ token: newToken
748
+ };
749
+ }
750
+ }
751
+
752
+ // 4. Email OTP request — uniform response to prevent customer email enumeration
753
+ const cleanEmail = (email || "").toLowerCase().trim();
754
+ if (!cleanEmail || !cleanEmail.includes("@")) {
755
+ return { ok: false, authenticated: false, error: "Enter your email address or pb_live_ license key." };
756
+ }
757
+
758
+ const uniformResponseMessage =
759
+ "If an active Launch Pass or Studio account is associated with that email, a 6-digit verification code has been sent.";
760
+
761
+ let resolvedPlan = "launch_pass";
762
+ let resolvedDomain = "postbuild.dev";
763
+ let resolvedCustomerId = null;
764
+ let hasActivePurchase = false;
765
+ let isExplicitlyRevoked = false;
766
+ let anyCustomerHadPbStatus = false;
767
+
768
+ const localEnt = getEntitlement({ email: cleanEmail });
769
+ if (localEnt && (localEnt.status === "revoked" || localEnt.status === "suspended")) {
770
+ isExplicitlyRevoked = true;
771
+ } else if (localEnt && localEnt.status === "active") {
772
+ hasActivePurchase = true;
773
+ resolvedPlan = localEnt.plan;
774
+ resolvedDomain = localEnt.domain;
775
+ resolvedCustomerId = localEnt.customerId || null;
776
+ }
777
+
778
+ if (!hasActivePurchase && !isExplicitlyRevoked && stripe) {
779
+ try {
780
+ const customers = await stripe.customers.list({ email: cleanEmail, limit: 10 });
781
+ if (Array.isArray(customers?.data)) {
782
+ for (const cust of customers.data) {
783
+ const pbStatus = cust.metadata?.pb_status;
784
+ if (pbStatus) {
785
+ anyCustomerHadPbStatus = true;
786
+ }
787
+ if (pbStatus === "revoked" || pbStatus === "suspended") {
788
+ // Do not break early in case a legitimate active customerId exists for this email
789
+ continue;
790
+ }
791
+ if (pbStatus === "cancelled") {
792
+ continue;
793
+ }
794
+ if (!pbStatus || pbStatus === "active") {
795
+ hasActivePurchase = true;
796
+ resolvedCustomerId = cust.id;
797
+ resolvedPlan = cust.metadata?.pb_plan || "launch_pass";
798
+ resolvedDomain = cust.metadata?.pb_domain || "postbuild.dev";
799
+ break;
800
+ }
801
+ }
802
+ }
803
+ } catch {}
804
+
805
+ // Skip session fallback if a customer record already carries an explicit pb_status (e.g. cancelled/revoked)
806
+ if (!hasActivePurchase && !isExplicitlyRevoked && !anyCustomerHadPbStatus) {
807
+ try {
808
+ const sessions = await stripe.checkout.sessions.list({ limit: 100 });
809
+ const matchingSessions = (sessions?.data || []).filter(
810
+ (s) =>
811
+ s.payment_status !== "unpaid" &&
812
+ (s.customer_details?.email || s.customer_email || "").toLowerCase().trim() === cleanEmail
813
+ );
814
+ const preferred =
815
+ matchingSessions.find((s) => s.mode === "payment" || s.metadata?.plan_id === "launch_pass") ||
816
+ matchingSessions[0];
817
+ if (preferred) {
818
+ hasActivePurchase = true;
819
+ resolvedPlan =
820
+ preferred.metadata?.plan_id || (preferred.mode === "subscription" ? "studio_monthly" : "launch_pass");
821
+ resolvedDomain = preferred.client_reference_id || preferred.metadata?.target_domain || "postbuild.dev";
822
+ resolvedCustomerId =
823
+ (typeof preferred.customer === "string" ? preferred.customer : preferred.customer?.id) ||
824
+ resolvedCustomerId;
825
+ }
826
+ } catch {}
827
+ }
828
+ }
829
+
830
+ if (!hasActivePurchase && !isExplicitlyRevoked && founderEmails.has(cleanEmail)) {
831
+ hasActivePurchase = true;
832
+ resolvedPlan = "studio_monthly";
833
+ resolvedDomain = "postbuild.dev";
834
+ }
835
+
836
+ if (hasActivePurchase) {
837
+ const generatedOtp = String(crypto.randomInt(100000, 1000000));
838
+ const challenge = createSignedOtpChallenge(cleanEmail, generatedOtp, resolvedPlan, resolvedDomain, {
839
+ customerId: resolvedCustomerId
840
+ });
841
+ await sendLoginOtpViaResend(cleanEmail, generatedOtp);
842
+ return {
843
+ ok: true,
844
+ authenticated: false,
845
+ requiresOtp: true,
846
+ email: cleanEmail,
847
+ challengeToken: challenge,
848
+ message: uniformResponseMessage
849
+ };
850
+ }
851
+
852
+ // Return uniform challenge shape with an encrypted dummy challenge that can NEVER mint a license
853
+ const dummyOtp = String(crypto.randomInt(100000, 1000000));
854
+ const dummyChallenge = createSignedOtpChallenge(cleanEmail, dummyOtp, "none", "unassigned.invalid", {
855
+ dummy: true
856
+ });
857
+ return {
858
+ ok: true,
859
+ authenticated: false,
860
+ requiresOtp: true,
861
+ email: cleanEmail,
862
+ challengeToken: dummyChallenge,
863
+ message: uniformResponseMessage
864
+ };
865
+ }
866
+