shippingszn 0.9.6 → 0.9.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.
Files changed (4) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +19 -18
  3. package/dist/index.js +257 -77
  4. package/package.json +3 -2
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Vibe Coder Launch Checklist contributors
3
+ Copyright (c) 2026 shippingszn llc
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # shippingszn
2
2
 
3
- Primary local scanner for shippingszn launch readiness. Run it inside the app
4
- you are about to ship to catch the launch debt AI builders commonly miss:
5
- leaked API keys, missing crawl assets, weak browser defenses, dangerous code
6
- patterns, and last-mile polish gaps.
3
+ Primary local launch inspector for AI-built apps. Run it inside the app you are
4
+ about to ship to catch the launch debt AI builders commonly miss: leaked API
5
+ keys, missing crawl assets, weak browser defenses, dangerous code patterns, and
6
+ last-mile polish gaps.
7
7
 
8
8
  ```bash
9
9
  npx shippingszn@latest
@@ -18,18 +18,18 @@ version, and safe stack tags. It never uploads source code, repo URLs, project
18
18
  names, secrets, handles, or emails. Finding-level detail stays out of public
19
19
  CLI/Wall output and opens inside the paid Launch Fix Kit.
20
20
 
21
- Human output starts with a Launch Debt Score, severity counts, a short
22
- plain-English launch-risk explanation, completed checks, and the locked Fix Kit
23
- handoff. Run with `--json` to get the same score-and-counts summary in a
24
- machine-readable shape:
21
+ Human output starts with a verdict, a higher-is-better Readiness Score,
22
+ severity counts, a short plain-English launch-risk explanation, completed
23
+ checks, and the locked Fix Kit handoff. Run with `--json` to get the same
24
+ score-and-counts summary in a machine-readable shape:
25
25
 
26
26
  ```json
27
27
  {
28
- "score": 43,
29
- "band": "no_go",
30
- "counts": { "critical": 1, "high": 3, "medium": 4, "lower": 2 },
28
+ "score": 60,
29
+ "band": "fix_first",
30
+ "counts": { "critical": 0, "high": 11, "medium": 1, "lower": 1 },
31
31
  "filesScanned": 128,
32
- "coverage": { "checksCompleted": 19, "checklistAreas": 46 },
32
+ "coverage": { "checksCompleted": 19, "checklistAreas": 51 },
33
33
  "detailsLocked": true,
34
34
  "unlockUrl": "https://shippingszn.com/fix-kit?scanResultId=00000000-0000-4000-8000-000000000123",
35
35
  "scanHandoff": {
@@ -40,9 +40,9 @@ machine-readable shape:
40
40
  }
41
41
  ```
42
42
 
43
- Finding titles, files, evidence, fix prompts, and verification steps are paid.
44
- The free CLI explains why the score matters, but the actual fixes unlock inside
45
- the Launch Fix Kit.
43
+ Finding titles, files, evidence, AI-builder punch list, and verification steps
44
+ are paid. The free CLI explains why the score matters, but the actual fixes
45
+ unlock inside the Launch Fix Kit.
46
46
 
47
47
  ## Aggregate summary
48
48
 
@@ -81,7 +81,8 @@ maps back to one of the items on the checklist.
81
81
 
82
82
  Internally each finding is tagged Critical, High, Medium, or Lower and maps to
83
83
  the relevant checklist item. The free CLI output keeps that finding-level detail
84
- locked.
84
+ locked; the Fix Kit turns it into the human launch decision and AI-builder punch
85
+ list.
85
86
 
86
87
  ## Suppressing false positives
87
88
 
@@ -122,8 +123,8 @@ These are deliberately out of scope for v1:
122
123
  quotas, etc.).
123
124
 
124
125
  A clean report is **not** a launch certificate — it just means none of the
125
- obvious things tripped a tripwire. Walk through the full checklist before you
126
- ship.
126
+ obvious things tripped a tripwire. Use the Fix Kit, owner-verification items, and
127
+ rerun loop before you ship.
127
128
 
128
129
  ## Usage
129
130
 
package/dist/index.js CHANGED
@@ -396,12 +396,14 @@ var ITEMS_CRITICAL_A = [
396
396
  "Click Run and let it finish. It usually takes a few minutes; longer for big projects.",
397
397
  "Sort by severity and look at everything marked Critical or High first. Ignore Low for now.",
398
398
  `For each finding, click into it. The scanner explains what's wrong in plain language and often suggests a fix. If you don't understand a finding, paste it into your AI builder and ask "what does this mean and how do I fix it?"`,
399
+ "Run your builder's own scanner too \u2014 Cursor, Claude Code, Lovable, Replit, Bolt, Snyk, Semgrep, or whatever your stack exposes. Treat it as the final gate after the manual checks, not a substitute for them.",
399
400
  "Re-run the scanner after each batch of fixes. You want zero Critical and as few High as possible before launch."
400
401
  ],
401
402
  redFlags: [
402
403
  "Any Critical finding you can't explain in your own words",
403
404
  `High-severity issues you're planning to "deal with later"`,
404
405
  'A long list of "hidden" or "ignored" issues with no notes on why',
406
+ "Your AI builder has a security/audit panel and you have never opened it",
405
407
  "Findings that show your app sending user data to companies you didn't intend",
406
408
  "Files flagged as malicious or unsafe that you don't recognize"
407
409
  ],
@@ -415,12 +417,13 @@ var ITEMS_CRITICAL_A = [
415
417
  category: "Security",
416
418
  priority: "critical",
417
419
  timeEstimate: "1 hr",
418
- prompt: "Act as a security engineer. Scan my entire codebase (including config, scripts, tests, frontend, build output, and any committed .env files) for hardcoded secrets \u2014 API keys, tokens, database passwords, OAuth client secrets, webhook signing keys, JWT signing keys, anything that should be private. For each finding, tell me: file, line, what kind of secret it appears to be, and which service it's for. Then refactor every finding to read from environment variables with clear names (OPENAI_API_KEY, STRIPE_SECRET_KEY, DATABASE_URL, etc.), and update any .env.example or docs to list the required variables. Also audit runtime leakage paths: any console.log / logger call that prints req.body, req.headers, full user objects, OAuth callback params, or error objects containing tokens; any Sentry (or equivalent) breadcrumbs that capture request bodies or auth headers without scrubbing; any TODO/debug comments that quote a real key. List every secret that needs to be ROTATED at the source service because it was previously committed, shared, or logged. Never print actual secret values back to me \u2014 only references and filenames.",
420
+ prompt: "Act as a security engineer. Scan my entire codebase (including config, scripts, tests, frontend, build output, and any committed .env files) for hardcoded secrets \u2014 API keys, tokens, database passwords, OAuth client secrets, webhook signing keys, JWT signing keys, anything that should be private. Classify every key as public-safe or secret-only: publishable Stripe keys, Supabase anon keys, and other documented browser keys may live in the frontend; service-role keys, Stripe secret keys, OpenAI/Anthropic keys, database URLs, webhook secrets, OAuth client secrets, and signing keys must stay server-side only. For each finding, tell me: file, line, what kind of secret it appears to be, and which service it's for. Then refactor every secret-only finding to read from environment variables with clear names (OPENAI_API_KEY, STRIPE_SECRET_KEY, DATABASE_URL, etc.), and update any .env.example or docs to list the required variables without values. Also audit runtime leakage paths: any console.log / logger call that prints req.body, req.headers, full user objects, OAuth callback params, or error objects containing tokens; any Sentry (or equivalent) breadcrumbs that capture request bodies or auth headers without scrubbing; any TODO/debug comments that quote a real key. List every secret that needs to be ROTATED at the source service because it was previously committed, shared, logged, screenshotted, or pasted into chat. Never print actual secret values back to me \u2014 only references and filenames.",
419
421
  what: "Every app you build talks to other services \u2014 Stripe for payments, OpenAI for AI, your database for data. Each of those gives you a long secret string (an API key) that proves it's you. If those strings are sitting inside your code, anyone who looks at your code can use them. That includes anyone you accidentally share a screenshot with, anyone you push to GitHub, and anyone who gets into your builder account.",
420
- why: `This is the #1 way vibe-coded apps get destroyed. One leaked OpenAI key can rack up thousands of dollars over a weekend. One leaked Stripe key can let someone refund every charge you've ever made. Every modern builder gives you a place to store these safely \u2014 usually called "Secrets," "Environment Variables," or ".env" \u2014 you just have to use it.`,
422
+ why: `This is the #1 way AI-built apps get destroyed. One leaked OpenAI key can rack up thousands of dollars over a weekend. One leaked Stripe key can let someone refund every charge you've ever made. Every modern builder gives you a place to store these safely \u2014 usually called "Secrets," "Environment Variables," or ".env" \u2014 you just have to use it.`,
421
423
  steps: [
422
424
  "Find the Secrets / Environment Variables panel in your builder (usually a lock or key icon in the sidebar). Replit calls it Secrets, Vercel calls them Environment Variables, Bolt calls it .env, etc.",
423
425
  'Search your code for anything that looks like a long random string \u2014 things starting with "sk-", "AIza", or any variable named "API_KEY", "SECRET", "TOKEN", or "PASSWORD".',
426
+ "Separate public keys from secret keys. Publishable Stripe keys and Supabase anon keys can be browser-visible when provider docs say so; service-role keys, secret keys, AI keys, database URLs, and signing secrets cannot.",
424
427
  "Run a git-history scan, not just a search through current files. Tools like gitleaks (free, one command: `gitleaks detect`) check every commit you've ever made \u2014 including ones where you 'removed' the key by editing the file. A removed key still lives in history forever, and bots scrape public GitHub for these patterns within minutes of a push.",
425
428
  'For each one, add it to Secrets / env vars with a clear name (like OPENAI_API_KEY), then ask your AI builder: "replace the hardcoded OPENAI_API_KEY in my code with the environment variable."',
426
429
  "Test that the app still works after the swap.",
@@ -429,6 +432,8 @@ var ITEMS_CRITICAL_A = [
429
432
  redFlags: [
430
433
  "You can search your project files and find an actual API key sitting in plain text",
431
434
  "A database connection URL with the password right in it, in code",
435
+ "A VITE_ / NEXT_PUBLIC_ / EXPO_PUBLIC_ variable whose name includes SECRET, TOKEN, PASSWORD, PRIVATE, SERVICE_ROLE, or ADMIN",
436
+ "A Supabase service-role key, Stripe secret key, OpenAI key, Anthropic key, webhook secret, OAuth client secret, or database URL visible in frontend code",
432
437
  "You've pasted real keys into chats, screenshots, or AI prompts and never rotated them",
433
438
  "Your .env or secrets file is sitting in your GitHub history",
434
439
  "You've never run gitleaks (or any history scanner) \u2014 only searched current files"
@@ -437,8 +442,8 @@ var ITEMS_CRITICAL_A = [
437
442
  cliPrompt: {
438
443
  whatFailed: "Your codebase contains what looks like a hardcoded API key or password. The scanner found a string that matches a common provider's secret format (OpenAI, Anthropic, Stripe, AWS, GitHub, etc.) sitting directly in source code or a committed config file.",
439
444
  whyItBlocksLaunch: "A leaked production key is the single fastest way to lose money or expose user data. Public-repo secrets get scraped within minutes of being committed, and the bots that find them spin up paid API calls or exfiltrate database content before you notice. Even private repos leak \u2014 screenshots, accidental publishes, and shared chat threads all count.",
440
- fixInstructions: "Move every detected secret out of source into environment variables (OPENAI_API_KEY, STRIPE_SECRET_KEY, DATABASE_URL, etc.) and read them at runtime via process.env. Add the variable names to .env.example so future runs know what to set. Then ROTATE the original secret at the provider \u2014 the leaked value must be considered compromised. Update .gitignore to exclude .env, and run gitleaks across history to catch earlier commits.",
441
- aiBuilderPrompt: "Find every hardcoded secret in this project (API keys, tokens, OAuth client secrets, JWT signing keys, webhook secrets, database passwords). For each: tell me the file, the line, and what kind of secret it is. Then refactor each to read from process.env with a clear name (OPENAI_API_KEY, STRIPE_SECRET_KEY, etc.). Add every required variable to .env.example. Make sure .env is in .gitignore. List every secret value that needs to be ROTATED at the source service. Never print actual secret values back to me \u2014 only references.",
445
+ fixInstructions: "Move every detected secret-only value out of source into environment variables (OPENAI_API_KEY, STRIPE_SECRET_KEY, DATABASE_URL, etc.) and read them at runtime via process.env. Browser-visible public keys are allowed only when provider docs explicitly mark them publishable or anonymous; everything else moves behind a backend endpoint or serverless function. Add variable names to .env.example without values. Then ROTATE the original secret at the provider \u2014 the leaked value must be considered compromised. Update .gitignore to exclude .env, and run gitleaks across history to catch earlier commits.",
446
+ aiBuilderPrompt: "Find every hardcoded credential in this project (API keys, tokens, OAuth client secrets, JWT signing keys, webhook secrets, database passwords). For each: tell me the file, the line, what kind of credential it is, and whether provider docs say it is public-safe or secret-only. Refactor every secret-only credential to read from process.env with a clear name (OPENAI_API_KEY, STRIPE_SECRET_KEY, etc.) or move the action behind a backend endpoint. Add every required variable name to .env.example without values. Make sure .env is in .gitignore. List every secret value that needs to be ROTATED at the source service. Never print actual secret values back to me \u2014 only references.",
442
447
  verificationStep: "Run `npx shippingszn` again and confirm zero findings under `secrets`. Run `gitleaks detect --source . --verbose` to scan git history. Rotate any value that ever appeared in a commit, screenshot, or chat \u2014 even removing it from current code doesn't unrotate it."
443
448
  }
444
449
  },
@@ -449,11 +454,12 @@ var ITEMS_CRITICAL_A = [
449
454
  category: "Operations",
450
455
  priority: "critical",
451
456
  timeEstimate: "2 hr+",
452
- prompt: "Act as a senior backend engineer. Audit my app for every paid third-party API I call (OpenAI, Anthropic, Replicate, ElevenLabs, Twilio, image generation, transcription, anything per-request). For each one: (1) tell me where in the provider's dashboard to set a HARD spending cap (not just an alert) and what number to start with \u2014 recommend a sane default for an early-stage app; (2) add an in-app per-user daily and per-IP daily quota for that endpoint, returning a polite 429 when exceeded; (3) add a global kill-switch I can toggle via env var to instantly stop calls if costs spike; (4) log every paid call with user ID, route, token count or unit cost so I can see who is consuming what; (5) add a simple cost dashboard or weekly email summary. After you're done, tell me the absolute worst-case daily spend if my app got pounded by a bot.",
457
+ prompt: "Act as a senior backend engineer. Audit my app for every paid third-party API I call (OpenAI, Anthropic, Replicate, ElevenLabs, Twilio, Resend, SendGrid, scraping, search, image generation, transcription, anything per-request). For each one: (1) tell me where in the provider's dashboard to set a HARD spending cap (not just an alert) and what number to start with \u2014 recommend a sane default for an early-stage app; (2) set dashboard alerts at 50% and 80% of that cap where the provider supports alerts; (3) add an in-app per-user daily and per-IP daily quota for that endpoint, returning a polite 429 when exceeded; (4) add a global kill-switch I can toggle via env var to instantly stop calls if costs spike; (5) log every paid call with user ID, route, token count or unit cost so I can see who is consuming what; (6) add a simple cost dashboard or weekly email summary. After you're done, tell me the absolute worst-case daily spend if my app got pounded by a bot.",
453
458
  what: "Every AI API and paid third-party service charges per request. If your app calls OpenAI, Anthropic, Replicate, ElevenLabs, Twilio, or anything similar, an attacker (or a bug) can run those calls in a loop and turn your free trial into a four-figure bill overnight. Spend caps and per-user quotas are the seatbelts.",
454
- why: "This is the most underrated risk for vibe-coded apps in 2026. AI builders happily wire up an OpenAI key for you with no quotas. One infinite loop, one abusive script, or one curious user can rack up $1K\u2013$10K in a weekend. The provider will not refund it. Caps cost nothing and take 10 minutes to set.",
459
+ why: "This is the most underrated risk for AI-built apps in 2026. AI builders happily wire up an OpenAI key for you with no quotas. One infinite loop, one abusive script, or one curious user can rack up $1K\u2013$10K in a weekend. The provider will not refund it. Caps cost nothing and take 10 minutes to set.",
455
460
  steps: [
456
461
  "Log into every paid API dashboard (OpenAI, Anthropic, Replicate, etc.) and set a HARD monthly spending cap, not just a warning email. Start low \u2014 you can raise it.",
462
+ "Set dashboard alerts before the cap, at minimum 50% and 80%, so you hear about abuse before the provider shuts off the service or the bill lands.",
457
463
  "In your app, add per-user quotas: 'this user can make at most N AI requests per day.' Even logged-in users need this.",
458
464
  "Add per-IP rate limits on AI endpoints, separate from your normal API rate limits \u2014 much stricter.",
459
465
  "Add a global kill-switch (an env variable like AI_ENABLED=false) you can flip in 10 seconds if costs spike.",
@@ -461,6 +467,8 @@ var ITEMS_CRITICAL_A = [
461
467
  ],
462
468
  redFlags: [
463
469
  "No spending cap set in your AI provider's dashboard",
470
+ "Only a warning email exists \u2014 no hard cap, quota, or kill switch",
471
+ "No alert before the cap is hit",
464
472
  "No per-user limit on your AI features \u2014 anyone signed up can call them unlimited times",
465
473
  "AI endpoints exposed to logged-out users without strict per-IP limits",
466
474
  "You can't quickly answer 'what's the worst case my AI bill could be tomorrow?'",
@@ -475,7 +483,7 @@ var ITEMS_CRITICAL_A = [
475
483
  category: "Security",
476
484
  priority: "critical",
477
485
  timeEstimate: "2 hr+",
478
- prompt: "Act as a senior application security engineer. I want to replace any custom or partial authentication in this app with a battle-tested provider. First, recommend the best fit for my stack from: Clerk, Auth0, Supabase Auth, Stytch, or my platform's built-in auth \u2014 and explain why in one paragraph. Then implement it end-to-end: signup, login, logout, password reset, email verification, session management, and protected routes (both pages and API). Use the provider's recommended secure defaults. Add server-side checks on every protected route \u2014 never trust the frontend. Add login and OTP rate limiting (max 5 attempts per 15 minutes per IP+identifier). If email or SMS one-time codes are used, normalize phone numbers to E.164, make resend/cooldown behavior explicit, use anti-enumeration responses, write clear mobile copy, verify real code delivery, and add a recovery path if paid access depends on OTP. Set session expiry to 24h of inactivity. Migrate any existing user data safely. After you're done, give me a checklist of what I need to verify in the provider dashboard before launch.",
486
+ prompt: "Act as a senior application security engineer. I want to replace any custom or partial authentication in this app with a battle-tested provider. First, recommend the best fit for my stack from: Clerk, Auth0, Supabase Auth, Stytch, or my platform's built-in auth \u2014 and explain why in one paragraph. Then implement it end-to-end: signup, login, logout, password reset, email verification, session management, and protected routes (both pages and API). Use the provider's recommended secure defaults. Add server-side checks on every protected route \u2014 never trust the frontend. Add login and OTP rate limiting (max 5 attempts per 15 minutes per IP+identifier). If email or SMS one-time codes are used, normalize phone numbers to E.164, make resend/cooldown behavior explicit, use anti-enumeration responses, write clear mobile copy, verify real code delivery, handle double-clicked/expired verification links gracefully, and add a recovery path if paid access depends on OTP. Set session expiry to 24h of inactivity. Migrate any existing user data safely. After you're done, give me a checklist of what I need to verify in the provider dashboard before launch.",
479
487
  what: "How users sign in, receive verification codes, recover paid access, and stay signed in. Done well, only the real user can get into their account. Done badly, an attacker can guess codes, enumerate customers, abuse SMS/email delivery, steal sessions, or read passwords straight out of your database.",
480
488
  why: "If your login or OTP flow is weak, every other security thing you did doesn't matter \u2014 the attacker just walks in the front door or your paid users get locked out. The good news: you almost never need to build login from scratch. Use a proven provider and let shippingszn scan for the launch risks AI builders usually miss.",
481
489
  steps: [
@@ -483,6 +491,7 @@ var ITEMS_CRITICAL_A = [
483
491
  "Never store passwords directly. Real auth providers store a one-way scrambled version (a hash) so even they can't read it.",
484
492
  "Turn on rate limiting on login (max 5 wrong attempts in 15 minutes) so attackers can't sit there guessing forever.",
485
493
  "If you use email or SMS OTP, normalize phone numbers, add resend cooldown copy, use generic success-shaped start responses, and smoke a real delivered code before launch.",
494
+ "Make expired, reused, and double-clicked verification links land in a safe state instead of throwing a scary error or leaking account state.",
486
495
  "If paid report or purchase access depends on OTP, add a recovery path: alternate contact, receipt/support handoff, or purchase history.",
487
496
  "Set sessions to expire (12\u201324 hours is normal) so a stolen laptop doesn't mean a permanent account takeover.",
488
497
  "Offer two-factor authentication (2FA) if your auth provider supports it \u2014 most do, with one toggle."
@@ -496,6 +505,7 @@ var ITEMS_CRITICAL_A = [
496
505
  "SMS OTP compares raw phone strings instead of normalized E.164 numbers",
497
506
  "The OTP start screen reveals whether an email, phone, purchase, or report exists",
498
507
  "Paid users have no fallback when the email or SMS code does not arrive",
508
+ "Verification links break when clicked twice, opened on mobile, or opened after expiry",
499
509
  "Session cookies missing HttpOnly, Secure, or SameSite (check Application \u2192 Cookies in dev tools)"
500
510
  ],
501
511
  cliCoverage: "automated",
@@ -514,7 +524,7 @@ var ITEMS_CRITICAL_A = [
514
524
  category: "Security",
515
525
  priority: "critical",
516
526
  timeEstimate: "2 hr+",
517
- prompt: "Act as a security auditor. Audit my full app against the OWASP Top 10, focusing on: (1) SQL injection \u2014 find any place I'm building queries with string concatenation or user input that bypasses my ORM's parameterization; (2) NoSQL injection (Mongo, Firestore) \u2014 any place user input becomes a filter object without validation; (3) command injection \u2014 any place user input reaches a shell-invocation API in my language (Node shelling out, Python shell wrappers, Ruby backticks, etc.); (4) LDAP injection if I talk to a directory service; (5) XSS \u2014 find any place user input is rendered as HTML without escaping (innerHTML, raw HTML props, v-html, raw template interpolation, legacy doc-write APIs); (6) CSRF \u2014 find any state-changing endpoint without CSRF protection or SameSite cookies; (7) SSRF \u2014 find any place user input becomes a URL my server fetches; (8) insecure deserialization; (9) open redirects; (10) file upload handling \u2014 verify every upload path enforces a MIME allowlist, max size, path-traversal-safe filenames, and stores outside the web root (or on object storage with no public list); (11) verbose error responses \u2014 verify NODE_ENV (or equivalent) is 'production' in production and no stack trace, ORM dump, or absolute file path ever leaks to an end-user response body. For each finding: file, line, severity, plain-English explanation, exact fix. Apply the fixes after listing them. Then add a Content Security Policy header tuned to my actual asset sources \u2014 no wildcards, minimal unsafe-inline, justify any exception.",
527
+ prompt: "Act as a security auditor. Audit my full app against the OWASP Top 10, focusing on: (1) SQL injection \u2014 find any place I'm building queries with string concatenation or user input that bypasses my ORM's parameterization; (2) NoSQL injection (Mongo, Firestore) \u2014 any place user input becomes a filter object without validation; (3) command injection \u2014 any place user input reaches a shell-invocation API in my language (Node shelling out, Python shell wrappers, Ruby backticks, etc.); (4) LDAP injection if I talk to a directory service; (5) XSS \u2014 find any place user input is rendered as HTML without escaping (innerHTML, raw HTML props, v-html, raw template interpolation, legacy doc-write APIs); (6) CSRF \u2014 find any state-changing endpoint without CSRF protection or SameSite cookies; (7) SSRF \u2014 find any place user input becomes a URL my server fetches; (8) insecure deserialization; (9) open redirects; (10) file upload handling \u2014 verify every upload path enforces a MIME allowlist, max size, path-traversal-safe filenames, and stores outside the web root (or on object storage with no public list); (11) verbose error responses \u2014 verify NODE_ENV (or equivalent) is 'production' in production and no stack trace, ORM dump, SQL query, table name, column name, internal service URL, or absolute file path ever leaks to an end-user response body. For each finding: file, line, severity, plain-English explanation, exact fix. Apply the fixes after listing them. Then add a Content Security Policy header tuned to my actual asset sources \u2014 no wildcards, minimal unsafe-inline, justify any exception.",
518
528
  what: "There are a handful of attacks that bots constantly run against every site on the internet. They have ugly names \u2014 XSS, SQL injection, CSRF \u2014 but the idea is simple: they trick your app into running code or queries it shouldn't. Modern frameworks have built-in defenses; you just have to use them correctly.",
519
529
  why: "These bots don't care who you are. They scan the entire internet looking for sites that forgot to defend. If yours is one of those, your data ends up dumped on a forum, your users get hijacked, and you find out by reading about yourself online.",
520
530
  steps: [
@@ -522,19 +532,21 @@ var ITEMS_CRITICAL_A = [
522
532
  "Never paste user input directly into HTML you display to other users. Frameworks like React handle this safely by default \u2014 don't disable that behavior.",
523
533
  "Add a Content Security Policy header (your AI builder can set this up in one prompt) that tells browsers what code they're allowed to run.",
524
534
  "Use anti-CSRF tokens on forms that change data, if your auth provider doesn't already handle them.",
535
+ "Show generic errors to users and log detailed errors server-side. Users should never see SQL, table names, stack traces, service URLs, or raw exception text.",
525
536
  'Ask your AI builder: "audit my app for XSS, SQL injection, and CSRF vulnerabilities and fix any you find."'
526
537
  ],
527
538
  redFlags: [
528
539
  "Anywhere you're building SQL queries by gluing strings together",
529
540
  "Anywhere user input gets shown back to other users without going through the framework's safe rendering",
530
541
  "Your framework is showing security warnings in the console you've been ignoring",
531
- "Forms that change data (delete, update, transfer money) work fine when called from random other websites"
542
+ "Forms that change data (delete, update, transfer money) work fine when called from random other websites",
543
+ "Production errors show stack traces, table names, SQL snippets, ORM errors, absolute file paths, or internal service URLs"
532
544
  ],
533
545
  cliCoverage: "automated",
534
546
  cliPrompt: {
535
547
  whatFailed: "The scanner caught a code pattern that maps to a well-known automated attack: unsafe HTML rendering (XSS), wildcard CORS (any origin can call your API), runtime code-execution calls, or a SQL/NoSQL query built from string concatenation instead of parameterization.",
536
548
  whyItBlocksLaunch: "These are not theoretical. Bots scan the entire internet looking for sites that forgot to use the framework's safe defaults. Your app is one bot away from a stolen session, a hijacked admin route, or a customer-data dump on a forum. The fix is almost always a one-line change to use the framework's safe primitive instead of the raw one.",
537
- fixInstructions: "Replace unsafe HTML rendering APIs (innerHTML, raw HTML props, v-html, legacy doc-write APIs) with the framework's safe interpolation (React's default {}, Vue's {{ }}, etc.). Replace wildcard CORS (`*`) with an explicit allowlist of trusted origins. Eliminate runtime code-execution calls \u2014 there is almost always a parser or DSL that does the job safely. Use parameterized queries everywhere \u2014 never glue strings into SQL/NoSQL filters. Add a Content Security Policy header tuned to your actual asset origins.",
549
+ fixInstructions: "Replace unsafe HTML rendering APIs (innerHTML, raw HTML props, v-html, legacy doc-write APIs) with the framework's safe interpolation (React's default {}, Vue's {{ }}, etc.). Replace wildcard CORS (`*`) with an explicit allowlist of trusted origins. Eliminate runtime code-execution calls \u2014 there is almost always a parser or DSL that does the job safely. Use parameterized queries everywhere \u2014 never glue strings into SQL/NoSQL filters. Return generic user-facing errors and log detailed exceptions only server-side. Add a Content Security Policy header tuned to your actual asset origins.",
538
550
  aiBuilderPrompt: "Audit every finding in this scan tagged `common-attacks`. For each: explain in plain English what the attack is, show me the exact line, and apply the smallest safe fix \u2014 escape HTML rather than disabling sanitization, parameterize queries rather than string-glue, narrow CORS to the actual production origin(s), replace runtime code-execution calls with a typed parser. Also add a Content Security Policy header that allowlists ONLY my actual asset origins (no wildcards, minimal unsafe-inline, justify every exception). Show me the diff before applying.",
539
551
  verificationStep: "Re-run the scanner and confirm zero findings tagged `common-attacks`. In the deployed app, open dev-tools Network and confirm responses include a tight CSP header. Try posting a `<script>alert(1)</script>` payload anywhere user content is rendered \u2014 confirm it shows as text, not as an executed alert."
540
552
  }
@@ -554,7 +566,7 @@ var ITEMS_CRITICAL_B = [
554
566
  what: "HTTPS is the little padlock in the browser bar. It encrypts everything between your user and your server, so people sharing the same WiFi can't read passwords as they're being typed. Security headers are extra instructions you send to the browser saying \u201Cnever trust content claiming to be from me unless it really is.\u201D",
555
567
  why: "Without HTTPS, anyone on the same coffee-shop WiFi can read your users' passwords as plaintext. Without security headers, attackers can wrap your site inside theirs (clickjacking) or trick browsers into running malicious scripts. The fixes are basically free.",
556
568
  steps: [
557
- "Most modern hosting (Replit, Vercel, Netlify, Cloudflare Pages, Railway) gives you HTTPS automatically when you publish \u2014 confirm the lock icon shows up in the browser bar.",
569
+ "Most modern hosting gives you HTTPS automatically when you publish \u2014 confirm the lock icon shows up in the browser bar.",
558
570
  "Set up an automatic redirect from http:// to https:// so no one accidentally lands on the unencrypted version.",
559
571
  "Add a Content-Security-Policy header that locks down where your scripts, images, and fonts are allowed to come from.",
560
572
  "Add X-Content-Type-Options: nosniff and X-Frame-Options: DENY (or SAMEORIGIN if you embed your own pages).",
@@ -608,7 +620,7 @@ var ITEMS_CRITICAL_B = [
608
620
  priority: "critical",
609
621
  timeEstimate: "1 hr",
610
622
  prompt: "Act as a senior infrastructure engineer. Tell me, for my specific database setup, exactly: (1) is automatic point-in-time backup turned on, and how far back can I restore? (2) where the backups physically live and who can access them; (3) the EXACT click-by-click procedure to restore my database to a point 24 hours ago in a non-destructive way (clone first, swap if good, never restore in place blind). Then walk me through ACTUALLY DOING a test restore right now to a clone \u2014 not in theory, in practice \u2014 so I know it works before I need it. Also recommend an additional manual backup strategy (weekly export to object storage I control) so I'm not 100% dependent on my host. Output a one-page DISASTER_RECOVERY.md I can keep with my project.",
611
- what: "An automatic, recent copy of your entire database stored somewhere safe \u2014 and the ability to restore from it without panicking. Most managed databases (Neon, Supabase, Replit DB, RDS, PlanetScale) include some form of backup, but defaults vary, and 'a backup exists' is not the same thing as 'a backup that works.'",
623
+ what: "An automatic, recent copy of your entire database stored somewhere safe \u2014 and the ability to restore from it without guessing under pressure. Most managed databases (Neon, Supabase, Replit DB, RDS, PlanetScale) include some form of backup, but defaults vary, and 'a backup exists' is not the same thing as 'a backup that works.'",
612
624
  why: "Sooner or later you, your AI builder, or a script will run the wrong query against the production database. Without a tested backup, your only options are 'rebuild from memory' and 'apologize to users in public.' With one, it's a 10-minute fix. The whole point of doing this BEFORE launch is that nobody is depending on the data yet \u2014 so testing the restore is free.",
613
625
  steps: [
614
626
  "Open your database's dashboard (Neon, Supabase, Replit, etc.) and find the Backups or Point-in-time Recovery section. Confirm automatic backups are on, and note how many days you can restore back.",
@@ -636,13 +648,14 @@ var ITEMS_CRITICAL_B = [
636
648
  category: "Security",
637
649
  priority: "critical",
638
650
  timeEstimate: "2 hr+",
639
- prompt: "Act as a backend security engineer. Go through every API endpoint in my app and check two things for each: (1) authentication \u2014 is the user logged in? (2) authorization \u2014 is THIS user allowed to access THIS specific resource? (the IDOR check). Pay especially close attention to endpoints that use IDs from the URL like /users/:id or /orders/:id \u2014 these are the most commonly broken. Output a table per endpoint: route, method, requires login? (Y/N), checks resource ownership? (Y/N), risk level. Then fix every endpoint that's missing checks. Add input validation (Zod, Yup, or my framework's equivalent) to every endpoint \u2014 validate types, ranges, lengths, formats. Lock down CORS to my own domain \u2014 no wildcards in production. After fixes, give me 3 curl commands I can run to verify a logged-out user, a regular logged-in user, and another user's account all get blocked appropriately.",
651
+ prompt: "Act as a backend security engineer. Go through every API endpoint in my app and check four things for each: (1) authentication \u2014 is the user logged in? (2) authorization \u2014 is THIS user allowed to access THIS specific resource? (the IDOR check); (3) server-side validation \u2014 does the backend validate types, ranges, lengths, formats, and required fields before writing or calling paid APIs? (4) safe errors \u2014 does the route return generic user-facing errors while logging detailed errors server-side only? Pay especially close attention to endpoints that use IDs from the URL like /users/:id or /orders/:id \u2014 these are the most commonly broken. Output a table per endpoint: route, method, requires login? (Y/N), checks resource ownership? (Y/N), validates input server-side? (Y/N), leaks internal error detail? (Y/N), risk level. Then fix every endpoint that's missing checks. Add input validation (Zod, Yup, Valibot, Joi, or my framework's equivalent) to every endpoint \u2014 validate types, ranges, lengths, formats, and payload shape on the server, not only in React. Lock down CORS to my own production domain plus localhost/preview as needed \u2014 no wildcards in production. After fixes, give me 3 curl commands I can run to verify a logged-out user, a regular logged-in user, and another user's account all get blocked appropriately.",
640
652
  what: "Your app has a frontend (what users see) and a backend (the URLs the frontend calls to load and save data \u2014 these are called API endpoints). If those backend URLs aren't checking who's asking and what they're allowed to do, anyone with browser developer tools can call them directly and do whatever they want.",
641
- why: 'This is one of the most common silent disasters in vibe-coded apps: the frontend hides the "delete account" button from non-admins, but the backend lets anyone call /api/delete-account if they know the URL. The button is decoration; the backend check is the actual lock.',
653
+ why: 'This is one of the most common silent disasters in AI-built apps: the frontend hides the "delete account" button from non-admins, but the backend lets anyone call /api/delete-account if they know the URL. The button is decoration; the backend check is the actual lock.',
642
654
  steps: [
643
655
  'Every endpoint that touches user data should check: "is this person logged in?"',
644
656
  `Every endpoint that touches a specific user's data should also check: "is this person allowed to access THIS user's data?" This is the most commonly missed step.`,
645
- "Validate every input on the backend \u2014 don't trust the frontend to send you clean data.",
657
+ "Validate every input on the backend \u2014 don't trust the frontend to send clean data. Client-side Zod is UX; server-side validation is security.",
658
+ "Return generic errors to users and log detailed errors server-side. Never hand an attacker table names, SQL, stack traces, or schema details.",
646
659
  `Ask your AI builder: "go through every API endpoint in my project and tell me which ones don't check authentication or authorization, and fix them."`,
647
660
  "Set up CORS so only your own frontend can call your backend."
648
661
  ],
@@ -650,6 +663,8 @@ var ITEMS_CRITICAL_B = [
650
663
  "Admin functions you can call from a logged-out browser",
651
664
  "You can change the user ID in a URL (/api/users/123 \u2192 /api/users/124) and read someone else's data",
652
665
  "Your backend trusts whatever the frontend sends without re-checking it",
666
+ "Forms only validate in the browser, with no matching server-side schema",
667
+ "API responses expose SQL, table names, stack traces, ORM errors, or internal service URLs",
653
668
  "CORS is wide open (Access-Control-Allow-Origin: *) on a non-public API"
654
669
  ],
655
670
  cliCoverage: "automated"
@@ -687,12 +702,12 @@ var ITEMS_CRITICAL_B = [
687
702
  category: "Product & Launch",
688
703
  priority: "critical",
689
704
  timeEstimate: "1 hr",
690
- prompt: 'Act as a product engineer. Add /terms and /privacy pages to my app. IMPORTANT: do not invent legal language. Instead, generate a structured outline of every section that needs to exist in each page, customized to what my app actually does. Detect what to include by inspecting my dependencies and code \u2014 list every third-party service I integrate (Stripe, OpenAI, Google Analytics, Resend, Sentry, etc.) and note which ones need to be disclosed in the privacy policy. Output the outline as headings with a 1-sentence description of what each section should cover. Add a clear banner at the top of each page: "This is a starting outline. Get the actual legal language from a lawyer or a service like Termly, iubenda, or Termageddon." Then add the page routes, link them from the footer / signup / cookie banner, and include an "Effective date" field and a real contact email.',
705
+ prompt: 'Act as a product engineer. Add /terms and /privacy pages to my app. IMPORTANT: do not invent legal language. Instead, generate a structured outline of every section that needs to exist in each page, customized to what my app actually does. Detect what to include by inspecting my dependencies and code \u2014 list every third-party service I integrate (Stripe, OpenAI, Google Analytics, Resend, Sentry, Supabase, Firebase, Neon, Vercel, Cloudflare, etc.) and note which ones need to be disclosed in the privacy policy. Include a data-location section that points to the separate data-location inventory: where user data lives, what processors touch it, and which deletion/export path covers each one. Output the outline as headings with a 1-sentence description of what each section should cover. Add a clear banner at the top of each page: "This is a starting outline. Get the actual legal language from a lawyer or a service like Termly, iubenda, or Termageddon." Then add the page routes, link them from the footer / signup / cookie banner, and include an "Effective date" field and a real contact email.',
691
706
  what: "Two pages most apps need: Terms of Service (the rules of using your app) and a Privacy Policy (what data you collect and what you do with it). They are legal documents \u2014 the words matter, and they have to actually describe what your app does.",
692
707
  why: "These protect you from getting sued and protect your users from being misled. Most platforms (App Store, Google, Stripe, even Google sign-in) require them. Generated or copy-pasted policies that don't match your actual product are worse than nothing \u2014 they're evidence in a lawsuit.",
693
708
  steps: [
694
709
  "Do not have an AI write your final legal pages. Use a reputable template service (Termly, iubenda, Termageddon) or pay an actual attorney for a few hours.",
695
- "Tailor whatever template you use to match what your app actually does \u2014 every third-party service you use (analytics, AI, payments, email) probably needs to be mentioned.",
710
+ "Tailor whatever template you use to match what your app actually does \u2014 every third-party service you use (analytics, AI, payments, database, auth, hosting, email) probably needs to be mentioned.",
696
711
  "Create /terms and /privacy pages and link them from the footer, signup, and any place you collect data.",
697
712
  "Include an effective date and a real way to contact you.",
698
713
  "If you collect cookies or run analytics, add a cookie banner where required (especially in the EU and UK)."
@@ -702,7 +717,8 @@ var ITEMS_CRITICAL_B = [
702
717
  "AI-generated policies that talk about features your app doesn't have",
703
718
  "Pages copied from another company (with their company name still in there)",
704
719
  "No effective date, no contact info",
705
- "Your privacy policy doesn't mention services you actually use (Stripe, OpenAI, Google Analytics, etc.)"
720
+ "Your privacy policy doesn't mention services you actually use (Stripe, OpenAI, Google Analytics, etc.)",
721
+ "No one can say which provider or region stores user data"
706
722
  ],
707
723
  cliCoverage: "automated"
708
724
  },
@@ -714,8 +730,8 @@ var ITEMS_CRITICAL_B = [
714
730
  priority: "critical",
715
731
  timeEstimate: "2 hr+",
716
732
  prompt: "Act as a privacy-aware product engineer. Add two flows to my app: (1) Account deletion \u2014 a button in account settings that, when confirmed (with a second-step modal), permanently deletes the user, all their personal data, and all their content, with a 7-day grace period during which the account can be recovered. After 7 days, the deletion is hard. Email confirmation when initiated and when finalized. (2) Data export \u2014 a button that emails the user a downloadable JSON or CSV of all their personal data and content within 24 hours. Audit my schema and tell me which tables/columns count as 'personal data' and need to be included. Make sure the deletion respects foreign-key constraints and removes data from any third party I forward to (Stripe customer, email provider, analytics). Don't break referential integrity for OTHER users (e.g., comments by deleted user become 'deleted user' instead of cascading). Output a short DATA_RIGHTS.md describing what's deleted and what's retained for legal reasons.",
717
- what: "Two buttons in account settings: 'Download my data' and 'Delete my account.' One emails the user a copy of everything you have on them; the other actually removes them. Both are required almost everywhere personal data is regulated, and both are missing from almost every vibe-coded app.",
718
- why: "GDPR (EU), CCPA (California), and an expanding list of US state laws make these legally required if you have any users in those places \u2014 which you will, because the internet is global. Beyond legal: it's the right thing, it builds trust, and it costs basically nothing to add now versus a panicked weekend later when someone files a complaint.",
733
+ what: "Two buttons in account settings: 'Download my data' and 'Delete my account.' One emails the user a copy of everything you have on them; the other actually removes them. Both are required almost everywhere personal data is regulated, and both are missing from almost every AI-built app.",
734
+ why: "GDPR (EU), CCPA (California), and an expanding list of US state laws make these legally required if you have any users in those places \u2014 which you will, because the internet is global. Beyond legal: it's the right thing, it builds trust, and it costs basically nothing to add now versus a rushed weekend later when someone files a complaint.",
719
735
  steps: [
720
736
  "Add a 'Delete my account' button in account settings. Require a second confirmation step ('type DELETE to confirm') so it's not accidental.",
721
737
  "Implement a 7-day grace period: the account is disabled immediately, deleted permanently after 7 days. Email the user when each happens.",
@@ -769,7 +785,7 @@ var ITEMS_CRITICAL_B = [
769
785
  timeEstimate: "2 hr+",
770
786
  prompt: "Act as a payments engineer. Audit my app's entire paid flow before launch. First, identify whether I use Stripe, Lemon Squeezy, Paddle, RevenueCat, app-store payments, or something else. Then verify and fix: (1) checkout can be created only by the server, never with a client-supplied price or product; (2) the success page does NOT unlock paid access just because the URL says success; it checks the backend for a paid order; (3) webhooks verify the provider signature and persist paid status, receipt ID, amount, currency, and customer email; (4) every paid product, subscription, upgrade, cancellation, refund, and failed-payment state has a real user-facing path; (5) test-mode checkout, webhook delivery, receipt email, refund, and cancellation all work end-to-end. Output exact test-card steps, webhook setup steps, env vars, and the one thing I should click in the provider dashboard to prove money would actually move.",
771
787
  what: "If your app charges money, the payment flow is not 'done' when the checkout button opens. It is done when payment succeeds, your backend hears the signed webhook, paid access unlocks, the receipt lands, and cancellation/refund paths do not strand the user.",
772
- why: "This is where vibe-coded apps embarrass themselves fast. A founder launches, someone pays, the success page lies, the webhook never arrives, and now the first customer is both confused and charged. Or worse: a user edits a client-side price and buys the expensive thing for $0. Payments need a real dry run before launch day.",
788
+ why: "This is where AI-built apps embarrass themselves fast. A team launches, someone pays, the success page lies, the webhook never arrives, and now the first customer is both confused and charged. Or worse: a user edits a client-side price and buys the expensive thing for $0. Payments need a real dry run before launch day.",
773
789
  steps: [
774
790
  "Run a full test-mode checkout from a logged-out or brand-new account. Pay with the provider's test card and confirm the success page shows the right purchased thing.",
775
791
  "Verify paid access comes from your backend's recorded payment state, not from a success URL, localStorage flag, or client-side boolean.",
@@ -838,6 +854,114 @@ var ITEMS_CRITICAL_B = [
838
854
  ],
839
855
  cliCoverage: "manual_only",
840
856
  whyManual: "AI guardrails need scenario testing against real prompts, tool permissions, data boundaries, and destructive-action confirmations. Static code cannot prove the model behaves safely under adversarial input."
857
+ },
858
+ {
859
+ id: "database-access-rules",
860
+ number: 19,
861
+ title: "Prove every user can only read their own rows",
862
+ category: "Security",
863
+ priority: "critical",
864
+ timeEstimate: "2 hr+",
865
+ prompt: "Act as a database security engineer. Audit every table, collection, bucket, and document store that contains user data. First identify the backend: Supabase/Postgres, Firebase/Firestore, MongoDB, Prisma/Drizzle/SQL, object storage, or something else. Then verify the real access boundary: (1) Supabase/Postgres: Row Level Security is enabled on every user-scoped table and policies exist for select/insert/update/delete; service-role keys never reach the browser; policies are tested with two different users. (2) Firebase/Firestore: security rules deny by default and explicitly check auth.uid/resource ownership; rules are tested in the emulator. (3) SQL/ORM: every query that reads user data filters by the authenticated user or permission model server-side. (4) Storage: private files live in private buckets with signed URLs and ownership checks. Output a table: data surface, user-scoped? rule/policy present? cross-user test passed? risk. Then write the exact SQL/rules/tests needed to close gaps.",
866
+ what: "Database access rules are the lock underneath your UI. Your frontend can hide another user's records, but the database or API still has to refuse the request when someone changes an ID, opens DevTools, or calls the endpoint directly.",
867
+ why: "This is the classic AI-built app data breach: the app looks private, but Supabase RLS is off, Firebase rules allow broad reads, or the server query forgets `where user_id = currentUser.id`. That is not a hack; it is an unlocked door.",
868
+ steps: [
869
+ "List every user-scoped table, collection, storage bucket, and API route that returns saved user data.",
870
+ "For Supabase, enable RLS on every user-scoped table and write policies for select, insert, update, and delete. Do not ship with zero policies.",
871
+ "For Firebase/Firestore, make rules deny by default and allow reads/writes only when auth.uid owns the document or has an explicit role.",
872
+ "Test with two real test users: create data as User A, log in as User B, and try to read, edit, or delete User A's records through the app, the API, and any SDK/browser console path.",
873
+ "Keep privileged service-role/admin keys server-side only. If a browser bundle can use the key to bypass rules, the rules do not matter."
874
+ ],
875
+ redFlags: [
876
+ "Supabase table shows RLS disabled or zero policies",
877
+ "Firestore rules include broad allow read/write statements",
878
+ "The frontend hides records but backend/API queries do not check ownership",
879
+ "Service-role/admin/database credentials are present in browser code",
880
+ "No cross-user test has ever been run with two separate accounts"
881
+ ],
882
+ cliCoverage: "manual_only",
883
+ whyManual: "Database access rules must be proven against the real provider and two real users. Static code can spot hints, but it cannot prove Supabase RLS, Firebase rules, storage policies, and cross-user ownership tests all work in the deployed environment."
884
+ },
885
+ {
886
+ id: "auth-failure-cases",
887
+ number: 20,
888
+ title: "Test the auth failures attackers try first",
889
+ category: "Security",
890
+ priority: "critical",
891
+ timeEstimate: "30 min",
892
+ prompt: "Act as a QA/security engineer. Run the auth failure-case drill before launch, using production-similar auth settings and throwaway test accounts. Test: (1) wrong password or wrong OTP 5-6 times in a row \u2014 confirm rate limiting/lockout and generic copy; (2) password reset or magic-link start for an email that does not exist \u2014 confirm it looks the same as a real email path and does not reveal account existence; (3) verification or magic link clicked twice, opened after expiry, and opened on a different device \u2014 confirm it lands in a safe state with a recovery path; (4) signup with an already registered email or phone \u2014 confirm it does not leak account state beyond the provider's safe default; (5) login from mobile and desktop \u2014 confirm session/cookie behavior is correct. Output pass/fail evidence, screenshots or curl commands where useful, and the exact copy users see.",
893
+ what: "Happy-path auth testing proves almost nothing. The dangerous bugs are in the weird edges: wrong codes, fake emails, repeated attempts, duplicate signup, expired links, and users opening links on a different device.",
894
+ why: "Attackers probe the failure paths first because they reveal whether an account exists, whether a code can be brute-forced, and whether a user can get locked out. Paid users hit the same paths accidentally on launch day.",
895
+ steps: [
896
+ "Try the wrong password, magic code, or OTP 5-6 times. Confirm a real limit kicks in and the message stays generic.",
897
+ "Start password reset or magic-link login for an email that does not exist. The response should look like success, not 'account not found.'",
898
+ "Click the same verification/magic link twice and after expiry. It should recover gracefully, not show a stack trace or reveal internals.",
899
+ "Sign up with an email or phone that already exists. Confirm the provider's safe default copy and rate limits are used.",
900
+ "Record the exact pass/fail evidence in the launch report so nobody hand-waves auth as 'probably fine.'"
901
+ ],
902
+ redFlags: [
903
+ "Wrong password or OTP can be tried indefinitely",
904
+ "Password reset says whether an email exists",
905
+ "Verification links crash or leak internals when reused",
906
+ "Signup copy reveals registered users in a way attackers can enumerate",
907
+ "No one has tested auth failure states outside the happy path"
908
+ ],
909
+ cliCoverage: "manual_only",
910
+ whyManual: "Auth failure behavior requires a live auth provider, delivered emails/SMS, real browser sessions, and throwaway accounts. The scanner can flag code signals, but it cannot honestly click links, trigger provider lockouts, or verify copy in every failure state."
911
+ },
912
+ {
913
+ id: "data-location-inventory",
914
+ number: 21,
915
+ title: "Know where every piece of user data lives",
916
+ category: "Product & Launch",
917
+ priority: "critical",
918
+ timeEstimate: "1 hr",
919
+ prompt: "Act as a privacy and infrastructure operator. Build a data-location inventory for this app before launch. Inspect the code, dependencies, env var names, docs, provider dashboards, and signup/payment/email/auth flows. Output a table with: data type collected, source form/route, primary store, provider, region/location if available, third parties that receive it, retention period, deletion/export path, and policy disclosure needed. Include email providers, auth providers, analytics, error monitoring, payment processors, AI providers, file storage, databases, logs, and support tools. If a provider dashboard is needed to confirm region, say exactly which dashboard page must be checked. Do not invent regions \u2014 mark unknown until verified.",
920
+ what: "The moment you collect an email, payment, file, prompt, or user profile, you need to know where it goes. 'It is somewhere in Supabase' or 'Vercel handles it' is not enough when a user asks for deletion or a platform asks for your privacy details.",
921
+ why: "Legal pages only protect you if they match reality. If your app sends user data to Stripe, OpenAI, Resend, Sentry, PostHog, Supabase, Firebase, Neon, or a support inbox, those processors and regions have to be known before launch.",
922
+ steps: [
923
+ "List every data collection point: signup, checkout, waitlist, contact form, uploaded file, AI prompt, analytics event, error log, support request.",
924
+ "For each one, write where it is stored first, which third parties receive it, and how long it is kept.",
925
+ "Open provider dashboards and record region/location where available: database, hosting, auth, email, analytics, AI, payments, storage, logs.",
926
+ "Tie every data store to a deletion/export path so account deletion is not just deleting one database row.",
927
+ "Update the privacy policy outline so it names the real categories of data, processors, retention, and deletion/export contact."
928
+ ],
929
+ redFlags: [
930
+ 'You cannot answer "where does a new user email go?"',
931
+ "Privacy policy names fewer processors than the code actually uses",
932
+ "No one knows the database/auth/storage region",
933
+ "Error monitoring or analytics receives personal data without disclosure",
934
+ "Account deletion only touches the app database and ignores Stripe, email, analytics, logs, or support tools"
935
+ ],
936
+ cliCoverage: "manual_only",
937
+ whyManual: "Data location depends on live provider dashboard settings, regions, processors, and retention choices. Static repo scanning can infer likely services, but it cannot prove where provider-held data physically lives or whether privacy disclosures match reality."
938
+ },
939
+ {
940
+ id: "public-form-abuse",
941
+ number: 22,
942
+ title: "Block bots from public forms before launch",
943
+ category: "Security",
944
+ priority: "critical",
945
+ timeEstimate: "1 hr",
946
+ prompt: "Act as an abuse-prevention engineer. Find every public form or unauthenticated write surface: signup, login/OTP send, password reset, waitlist, contact, feedback, newsletter, invite request, file upload, AI demo, support, and checkout-start. For each one, decide whether it needs CAPTCHA/Turnstile, rate limiting, email/phone verification, honeypot fields, server-side validation, or all of the above. Prefer Cloudflare Turnstile for public marketing/contact/waitlist forms because it is free and privacy-friendly. Add the provider secret server-side only, verify tokens on the backend before accepting the submission, keep localhost/dev bypasses development-only, and add abuse logging. Then test with a missing token, invalid token, repeated submissions, and a real browser pass.",
947
+ what: "Public forms are doors bots can walk through without an account. If they can submit freely, they will fill your waitlist with spam, burn SMS/email credits, scrape paid APIs, or bury real leads under junk.",
948
+ why: "A polished launch can still get wrecked by a public form with no bot guard. The app looks fine until the first traffic spike turns into 500 spam contacts, SMS pumping, or an AI endpoint bill.",
949
+ steps: [
950
+ "Inventory every unauthenticated form or POST route, not just the contact form.",
951
+ "Put CAPTCHA or Cloudflare Turnstile on public marketing/contact/waitlist/feedback forms that accept arbitrary submissions.",
952
+ "Keep rate limits on every public write endpoint even when CAPTCHA is present. CAPTCHA slows bots; rate limits cap damage.",
953
+ "Verify bot tokens server-side before writing to the database, sending email/SMS, or calling a paid API.",
954
+ "Test missing-token, invalid-token, repeated-submit, and real-browser success paths before launch."
955
+ ],
956
+ redFlags: [
957
+ "Contact, waitlist, feedback, signup, or OTP forms accept unlimited public submissions",
958
+ "CAPTCHA token is checked only in the frontend",
959
+ "CAPTCHA is present but the endpoint still writes data when the token is missing",
960
+ "No rate limit backs up the CAPTCHA",
961
+ "Development/test bypass can run in production"
962
+ ],
963
+ cliCoverage: "manual_only",
964
+ whyManual: "Bot protection has to be verified against live public forms and provider token validation. The scanner can look for rate-limit or CAPTCHA libraries, but it cannot prove every unauthenticated form rejects missing tokens and survives repeated submissions."
841
965
  }
842
966
  ];
843
967
 
@@ -851,7 +975,7 @@ var ITEMS_CRITICAL = [
851
975
  var ITEMS_HIGH = [
852
976
  {
853
977
  id: "error-monitoring",
854
- number: 19,
978
+ number: 23,
855
979
  title: "Get notified the moment something breaks",
856
980
  category: "Operations",
857
981
  priority: "high",
@@ -876,7 +1000,7 @@ var ITEMS_HIGH = [
876
1000
  },
877
1001
  {
878
1002
  id: "uptime-monitoring",
879
- number: 20,
1003
+ number: 24,
880
1004
  title: "Get pinged the moment your app goes completely down",
881
1005
  category: "Operations",
882
1006
  priority: "high",
@@ -903,17 +1027,18 @@ var ITEMS_HIGH = [
903
1027
  },
904
1028
  {
905
1029
  id: "rate-limiting",
906
- number: 21,
1030
+ number: 25,
907
1031
  title: "Cap how often someone can hit your app",
908
1032
  category: "Security",
909
1033
  priority: "high",
910
1034
  timeEstimate: "1 hr",
911
- prompt: "Act as a backend security engineer. Add rate limiting to every public-facing endpoint. Use a sensible default (60 req/min/IP) for read endpoints, and tighter limits for sensitive ones: login (5 per 15 min per IP+email), signup (3 per hour per IP), password reset (3 per hour per email), AI/expensive endpoints (whatever fits a daily budget \u2014 ask me what I'm willing to spend per day). Return a clean 429 response with a Retry-After header and a friendly JSON message. Log every rate-limit hit with IP and route so I can see attacks. Use my framework's recommended middleware (express-rate-limit, hono-rate-limit, etc.). Recommend in-memory vs Redis backing based on whether I'm running multiple instances. After you're done, give me a curl one-liner I can run to verify it actually blocks me after N attempts.",
1035
+ prompt: "Act as a backend security engineer. Add rate limiting to every public-facing endpoint. Use a sensible default (60 req/min/IP) for normal public reads, around 100 req/min/IP for low-risk public utility endpoints, and tighter limits for sensitive ones: login (5 per 15 min per IP+email), signup (3 per hour per IP), password reset (3 per hour per email), OTP send (3-5 per hour per identifier), public forms (strict enough to stop spam), and AI/expensive endpoints (whatever fits a daily budget \u2014 ask me what I'm willing to spend per day). Return a clean 429 response with a Retry-After header and a friendly JSON message. Log every rate-limit hit with IP and route so I can see attacks. Use my framework's recommended middleware (express-rate-limit, hono-rate-limit, @upstash/ratelimit, Arcjet, etc.). Recommend in-memory vs Redis/Upstash backing based on whether I'm running multiple instances. After you're done, give me a curl one-liner I can run to verify it actually blocks me after N attempts.",
912
1036
  what: "Rate limiting puts a maximum on how many requests one person (or one IP address) can make in a given window \u2014 say, 60 requests a minute. Without it, a single bot can hammer your backend until it falls over, run up your AI bill, or brute-force passwords until something works.",
913
1037
  why: "Without rate limits you are one bad actor away from a $10,000 OpenAI bill, a crashed server, or a leaked password. Rate limits are cheap to add and save you from a long list of nightmares.",
914
1038
  steps: [
915
1039
  "Add rate limiting to every public-facing endpoint.",
916
1040
  "Use stricter limits on the sensitive ones: login (5 per 15 minutes), signup (3 per hour), password reset (3 per hour), AI calls (whatever fits your budget).",
1041
+ "Use a shared backing store such as Redis/Upstash when the app runs more than one instance. In-memory limits reset per instance.",
917
1042
  `Return a clear "you're going too fast, try again in X seconds" message \u2014 don't just silently fail.`,
918
1043
  "Watch for repeated rate-limit hits \u2014 they're usually attacks. Have alerts set up for spikes.",
919
1044
  'Ask your AI builder: "add per-IP rate limiting to all my endpoints with stricter limits on login, signup, and password reset."'
@@ -922,6 +1047,7 @@ var ITEMS_HIGH = [
922
1047
  "No rate limits at all",
923
1048
  "Same limits everywhere (login should be way stricter than browsing)",
924
1049
  "Auth endpoints (login, signup, password reset) not strictly capped \u2014 brute-force and credential-stuffing bots will find you",
1050
+ "Rate limits are in-memory even though production runs multiple instances",
925
1051
  "No alerting when someone repeatedly hits the limit",
926
1052
  "You can't see how often this is happening"
927
1053
  ],
@@ -929,12 +1055,12 @@ var ITEMS_HIGH = [
929
1055
  },
930
1056
  {
931
1057
  id: "dependency-audit",
932
- number: 22,
1058
+ number: 26,
933
1059
  title: "Patch your dependencies for known vulnerabilities",
934
1060
  category: "Security",
935
1061
  priority: "high",
936
1062
  timeEstimate: "30 min",
937
- prompt: "Act as a senior security engineer. Audit my project's installed dependencies for known vulnerabilities AND for supply-chain hygiene, and apply fixes. Tasks: (1) run the right auditor for my package manager \u2014 `npm audit` or `pnpm audit` for Node, `pip-audit` for Python, `bundle audit` for Ruby, `cargo audit` for Rust \u2014 and report the severity counts (critical / high / moderate / low). (2) for every Critical or High finding, try the auto-fix first (`npm audit fix`); if that doesn't clear it, upgrade the offending package manually and test the app after each change. Don't blindly `--force`. (3) if clearing a CVE needs a major-version bump that would break my app, document the CVE + the blocked upgrade + my exposure, then add a temporary mitigation if one's possible. (4) supply-chain hygiene \u2014 for every direct dependency, look up weekly download count and most-recent release date on the registry. Flag any dep with <1k weekly downloads (could be a typosquat \u2014 check the name against the obvious legitimate package) and any dep with no release in 12+ months (unmaintained \u2014 plan a replacement). (5) turn on automated dependency updates so this doesn't rot: Dependabot (free on GitHub) or Renovate \u2014 PR weekly for minor/patch, prompt on major. (6) add an audit step to my CI pipeline that fails the build on any new Critical CVE. When you're done, tell me plainly: any CVEs that made it to prod, any I deferred (with reasoning), any suspicious or abandoned deps I should swap, and the auto-update cadence you configured.",
1063
+ prompt: "Act as a senior security engineer. Audit my project's installed dependencies for known vulnerabilities AND for supply-chain hygiene, and apply fixes. Tasks: (1) run the right auditor for my package manager \u2014 `npm audit` or `pnpm audit` for Node, `pip-audit` for Python, `bundle audit` for Ruby, `cargo audit` for Rust \u2014 and report the severity counts (critical / high / moderate / low). (2) for every Critical or High finding, try the auto-fix first (`npm audit fix`); if that doesn't clear it, upgrade the offending package manually and test the app after each change. Don't blindly `--force`. (3) if clearing a CVE needs a major-version bump that would break my app, document the CVE + the blocked upgrade + my exposure, then add a temporary mitigation if one's possible. (4) run or configure the builder/platform's built-in dependency/security scanner too, then compare its findings against the package-manager audit so nothing is silently ignored. (5) supply-chain hygiene \u2014 for every direct dependency, look up weekly download count and most-recent release date on the registry. Flag any dep with <1k weekly downloads (could be a typosquat \u2014 check the name against the obvious legitimate package) and any dep with no release in 12+ months (unmaintained \u2014 plan a replacement). (6) turn on automated dependency updates so this doesn't rot: Dependabot (free on GitHub) or Renovate \u2014 PR weekly for minor/patch, prompt on major. (7) add an audit step to my CI pipeline that fails the build on any new Critical CVE. When you're done, tell me plainly: any CVEs that made it to prod, any I deferred (with reasoning), any suspicious or abandoned deps I should swap, and the auto-update cadence you configured.",
938
1064
  what: "Your app pulls in hundreds of third-party packages via `npm install` (or pip, bundle, cargo). Some of those packages have known security bugs with public write-ups and working exploits. Auditing means running one command to list every known bug in your dependencies, then upgrading to the fixed versions.",
939
1065
  why: "Most successful attacks on small apps aren't clever \u2014 they're automated scanners finding sites that ship an old version of a popular library with a published CVE. The fix is usually a one-command upgrade. Skipping this is handing attackers the easiest version of your app.",
940
1066
  steps: [
@@ -942,12 +1068,14 @@ var ITEMS_HIGH = [
942
1068
  "For every Critical or High finding, try the auto-fix first (e.g. `npm audit fix`). Test that the app still works after each fix.",
943
1069
  "For fixes that require a major-version bump, read the package's upgrade notes before updating \u2014 breaking changes are real.",
944
1070
  "If a CVE has no fix yet, at least know you have it. Document it and subscribe to the package's security advisories.",
1071
+ "Check your builder or hosting scanner output too. If Cursor, Claude Code, Lovable, Replit, GitHub, Snyk, or Semgrep flags dependency risk, reconcile it with the package audit.",
945
1072
  "Turn on Dependabot or Renovate in your GitHub repo so it opens PRs as new versions ship \u2014 you're not manually checking anymore.",
946
1073
  "Add the audit command to your CI pipeline so shipping a new Critical CVE breaks the build."
947
1074
  ],
948
1075
  redFlags: [
949
1076
  "You have never run `npm audit` (or your language's equivalent) on this project",
950
1077
  "The audit shows Critical severity \u2014 and you shipped anyway",
1078
+ "Your builder or GitHub security tab shows warnings you have not read",
951
1079
  "Dependabot / Renovate is not turned on",
952
1080
  "Your lockfile hasn't been touched in more than 6 months",
953
1081
  "CI never fails on a new CVE \u2014 you'll only find out from a bug report or a breach"
@@ -957,7 +1085,7 @@ var ITEMS_HIGH = [
957
1085
  },
958
1086
  {
959
1087
  id: "logging",
960
- number: 23,
1088
+ number: 27,
961
1089
  title: "Keep a paper trail of what your app is doing",
962
1090
  category: "Operations",
963
1091
  priority: "high",
@@ -983,7 +1111,7 @@ var ITEMS_HIGH = [
983
1111
  },
984
1112
  {
985
1113
  id: "session-management",
986
- number: 24,
1114
+ number: 28,
987
1115
  title: "Make sessions feel safe AND convenient",
988
1116
  category: "Security",
989
1117
  priority: "high",
@@ -1008,7 +1136,7 @@ var ITEMS_HIGH = [
1008
1136
  },
1009
1137
  {
1010
1138
  id: "github",
1011
- number: 25,
1139
+ number: 29,
1012
1140
  title: "Connect to GitHub for backups and history",
1013
1141
  category: "Infrastructure",
1014
1142
  priority: "high",
@@ -1033,19 +1161,19 @@ var ITEMS_HIGH = [
1033
1161
  },
1034
1162
  {
1035
1163
  id: "rollback",
1036
- number: 26,
1164
+ number: 30,
1037
1165
  title: "Know how to roll back a bad deploy in under a minute",
1038
1166
  category: "Operations",
1039
1167
  priority: "high",
1040
1168
  timeEstimate: "15 min",
1041
1169
  prompt: "Act as a deployment engineer. For my specific hosting platform, walk me through the EXACT click-by-click procedure to roll back to the previous deploy. Then have me actually do it once \u2014 to a previous commit, then forward again \u2014 so I know the muscle memory before I need it. Also: (1) tell me what's preserved during a rollback (env variables, secrets, data) and what isn't (any DB migration that ran on the bad deploy is NOT undone \u2014 call this out); (2) recommend whether I should enable preview deployments on every PR/branch so I can test changes before they hit production; (3) give me a 5-line emergency runbook (ROLLBACK.md) I can paste into my repo: 'If production is broken, do these 3 things in this order.'",
1042
- what: "The ability to undo a deployment in 30 seconds and get back to the last known good version. Almost every modern host (Replit, Vercel, Netlify, Railway, Render, Fly) supports one-click rollback to any prior version \u2014 but you need to know where the button is BEFORE the bad deploy.",
1043
- why: "You will ship something broken to production. Your AI builder will help you 'fix' something at midnight and the fix will be worse. The difference between a 30-second outage and a 3-hour panic is whether you've practiced rolling back once when nothing was wrong.",
1170
+ what: "The ability to undo a deployment in 30 seconds and get back to the last known good version. Almost every modern host supports one-click rollback to any prior version \u2014 but you need to know where the button is BEFORE the bad deploy.",
1171
+ why: "You will ship something broken to production. Your AI builder will help you 'fix' something at midnight and the fix will be worse. The difference between a 30-second outage and a 3-hour incident is whether you've practiced rolling back once when nothing was wrong.",
1044
1172
  steps: [
1045
1173
  "Find the Deployments or Releases panel in your hosting platform (most have 'Promote to production' or 'Rollback' next to each version).",
1046
1174
  "Do a practice rollback NOW, while everything is fine. Roll back one version, confirm the site still works, then roll forward again. You want this in muscle memory.",
1047
1175
  "Understand what rollback does NOT undo: any database migration that ran on the bad version is still applied \u2014 your code is rolled back but your schema isn't. Plan accordingly (migrations should be backwards compatible).",
1048
- "Turn on preview deployments (Vercel, Netlify, Railway all do this) so every change gets a temporary URL you can test before it touches production.",
1176
+ "Turn on preview deployments so every change gets a temporary URL you can test before it touches production.",
1049
1177
  "Write a 5-line ROLLBACK.md in your repo: '1. Open hosting dashboard. 2. Find latest known-good version. 3. Click Rollback. 4. Verify site works. 5. Tell users in status page / Twitter what happened.'"
1050
1178
  ],
1051
1179
  redFlags: [
@@ -1053,14 +1181,14 @@ var ITEMS_HIGH = [
1053
1181
  "You've never actually performed a rollback even once",
1054
1182
  "All your changes go straight to production without a preview deployment",
1055
1183
  "Database migrations run automatically and are not reversible",
1056
- "No written runbook \u2014 at 2am you'll be improvising in panic"
1184
+ "No written runbook \u2014 at 2am you'll be improvising under pressure"
1057
1185
  ],
1058
1186
  cliCoverage: "manual_only",
1059
1187
  whyManual: "Rollback readiness is muscle memory in the hosting dashboard plus a real recovery path for data migrations. The scanner cannot prove you practiced rollback and forward again."
1060
1188
  },
1061
1189
  {
1062
1190
  id: "soft-launch",
1063
- number: 27,
1191
+ number: 31,
1064
1192
  title: "Soft-launch to 5 friends before you launch publicly",
1065
1193
  category: "Product & Launch",
1066
1194
  priority: "high",
@@ -1087,7 +1215,7 @@ var ITEMS_HIGH = [
1087
1215
  },
1088
1216
  {
1089
1217
  id: "interviews",
1090
- number: 28,
1218
+ number: 32,
1091
1219
  title: "Talk to your first 10 users on a real call",
1092
1220
  category: "Growth",
1093
1221
  priority: "high",
@@ -1114,7 +1242,7 @@ var ITEMS_HIGH = [
1114
1242
  },
1115
1243
  {
1116
1244
  id: "onboarding",
1117
- number: 29,
1245
+ number: 33,
1118
1246
  title: "Make the first 5 minutes obvious",
1119
1247
  category: "Product & Launch",
1120
1248
  priority: "high",
@@ -1145,7 +1273,7 @@ var ITEMS_HIGH = [
1145
1273
  var ITEMS_MEDIUM = [
1146
1274
  {
1147
1275
  id: "analytics",
1148
- number: 30,
1276
+ number: 34,
1149
1277
  title: "Set up basic usage tracking",
1150
1278
  category: "Operations",
1151
1279
  priority: "medium",
@@ -1171,7 +1299,7 @@ var ITEMS_MEDIUM = [
1171
1299
  },
1172
1300
  {
1173
1301
  id: "db-performance",
1174
- number: 31,
1302
+ number: 35,
1175
1303
  title: "Make your database queries fast",
1176
1304
  category: "Infrastructure",
1177
1305
  priority: "medium",
@@ -1197,7 +1325,7 @@ var ITEMS_MEDIUM = [
1197
1325
  },
1198
1326
  {
1199
1327
  id: "custom-domain",
1200
- number: 32,
1328
+ number: 36,
1201
1329
  title: "Get your own domain (yourname.com)",
1202
1330
  category: "Infrastructure",
1203
1331
  priority: "medium",
@@ -1229,7 +1357,7 @@ var ITEMS_MEDIUM = [
1229
1357
  },
1230
1358
  {
1231
1359
  id: "email",
1232
- number: 33,
1360
+ number: 37,
1233
1361
  title: "Make app emails actually arrive",
1234
1362
  category: "Infrastructure",
1235
1363
  priority: "medium",
@@ -1257,7 +1385,7 @@ var ITEMS_MEDIUM = [
1257
1385
  },
1258
1386
  {
1259
1387
  id: "mobile",
1260
- number: 34,
1388
+ number: 38,
1261
1389
  title: "Make it not suck on phones",
1262
1390
  category: "Product & Launch",
1263
1391
  priority: "medium",
@@ -1284,7 +1412,7 @@ var ITEMS_MEDIUM = [
1284
1412
  },
1285
1413
  {
1286
1414
  id: "performance",
1287
- number: 35,
1415
+ number: 39,
1288
1416
  title: "Make it fast",
1289
1417
  category: "Product & Launch",
1290
1418
  priority: "medium",
@@ -1310,7 +1438,7 @@ var ITEMS_MEDIUM = [
1310
1438
  },
1311
1439
  {
1312
1440
  id: "accessibility",
1313
- number: 36,
1441
+ number: 40,
1314
1442
  title: "Make it usable for everyone, including people with disabilities",
1315
1443
  category: "Product & Launch",
1316
1444
  priority: "medium",
@@ -1337,12 +1465,12 @@ var ITEMS_MEDIUM = [
1337
1465
  },
1338
1466
  {
1339
1467
  id: "seo",
1340
- number: 37,
1468
+ number: 41,
1341
1469
  title: "Set up the basics so search and social shares work",
1342
1470
  category: "Growth",
1343
1471
  priority: "medium",
1344
1472
  timeEstimate: "1 hr",
1345
- prompt: "Act as a technical SEO engineer. Add complete meta tags to every page. For each route, generate a unique <title> (\u226460 chars, includes brand) and meta description (\u2264160 chars, action-oriented and not stuffed with keywords). Add full Open Graph tags (og:title, og:description, og:image with absolute URL, og:url, og:type, og:site_name) and Twitter Card tags (summary_large_image variant with the same image). Add a canonical link per page. If my framework supports it (Next, Remix, Nuxt, SvelteKit, etc.), use the framework's metadata API; otherwise inject into <head>. Generate /sitemap.xml and /robots.txt using the real production domain (ask me what it is). When done, give me 3 URLs from my site I can paste into opengraph.xyz to verify previews look right.",
1473
+ prompt: "Act as a technical SEO engineer. Add complete meta tags to every page. For each route, generate a unique <title> (\u226460 chars, includes brand) and meta description (\u2264160 chars, action-oriented and not stuffed with keywords). Add full Open Graph tags (og:title, og:description, og:image with absolute URL, og:url, og:type, og:site_name) and Twitter Card tags (summary_large_image variant with the same image). Add a canonical link per page. If my framework supports it (Next, Remix, Nuxt, SvelteKit, etc.), use the framework's metadata API; otherwise inject into <head>. Generate /sitemap.xml and /robots.txt using the real production domain (ask me what it is). Do not claim Google has indexed the site from this work alone; Search Console submission and URL Inspection are a separate launch closeout step. When done, give me 3 URLs from my site I can paste into opengraph.xyz to verify previews look right.",
1346
1474
  what: "Tiny bits of metadata in your HTML that tell Google what each page is about, and tell Twitter, LinkedIn, Slack, and iMessage how to display a preview when someone shares your link.",
1347
1475
  why: "These are your free billboards. A page with no title gets no clicks in Google. A link in iMessage with no preview image looks broken or sketchy. You only have to do this once per page.",
1348
1476
  steps: [
@@ -1350,6 +1478,7 @@ var ITEMS_MEDIUM = [
1350
1478
  "Add a unique meta description (under 160 characters) \u2014 this is what shows under your link in Google.",
1351
1479
  "Add Open Graph tags (og:title, og:description, og:image, og:url) \u2014 these power link previews in iMessage, Slack, LinkedIn.",
1352
1480
  "Add Twitter Card tags (twitter:card = summary_large_image plus title/description/image) for Twitter/X previews.",
1481
+ "Make sure robots.txt points at the real production sitemap and important pages are not accidentally noindexed.",
1353
1482
  "Test how your link looks BEFORE you announce it: opengraph.xyz, metatags.io, or just paste it into a Slack channel and see."
1354
1483
  ],
1355
1484
  redFlags: [
@@ -1357,13 +1486,43 @@ var ITEMS_MEDIUM = [
1357
1486
  "No meta descriptions (Google generates random snippets for you)",
1358
1487
  "Sharing your link shows no preview image",
1359
1488
  "Preview image is broken or shows a default builder-supplied placeholder",
1360
- "No canonical URL set (causes duplicate-content issues in search)"
1489
+ "No canonical URL set (causes duplicate-content issues in search)",
1490
+ "Saying the site is indexed when you have only generated sitemap and robots files"
1361
1491
  ],
1362
1492
  cliCoverage: "automated"
1363
1493
  },
1494
+ {
1495
+ id: "search-discovery",
1496
+ number: 42,
1497
+ title: "Submit and inspect your site before launch",
1498
+ category: "Growth",
1499
+ priority: "medium",
1500
+ timeEstimate: "30 min",
1501
+ prompt: "Act as a search discoverability engineer doing a production launch closeout. First confirm the real production domain and verify the deployed version is the one I intend to launch. Then verify /robots.txt returns 200, does not block public pages, and points to the production sitemap. Verify /sitemap.xml returns 200, uses absolute production URLs, and includes the homepage plus every important public page. Check important public pages for self-referential canonical URLs, unique titles and meta descriptions, internal links, and no accidental noindex tags or X-Robots-Tag headers. In Google Search Console, use the domain property if available, submit the production sitemap, use URL Inspection for the homepage and high-value changed URLs, and report indexed status, crawl allowed status, Google-selected canonical, user-declared canonical, and last crawl time. Request indexing for high-value new or changed pages when the UI allows it. Start validation for fixed Discovered - currently not indexed or Crawled - currently not indexed buckets when appropriate. For Bing/Microsoft, configure or use IndexNow in production only and submit the sitemap URLs. If Search Console or Bing access is missing, do not pretend this is complete: tell me exactly which property, sitemap URL, and page URLs to verify or submit.",
1502
+ what: "This is the handoff from 'my site has SEO files' to 'search engines have actually been told about the launch.' A sitemap sitting in your repo is not the same thing as Google knowing about the pages, crawling them, and deciding whether to index them.",
1503
+ why: "Prelaunch founders often announce a product and then discover that Google has not crawled the homepage, the new guide pages are only 'discovered,' or the sitemap was never submitted. That turns launch momentum into dead air. This check makes discovery status visible before you start sending people to the site.",
1504
+ steps: [
1505
+ "Use the real production domain, not localhost, preview, or a platform fallback URL.",
1506
+ "Verify robots.txt, sitemap.xml, canonical URLs, internal links, and noindex/X-Robots-Tag on the live production pages.",
1507
+ "Submit the production sitemap in Google Search Console for the domain property, preferably sc-domain:yourdomain.com.",
1508
+ "Inspect the homepage and high-value launch pages in Search Console. Record whether each page is live, crawlable, submitted, discovered, crawled, and indexed.",
1509
+ "Request indexing for high-value new or changed pages when Search Console allows it, and start validation for fixed indexing buckets when appropriate.",
1510
+ "Submit the production sitemap URLs to Bing/Microsoft with IndexNow. Do not submit preview or development URLs."
1511
+ ],
1512
+ redFlags: [
1513
+ "Assuming a page is indexed because it exists on production",
1514
+ "A sitemap exists, but Search Console has never downloaded it",
1515
+ "No verified Search Console property for the real production domain",
1516
+ "Important pages are live but orphaned from nav, footer, homepage, or related pages",
1517
+ "Preview or development domains are submitted to search engines",
1518
+ 'Final report says "indexed" when the page was only submitted or requested'
1519
+ ],
1520
+ cliCoverage: "manual_only",
1521
+ whyManual: "Search discovery requires authenticated Search Console/Bing access, production-domain checks, and third-party crawl/index state. The local scanner can verify files and page HTML, but it cannot honestly prove sitemap submission, URL Inspection status, manual indexing requests, or whether Google has indexed a page."
1522
+ },
1364
1523
  {
1365
1524
  id: "feedback",
1366
- number: 38,
1525
+ number: 43,
1367
1526
  title: "Give users an easy way to tell you what's broken",
1368
1527
  category: "Product & Launch",
1369
1528
  priority: "medium",
@@ -1390,7 +1549,7 @@ var ITEMS_MEDIUM = [
1390
1549
  },
1391
1550
  {
1392
1551
  id: "responsive-actions",
1393
- number: 39,
1552
+ number: 44,
1394
1553
  title: "Make every action tell the user what's happening",
1395
1554
  category: "Product & Launch",
1396
1555
  priority: "medium",
@@ -1417,7 +1576,7 @@ var ITEMS_MEDIUM = [
1417
1576
  },
1418
1577
  {
1419
1578
  id: "launch-polish",
1420
- number: 40,
1579
+ number: 45,
1421
1580
  title: "Walk through the whole app one last time",
1422
1581
  category: "Product & Launch",
1423
1582
  priority: "medium",
@@ -1445,7 +1604,7 @@ var ITEMS_MEDIUM = [
1445
1604
  },
1446
1605
  {
1447
1606
  id: "launch-list",
1448
- number: 41,
1607
+ number: 46,
1449
1608
  title: "Map exactly where your first 100 users will come from",
1450
1609
  category: "Growth",
1451
1610
  priority: "medium",
@@ -1472,14 +1631,14 @@ var ITEMS_MEDIUM = [
1472
1631
  },
1473
1632
  {
1474
1633
  id: "support",
1475
- number: 42,
1634
+ number: 47,
1476
1635
  title: "Give users a real way to get help",
1477
1636
  category: "Product & Launch",
1478
1637
  priority: "medium",
1479
1638
  timeEstimate: "1 hr",
1480
1639
  prompt: "Act as a customer support systems designer for a tiny early-stage app. Add a simple, real support path that a paying or stuck user can find in under 10 seconds. Requirements: (1) a support email or help link visible from footer, account/settings, checkout/success, receipts, and error states; (2) route support messages somewhere I actually monitor daily; (3) include enough context automatically when possible: user ID, email, current URL, order/report ID, browser, and last error; (4) create canned first replies for payment issue, login/code not arriving, data deletion/export, bug report, and refund request; (5) add a simple SUPPORT.md with where messages land, expected response time during launch week, and refund/escalation rules. Do not add a giant help center. Make the smallest support loop that prevents paying users from feeling abandoned.",
1481
- what: "Feedback is how users tell you what is broken. Support is how a real person gets help when they are stuck, locked out, charged, confused, or angry. For a vibe-coded launch, this can be one monitored email address \u2014 but it has to be visible and it has to work.",
1482
- why: "The worst early customer experience is not a bug. It is paying, getting stuck, and finding no human way out. A simple support path turns panic into a conversation and keeps small launch issues from becoming public complaints.",
1640
+ what: "Feedback is how users tell you what is broken. Support is how a real person gets help when they are stuck, locked out, charged, confused, or angry. For an AI-built launch, this can be one monitored email address \u2014 but it has to be visible and it has to work.",
1641
+ why: "The worst early customer experience is not a bug. It is paying, getting stuck, and finding no human way out. A simple support path turns a bad moment into a conversation and keeps small launch issues from becoming public complaints.",
1483
1642
  steps: [
1484
1643
  "Create one support destination you will actually watch during launch week: support@yourdomain.com, a shared inbox, Plain, Crisp, Intercom, HelpScout, or even a monitored Gmail alias.",
1485
1644
  "Put the support link where pain happens: footer, account/settings, checkout success, receipt email, error pages, login/OTP screens, and paid-report pages.",
@@ -1503,7 +1662,7 @@ var ITEMS_MEDIUM = [
1503
1662
  var ITEMS_LOWER = [
1504
1663
  {
1505
1664
  id: "aeo",
1506
- number: 43,
1665
+ number: 48,
1507
1666
  title: "Make AI assistants able to recommend you",
1508
1667
  category: "Growth",
1509
1668
  priority: "lower",
@@ -1533,7 +1692,7 @@ var ITEMS_LOWER = [
1533
1692
  },
1534
1693
  {
1535
1694
  id: "community",
1536
- number: 44,
1695
+ number: 49,
1537
1696
  title: "Start a small community space",
1538
1697
  category: "Growth",
1539
1698
  priority: "lower",
@@ -1560,7 +1719,7 @@ var ITEMS_LOWER = [
1560
1719
  },
1561
1720
  {
1562
1721
  id: "iteration",
1563
- number: 45,
1722
+ number: 50,
1564
1723
  title: "Plan what you'll improve in week 1",
1565
1724
  category: "Growth",
1566
1725
  priority: "lower",
@@ -1587,14 +1746,14 @@ var ITEMS_LOWER = [
1587
1746
  },
1588
1747
  {
1589
1748
  id: "installable-app",
1590
- number: 46,
1749
+ number: 51,
1591
1750
  title: "Make your app installable on phones",
1592
1751
  category: "Product & Launch",
1593
1752
  priority: "lower",
1594
1753
  timeEstimate: "30 min",
1595
1754
  prompt: "Act as a frontend engineer. Make my web app a proper Progressive Web App (PWA) so phone users can install it to their home screen. Tasks: (1) create a /manifest.json with name, short_name (\u226412 chars), start_url: '/', display: 'standalone', background_color, theme_color matching my brand, and an icons array with at least 192\xD7192 and 512\xD7512 PNGs (generate them from my logo if I don't have them yet \u2014 lossless, transparent background). (2) add <link rel='manifest' href='/manifest.json'> and <meta name='theme-color' content='#yourbrand'> to <head>. (3) generate a proper favicon bundle: favicon.ico (multi-size), favicon.svg, apple-touch-icon.png (180\xD7180), and wire them into <head> with <link rel='icon'> and <link rel='apple-touch-icon'>. (4) verify in Chrome DevTools \u2192 Application \u2192 Manifest that every field validates and the install prompt appears. (5) test on a real phone: open in Safari or Chrome, use Share \u2192 'Add to Home Screen', confirm the app launches full-screen without the browser chrome. Report any warnings from Lighthouse's PWA audit.",
1596
1755
  what: "A tiny /manifest.json plus proper-sized icons lets people add your site to their phone's home screen with one tap. When they launch it, it opens full-screen \u2014 no browser URL bar in the way \u2014 like a real app. Zero app-store submission needed.",
1597
- why: "Most vibe-coded sites stop at the default favicon and never get installed. Users who add your app to their home screen return two to three times more often than bookmark-only users \u2014 and it's a 30-minute setup to enable it forever. Also closes the subtle credibility gap of a generic browser favicon on your production URL.",
1756
+ why: "Most AI-built sites stop at the default favicon and never get installed. Users who add your app to their home screen return two to three times more often than bookmark-only users \u2014 and it's a 30-minute setup to enable it forever. Also closes the subtle credibility gap of a generic browser favicon on your production URL.",
1598
1757
  steps: [
1599
1758
  "Generate your icon set: at minimum a 512\xD7512 PNG, a 192\xD7192 PNG, a 180\xD7180 apple-touch-icon.png, and a favicon.ico or favicon.svg. Transparent backgrounds.",
1600
1759
  "Create a /manifest.json with your app name, short name (\u226412 chars), start URL, standalone display mode, theme color, and the icons.",
@@ -3883,6 +4042,24 @@ var SCORE_BANDS = [
3883
4042
  goNoGoLabel: "Go"
3884
4043
  }
3885
4044
  ];
4045
+ function scoreWithinBand(rawScore, band) {
4046
+ return Math.max(band.minScore, Math.min(band.maxScore, rawScore));
4047
+ }
4048
+ function severityBandedScore(rawScore, counts) {
4049
+ if (counts.critical > 0) {
4050
+ return Math.min(rawScore, SCORE_BANDS[0].maxScore);
4051
+ }
4052
+ if (counts.high > 0) {
4053
+ return scoreWithinBand(rawScore, SCORE_BANDS[1]);
4054
+ }
4055
+ if (counts.medium > 0) {
4056
+ return scoreWithinBand(rawScore, SCORE_BANDS[2]);
4057
+ }
4058
+ if (counts.lower > 0) {
4059
+ return scoreWithinBand(rawScore, SCORE_BANDS[3]);
4060
+ }
4061
+ return SCORE_BANDS[3].maxScore;
4062
+ }
3886
4063
  var SOURCE_SCORE_CAP = {
3887
4064
  url: 88,
3888
4065
  cli: 100,
@@ -4219,13 +4396,14 @@ function prioritizeLaunchFindings(findings, source = "manual", promptBuilder) {
4219
4396
  function scoreFromCounts(counts, source) {
4220
4397
  const weightedPenalty = counts.critical * SEVERITY_WEIGHTS.critical + counts.high * SEVERITY_WEIGHTS.high + counts.medium * SEVERITY_WEIGHTS.medium + counts.lower * SEVERITY_WEIGHTS.lower;
4221
4398
  const rawScore = clampScore(100 - weightedPenalty);
4399
+ const severityScore = severityBandedScore(rawScore, counts);
4222
4400
  const scoreCap = SOURCE_SCORE_CAP[source];
4223
- const score = Math.min(rawScore, scoreCap);
4401
+ const score = Math.min(severityScore, scoreCap);
4224
4402
  return {
4225
4403
  score,
4226
4404
  rawScore,
4227
4405
  weightedPenalty,
4228
- coveragePenalty: Math.max(0, rawScore - score)
4406
+ coveragePenalty: Math.max(0, severityScore - score)
4229
4407
  };
4230
4408
  }
4231
4409
  function scoreBandFor(score) {
@@ -4419,8 +4597,8 @@ function printHelp() {
4419
4597
  process.stdout.write(
4420
4598
  `shippingszn v${PKG_VERSION}
4421
4599
 
4422
- Read-only scanner that checks the current project against a small set of
4423
- high-signal launch-readiness items from shippingszn.
4600
+ Read-only launch inspector that checks the current project against high-signal
4601
+ launch-readiness items from shippingszn.
4424
4602
 
4425
4603
  Usage:
4426
4604
  npx shippingszn@latest [path] [options]
@@ -4436,14 +4614,14 @@ Options:
4436
4614
  -h, --help Show this help.
4437
4615
  -v, --version Print version.
4438
4616
 
4439
- This is the FREE preflight. It tells you launch debt exists. It does NOT tell
4617
+ This is the FREE inspection. It tells you launch risk exists. It does NOT tell
4440
4618
  you what's broken or how to fix it \u2014 that's gated behind the $49 Launch Fix Kit
4441
- (unlocks full findings + 46-item workbook + paste-ready AI-builder prompts +
4442
- verification steps + red-flag checks). Read-only on disk. Normal runs create a
4443
- locked scan handoff for checkout and send anonymous Wall stats (score, severity
4444
- counts, file count, scanner version \u2014 no project name, no paths, no
4445
- finding-level detail); there is no Wall publish opt-in or opt-out flag. Exit
4446
- code non-zero if any critical findings.
4619
+ (unlocks full findings + 51-item workbook + human ship decision + AI-builder
4620
+ punch list + verification steps + red-flag checks). Read-only on
4621
+ disk. Normal runs create a locked scan handoff for checkout and send anonymous
4622
+ Wall stats (score, severity counts, file count, scanner version \u2014 no project
4623
+ name, no paths, no finding-level detail); there is no Wall publish opt-in or
4624
+ opt-out flag. Exit code non-zero if any critical findings.
4447
4625
  `
4448
4626
  );
4449
4627
  }
@@ -4699,11 +4877,13 @@ ${c.bold("shippingszn")} ${c.dim(`v${PKG_VERSION}`)}
4699
4877
  process.stdout.write(c.dim(`Scanned ${files.length} files.
4700
4878
 
4701
4879
  `));
4880
+ process.stdout.write(`${c.bold("Verdict:")} ${bandColor(c.bold(bandLabel))}
4881
+ `);
4702
4882
  process.stdout.write(
4703
- `${c.bold("Launch Debt Score:")} ${launchReadiness.score}/100 \u2014 ${bandColor(c.bold(bandLabel))}
4704
-
4883
+ `${c.bold("Readiness Score:")} ${launchReadiness.score}/100
4705
4884
  `
4706
4885
  );
4886
+ process.stdout.write(c.dim("Higher is better.\n\n"));
4707
4887
  process.stdout.write(`${c.bold("Findings detected:")}
4708
4888
  `);
4709
4889
  process.stdout.write(` ${c.red(`${totals.critical} critical`)}
@@ -4746,19 +4926,19 @@ ${c.bold("shippingszn")} ${c.dim(`v${PKG_VERSION}`)}
4746
4926
  if (totals.high > 0) {
4747
4927
  process.stdout.write(
4748
4928
  c.dim(
4749
- "High and medium launch debt can expose trust gaps, break conversion moments, or make the project feel unfinished.\n\n"
4929
+ "High and medium launch gaps can expose trust gaps, break conversion moments, or make the project feel unfinished.\n\n"
4750
4930
  )
4751
4931
  );
4752
4932
  } else if (totals.medium > 0) {
4753
4933
  process.stdout.write(
4754
4934
  c.dim(
4755
- "Medium launch debt usually will not crash the app, but it can make users hesitate, miss conversion moments, or make the project feel unfinished.\n\n"
4935
+ "Medium launch gaps usually will not crash the app, but they can make users hesitate, miss conversion moments, or make the project feel unfinished.\n\n"
4756
4936
  )
4757
4937
  );
4758
4938
  } else {
4759
4939
  process.stdout.write(
4760
4940
  c.dim(
4761
- "Lower launch debt usually will not break the app, but it can still make the project feel rough or unfinished.\n\n"
4941
+ "Lower launch gaps usually will not break the app, but they can still make the project feel rough or unfinished.\n\n"
4762
4942
  )
4763
4943
  );
4764
4944
  }
@@ -4778,7 +4958,7 @@ ${c.bold("shippingszn")} ${c.dim(`v${PKG_VERSION}`)}
4778
4958
  `);
4779
4959
  process.stdout.write(
4780
4960
  c.dim(
4781
- ` Includes the ${totalFindings} specific ${pluralize(totalFindings, "finding")}, file evidence, and AI-builder fix prompts.
4961
+ ` Includes the ${totalFindings} specific ${pluralize(totalFindings, "finding")}, file evidence, human launch decision, and AI-builder punch list.
4782
4962
 
4783
4963
  `
4784
4964
  )
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shippingszn",
3
- "version": "0.9.6",
4
- "description": "Read-only CLI scanner that checks a project for common pre-launch issues from the shippingszn.com launch checklist.",
3
+ "version": "0.9.7",
4
+ "description": "Read-only launch inspector for AI-built apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "keywords": [
16
16
  "cli",
17
17
  "launch",
18
+ "launch-inspector",
18
19
  "checklist",
19
20
  "preflight",
20
21
  "security",