dsh-plugin-inspector 0.1.0 → 0.2.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/lib/source.js CHANGED
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import { createReadStream, openSync, readdirSync, readFileSync, readSync, closeSync, statSync } from 'node:fs';
22
22
  import { join, posix, relative, resolve, sep } from 'node:path';
23
- import { PassThrough, Transform } from 'node:stream';
23
+ import { PassThrough, Readable, Transform } from 'node:stream';
24
24
  import { pipeline } from 'node:stream/promises';
25
25
  import { createGunzip } from 'node:zlib';
26
26
  import { Parser } from 'tar';
@@ -249,10 +249,11 @@ function isGzip(file) {
249
249
  * `await`, so every one of them is captured here and rethrown on the awaited
250
250
  * path — an emitter-thrown `RangeError` that escapes becomes an uncaught
251
251
  * exception and a raw stack trace on a user's terminal.
252
- * @param file - absolute path to the `.tgz`.
252
+ * @param bytes - the arriving tar stream, gzipped or not.
253
+ * @param gzipped - whether to inflate before parsing.
253
254
  * @param collector - the shared accumulator.
254
255
  */
255
- async function readTarball(file, collector) {
256
+ async function readTarStream(bytes, gzipped, collector) {
256
257
  const pending = [];
257
258
  let failure = null;
258
259
  const parser = new Parser({
@@ -306,8 +307,8 @@ async function readTarball(file, collector) {
306
307
  }));
307
308
  },
308
309
  });
309
- const inflate = isGzip(file) ? createGunzip() : new PassThrough();
310
- await pipeline(createReadStream(file), inflate, byteCeiling(MAX_STREAM_BYTES), parser);
310
+ const inflate = gzipped ? createGunzip() : new PassThrough();
311
+ await pipeline(bytes, inflate, byteCeiling(MAX_STREAM_BYTES), parser);
311
312
  await Promise.all(pending);
312
313
  if (failure !== null)
313
314
  throw failure;
@@ -384,17 +385,58 @@ export async function loadSource(target, limits = DEFAULT_LIMITS) {
384
385
  }
385
386
  else {
386
387
  published = TARBALL_PUBLISH_SET;
387
- try {
388
- await readTarball(path, collector);
389
- }
390
- catch (error) {
391
- const detail = error instanceof Error ? error.message : String(error);
392
- throw new SourceError(`cannot read tarball ${path}: ${detail}`);
393
- }
394
- if (collector.entries === 0) {
395
- throw new SourceError(`${path} holds no tar entries this is not a readable npm tarball`);
396
- }
388
+ await decodeTar(createReadStream(path), isGzip(path), collector, path);
389
+ }
390
+ return finish(kind, path, collector, published);
391
+ }
392
+ /**
393
+ * Decode an npm tarball held in memory, for bytes that never touched the disk.
394
+ *
395
+ * The only caller is the `--from-npm` path, which fetches a tarball and
396
+ * verifies its `dist.integrity` hash before handing the buffer here. Reading it
397
+ * goes through the same `Parser` as the on-disk path, so the "no extraction,
398
+ * ever" property covers both.
399
+ * @param bytes - the complete tarball, gzipped or not.
400
+ * @param label - what to report as the target path, e.g. `npm:pkg@1.2.3`.
401
+ * @param limits - resource ceilings; defaults to {@link DEFAULT_LIMITS}.
402
+ * @returns the decoded package.
403
+ * @throws SourceError when the buffer is not a readable npm tarball.
404
+ */
405
+ export async function loadTarballBuffer(bytes, label, limits = DEFAULT_LIMITS) {
406
+ const collector = { files: new Map(), skipped: [], limits, bytes: 0, entries: 0, unpublished: 0 };
407
+ const gzipped = bytes[0] === 0x1f && bytes[1] === 0x8b;
408
+ await decodeTar(Readable.from(bytes), gzipped, collector, label);
409
+ return finish('registry', label, collector, TARBALL_PUBLISH_SET);
410
+ }
411
+ /**
412
+ * Run the tar pipeline and turn every failure into a `SourceError` naming the
413
+ * target, so a caller can tell "this is not a tarball" from a crash.
414
+ * @param bytes - the arriving tar stream.
415
+ * @param gzipped - whether to inflate before parsing.
416
+ * @param collector - the shared accumulator.
417
+ * @param label - the target as it should appear in an error message.
418
+ */
419
+ async function decodeTar(bytes, gzipped, collector, label) {
420
+ try {
421
+ await readTarStream(bytes, gzipped, collector);
422
+ }
423
+ catch (error) {
424
+ const detail = error instanceof Error ? error.message : String(error);
425
+ throw new SourceError(`cannot read tarball ${label}: ${detail}`);
397
426
  }
427
+ if (collector.entries === 0) {
428
+ throw new SourceError(`${label} holds no tar entries — this is not a readable npm tarball`);
429
+ }
430
+ }
431
+ /**
432
+ * Assemble the decoded package, refusing a target that holds no manifest.
433
+ * @param kind - where the bytes came from.
434
+ * @param path - the target as it should appear in the report.
435
+ * @param collector - the shared accumulator.
436
+ * @param published - the publish-set membership test that was used.
437
+ * @returns the decoded package.
438
+ */
439
+ function finish(kind, path, collector, published) {
398
440
  if (!collector.files.has('package.json')) {
399
441
  throw new SourceError(`no readable package.json in ${path} — this is not an npm package`);
400
442
  }
@@ -15,13 +15,25 @@ export declare const EXIT: {
15
15
  readonly findings: 1;
16
16
  readonly unanalysable: 2;
17
17
  };
18
- /** One parsed command line. */
19
- interface Options {
20
- readonly target: string;
18
+ /** Options that do not depend on which target form was given. */
19
+ interface CommonOptions {
20
+ /** Registry base URL, used only in `--from-npm` mode. */
21
+ readonly registry: string;
21
22
  readonly json: boolean;
22
23
  readonly failOn: Severity | 'none';
23
24
  readonly color: boolean;
24
25
  }
26
+ /**
27
+ * One parsed command line. Exactly one of the two target forms is present:
28
+ * a local path, or a registry spec that opts this invocation into fetching.
29
+ */
30
+ type Options = CommonOptions & ({
31
+ readonly target: string;
32
+ readonly fromNpm: null;
33
+ } | {
34
+ readonly target: null;
35
+ readonly fromNpm: string;
36
+ });
25
37
  /** Raised for a malformed command line; the message is printed and the tool exits 2. */
26
38
  export declare class UsageError extends Error {
27
39
  }
@@ -11,13 +11,15 @@
11
11
  * The ceiling is triage, not containment. See `README.md` §Limitations.
12
12
  * @module dsh-plugin-inspector
13
13
  */
14
- export { exceedsThreshold, inspect, TOOL_NAME, TOOL_VERSION } from './inspect.ts';
14
+ export { analyze, exceedsThreshold, inspect, TOOL_NAME, TOOL_VERSION } from './inspect.ts';
15
+ export { inspectFromNpm, precheck } from './npm.ts';
16
+ export { DEFAULT_REGISTRY, fetchVerifiedTarball, parseSpec, RegistryError, resolvePackage, verifyIntegrity, type PackageSpec, type RegistryOptions, type ResolvedPackage, type VerifiedTarball, } from './registry.ts';
15
17
  export { renderHuman, renderJson } from './report.ts';
16
18
  export { classifyExpression, isJsExpr, parsePatchDocument, patchSchema, PatchParseError, type ExpressionClass, type ExpressionSite, type ExpressionSlot, type InsertedRow, type JsExprNode, type OverridePatch, type PatchDocument, } from './cordis-yaml.ts';
17
19
  export { declaredPackages, ManifestError, parseManifest, type PackageManifest } from './manifest.ts';
18
- export { DEFAULT_LIMITS, loadSource, SourceError, type PluginSource, type ReadLimits } from './source.ts';
20
+ export { DEFAULT_LIMITS, loadSource, loadTarballBuffer, SourceError, type PluginSource, type ReadLimits, type SourceKind, } from './source.ts';
19
21
  export { globMatch, publishSet, type PublishBasis, type PublishInputs, type PublishSet } from './publish.ts';
20
22
  export { INJECTION_RULES, scanInjection, type InjectionMatch, type InjectionRule } from './injection.ts';
21
- export { compareFindings, SEVERITIES, SEVERITY_RANK, summarize, type AnalysisIntegrity, type Confidence, type Evidence, type Facts, type Finding, type Report, type Severity, type Tier, } from './model.ts';
23
+ export { aggregateFindings, compareFindings, MAX_EXAMPLES, SEVERITIES, SEVERITY_RANK, summarize, type AnalysisIntegrity, type Confidence, type Evidence, type Facts, type Finding, type RegistryProvenance, type Report, type Severity, type Tier, } from './model.ts';
22
24
  export { CORE_ROWS, CORE_ROW_IDS, HARNESS_BUNDLE_PACKAGES, HARNESS_REFERENCE, SEAM_KEYS, SECURITY_ROW_IDS, WATERFALL_EVENTS, type BundleName, type CoreRow, } from './knowledge.ts';
23
25
  //# sourceMappingURL=index.d.ts.map
@@ -9,18 +9,31 @@
9
9
  * `cordis-yaml.ts`, and its result is discarded without being called.
10
10
  * @module dsh-plugin-inspector/inspect
11
11
  */
12
- import { type Report, type Severity } from './model.ts';
12
+ import { type RegistryProvenance, type Report, type Severity } from './model.ts';
13
+ import { type PluginSource } from './source.ts';
13
14
  /** This tool's own version, reported in the JSON document. */
14
15
  export declare const TOOL_VERSION = "0.1.0";
15
16
  /** This tool's package name, reported in the JSON document. */
16
17
  export declare const TOOL_NAME = "dsh-plugin-inspector";
17
18
  /**
18
- * Inspect a plugin package.
19
+ * Inspect a plugin package on disk.
20
+ *
21
+ * Nothing on this path opens a socket: neither this module nor anything it
22
+ * imports can reach the registry, which is what makes "a directory or tarball
23
+ * scan never fetches" structural rather than a promise.
19
24
  * @param target - a plugin directory, or a `.tgz` / `.tar.gz` npm tarball.
20
25
  * @returns the complete report.
21
26
  * @throws SourceError or ManifestError when the target cannot be analysed at all.
22
27
  */
23
28
  export declare function inspect(target: string): Promise<Report>;
29
+ /**
30
+ * Run every check over an already-decoded package.
31
+ * @param source - the decoded package.
32
+ * @param registry - provenance, when the bytes were fetched from a registry.
33
+ * @returns the complete report.
34
+ * @throws ManifestError when the manifest cannot be read.
35
+ */
36
+ export declare function analyze(source: PluginSource, registry?: RegistryProvenance): Report;
24
37
  /**
25
38
  * Whether a report should fail a CI gate.
26
39
  * @param report - the report.
@@ -32,7 +32,22 @@ export interface Evidence {
32
32
  /** A short verbatim excerpt, truncated and single-lined for display. */
33
33
  readonly snippet?: string;
34
34
  }
35
- /** One thing the inspector believes warrants a decision. */
35
+ /**
36
+ * How many example sites one finding carries. Three is enough to show a
37
+ * pattern — one file, a second confirming it is not isolated, a third for
38
+ * spread — and few enough that the report stays readable when a package
39
+ * imports `node:fs` from ninety files.
40
+ */
41
+ export declare const MAX_EXAMPLES = 3;
42
+ /**
43
+ * One thing the inspector believes warrants a decision, in one package.
44
+ *
45
+ * A finding is per package, not per syntax site. A package that imports
46
+ * `node:fs` from eleven files has one B13 finding with `occurrences: 11`, not
47
+ * eleven findings — the eleventh import warrants no decision the first did not
48
+ * already warrant, and a report that lists it eleven times buries the one
49
+ * finding that does.
50
+ */
36
51
  export interface Finding {
37
52
  /** Catalogue id from PLAN.md §6, e.g. `A2`. Stable across releases. */
38
53
  readonly checkId: string;
@@ -41,11 +56,24 @@ export interface Finding {
41
56
  readonly tier: Tier;
42
57
  readonly severity: Severity;
43
58
  readonly confidence: Confidence;
59
+ /**
60
+ * What this finding is about, independent of where it was seen: the module
61
+ * specifier, the row id, the seam name, the matched rule. Two findings that
62
+ * share a `checkId` and a `subject` are the same finding observed twice, and
63
+ * are collapsed into one. Stable across releases, so a gate can allow a
64
+ * specific `B13`/`node:fs` without allowing every `B13`.
65
+ */
66
+ readonly subject: string;
44
67
  /** One line naming what was found. */
45
68
  readonly title: string;
46
69
  /** Why it matters, in terms of what the harness does with the declaration. */
47
70
  readonly detail: string;
71
+ /** The first site the check matched. Always equal to `examples[0]`. */
48
72
  readonly evidence: Evidence;
73
+ /** Up to {@link MAX_EXAMPLES} sites, in the order they were found. */
74
+ readonly examples: readonly Evidence[];
75
+ /** How many sites the check matched in this package. At least 1. */
76
+ readonly occurrences: number;
49
77
  /**
50
78
  * The one-line evasion for this specific check, or `null` when there is none.
51
79
  * Non-null for every Tier B and Tier C check. Carried inside the finding
@@ -119,9 +147,38 @@ export interface AnalysisIntegrity {
119
147
  readonly degradedBy: readonly string[];
120
148
  readonly filesSkipped: readonly SkippedFile[];
121
149
  }
122
- /** The complete inspection result, and the shape of `--json` output. */
150
+ /**
151
+ * Where a `--from-npm` target came from and how the bytes were checked.
152
+ *
153
+ * Present only in registry mode. Its presence in a report is the record that
154
+ * this invocation opened a socket, which a directory or tarball scan never
155
+ * does.
156
+ */
157
+ export interface RegistryProvenance {
158
+ /** The spec as the user typed it. */
159
+ readonly spec: string;
160
+ /** The registry base URL the packument was read from. */
161
+ readonly registry: string;
162
+ readonly resolvedVersion: string;
163
+ readonly tarball: string;
164
+ /** The digest that matched, in the registry's own encoding. */
165
+ readonly digest: string;
166
+ /** The algorithm that digest was taken with. `sha1` means no `dist.integrity` was published. */
167
+ readonly algorithm: string;
168
+ /** The registry's own flag, read before the tarball was fetched. */
169
+ readonly hasInstallScript: boolean;
170
+ readonly metadataBytes: number;
171
+ readonly tarballBytes: number;
172
+ }
173
+ /**
174
+ * The complete inspection result, and the shape of `--json` output.
175
+ *
176
+ * Version 2 is the first release where a finding is per package rather than per
177
+ * syntax site: every finding gained `subject`, `examples` and `occurrences`,
178
+ * and `target.kind` gained `registry`.
179
+ */
123
180
  export interface Report {
124
- readonly schemaVersion: 1;
181
+ readonly schemaVersion: 2;
125
182
  readonly tool: {
126
183
  readonly name: string;
127
184
  readonly version: string;
@@ -133,8 +190,10 @@ export interface Report {
133
190
  readonly harnessReference: string;
134
191
  };
135
192
  readonly target: {
136
- readonly kind: 'directory' | 'tarball';
193
+ readonly kind: 'directory' | 'tarball' | 'registry';
137
194
  readonly path: string;
195
+ /** Present only when the bytes were fetched from a registry. */
196
+ readonly registry?: RegistryProvenance;
138
197
  };
139
198
  readonly facts: Facts;
140
199
  readonly analysis: AnalysisIntegrity;
@@ -142,14 +201,33 @@ export interface Report {
142
201
  readonly findings: readonly Finding[];
143
202
  }
144
203
  /**
145
- * Order findings for display: most severe first, then by tier (A before B
146
- * before C, since A carries verdicts), then by check id, then by evidence
147
- * location. Total and deterministic, so two runs diff cleanly.
204
+ * Order findings for display: verdicts first, then most severe, then by tier,
205
+ * then by check id, then by evidence location. Total and deterministic, so two
206
+ * runs diff cleanly.
207
+ *
208
+ * Tier A outranks every Tier B finding whatever their severities, because a
209
+ * verdict and a capability report answer different questions and the verdict is
210
+ * the one the reader came for. Within each group severity decides.
148
211
  * @param a - left finding.
149
212
  * @param b - right finding.
150
213
  * @returns negative when `a` sorts first.
151
214
  */
152
215
  export declare function compareFindings(a: Finding, b: Finding): number;
216
+ /**
217
+ * Collapse per-site findings into one finding per check per subject.
218
+ *
219
+ * This is where the report stops being a list of syntax sites and becomes a
220
+ * list of decisions. The count and the examples are kept, so nothing a reader
221
+ * would act on is lost: the number says how widespread it is, the examples say
222
+ * where to look first, and both live in the finding rather than in a footnote.
223
+ *
224
+ * Order is preserved: the first occurrence of each group decides where the
225
+ * group sits, and `compareFindings` sorts afterwards.
226
+ * @param findings - findings as the checks emitted them, one per site.
227
+ * @param maxExamples - how many sites to keep; defaults to {@link MAX_EXAMPLES}.
228
+ * @returns one finding per `checkId` and `subject`.
229
+ */
230
+ export declare function aggregateFindings(findings: readonly Finding[], maxExamples?: number): Finding[];
153
231
  /**
154
232
  * Count findings per severity, including zeroes, so the JSON summary has a
155
233
  * fixed key set that consumers can rely on.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `--from-npm` — inspecting a published package without installing it.
3
+ *
4
+ * This module is the only path in the tool that reaches a network, and it is
5
+ * separate from `inspect.ts` on purpose: a directory or tarball scan cannot
6
+ * arrive here, because nothing on that path imports this file. The steps are
7
+ * fixed and their order is the guarantee:
8
+ *
9
+ * 1. read the version document (~3 KB) — which already answers
10
+ * `hasInstallScript`, the install lifecycle scripts, and `dsh.bundle`;
11
+ * 2. download the tarball into memory;
12
+ * 3. verify `dist.integrity` **before** anything parses a byte of it;
13
+ * 4. decode in memory and analyse, exactly as the tarball path does.
14
+ *
15
+ * No subprocess, no disk write, no lifecycle script, and no `npm pack`.
16
+ * @module dsh-plugin-inspector/npm
17
+ */
18
+ import type { Report } from './model.ts';
19
+ import { type RegistryOptions, type ResolvedPackage } from './registry.ts';
20
+ /**
21
+ * The metadata pre-check, which needs no tarball.
22
+ *
23
+ * A caller sweeping many packages can read this for each of them at a few
24
+ * kilobytes apiece and decide which ones are worth downloading.
25
+ * @param spec - `<name>` or `<name>@<version>`.
26
+ * @param options - where to fetch from.
27
+ * @returns what the version document says.
28
+ * @throws RegistryError when the package or version does not resolve.
29
+ */
30
+ export declare function precheck(spec: string, options?: RegistryOptions): Promise<ResolvedPackage>;
31
+ /**
32
+ * Fetch a published package and inspect it in memory.
33
+ * @param spec - `<name>` or `<name>@<version>`; no version means the `latest` tag.
34
+ * @param options - where to fetch from.
35
+ * @returns the complete report, carrying the registry provenance.
36
+ * @throws RegistryError when the package cannot be resolved, fetched, or verified.
37
+ * @throws SourceError or ManifestError when the fetched tarball is not a package.
38
+ */
39
+ export declare function inspectFromNpm(spec: string, options?: RegistryOptions): Promise<Report>;
40
+ //# sourceMappingURL=npm.d.ts.map
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Fetching a published package from an npm registry, and proving the bytes are
3
+ * the ones the registry vouched for.
4
+ *
5
+ * This is the one module in the tool that opens a socket, and it exists behind
6
+ * one explicit flag. A network fetch is not execution — nothing here installs,
7
+ * unpacks to disk, or runs a lifecycle script — but it is a side effect the
8
+ * tool's other two modes do not have, so it is never reached implicitly: a
9
+ * directory or tarball scan cannot get here, because neither imports this
10
+ * module.
11
+ *
12
+ * The order of operations is the security property. The packument is read
13
+ * first, which is ~3 KB and already answers `hasInstallScript`, the install
14
+ * lifecycle scripts, and whether the package declares `dsh.bundle` — a
15
+ * pre-check that needs no tarball at all. Only then is the tarball fetched, and
16
+ * its `dist.integrity` hash is verified **before** any byte of it reaches the
17
+ * tar parser. A hash mismatch is a refusal, not a warning: the whole point of
18
+ * the mode is that the analysed bytes are the published bytes.
19
+ * @module dsh-plugin-inspector/registry
20
+ */
21
+ /** The public npm registry, used when no other is named. */
22
+ export declare const DEFAULT_REGISTRY = "https://registry.npmjs.org";
23
+ /** Largest packument the tool will read. A version document is a few kilobytes. */
24
+ export declare const MAX_METADATA_BYTES: number;
25
+ /**
26
+ * Largest compressed tarball the tool will download. The decompressed stream is
27
+ * capped separately, and lower, by the tar reader.
28
+ */
29
+ export declare const MAX_TARBALL_BYTES: number;
30
+ /** Thrown when a package cannot be resolved, fetched, or verified. */
31
+ export declare class RegistryError extends Error {
32
+ }
33
+ /** One `<name>` or `<name>@<version>` argument, split. */
34
+ export interface PackageSpec {
35
+ readonly name: string;
36
+ /** A version or dist-tag, or `null` to mean the `latest` tag. */
37
+ readonly version: string | null;
38
+ }
39
+ /**
40
+ * What the packument says, before anything is downloaded. Every field here
41
+ * costs one small request and no tarball.
42
+ */
43
+ export interface ResolvedPackage {
44
+ readonly name: string;
45
+ readonly version: string;
46
+ readonly tarball: string;
47
+ /** The registry's own SRI string, e.g. `sha512-…`, or `null` on old packages. */
48
+ readonly integrity: string | null;
49
+ /** The legacy SHA-1 digest, the only check available when `integrity` is absent. */
50
+ readonly shasum: string | null;
51
+ /** The registry's own flag, which is set when npm would run an install script. */
52
+ readonly hasInstallScript: boolean;
53
+ /** Install lifecycle script names the manifest declares. */
54
+ readonly lifecycleScripts: readonly string[];
55
+ /** The `dsh.bundle.patch` value, which is what makes a package a mounted layer. */
56
+ readonly bundlePatch: string | null;
57
+ /** Bytes of metadata read to learn all of the above. */
58
+ readonly metadataBytes: number;
59
+ }
60
+ /** How a fetched tarball was verified. */
61
+ export interface VerifiedTarball {
62
+ readonly bytes: Buffer;
63
+ /** The algorithm the digest was taken with, e.g. `sha512`. */
64
+ readonly algorithm: string;
65
+ /** The digest that matched, in the registry's own encoding. */
66
+ readonly digest: string;
67
+ }
68
+ /** Where to fetch from. */
69
+ export interface RegistryOptions {
70
+ /** Registry base URL, without a trailing slash. Defaults to {@link DEFAULT_REGISTRY}. */
71
+ readonly registry?: string;
72
+ /** Injected for tests; defaults to the global `fetch`. */
73
+ readonly fetch?: typeof globalThis.fetch;
74
+ }
75
+ /**
76
+ * Split a `<name>` or `<name>@<version>` argument.
77
+ *
78
+ * The `@` that starts a scope is not a separator, so `@scope/name` has no
79
+ * version and `@scope/name@1.2.3` has one.
80
+ * @param spec - the argument as typed.
81
+ * @returns the package name and the requested version, if any.
82
+ * @throws RegistryError when the name or version is not one the registry accepts.
83
+ */
84
+ export declare function parseSpec(spec: string): PackageSpec;
85
+ /**
86
+ * Read the packument for one version, which is the pre-check.
87
+ * @param spec - the package name and requested version.
88
+ * @param options - where to fetch from.
89
+ * @returns everything the metadata says, including the tarball URL and its hash.
90
+ * @throws RegistryError when the package or version does not resolve, or when
91
+ * the document does not carry a tarball URL on the registry's own origin.
92
+ */
93
+ export declare function resolvePackage(spec: PackageSpec, options?: RegistryOptions): Promise<ResolvedPackage>;
94
+ /**
95
+ * Check downloaded bytes against what the registry published.
96
+ *
97
+ * `dist.integrity` is preferred and is a real check. `dist.shasum` is SHA-1 and
98
+ * is accepted only when there is no `integrity` field at all, which happens on
99
+ * packages published before npm 5; it is recorded in the report as the weaker
100
+ * check it is. No digest at all is a refusal, because "verified" would then be
101
+ * a claim the tool cannot make.
102
+ * @param bytes - the downloaded tarball.
103
+ * @param resolved - what the packument said about it.
104
+ * @returns the algorithm and digest that matched.
105
+ * @throws RegistryError on a mismatch, an unusable digest, or no digest at all.
106
+ */
107
+ export declare function verifyIntegrity(bytes: Buffer, resolved: ResolvedPackage): VerifiedTarball;
108
+ /**
109
+ * Download a resolved package's tarball and verify it before returning it.
110
+ *
111
+ * Nothing parses the bytes on the way in — they are counted against a ceiling
112
+ * and hashed, and a failed hash throws before any caller can see them.
113
+ * @param resolved - the packument reading for the version to fetch.
114
+ * @param options - where to fetch from.
115
+ * @returns the verified tarball, in memory.
116
+ * @throws RegistryError on a transport failure, an oversized body, or a hash mismatch.
117
+ */
118
+ export declare function fetchVerifiedTarball(resolved: ResolvedPackage, options?: RegistryOptions): Promise<VerifiedTarball>;
119
+ //# sourceMappingURL=registry.d.ts.map
@@ -37,9 +37,15 @@ export declare const DEFAULT_LIMITS: ReadLimits;
37
37
  /** Thrown when the target cannot be read at all, which is exit code 2, not a finding. */
38
38
  export declare class SourceError extends Error {
39
39
  }
40
+ /**
41
+ * Where the analysed bytes came from. `registry` is a tarball too — the same
42
+ * in-memory decoding, over bytes fetched by `--from-npm` instead of read from
43
+ * disk — and is kept distinct so a report says which one it was.
44
+ */
45
+ export type SourceKind = 'directory' | 'tarball' | 'registry';
40
46
  /** The analysed package, decoded into memory. */
41
47
  export interface PluginSource {
42
- readonly kind: 'directory' | 'tarball';
48
+ readonly kind: SourceKind;
43
49
  /** The target as the user gave it, resolved to an absolute path. */
44
50
  readonly path: string;
45
51
  /** Package-relative POSIX path to UTF-8 text, for every readable text file. */
@@ -68,4 +74,18 @@ export declare const MAX_STREAM_BYTES: number;
68
74
  * tarball, or holds no `package.json`.
69
75
  */
70
76
  export declare function loadSource(target: string, limits?: ReadLimits): Promise<PluginSource>;
77
+ /**
78
+ * Decode an npm tarball held in memory, for bytes that never touched the disk.
79
+ *
80
+ * The only caller is the `--from-npm` path, which fetches a tarball and
81
+ * verifies its `dist.integrity` hash before handing the buffer here. Reading it
82
+ * goes through the same `Parser` as the on-disk path, so the "no extraction,
83
+ * ever" property covers both.
84
+ * @param bytes - the complete tarball, gzipped or not.
85
+ * @param label - what to report as the target path, e.g. `npm:pkg@1.2.3`.
86
+ * @param limits - resource ceilings; defaults to {@link DEFAULT_LIMITS}.
87
+ * @returns the decoded package.
88
+ * @throws SourceError when the buffer is not a readable npm tarball.
89
+ */
90
+ export declare function loadTarballBuffer(bytes: Buffer, label: string, limits?: ReadLimits): Promise<PluginSource>;
71
91
  //# sourceMappingURL=source.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-inspector",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Know what a DeepSeek Harness plugin does before you install it — static pre-install analysis of a plugin directory or tarball",
5
5
  "license": "MIT",
6
6
  "author": "Ivan Tyshchenko",
@@ -56,6 +56,7 @@
56
56
  "test": "vitest run --config vitest.config.ts",
57
57
  "test:coverage": "vitest run --config vitest.config.ts --coverage",
58
58
  "inspect": "node --experimental-strip-types src/cli.ts",
59
+ "sweep": "node --experimental-strip-types scripts/ecosystem-sweep.ts",
59
60
  "test:e2e": "pnpm run build && vitest run --config vitest.e2e.config.ts"
60
61
  }
61
62
  }