shippingszn 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vibe Coder Launch Checklist contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # shippingszn
2
+
3
+ A small, read-only CLI that scans a project for common pre-launch issues from
4
+ the [shippingszn.com launch checklist](https://shippingszn.com). Run it
5
+ before you ship to catch obvious mistakes — leaked API keys, missing
6
+ `robots.txt`, no security headers, and so on — and get a friendly report
7
+ linking each finding back to the matching checklist item.
8
+
9
+ ```bash
10
+ npx shippingszn
11
+ # or
12
+ pnpm dlx shippingszn
13
+ ```
14
+
15
+ Run it inside any project root. The CLI **never writes, modifies, or deletes**
16
+ any files — it only reads. Everything stays on your machine.
17
+
18
+ ## What gets checked
19
+
20
+ The initial check set is intentionally small and high-signal. Each finding
21
+ maps back to one of the items on the checklist.
22
+
23
+ - Hardcoded API keys across many providers (OpenAI, Anthropic, Stripe, AWS,
24
+ Google, GitHub, Slack, private key blocks).
25
+ - `.env` present but not ignored in `.gitignore`, or `.env` present but no
26
+ `.env.example`.
27
+ - Missing `.gitignore`, `robots.txt`, `sitemap.xml`, or a custom favicon.
28
+ - Missing security-header middleware in common server configs.
29
+ - Dangerous code patterns: unsafe HTML injection in React, runtime
30
+ code-execution calls, wildcard CORS.
31
+ - Python: common debug-mode slip-ups, hardcoded framework secrets, missing
32
+ env-var loading.
33
+ - Ruby: unsafe string rendering, hardcoded Rails secrets.
34
+ - Go: `http.ListenAndServe` without TLS, hardcoded token / apiKey / secret
35
+ literals.
36
+ - Placeholder content (`lorem ipsum`, `John Doe`, `test@example.com`) and
37
+ `TODO` / `FIXME` / `XXX` / `HACK` comments.
38
+
39
+ Each finding is tagged Critical, High, Medium, or Lower and links back to the
40
+ relevant checklist item on shippingszn.com.
41
+
42
+ ## What does NOT get checked
43
+
44
+ These are deliberately out of scope for v1:
45
+
46
+ - Anything that requires running your app (no live HTTP probing, no auth
47
+ flows).
48
+ - Auto-fixing problems. The CLI is read-only.
49
+ - Deep static analysis or language-specific lints. Use ESLint, Semgrep, or
50
+ Snyk for that.
51
+ - Validating your actual third-party dashboards (Stripe spend caps, OpenAI
52
+ quotas, etc.).
53
+
54
+ A clean report is **not** a launch certificate — it just means none of the
55
+ obvious things tripped a tripwire. Walk through the full checklist before you
56
+ ship.
57
+
58
+ ## Usage
59
+
60
+ ```text
61
+ shippingszn [path] [options]
62
+
63
+ Options:
64
+ --json Output a machine-readable JSON report.
65
+ --base-url <url> Base URL used to build links back to checklist items.
66
+ --cwd <path> Directory to scan. Default: current working directory.
67
+ --no-color Disable ANSI colors in the human-readable report.
68
+ -h, --help Show help.
69
+ -v, --version Print version.
70
+ ```
71
+
72
+ ## Exit codes
73
+
74
+ - `0` — No critical findings.
75
+ - `1` — One or more critical findings detected.
76
+ - `2` — The scanner itself crashed.
77
+
78
+ This makes the CLI suitable for CI:
79
+
80
+ ```yaml
81
+ # .github/workflows/launch-check.yml
82
+ - run: npx shippingszn --json > launch-check.json
83
+ ```
84
+
85
+ ## Privacy
86
+
87
+ `shippingszn` reads files on your machine. It never uploads source code,
88
+ makes outbound network calls, or phones home. No telemetry. No accounts.
89
+ Inspect the source or audit `npm pack --dry-run` to confirm.
90
+
91
+ ## License
92
+
93
+ MIT. See [LICENSE](./LICENSE).
@@ -0,0 +1,58 @@
1
+ import * as path from "node:path";
2
+ import { isTextFile, readFileSafe } from "../scan.js";
3
+ import { findLine, isScanExempt, lineContainsIgnoreMarker, relPosix, } from "./helpers.js";
4
+ const DANGEROUS_PATTERNS = [
5
+ {
6
+ id: "dangerously-set-inner-html",
7
+ regex: /dangerouslySetInnerHTML/,
8
+ itemId: "common-attacks",
9
+ severity: "high",
10
+ message: "Use of dangerouslySetInnerHTML — make sure the content is sanitized or comes from a trusted source.",
11
+ },
12
+ {
13
+ id: "eval-call",
14
+ regex: /(^|[^A-Za-z0-9_$])eval\s*\(/,
15
+ itemId: "common-attacks",
16
+ severity: "high",
17
+ message: "Use of eval() — almost always avoidable and a common path to remote code execution if any input is user-controlled.",
18
+ },
19
+ {
20
+ id: "cors-wildcard",
21
+ regex: /Access-Control-Allow-Origin\s*[:=]\s*['"`]\*['"`]/i,
22
+ itemId: "common-attacks",
23
+ severity: "medium",
24
+ message: "Wildcard CORS (Access-Control-Allow-Origin: *). Lock this down to specific origins for any authenticated endpoint.",
25
+ },
26
+ ];
27
+ export async function checkDangerousPatterns(ctx) {
28
+ const findings = [];
29
+ for (const file of ctx.files) {
30
+ if (!isTextFile(file))
31
+ continue;
32
+ if (isScanExempt(file.relPath))
33
+ continue;
34
+ const ext = path.extname(file.relPath).toLowerCase();
35
+ if (![".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".html"].includes(ext))
36
+ continue;
37
+ const content = await readFileSafe(file);
38
+ if (!content)
39
+ continue;
40
+ for (const pat of DANGEROUS_PATTERNS) {
41
+ const m = pat.regex.exec(content);
42
+ if (!m)
43
+ continue;
44
+ if (lineContainsIgnoreMarker(content, m.index))
45
+ continue;
46
+ const line = findLine(content, m.index);
47
+ findings.push({
48
+ checkId: pat.id,
49
+ itemId: pat.itemId,
50
+ severity: pat.severity,
51
+ message: pat.message,
52
+ file: relPosix(file.relPath),
53
+ line,
54
+ });
55
+ }
56
+ }
57
+ return findings;
58
+ }
@@ -0,0 +1,59 @@
1
+ import * as path from "node:path";
2
+ import { fileExists, readFileSafe } from "../scan.js";
3
+ import { relPosix } from "./helpers.js";
4
+ export async function checkEnvCommitted(ctx) {
5
+ const findings = [];
6
+ const gitignorePath = path.join(ctx.rootDir, ".gitignore");
7
+ let gitignore = "";
8
+ if (await fileExists(gitignorePath)) {
9
+ gitignore = (await readFileSafe({ absPath: gitignorePath, relPath: ".gitignore", size: 0 })) ?? "";
10
+ }
11
+ const ignoresEnv = /^\s*\.env(\s|$)/m.test(gitignore) || /^\s*\*\.env(\s|$)/m.test(gitignore);
12
+ for (const file of ctx.files) {
13
+ const base = path.basename(file.relPath);
14
+ if (base !== ".env" && base !== ".env.local" && base !== ".env.production")
15
+ continue;
16
+ if (!ignoresEnv) {
17
+ findings.push({
18
+ checkId: "env-not-ignored",
19
+ itemId: "secrets",
20
+ severity: "high",
21
+ message: `${base} found and your .gitignore does not appear to ignore .env files.`,
22
+ file: relPosix(file.relPath),
23
+ });
24
+ }
25
+ }
26
+ return findings;
27
+ }
28
+ export async function checkEnvExample(ctx) {
29
+ const hasEnv = ctx.files.some((f) => path.basename(f.relPath) === ".env");
30
+ const hasExample = ctx.files.some((f) => {
31
+ const b = path.basename(f.relPath);
32
+ return b === ".env.example" || b === ".env.sample" || b === ".env.template";
33
+ });
34
+ if (hasEnv && !hasExample) {
35
+ return [
36
+ {
37
+ checkId: "missing-env-example",
38
+ itemId: "secrets",
39
+ severity: "medium",
40
+ message: "Found a .env file but no .env.example. Add a sanitized .env.example so collaborators know which variables are required.",
41
+ },
42
+ ];
43
+ }
44
+ return [];
45
+ }
46
+ export async function checkGitignore(ctx) {
47
+ const gitignorePath = path.join(ctx.rootDir, ".gitignore");
48
+ if (!(await fileExists(gitignorePath))) {
49
+ return [
50
+ {
51
+ checkId: "missing-gitignore",
52
+ itemId: "github",
53
+ severity: "high",
54
+ message: "No .gitignore at the project root. Add one tuned to your stack so you don't accidentally commit secrets, local DBs, or build artifacts.",
55
+ },
56
+ ];
57
+ }
58
+ return [];
59
+ }
@@ -0,0 +1,60 @@
1
+ import * as path from "node:path";
2
+ import { readFileSafe } from "../scan.js";
3
+ const SECURITY_HEADER_NAMES = [
4
+ "Strict-Transport-Security",
5
+ "Content-Security-Policy",
6
+ "X-Content-Type-Options",
7
+ "X-Frame-Options",
8
+ "Referrer-Policy",
9
+ ];
10
+ export async function checkSecurityHeaders(ctx) {
11
+ const candidateFiles = ctx.files.filter((f) => {
12
+ const rp = f.relPath.toLowerCase();
13
+ if (rp.endsWith(".test.ts") || rp.endsWith(".spec.ts"))
14
+ return false;
15
+ return (rp.endsWith("vite.config.ts") ||
16
+ rp.endsWith("vite.config.js") ||
17
+ rp.endsWith("next.config.js") ||
18
+ rp.endsWith("next.config.mjs") ||
19
+ rp.endsWith("next.config.ts") ||
20
+ rp.endsWith("nuxt.config.ts") ||
21
+ rp.endsWith("svelte.config.js") ||
22
+ rp.endsWith("astro.config.mjs") ||
23
+ rp.endsWith("astro.config.ts") ||
24
+ rp.endsWith("vercel.json") ||
25
+ rp.endsWith("netlify.toml") ||
26
+ /server\/index\.(t|j)s$/.test(rp) ||
27
+ /^server\.(t|j)s$/.test(path.basename(rp)) ||
28
+ /\bapp\.(t|j)s$/.test(rp) ||
29
+ /\bindex\.(t|j)s$/.test(rp));
30
+ });
31
+ if (candidateFiles.length === 0)
32
+ return [];
33
+ let foundAny = false;
34
+ for (const file of candidateFiles) {
35
+ const content = await readFileSafe(file);
36
+ if (!content)
37
+ continue;
38
+ for (const h of SECURITY_HEADER_NAMES) {
39
+ if (content.toLowerCase().includes(h.toLowerCase())) {
40
+ foundAny = true;
41
+ break;
42
+ }
43
+ }
44
+ if (content.includes("helmet(") || content.includes('require("helmet")') || content.includes('from "helmet"')) {
45
+ foundAny = true;
46
+ }
47
+ if (foundAny)
48
+ break;
49
+ }
50
+ if (foundAny)
51
+ return [];
52
+ return [
53
+ {
54
+ checkId: "missing-security-headers",
55
+ itemId: "https-headers",
56
+ severity: "high",
57
+ message: "Couldn't find any common security headers (CSP, HSTS, X-Frame-Options, etc.) or helmet() middleware in your server/host configs. Add them so the browser enforces baseline defenses.",
58
+ },
59
+ ];
60
+ }
@@ -0,0 +1,155 @@
1
+ import * as path from "node:path";
2
+ import { fileExists, isTextFile, readFileSafe, } from "../scan.js";
3
+ export function relPosix(p) {
4
+ return p.split(path.sep).join("/");
5
+ }
6
+ export function findLine(content, idx) {
7
+ let line = 1;
8
+ for (let i = 0; i < idx; i++)
9
+ if (content.charCodeAt(i) === 10)
10
+ line++;
11
+ return line;
12
+ }
13
+ /**
14
+ * Inline opt-out marker. Any line containing this token is exempt from
15
+ * substring/regex-based checks (placeholder content, dangerous patterns).
16
+ * Used by the scanner's own source to avoid matching its pattern definitions.
17
+ *
18
+ * The literal value is split across a concatenation so that grep'ing for the
19
+ * marker only finds the *uses*, not this definition.
20
+ */
21
+ export const IGNORE_MARKER = "shippingszn" + ":ignore";
22
+ export function lineContainsIgnoreMarker(content, charIndex) {
23
+ const lineStart = content.lastIndexOf("\n", charIndex - 1) + 1;
24
+ const lineEnd = content.indexOf("\n", charIndex);
25
+ const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
26
+ return line.includes(IGNORE_MARKER);
27
+ }
28
+ const PUBLIC_DIR_CANDIDATES = [
29
+ "public",
30
+ "static",
31
+ "www",
32
+ "dist",
33
+ "build",
34
+ "out",
35
+ ];
36
+ export async function findPublicDirs(ctx) {
37
+ const dirs = [];
38
+ for (const cand of PUBLIC_DIR_CANDIDATES) {
39
+ const p = path.join(ctx.rootDir, cand);
40
+ if (await fileExists(p))
41
+ dirs.push(cand);
42
+ }
43
+ // Also any artifact public dirs in monorepo style.
44
+ const seen = new Set(dirs);
45
+ for (const f of ctx.files) {
46
+ const parts = f.relPath.split("/");
47
+ for (let i = 0; i < parts.length - 1; i++) {
48
+ if (parts[i] === "public" || parts[i] === "static") {
49
+ const dir = parts.slice(0, i + 1).join("/");
50
+ if (!seen.has(dir)) {
51
+ seen.add(dir);
52
+ dirs.push(dir);
53
+ }
54
+ }
55
+ }
56
+ }
57
+ return dirs;
58
+ }
59
+ /**
60
+ * Narrow per-file exemption for substring/regex checks (placeholder
61
+ * content, dangerous patterns, language patterns).
62
+ *
63
+ * This deliberately does NOT exempt the CLI source or test trees as a
64
+ * whole — broad exemptions hide real bugs. It only lists files that
65
+ * define or document the patterns the scanner looks for, where a literal
66
+ * pattern in source is the file's whole purpose:
67
+ *
68
+ * - `tools/cli/src/checks/{dangerous,quality,language}.ts` define the
69
+ * regexes and human-readable messages, both of which contain the
70
+ * literal pattern strings.
71
+ * - `tools/cli/README.md` documents what the scanner detects, citing
72
+ * the pattern strings verbatim.
73
+ * - `tools/cli/test/fixtures/` contains intentional positive fixtures.
74
+ * - `artifacts/checklist/src/data/checklist/` is user-facing checklist
75
+ * copy that names the patterns by name (e.g. "look for TODO/FIXME"). shippingszn:ignore
76
+ *
77
+ * Secret scanning is NOT exempted from any of these paths — the secret
78
+ * regexes contain regex metacharacters in source and don't self-match,
79
+ * so real hardcoded secrets in the CLI source would still be caught.
80
+ *
81
+ * For one-off cases in normal source files, prefer the inline
82
+ * `shippingszn:ignore` marker (handled by lineContainsIgnoreMarker)
83
+ * instead of adding paths here.
84
+ */
85
+ const PATTERN_DEFINITION_FILES = new Set([
86
+ "tools/cli/src/checks/dangerous.ts",
87
+ "tools/cli/src/checks/quality.ts",
88
+ "tools/cli/src/checks/language.ts",
89
+ "tools/cli/README.md",
90
+ ]);
91
+ const PATTERN_DEFINITION_PREFIXES = [
92
+ "tools/cli/test/fixtures/",
93
+ "artifacts/checklist/src/data/checklist/",
94
+ ];
95
+ export function isScanExempt(relPath) {
96
+ const p = relPosix(relPath);
97
+ if (PATTERN_DEFINITION_FILES.has(p))
98
+ return true;
99
+ for (const prefix of PATTERN_DEFINITION_PREFIXES) {
100
+ if (p.startsWith(prefix) || p.includes("/" + prefix))
101
+ return true;
102
+ }
103
+ return false;
104
+ }
105
+ /**
106
+ * Does any source file in the project look like it dynamically emits or
107
+ * serves the given asset (e.g. `robots.txt`, `sitemap.xml`)? Used by
108
+ * `checkRobotsTxt` / `checkSitemapXml` to suppress false positives in
109
+ * projects that generate these at build time via a Vite plugin or serve
110
+ * them from an Express/Next route.
111
+ *
112
+ * Heuristic: a `.ts`/`.js`/`.mjs`/`.cjs` source file that mentions the
113
+ * asset filename as a string literal AND contains an emission/serve
114
+ * indicator (`emitFile`, `setHeader`, `res.end`, `res.send`,
115
+ * `configureServer`, `app.get`, `router.get`, `next/headers`).
116
+ */
117
+ const SERVE_INDICATORS = [
118
+ "emitFile",
119
+ "setHeader",
120
+ "res.end",
121
+ "res.send",
122
+ "configureServer",
123
+ "configurePreviewServer",
124
+ "app.get",
125
+ "app.use",
126
+ "router.get",
127
+ "router.use",
128
+ ];
129
+ const DYNAMIC_ASSET_SOURCE_EXTS = new Set([
130
+ ".ts",
131
+ ".tsx",
132
+ ".js",
133
+ ".jsx",
134
+ ".mjs",
135
+ ".cjs",
136
+ ]);
137
+ export async function isAssetEmittedDynamically(ctx, assetName) {
138
+ const literalPatterns = [`"${assetName}"`, `'${assetName}'`, `/${assetName}`];
139
+ for (const file of ctx.files) {
140
+ if (!isTextFile(file))
141
+ continue;
142
+ const ext = path.extname(file.relPath).toLowerCase();
143
+ if (!DYNAMIC_ASSET_SOURCE_EXTS.has(ext))
144
+ continue;
145
+ const content = await readFileSafe(file);
146
+ if (!content)
147
+ continue;
148
+ if (!literalPatterns.some((p) => content.includes(p)))
149
+ continue;
150
+ if (!SERVE_INDICATORS.some((s) => content.includes(s)))
151
+ continue;
152
+ return true;
153
+ }
154
+ return false;
155
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Public entry point for the scanner's check registry.
3
+ *
4
+ * Adding a new check: implement it in one of the per-domain modules
5
+ * (secrets, env, headers, dangerous, language, public-assets, quality)
6
+ * and append a `{ id, run }` entry to `ALL_CHECKS` below. The id is the
7
+ * stable identifier surfaced in CLI output and is what users would put
8
+ * in any future per-check disable list.
9
+ */
10
+ import { checkHardcodedSecrets, checkConfigSecretLeaks } from "./secrets.js";
11
+ import { checkEnvCommitted, checkEnvExample, checkGitignore } from "./env.js";
12
+ import { checkRobotsTxt, checkSitemapXml, checkFavicon } from "./public-assets.js";
13
+ import { checkSecurityHeaders } from "./headers.js";
14
+ import { checkDangerousPatterns } from "./dangerous.js";
15
+ import { checkLanguagePatterns, checkPythonSecretKeyEnv, checkRubySecretKeyBaseEnv, } from "./language.js";
16
+ import { checkPlaceholderContent } from "./quality.js";
17
+ export * from "./types.js";
18
+ export { checkHardcodedSecrets, checkConfigSecretLeaks, checkEnvCommitted, checkEnvExample, checkGitignore, checkRobotsTxt, checkSitemapXml, checkFavicon, checkSecurityHeaders, checkDangerousPatterns, checkLanguagePatterns, checkPythonSecretKeyEnv, checkRubySecretKeyBaseEnv, checkPlaceholderContent, };
19
+ export const ALL_CHECKS = [
20
+ { id: "hardcoded-secrets", run: checkHardcodedSecrets },
21
+ { id: "config-secret-leaks", run: checkConfigSecretLeaks },
22
+ { id: "env-committed", run: checkEnvCommitted },
23
+ { id: "env-example", run: checkEnvExample },
24
+ { id: "gitignore", run: checkGitignore },
25
+ { id: "robots-txt", run: checkRobotsTxt },
26
+ { id: "sitemap-xml", run: checkSitemapXml },
27
+ { id: "favicon", run: checkFavicon },
28
+ { id: "security-headers", run: checkSecurityHeaders },
29
+ { id: "dangerous-patterns", run: checkDangerousPatterns },
30
+ { id: "language-patterns", run: checkLanguagePatterns },
31
+ { id: "python-secret-key-env", run: checkPythonSecretKeyEnv },
32
+ { id: "ruby-secret-key-base-env", run: checkRubySecretKeyBaseEnv },
33
+ { id: "placeholder-content", run: checkPlaceholderContent },
34
+ ];