shingan-lint 0.8.7 → 0.9.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 CHANGED
@@ -3,18 +3,26 @@
3
3
  > AI Agent Workflow Static Analyzer — `npx`-installable wrapper for [Shingan](https://github.com/hatyibei/shingan).
4
4
 
5
5
  ```bash
6
- # zero-install one-shot
7
- npx shingan-lint analyze --format adk-go --input ./agents
6
+ # 30-second smoke test — no input file needed
7
+ npx --yes shingan-lint demo
8
8
 
9
- # project-local
9
+ # Analyze your own JSON workflow
10
+ npx --yes shingan-lint analyze --input workflow.json --output markdown
11
+
12
+ # Analyze ADK-Go agents
13
+ npx --yes shingan-lint analyze --format adk-go --input ./agents/
14
+
15
+ # Project-local install
10
16
  pnpm add -D shingan-lint
11
- pnpm exec shingan analyze --format json --input ./testdata/buggy.json
17
+ pnpm exec shingan demo
12
18
 
13
- # global
19
+ # Global install
14
20
  npm install -g shingan-lint
15
- shingan analyze --since main
21
+ shingan demo
16
22
  ```
17
23
 
24
+ Run `shingan --help` (or `npx shingan-lint --help`) to see every command and flag with copy-paste examples.
25
+
18
26
  ## What it does
19
27
 
20
28
  Shingan detects 20 classes of design-time bugs in AI agent workflows **before they ship**:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shingan-lint",
3
- "version": "0.8.7",
3
+ "version": "0.9.1",
4
4
  "description": "AI Agent Workflow Static Analyzer — detect infinite loops, cost explosions, PII leaks, prompt injection sinks before execution. CLI wrapper that downloads the platform-specific Go binary from GitHub Releases.",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -42,7 +42,8 @@
42
42
  "shingan": "./bin/shingan.js"
43
43
  },
44
44
  "scripts": {
45
- "postinstall": "node ./scripts/postinstall.js"
45
+ "postinstall": "node ./scripts/postinstall.js",
46
+ "test": "node --test \"test/**/*.test.js\""
46
47
  },
47
48
  "files": [
48
49
  "bin/shingan.js",
@@ -4,9 +4,14 @@
4
4
  // checksums.txt sha256 entry, extracts it from the goreleaser tarball,
5
5
  // and installs it under ~/.cache/shingan-lint/v<version>/.
6
6
  //
7
- // Skipped silently when SHINGAN_SKIP_POSTINSTALL=1 (CI mirroring use
8
- // cases like air-gapped builds where the binary is provided
9
- // externally).
7
+ // Integrity is FAIL-CLOSED (#29): the install aborts (non-zero exit) on a
8
+ // checksum mismatch, a missing/unreachable checksums.txt, OR the archive
9
+ // being absent from checksums.txt. There is no "download but skip
10
+ // verification" path.
11
+ //
12
+ // The one intentional, explicit escape hatch is SHINGAN_SKIP_POSTINSTALL=1
13
+ // — it skips the download entirely (for air-gapped/CI-mirror builds where
14
+ // the binary is provided externally). It is opt-in and never the default.
10
15
 
11
16
  'use strict';
12
17
 
@@ -14,8 +19,10 @@ const fs = require('fs');
14
19
  const path = require('path');
15
20
  const os = require('os');
16
21
  const crypto = require('crypto');
17
- const { pipeline } = require('stream/promises');
18
- const tar = require('tar');
22
+ // NOTE: `tar` is require()d lazily inside extractTar(), not at module load.
23
+ // Extraction only happens AFTER the fail-closed checksum gate passes, so a
24
+ // verification failure never reaches the archive-handling code — and the
25
+ // regression test can require() this module without the `tar` dep present.
19
26
 
20
27
  const PACKAGE_VERSION = require('../package.json').version;
21
28
 
@@ -84,12 +91,41 @@ function findExpectedHash(checksumsText, archiveName) {
84
91
  return null;
85
92
  }
86
93
 
94
+ // verifyChecksum is the fail-CLOSED integrity gate (#29). It throws on
95
+ // *any* condition that prevents proving the downloaded archive is
96
+ // authentic — a sha256 mismatch, or the archive being absent from
97
+ // checksums.txt — so the only way past it is a genuine, matching hash.
98
+ // Earlier this logic warned-and-continued (fail-OPEN): a tampered
99
+ // binary, or a stripped/empty checksums.txt, would silently install.
100
+ // Pure + synchronous so it is unit-testable without any network.
101
+ function verifyChecksum(archiveBuf, archive, checksumsText) {
102
+ const expected = findExpectedHash(checksumsText, archive);
103
+ if (!expected) {
104
+ throw new Error(
105
+ `${archive} not found in checksums.txt — cannot verify integrity. ` +
106
+ `Refusing to install an unverifiable binary. ` +
107
+ `If the release is genuinely missing checksums and you accept the risk, ` +
108
+ `set SHINGAN_SKIP_POSTINSTALL=1 and install the binary yourself.`
109
+ );
110
+ }
111
+ const actual = sha256(archiveBuf);
112
+ if (actual !== expected) {
113
+ throw new Error(
114
+ `sha256 mismatch for ${archive}: expected ${expected}, got ${actual}. ` +
115
+ `The downloaded archive does NOT match the published checksum — ` +
116
+ `aborting install (possible tampering or a corrupted download).`
117
+ );
118
+ }
119
+ return expected;
120
+ }
121
+
87
122
  async function extractTar(buf, dest, tag) {
88
123
  // Write to temp file so `tar` can stream it back.
89
124
  const tmpFile = path.join(dest, `_archive.${tag.ext}`);
90
125
  fs.writeFileSync(tmpFile, buf);
91
126
  try {
92
127
  if (tag.ext === 'tar.gz') {
128
+ const tar = require('tar');
93
129
  await tar.x({ file: tmpFile, cwd: dest, strict: true });
94
130
  } else if (tag.ext === 'zip') {
95
131
  // Minimal zip extraction without an extra dependency: shell out.
@@ -145,25 +181,28 @@ async function main() {
145
181
  process.exit(1);
146
182
  }
147
183
 
148
- // Verify checksum if checksums.txt is reachable. Soft-fail when the
149
- // checksum file is missing (e.g. early-stage release without
150
- // goreleaser's checksum step), so users still get a usable binary
151
- // at the cost of trust on first install.
184
+ // Fail-CLOSED integrity check (#29). The checksums.txt file MUST be
185
+ // reachable and MUST contain a matching sha256 for this archive, or we
186
+ // abort the install. A checksum mismatch, a missing/unreachable
187
+ // checksums.txt, or the archive being absent from it all raise here and
188
+ // propagate to main().catch → exit(1). The documented escape hatch for
189
+ // air-gapped/dev environments is SHINGAN_SKIP_POSTINSTALL=1 (handled at
190
+ // the top of main()), which skips the download entirely — there is no
191
+ // "download but skip verification" mode by design.
192
+ let checksumsText;
152
193
  try {
153
- const checksumsText = await fetchText(checksumURL());
154
- const expected = findExpectedHash(checksumsText, archive);
155
- if (expected) {
156
- const actual = sha256(archiveBuf);
157
- if (actual !== expected) {
158
- throw new Error(`sha256 mismatch: expected ${expected}, got ${actual}`);
159
- }
160
- console.log(`shingan-lint: sha256 verified (${expected.slice(0, 12)}…)`);
161
- } else {
162
- console.warn(`shingan-lint: ${archive} not found in checksums.txt — proceeding without verification`);
163
- }
194
+ checksumsText = await fetchText(checksumURL());
164
195
  } catch (e) {
165
- console.warn(`shingan-lint: checksum verification skipped: ${e.message}`);
196
+ throw new Error(
197
+ `unable to fetch checksums.txt for integrity verification: ${e.message}. ` +
198
+ `Refusing to install an unverified binary. ` +
199
+ `If the release was just published, GitHub may need a few seconds to ` +
200
+ `propagate — retry shortly. For air-gapped installs set ` +
201
+ `SHINGAN_SKIP_POSTINSTALL=1 and provide the binary yourself.`
202
+ );
166
203
  }
204
+ const expected = verifyChecksum(archiveBuf, archive, checksumsText);
205
+ console.log(`shingan-lint: sha256 verified (${expected.slice(0, 12)}…)`);
167
206
 
168
207
  await extractTar(archiveBuf, dest, tag);
169
208
 
@@ -180,7 +219,15 @@ async function main() {
180
219
  console.log(`shingan-lint: installed ${tag.exe} → ${binPath}`);
181
220
  }
182
221
 
183
- main().catch((err) => {
184
- console.error(`shingan-lint: postinstall failed: ${err.stack || err.message}`);
185
- process.exit(1);
186
- });
222
+ // Export the pure helpers so the regression test (test/postinstall.test.js)
223
+ // can exercise the fail-closed checksum gate without any network.
224
+ module.exports = { verifyChecksum, findExpectedHash, sha256, main };
225
+
226
+ // Only auto-run the installer when invoked directly (`node postinstall.js`,
227
+ // i.e. as npm's postinstall hook). When `require()`d by the test, do nothing.
228
+ if (require.main === module) {
229
+ main().catch((err) => {
230
+ console.error(`shingan-lint: postinstall failed: ${err.stack || err.message}`);
231
+ process.exit(1);
232
+ });
233
+ }