shippingszn 0.10.0 → 0.10.1
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/README.md +24 -14
- package/dist/index.js +145 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,11 +27,13 @@ will be; the Fix Kit is how you fix what it finds.
|
|
|
27
27
|
The CLI **never writes, modifies, or deletes** any files - it only reads. (The
|
|
28
28
|
one exception is a tiny first-run marker under your config dir,
|
|
29
29
|
`~/.config/shippingszn/seen`, used to show the telemetry notice once.) By
|
|
30
|
-
default each run
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
default each run makes two anonymous requests: a scan handoff for checkout that
|
|
31
|
+
carries finding-level detail (severity, checklist item, `file:line`, and a short
|
|
32
|
+
evidence snippet — secret values always redacted before upload), and an
|
|
33
|
+
aggregate Wall summary with score, severity counts, files scanned, scanner
|
|
34
|
+
version, and safe stack tags. Neither uploads full source files, repo URLs,
|
|
35
|
+
project names, unredacted secrets, handles, or emails. Pass `--no-telemetry` to
|
|
36
|
+
run fully offline (zero network calls). Details in [Telemetry](#telemetry).
|
|
35
37
|
|
|
36
38
|
**The free scan is the full diagnosis.** Human output prints a verdict, a
|
|
37
39
|
higher-is-better Readiness Score, severity counts, and **every finding grouped
|
|
@@ -76,15 +78,23 @@ exactly what's wrong; the Launch Fix Kit is how you fix it.
|
|
|
76
78
|
|
|
77
79
|
## Telemetry
|
|
78
80
|
|
|
79
|
-
Plain `npx shippingszn@latest`
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
Plain `npx shippingszn@latest` makes **two** anonymous requests per run:
|
|
82
|
+
|
|
83
|
+
1. **Scan handoff** (`POST /api/scan-results`) — powers the `/fix-kit` link the
|
|
84
|
+
CLI prints. It carries finding-level detail: each finding's severity, the
|
|
85
|
+
checklist item it maps to, its `file:line` location, and a short evidence
|
|
86
|
+
snippet from the matched line. Secret values are always redacted to a
|
|
87
|
+
`abc123…x9z2` form before upload. It does not include your repo URL, project
|
|
88
|
+
name, or full source files.
|
|
89
|
+
2. **Aggregate Wall summary** (`POST /api/wall`) — intentionally small: score,
|
|
90
|
+
launch label, files scanned, finding counts by severity, detected stack
|
|
91
|
+
tags, scanner version, and timestamp. No paths, no filenames, no
|
|
92
|
+
finding-level detail.
|
|
93
|
+
|
|
94
|
+
On the **first run on a machine**, the CLI prints a description of both requests
|
|
95
|
+
plus the exact aggregate payload (to stderr, so it never corrupts `--json`
|
|
96
|
+
output) and a note that you can turn it off. Telemetry is default-on but fully
|
|
97
|
+
transparent and opt-out-able:
|
|
88
98
|
|
|
89
99
|
```bash
|
|
90
100
|
npx shippingszn@latest --no-telemetry # zero network calls, fully offline
|
package/dist/index.js
CHANGED
|
@@ -99,22 +99,59 @@ var TEXT_EXT = /* @__PURE__ */ new Set([
|
|
|
99
99
|
var MAX_FILE_BYTES = 512 * 1024;
|
|
100
100
|
var MAX_DEPTH = 24;
|
|
101
101
|
var MAX_FILES = 5e4;
|
|
102
|
-
|
|
102
|
+
var ScanInputError = class extends Error {
|
|
103
|
+
constructor(message) {
|
|
104
|
+
super(message);
|
|
105
|
+
this.name = "ScanInputError";
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var ScanLimitError = class extends Error {
|
|
109
|
+
constructor(message) {
|
|
110
|
+
super(message);
|
|
111
|
+
this.name = "ScanLimitError";
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
async function listFiles(rootDir, options = {}) {
|
|
115
|
+
const maxFiles = options.maxFiles ?? MAX_FILES;
|
|
116
|
+
const maxDepth = options.maxDepth ?? MAX_DEPTH;
|
|
117
|
+
if (!Number.isSafeInteger(maxFiles) || maxFiles <= 0) {
|
|
118
|
+
throw new ScanInputError("maxFiles must be a positive safe integer");
|
|
119
|
+
}
|
|
120
|
+
if (!Number.isSafeInteger(maxDepth) || maxDepth < 0) {
|
|
121
|
+
throw new ScanInputError("maxDepth must be a non-negative safe integer");
|
|
122
|
+
}
|
|
123
|
+
let rootStat;
|
|
124
|
+
try {
|
|
125
|
+
rootStat = await fs.stat(rootDir);
|
|
126
|
+
} catch {
|
|
127
|
+
throw new ScanInputError(
|
|
128
|
+
`Scan target does not exist or cannot be read: ${rootDir}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (!rootStat.isDirectory()) {
|
|
132
|
+
throw new ScanInputError(`Scan target is not a directory: ${rootDir}`);
|
|
133
|
+
}
|
|
103
134
|
const out = [];
|
|
104
135
|
const visited = /* @__PURE__ */ new Set();
|
|
105
|
-
const rootResolved =
|
|
136
|
+
const rootResolved = await fs.realpath(rootDir);
|
|
137
|
+
visited.add(rootResolved);
|
|
106
138
|
const allowed = getNotIgnoredFiles(rootDir);
|
|
107
139
|
async function walk(dir, depth) {
|
|
108
|
-
if (depth >
|
|
109
|
-
|
|
140
|
+
if (depth > maxDepth) {
|
|
141
|
+
throw new ScanLimitError(
|
|
142
|
+
`Scan stopped: directory depth exceeds the ${maxDepth}-level safety limit.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
110
145
|
let entries;
|
|
111
146
|
try {
|
|
112
147
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
113
148
|
} catch {
|
|
149
|
+
if (depth === 0) {
|
|
150
|
+
throw new ScanInputError(`Scan target cannot be read: ${rootDir}`);
|
|
151
|
+
}
|
|
114
152
|
return;
|
|
115
153
|
}
|
|
116
154
|
for (const entry of entries) {
|
|
117
|
-
if (out.length >= MAX_FILES) return;
|
|
118
155
|
if (entry.name.startsWith(".") && DEFAULT_IGNORES.has(entry.name))
|
|
119
156
|
continue;
|
|
120
157
|
if (DEFAULT_IGNORES.has(entry.name)) continue;
|
|
@@ -153,6 +190,11 @@ async function listFiles(rootDir) {
|
|
|
153
190
|
} catch {
|
|
154
191
|
continue;
|
|
155
192
|
}
|
|
193
|
+
if (out.length >= maxFiles) {
|
|
194
|
+
throw new ScanLimitError(
|
|
195
|
+
`Scan stopped: project contains more than ${maxFiles.toLocaleString("en-US")} files, above the safety limit.`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
156
198
|
out.push({ absPath: abs, relPath: rel, size });
|
|
157
199
|
}
|
|
158
200
|
}
|
|
@@ -172,10 +214,27 @@ function isTextFile(file) {
|
|
|
172
214
|
}
|
|
173
215
|
async function readFileSafe(file) {
|
|
174
216
|
if (file.size > MAX_FILE_BYTES) return null;
|
|
217
|
+
let handle = null;
|
|
175
218
|
try {
|
|
176
|
-
|
|
219
|
+
handle = await fs.open(file.absPath, "r");
|
|
220
|
+
const buffer = Buffer.allocUnsafe(MAX_FILE_BYTES + 1);
|
|
221
|
+
let offset = 0;
|
|
222
|
+
while (offset < buffer.length) {
|
|
223
|
+
const { bytesRead } = await handle.read(
|
|
224
|
+
buffer,
|
|
225
|
+
offset,
|
|
226
|
+
buffer.length - offset,
|
|
227
|
+
offset
|
|
228
|
+
);
|
|
229
|
+
if (bytesRead === 0) break;
|
|
230
|
+
offset += bytesRead;
|
|
231
|
+
}
|
|
232
|
+
if (offset > MAX_FILE_BYTES) return null;
|
|
233
|
+
return buffer.subarray(0, offset).toString("utf8");
|
|
177
234
|
} catch {
|
|
178
235
|
return null;
|
|
236
|
+
} finally {
|
|
237
|
+
await handle?.close().catch(() => void 0);
|
|
179
238
|
}
|
|
180
239
|
}
|
|
181
240
|
async function fileExists(p) {
|
|
@@ -4189,6 +4248,30 @@ var PKG_VERSION = (() => {
|
|
|
4189
4248
|
const pkg = require2("../package.json");
|
|
4190
4249
|
return pkg.version;
|
|
4191
4250
|
})();
|
|
4251
|
+
var CliInputError = class extends Error {
|
|
4252
|
+
constructor(message) {
|
|
4253
|
+
super(message);
|
|
4254
|
+
this.name = "CliInputError";
|
|
4255
|
+
}
|
|
4256
|
+
};
|
|
4257
|
+
function normalizeBaseUrl(raw) {
|
|
4258
|
+
let parsed;
|
|
4259
|
+
try {
|
|
4260
|
+
parsed = new URL(raw);
|
|
4261
|
+
} catch {
|
|
4262
|
+
throw new CliInputError(`Invalid --base-url value: ${raw}`);
|
|
4263
|
+
}
|
|
4264
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
4265
|
+
throw new CliInputError("--base-url must use http:// or https://");
|
|
4266
|
+
}
|
|
4267
|
+
if (parsed.username || parsed.password) {
|
|
4268
|
+
throw new CliInputError("--base-url must not contain credentials");
|
|
4269
|
+
}
|
|
4270
|
+
parsed.hash = "";
|
|
4271
|
+
parsed.search = "";
|
|
4272
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, "") || "/";
|
|
4273
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
4274
|
+
}
|
|
4192
4275
|
function parseArgs(argv2) {
|
|
4193
4276
|
const opts = {
|
|
4194
4277
|
cwd: process.cwd(),
|
|
@@ -4200,8 +4283,32 @@ function parseArgs(argv2) {
|
|
|
4200
4283
|
proof: true,
|
|
4201
4284
|
telemetry: true
|
|
4202
4285
|
};
|
|
4286
|
+
let targetWasSet = false;
|
|
4287
|
+
let positionalOnly = false;
|
|
4288
|
+
const requireValue = (flag, index) => {
|
|
4289
|
+
const value = argv2[index + 1];
|
|
4290
|
+
if (!value || value.startsWith("-")) {
|
|
4291
|
+
throw new CliInputError(`${flag} requires a value`);
|
|
4292
|
+
}
|
|
4293
|
+
return value;
|
|
4294
|
+
};
|
|
4295
|
+
const setTarget = (raw) => {
|
|
4296
|
+
if (targetWasSet) {
|
|
4297
|
+
throw new CliInputError("Specify exactly one scan target");
|
|
4298
|
+
}
|
|
4299
|
+
opts.cwd = path21.resolve(raw);
|
|
4300
|
+
targetWasSet = true;
|
|
4301
|
+
};
|
|
4203
4302
|
for (let i = 0; i < argv2.length; i++) {
|
|
4204
4303
|
const a = argv2[i];
|
|
4304
|
+
if (positionalOnly) {
|
|
4305
|
+
setTarget(a);
|
|
4306
|
+
continue;
|
|
4307
|
+
}
|
|
4308
|
+
if (a === "--") {
|
|
4309
|
+
positionalOnly = true;
|
|
4310
|
+
continue;
|
|
4311
|
+
}
|
|
4205
4312
|
if (a === "--help" || a === "-h") opts.help = true;
|
|
4206
4313
|
else if (a === "--version" || a === "-v") opts.version = true;
|
|
4207
4314
|
else if (a === "--json") opts.json = true;
|
|
@@ -4209,10 +4316,19 @@ function parseArgs(argv2) {
|
|
|
4209
4316
|
else if (a === "--no-color") opts.noColor = true;
|
|
4210
4317
|
else if (a === "--no-telemetry" || a === "--no-wall" || a === "--no-publish")
|
|
4211
4318
|
opts.telemetry = false;
|
|
4212
|
-
else if (a === "--base-url")
|
|
4213
|
-
|
|
4214
|
-
|
|
4319
|
+
else if (a === "--base-url") {
|
|
4320
|
+
opts.baseUrl = requireValue(a, i);
|
|
4321
|
+
i += 1;
|
|
4322
|
+
} else if (a === "--cwd") {
|
|
4323
|
+
setTarget(requireValue(a, i));
|
|
4324
|
+
i += 1;
|
|
4325
|
+
} else if (a.startsWith("-")) {
|
|
4326
|
+
throw new CliInputError(`Unknown option: ${a}`);
|
|
4327
|
+
} else {
|
|
4328
|
+
setTarget(a);
|
|
4329
|
+
}
|
|
4215
4330
|
}
|
|
4331
|
+
opts.baseUrl = normalizeBaseUrl(opts.baseUrl);
|
|
4216
4332
|
return opts;
|
|
4217
4333
|
}
|
|
4218
4334
|
function color(enabled) {
|
|
@@ -4277,16 +4393,19 @@ function printTelemetryDisclosure(info) {
|
|
|
4277
4393
|
const stackList = stack.length ? stack.join(", ") : "(none detected)";
|
|
4278
4394
|
process.stderr.write(
|
|
4279
4395
|
`
|
|
4280
|
-
shippingszn sends
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4396
|
+
shippingszn sends two anonymous telemetry requests per run by default:
|
|
4397
|
+
1. A scan handoff (creates your /fix-kit link): each finding's severity,
|
|
4398
|
+
checklist item, file:line location, and a short evidence snippet from the
|
|
4399
|
+
matched line. Secret values are always redacted before upload.
|
|
4400
|
+
2. An aggregate Wall ping. This run's aggregate payload:
|
|
4401
|
+
- score: ${score}
|
|
4402
|
+
- severity counts: ${totals.critical} critical, ${totals.high} high, ${totals.medium} medium, ${totals.lower} lower
|
|
4403
|
+
- files scanned: ${filesScanned}
|
|
4404
|
+
- scanner version: ${scannerVersion}
|
|
4405
|
+
- stack tags: ${stackList}
|
|
4406
|
+
Neither request includes your repo URL, project name, full source files, or
|
|
4407
|
+
unredacted secret values. Disable both with --no-telemetry (the CLI then runs
|
|
4408
|
+
fully offline, zero network calls). This notice shows once per machine.
|
|
4290
4409
|
|
|
4291
4410
|
`
|
|
4292
4411
|
);
|
|
@@ -4320,11 +4439,13 @@ finding \u2014 severity, the checklist item it maps to, the file and line, and w
|
|
|
4320
4439
|
wrong \u2014 plus a 0-100 readiness score. The $49 Launch Fix Kit is the REMEDIATION
|
|
4321
4440
|
layer: per-finding fix instructions, prompts to paste straight into your AI
|
|
4322
4441
|
builder, the 58-item launch workbook, unlimited re-scans, and launch monitoring.
|
|
4323
|
-
Read-only on disk. By default each run sends
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4442
|
+
Read-only on disk. By default each run sends two anonymous requests: a scan
|
|
4443
|
+
handoff carrying finding-level detail (severity, checklist item, file:line, and
|
|
4444
|
+
a short evidence snippet \u2014 secret values always redacted) so checkout can carry
|
|
4445
|
+
this exact scan into the Fix Kit, and an aggregate Wall ping (score, severity
|
|
4446
|
+
counts, file count, scanner version, stack tags). Neither includes your repo
|
|
4447
|
+
URL, project name, or full source files. Pass --no-telemetry to disable both.
|
|
4448
|
+
Exit code non-zero if any critical findings.
|
|
4328
4449
|
`
|
|
4329
4450
|
);
|
|
4330
4451
|
}
|