@npm-safe/core API Reference (Phase 1)
August 29, 2026 · View on GitHub
This document describes every public export from @npm-safe/core in Phase 1. Signatures are copied from the source and reflect the exact runtime behaviour of the code at packages/core/src/.
Table of Contents
- Installation
- Import Notes
- NpmSafeEngine
- NpmSafeEngineOptions
- CheckResult
- DSH Installation Risk
- Enums
- Scanner Types
- LLM Providers
- Registry Types
- Internal But Reusable Exports
Installation
pnpm add @npm-safe/core
Import Notes
All TypeScript enums in this package are string enums. They exist at runtime as values and must be imported with a value import, not a type-only import:
// Correct — SecurityLevel is a runtime value
import { SecurityLevel } from '@npm-safe/core';
// Incorrect — will not compile
import type { SecurityLevel } from '@npm-safe/core';
Interfaces and type aliases may use import type:
import type { CheckResult, ScanFinding } from '@npm-safe/core';
The same distinction applies to the LLM provider exports. LlmProviderType is
a runtime value, so it must use a value import:
import { LlmProviderType } from '@npm-safe/core';
LlmProviderOptions and the other LLM interfaces are types and may use
import type.
NpmSafeEngine
The main facade class. Composes the database, cache, registry client, rate limiter, static analyser, and refresh scheduler into a single public API surface.
class NpmSafeEngine
Constructor
constructor(options?: NpmSafeEngineOptions)
Creates all internal collaborators (DatabaseManager, CacheManager, NpmRegistryClient, TokenBucket, StaticAnalyzer, RefreshScheduler) using the defaults or values provided in options.
Methods
checkPackage
checkPackage(name: string, options?: PackageCheckOptions): Promise<CheckResult>
Check a package by name. Cache-first: returns cached data if still fresh,
otherwise fetches from the registry, runs static analysis, and caches the
result. Set options.deep to download, verify, and inspect the selected
version's published tarball. A cached metadata-only result is upgraded on the
first deep check, then the content summary is reused.
interface PackageCheckOptions {
readonly deep?: boolean; // default false
}
When the package does not exist on the registry (HTTP 404), the returned CheckResult has exists: false and the security / registryInfo fields are empty. All other errors (network failure, timeout) are rethrown.
assessInstallRisk
assessInstallRisk(input: string, profile?: string): Promise<InstallRiskAssessment>
Resolve an npm package spec or public GitHub repository and return the data for a red / amber / green DSH installation risk card. npm tags are resolved to an exact published version and their tarballs are downloaded, integrity-checked, and inspected in memory. GitHub refs are resolved to a commit SHA through the public GitHub API.
The assessment checks install lifecycle scripts, suspicious dependency specs,
DSH core packages bundled in dependencies, duplicate core declarations,
dsh.bundle.patch, the presence of the declared patch file, and the
@deepseek-ai/dsh-tools peer range. safeInstallCommand always uses the
resolved immutable source and adds --ignore-scripts when install lifecycle
scripts are present. The method is advisory and never executes the command or
installs a package. profile defaults to web.
const card = await engine.assessInstallRisk('@scope/dsh-plugin');
console.log(card.riskLevel, card.safeInstallCommand);
checkPackages
checkPackages(
names: readonly string[],
options?: BatchCheckOptions,
): Promise<BatchPackageResult[]>
Check many packages in parallel with a shared concurrency cap. Every check consumes one token from the rate limiter, so the batch respects the configured request budget even when running concurrently. Individual failures are isolated: a package that throws (network error, timeout, …) yields a { ok: false, error } entry instead of rejecting the whole batch. Results are returned in input order.
interface BatchCheckOptions {
readonly concurrency?: number; // default 5
readonly deep?: boolean; // inspect every selected package tarball
readonly onProgress?: (done: number, total: number, entry: BatchPackageResult) => void;
}
interface BatchPackageResult {
readonly name: string;
readonly ok: boolean;
readonly result?: CheckResult;
readonly error?: string;
}
Use checkPackage when the raw error must propagate to the caller.
searchPackages
searchPackages(query: string, size?: number): Promise<SearchResult[]>
Search the npm registry for packages matching a text query. Delegates to NpmRegistryClient.searchPackages. size defaults to 20.
getWatchlist
getWatchlist(): Promise<string[]>
Returns the list of package names currently on the watchlist, in insertion order.
addToWatchlist
addToWatchlist(name: string): Promise<void>
Add a package to the watchlist. Idempotent: adding a name already watched is a no-op.
removeFromWatchlist
removeFromWatchlist(name: string): Promise<void>
Remove a package from the watchlist. No-op if the name was not watched.
refreshPackage
refreshPackage(name: string): Promise<boolean>
Refresh a single package: fetch its latest metadata from the registry, re-run static analysis, and persist the results. Per-package failures are surfaced via the scheduler's refresh:error event rather than thrown; the returned promise resolves after emitting the error so a failing package does not abort a batch.
refreshAll
refreshAll(): Promise<boolean>
Refresh every package whose cached metadata has passed its TTL. Packages are processed sequentially so the rate limiter is respected.
startAutoRefresh
startAutoRefresh(intervalMs?: number): void
Start the periodic auto-refresh loop. Returns void, not a Promise. The first refresh cycle kicks off immediately in the background; subsequent cycles repeat at intervalMs. Defaults to 3_600_000 (1 hour). Calling while already running resets the interval (idempotent restart).
stopAutoRefresh
stopAutoRefresh(): void
Stop the periodic auto-refresh loop. Safe to call when the scheduler is not running. Any in-flight refresh continues to completion.
getSetting
getSetting(key: string): Promise<string | null>
Retrieve a setting value by key. Returns null when the key is unset.
setSetting
setSetting(key: string, value: string): Promise<void>
Upsert a setting value by key (INSERT OR REPLACE semantics).
recordCheckHistory
recordCheckHistory(result: CheckResult): Promise<void>
Append a successful check to the persistent history table
(check_history, newest-first, capped at 1000). No-op when result.exists
is false. Both the CLI and the desktop extension use this, so history is
shared across frontends.
recordHistoryEntry
recordHistoryEntry(entry: {
readonly packageName: string;
readonly level: string;
readonly score: number;
readonly timestamp: string;
}): Promise<void>
Append a raw history entry directly (used for legacy history.json
migration).
getCheckHistory
getCheckHistory(limit?: number): Promise<ReadonlyArray<{
readonly packageName: string;
readonly level: string;
readonly score: number;
readonly timestamp: string;
}>>
Return the persistent check history, newest first.
clearCheckHistory
clearCheckHistory(): Promise<void>
Remove every entry from the persistent check history.
close
close(): void
Release all resources held by the engine. Stops the auto-refresh scheduler, disposes the rate limiter timer, and closes the database connection. Idempotent. After calling this method the engine instance must not be used for further operations.
registerRule
registerRule(rule: ScanRule): void
Register a scan rule at runtime. A rule with the same id replaces the existing one (keeping its position in the registration order).
unregisterRule
unregisterRule(ruleId: string): boolean
Remove a scan rule by id. Returns true if a rule was removed, false if no such rule exists.
listRules
listRules(): RuleDescriptor[]
Describe every registered rule with its effective status (enabled state and severity after config overrides, plus source: 'builtin' | 'plugin'), in registration order.
setRuleEnabled
setRuleEnabled(ruleId: string, enabled: boolean): void
Enable or disable a rule. Persisted in the rules config file (~/.npm-safe/rules.json by default).
setRuleSeverity
setRuleSeverity(ruleId: string, severity: Severity | undefined): void
Override a rule's severity. Persisted. Pass undefined to clear the override and return to the rule's default severity.
setRuleOptions
setRuleOptions(ruleId: string, options: Readonly<Record<string, unknown>>): void
Set free-form options for a rule. Persisted. Rule implementations can read these via RuleConfigManager.
getRuleConfig
getRuleConfig(): RuleConfigManager
Access the rule configuration manager for low-level inspection.
loadRulePlugins
loadRulePlugins(dir?: string): Promise<number>
Load third-party rules from a directory of ES module files (*.mjs / *.js). Each file may export rule, rules, or default holding one or more ScanRules. Files that fail to load are skipped. Defaults to ~/.npm-safe/rules/. Returns the number of rules loaded. Also invoked automatically at engine startup.
getLlmConfig
getLlmConfig(): LlmConfig
Returns the current persisted LLM configuration, including the raw API key. This is useful for programmatic editing; prefer getLlmStatus() for display.
getLlmStatus
getLlmStatus(): LlmStatus
Returns a masked, display-safe view of the LLM configuration: enabled state, provider, whether an API key is configured, effective model, base URL, and a masked API key.
setLlmConfig
setLlmConfig(update: Partial<LlmConfig>): void
Update the persisted LLM configuration and recreate the provider. Pass { enabled: false } to disable LLM scanning entirely. The provider is re-evaluated immediately, so the next checkPackage or refresh will use the new configuration.
testLlmConnection
testLlmConnection(): Promise<boolean>
Send a test request to the configured LLM provider. Returns true if the provider is enabled, configured, and the test request succeeds. Returns false when disabled or unconfigured.
DSH Installation Risk
InstallRiskAssessment
interface InstallRiskAssessment {
readonly input: string;
readonly sourceKind: 'npm' | 'github';
readonly sourceLabel: string;
readonly sourceUrl: string;
readonly packageName: string;
readonly version: string;
readonly pinnedSpec: string;
readonly safeInstallCommand: string;
readonly riskLevel: 'low' | 'medium' | 'high';
readonly safetyScore: number;
readonly summary: string;
readonly checks: readonly InstallRiskCheck[];
readonly findings: readonly InstallRiskFinding[];
readonly integrityVerified: boolean | null;
readonly inspectedAt: string;
}
InstallRiskCheck
interface InstallRiskCheck {
readonly id: string;
readonly label: string;
readonly status: 'pass' | 'warning' | 'danger' | 'unknown';
readonly detail: string;
}
InstallRiskFinding
interface InstallRiskFinding {
readonly id: string;
readonly title: string;
readonly detail: string;
readonly severity: 'info' | 'warning' | 'danger';
readonly recommendation: string;
}
NpmSafeEngineOptions
Options accepted by the NpmSafeEngine constructor. All fields are optional.
interface NpmSafeEngineOptions {
readonly dbPath?: string;
readonly registryUrl?: string;
readonly rateLimit?: number;
readonly rateLimitBurst?: number;
readonly cacheTtlMs?: number;
readonly llm?: LlmProviderOptions;
readonly llmConfigPath?: string;
readonly rulesConfigPath?: string;
readonly rulesDir?: string;
}
| Field | Type | Default | Description |
|---|---|---|---|
dbPath | string | './npm-safe.db' | Filesystem path to the SQLite database file. |
registryUrl | string | 'https://registry.npmjs.org' | Base URL of the npm registry. |
rateLimit | number | 5 | Token bucket refill rate (tokens per second). |
rateLimitBurst | number | 10 | Maximum burst size for the token bucket. |
cacheTtlMs | number | 3600000 | Cache TTL for package metadata in milliseconds. |
llm | LlmProviderOptions | unset | Optional programmatic semantic security scan backed by any supported LLM provider. |
llmConfigPath | string | '~/.npm-safe/llm.json' | Path to the LLM provider configuration JSON file. |
rulesConfigPath | string | '~/.npm-safe/rules.json' | Path to the per-rule configuration JSON file. |
rulesDir | string | '~/.npm-safe/rules/' | Directory scanned for third-party rule plugin files. |
LLM scanning is optional. The engine loads configuration from ~/.npm-safe/llm.json (or llmConfigPath). Each provider also falls back to its conventional environment variable (OPENAI_API_KEY, GEMINI_API_KEY, or ANTHROPIC_API_KEY) when no API key is persisted. When no key is configured, LLM scanning is silently disabled and static scanning remains available.
CheckResult
Returned by NpmSafeEngine.checkPackage.
interface CheckResult {
readonly packageName: string;
readonly exists: boolean;
readonly latestVersion: string;
readonly security: {
readonly overallLevel: SecurityLevel;
readonly overallScore: number;
readonly staticScan: StaticScanReport | null;
readonly llmScan?: LlmScanReport;
};
readonly registryInfo: {
readonly description: string;
readonly homepage: string;
readonly repository: string;
} | null;
readonly cachedAt: string | null;
}
| Field | Description |
|---|---|
packageName | Name of the checked package. |
exists | Whether the package exists on the registry. |
latestVersion | The latest available version, or an empty string when exists is false. |
security.overallLevel | Overall security level derived from the combined scan. |
security.overallScore | Numeric security score (0-100, higher is safer). |
security.staticScan | Full static scan report, or null if one is not available. |
registryInfo.description | Human-readable description of the package. |
registryInfo.homepage | URL to the package's homepage. |
registryInfo.repository | Repository descriptor as a string (e.g. "github:user/repo"). |
cachedAt | ISO-8601 timestamp of when this result was last cached, or null when the exact cached-at time is not known. |
Enums
SecurityLevel
enum SecurityLevel {
Safe = 'safe',
Suspicious = 'suspicious',
Dangerous = 'dangerous',
Unknown = 'unknown',
}
Overall security classification for a scanned package. String enum, usable in switch statements.
Severity
enum Severity {
Low = 'low',
Medium = 'medium',
High = 'high',
Critical = 'critical',
}
Severity of an individual scan finding. String enum.
ScanType
enum ScanType {
Static = 'static',
Llm = 'llm',
}
The kind of scan that produced a report or finding. String enum.
FindingCategory
enum FindingCategory {
InstallScript = 'install-script',
CodeObfuscation = 'code-obfuscation',
BinaryDownload = 'binary-download',
SensitiveExposure = 'sensitive-exposure',
Typosquatting = 'typosquatting',
SuspiciousDep = 'suspicious-dependency',
HomographAttack = 'homograph-attack',
RegistryMismatch = 'registry-mismatch',
KnownMalicious = 'known-malicious',
Informational = 'informational',
}
Categorisation of what a finding represents. String enum.
Scanner Types
ScanFinding
A single issue discovered during a security scan.
interface ScanFinding {
readonly ruleId: string;
readonly ruleName: string;
readonly severity: Severity;
readonly message: string;
readonly codeSnippet?: string;
readonly lineNumber?: number;
readonly filePath?: string;
readonly recommendation?: string;
readonly category: FindingCategory;
}
| Field | Description |
|---|---|
ruleId | Stable identifier of the rule that produced this finding. |
ruleName | Human-readable name of the rule. |
severity | How severe this finding is. |
message | Human-readable description of the issue. |
codeSnippet | Optional code snippet that triggered the finding. |
lineNumber | Optional 1-based line number where the issue was detected. |
filePath | Optional archive-relative path for a package-content finding. |
recommendation | Optional remediation guidance. |
category | Category classifying the nature of this finding. |
ScanRule
A rule that inspects package metadata and README content to produce findings.
interface ScanRule {
readonly id: string;
readonly name: string;
readonly description: string;
readonly severity: Severity;
readonly category: FindingCategory;
readonly enabled: boolean;
match(readme: string, packageJson?: Record<string, unknown>): ScanFinding[];
}
| Field | Description |
|---|---|
id | Stable unique identifier for the rule. |
name | Human-readable name of the rule. |
description | Description of what the rule detects. |
severity | Default severity assigned to findings produced by this rule. |
category | Category assigned to findings produced by this rule. |
enabled | Whether the rule is enabled by default. |
match() | Inspect the given README and/or package.json and return any findings. |
StaticScanReport
Report produced by a static (non-LLM) scan of a package.
interface StaticScanReport {
readonly packageName: string;
readonly version: string;
readonly overallLevel: SecurityLevel;
readonly score: number;
readonly findings: readonly ScanFinding[];
readonly contentScan?: ContentScanSummary;
readonly scannedAt: string;
}
| Field | Description |
|---|---|
packageName | Name of the scanned package. |
version | Version of the scanned package. |
overallLevel | Overall security level derived from static findings. |
score | Numeric score from 0 to 100 (higher is safer). |
findings | Findings produced by the static scan. |
contentScan | Deep-scan status and resource counters when content inspection was requested. |
scannedAt | ISO 8601 timestamp of when the scan ran. |
ContentScanSummary
interface ContentScanSummary {
readonly status: 'complete' | 'partial' | 'failed';
readonly archiveBytes: number;
readonly unpackedBytes: number;
readonly filesScanned: number;
readonly filesSkipped: number;
readonly integrityVerified: boolean;
readonly truncated: boolean;
readonly reason?: string;
}
partial means a configured archive, entry, file, or text-byte limit stopped
complete inspection. failed means download, integrity validation,
decompression, or archive parsing did not complete. integrityVerified is
false when neither a supported SRI value nor a valid legacy shasum was present.
PackageContentScan API
analyzePackageTarball(
archive: Buffer,
options?: PackageContentScanOptions,
): PackageContentScanResult
This low-level export verifies and scans an already-downloaded .tgz entirely
in memory. Options include integrity, shasum, maxUnpackedBytes,
maxEntries, maxFileBytes, and maxScannedBytes. Default-limit constants
and CONTENT_SCAN_RULES are also exported. Registry origin checks and the
20 MiB compressed download ceiling are enforced by
NpmRegistryClient.downloadTarball, not by this buffer-only function.
LlmScanReport
Report produced by an LLM-based scan of a package.
interface LlmScanReport {
readonly enabled: boolean;
readonly reason?: string;
readonly summary?: string;
readonly functionalMatch?: boolean;
readonly suspiciousScore?: number;
readonly findings?: readonly ScanFinding[];
readonly scannedAt?: string;
}
| Field | Description |
|---|---|
enabled | Whether the LLM scan was actually performed. |
reason | Reason the LLM scan was skipped or disabled, if applicable. |
summary | Natural-language summary of the LLM analysis. |
functionalMatch | Whether the package matches its declared functionality per the LLM. |
suspiciousScore | Suspiciousness score assigned by the LLM (0-100, higher is more suspicious). |
findings | Findings produced by the LLM scan. |
scannedAt | ISO 8601 timestamp of when the LLM scan ran. |
ScanReport
Combined report for a package, including static and optional LLM scans.
interface ScanReport {
readonly packageName: string;
readonly version: string;
readonly staticScan: StaticScanReport;
readonly llmScan?: LlmScanReport;
readonly overallLevel: SecurityLevel;
readonly overallScore: number;
readonly scannedAt: string;
}
| Field | Description |
|---|---|
packageName | Name of the scanned package. |
version | Version of the scanned package. |
staticScan | Results of the static scan. |
llmScan | Results of the LLM scan, if performed. |
overallLevel | Overall security level combining static and LLM results. |
overallScore | Overall numeric score (0-100, higher is safer). |
scannedAt | ISO 8601 timestamp of when the combined scan ran. |
SecuritySummary
Aggregated security summary for a package, suitable for storage and display.
interface SecuritySummary {
readonly packageName: string;
readonly latestVersion: string;
readonly overallLevel: SecurityLevel;
readonly overallScore: number;
readonly findingCount: number;
readonly criticalCount: number;
readonly highCount: number;
readonly mediumCount: number;
readonly lowCount: number;
readonly lastScanned: string | null;
readonly cachedAt: string;
}
| Field | Description |
|---|---|
packageName | Name of the package. |
latestVersion | Latest known version of the package. |
overallLevel | Overall security level for the package. |
overallScore | Overall numeric score (0-100, higher is safer). |
findingCount | Total number of findings across all scans. |
criticalCount | Number of findings with critical severity. |
highCount | Number of findings with high severity. |
mediumCount | Number of findings with medium severity. |
lowCount | Number of findings with low severity. |
lastScanned | ISO 8601 timestamp of the last scan, or null if never scanned. |
cachedAt | ISO 8601 timestamp of when this summary was cached. |
LLM Providers
The optional semantic scan can run against one of three LLM backends:
OpenAI-compatible chat-completions endpoints, Google Gemini, and Anthropic
Claude. All three are configured through the unified LlmProviderOptions
interface and constructed with the createLlmProvider factory. The provider
implementations live under src/llm/ and share the same parsing and
validation helpers (src/llm/parse.ts).
LlmProviderType
String enum identifying a supported LLM backend. This is a runtime value, so it must use a value import (see Import Notes).
enum LlmProviderType {
OpenAi = 'openai',
Gemini = 'gemini',
Anthropic = 'anthropic',
}
LlmProviderOptions
Unified options accepted by createLlmProvider and every concrete provider
constructor. Fields that do not apply to a given backend are ignored by that
backend.
interface LlmProviderOptions {
readonly provider?: LlmProviderType;
readonly apiKey?: string;
readonly baseUrl?: string;
readonly model?: string;
readonly timeoutMs?: number;
readonly maxInputChars?: number;
readonly maxTokens?: number;
}
| Field | Type | Default | Description |
|---|---|---|---|
provider | LlmProviderType | LlmProviderType.OpenAi | Backend to instantiate. |
apiKey | string | unset | API key. Falls back to a provider-specific environment variable when omitted. |
baseUrl | string | provider default | Base URL of the LLM API endpoint. |
model | string | provider default | Model identifier to use for completions. |
timeoutMs | number | 30000 | Request timeout in milliseconds. |
maxInputChars | number | 12000 | Maximum README characters to send to the model. |
maxTokens | number | 2000 | Anthropic-only maximum response tokens. Ignored by the other providers. |
The deprecated alias OpenAICompatibleLlmOptions is kept for backward
compatibility and is identical to LlmProviderOptions.
LlmConfig
Persisted LLM configuration read from ~/.npm-safe/llm.json (or the path
configured via NpmSafeEngineOptions.llmConfigPath).
interface LlmConfig {
readonly enabled: boolean;
readonly provider: LlmProviderType;
readonly apiKey?: string;
readonly baseUrl?: string;
readonly model?: string;
readonly timeoutMs?: number;
readonly maxInputChars?: number;
readonly maxTokens?: number;
}
LlmStatus
Display-safe view of the LLM configuration. Returned by
NpmSafeEngine.getLlmStatus().
interface LlmStatus {
readonly enabled: boolean;
readonly provider: LlmProviderType;
readonly configured: boolean;
readonly model?: string;
readonly baseUrl?: string;
readonly apiKey?: string; // masked, e.g. "sk-****1234"
}
createLlmProvider
function createLlmProvider(options?: LlmProviderOptions): LlmScanProvider
Constructs an LlmScanProvider for the backend selected via
options.provider, defaulting to the OpenAI-compatible provider when
provider is omitted. The returned provider implements scan(input) and
testConnection().
OpenAICompatibleLlmProvider
Talks to any endpoint that implements the /chat/completions surface
(OpenAI, Azure OpenAI, local LM Studio / Ollama OpenAI shims). Source:
src/llm/provider.ts.
Env-var fallback: apiKey defaults to process.env.OPENAI_API_KEY. Default
base URL https://api.openai.com/v1, default model gpt-4o-mini.
GeminiLlmProvider
Talks to the Google Generative Language API
(generativelanguage.googleapis.com/v1beta) using the
models/<model>:generateContent surface. Source: src/llm/gemini.ts.
Env-var fallback: apiKey defaults to process.env.GEMINI_API_KEY. Default
base URL https://generativelanguage.googleapis.com/v1beta, default model
gemini-2.0-flash.
AnthropicLlmProvider
Talks to the Anthropic Messages API (/v1/messages). Source:
src/llm/anthropic.ts.
Env-var fallback: apiKey defaults to process.env.ANTHROPIC_API_KEY.
Default base URL https://api.anthropic.com, default model
claude-3-5-sonnet-latest. maxTokens is required on every request and
defaults to 2000.
LlmProviderError
Error thrown when an LLM provider request or response is invalid. Defined in
src/llm/parse.ts and re-exported from the provider module for backward
compatibility.
class LlmProviderError extends Error {
readonly statusCode?: number;
constructor(message: string, statusCode?: number);
}
Environment Variables
The engine and CLI resolve LLM configuration from ~/.npm-safe/llm.json
first, then fall back to provider-specific environment variables. If neither a
persisted key nor an environment variable is present, LLM scanning is
silently disabled and static scanning continues normally.
| Variable | Provider | Purpose |
|---|---|---|
OPENAI_API_KEY | OpenAI-compatible | API key. |
OPENAI_BASE_URL | OpenAI-compatible | API endpoint override. |
OPENAI_MODEL | OpenAI-compatible | Model override (default gpt-4o-mini). |
GEMINI_API_KEY | Google Gemini | API key. |
GEMINI_BASE_URL | Google Gemini | API endpoint override. |
GEMINI_MODEL | Google Gemini | Model override (default gemini-2.0-flash). |
ANTHROPIC_API_KEY | Anthropic Claude | API key. |
ANTHROPIC_BASE_URL | Anthropic Claude | API endpoint override. |
ANTHROPIC_MODEL | Anthropic Claude | Model override (default claude-3-5-sonnet-latest). |
Registry Types
NpmRegistryError
Typed error thrown by registry clients when a request fails or returns a non-success status.
class NpmRegistryError extends Error {
readonly statusCode?: number;
readonly statusText?: string;
constructor(
message: string,
statusCode?: number,
statusText?: string,
);
}
Carries the HTTP status code and status text when available so callers can branch on specific failure modes. The name property is set to 'NpmRegistryError'.
PackageMetadata
Full package metadata (packument) as returned by the registry's GET /{package} endpoint.
interface PackageMetadata {
readonly name: string;
readonly modified: string;
readonly 'dist-tags': Readonly<Record<string, string>>;
readonly versions: Readonly<Record<string, AbbreviatedVersion>>;
readonly description?: string;
readonly homepage?: string;
readonly repository?: PackageRepository;
readonly keywords?: ReadonlyArray<string>;
readonly author?: PackagePerson;
readonly maintainers?: ReadonlyArray<{
readonly name: string;
readonly email: string;
}>;
readonly license?: string;
readonly readme?: string;
readonly readmeFilename?: string;
readonly time?: Readonly<Record<string, string>>;
}
| Field | Description |
|---|---|
name | Package name (scoped names include the leading @). |
modified | ISO-8601 timestamp of the most recent modification. |
dist-tags | Distribution tags keyed by tag name (e.g. latest) pointing to versions. |
versions | All published versions keyed by semver version string. |
description | Short human-readable description. |
homepage | URL to the package's homepage. |
repository | Source repository descriptor. |
keywords | Search/discoverability keywords. |
author | Original package author. |
maintainers | Current maintainers of the package. |
license | SPDX license identifier or license text. |
readme | Full readme contents. |
readmeFilename | Filename of the readme (e.g. README.md). |
time | Publication timestamps keyed by version (plus created/modified). |
AbbreviatedVersion
Abbreviated packument for a single published version.
interface AbbreviatedVersion {
readonly name: string;
readonly version: string;
readonly shasum?: string;
readonly integrity?: string;
readonly dependencies?: Readonly<Record<string, string>>;
readonly devDependencies?: Readonly<Record<string, string>>;
readonly optionalDependencies?: Readonly<Record<string, string>>;
readonly peerDependencies?: Readonly<Record<string, string>>;
readonly bundleDependencies?: ReadonlyArray<string>;
readonly deprecated?: string;
readonly hasInstallScript?: boolean;
readonly hasShrinkwrap?: boolean;
readonly dist: DistMetadata;
readonly engines?: Readonly<Record<string, string>>;
readonly _hasShrinkwrap?: boolean;
readonly scripts?: Readonly<Record<string, string>>;
readonly bin?: Readonly<Record<string, string>>;
readonly directories?: Readonly<Record<string, string>>;
}
DistMetadata
Distribution metadata attached to a published package version.
interface DistMetadata {
readonly integrity?: string;
readonly shasum?: string;
readonly tarball: string;
readonly fileCount?: number;
readonly unpackedSize?: number;
readonly signatures?: ReadonlyArray<{
readonly keyid: string;
readonly sig: string;
}>;
}
| Field | Description |
|---|---|
integrity | Subresource Integrity (SRI) hash, e.g. sha512-.... |
shasum | Legacy SHA-1 hex digest of the tarball. |
tarball | Absolute URL to the downloadable .tgz tarball. |
fileCount | Number of files contained in the tarball. |
unpackedSize | Unpacked size of the tarball contents in bytes. |
signatures | Cryptographic signatures attached to the tarball. |
SearchResult
A single hit from the registry's GET /-/v1/search endpoint.
interface SearchResult {
readonly package: {
readonly name: string;
readonly scope: string;
readonly version: string;
readonly description?: string;
readonly keywords?: ReadonlyArray<string>;
readonly date: string;
readonly links: {
readonly npm: string;
readonly homepage?: string;
readonly repository?: string;
readonly bugs?: string;
};
readonly publisher: {
readonly username: string;
readonly email: string;
};
readonly maintainers: ReadonlyArray<{
readonly username: string;
readonly email: string;
}>;
};
readonly score: {
readonly final: number;
readonly detail: {
readonly quality: number;
readonly popularity: number;
readonly maintenance: number;
};
};
readonly searchScore: number;
}
ValidationResult
Outcome of validating a package identifier or version string.
interface ValidationResult {
readonly valid: boolean;
readonly reason?: string;
}
PackageIdentifier
A parsed package identifier combining name, optional scope, and optional version.
interface PackageIdentifier {
readonly name: string;
readonly version?: string;
readonly scope?: string;
readonly fullName: string;
}
| Field | Description |
|---|---|
name | Package name without scope prefix. |
version | Semver version string, if specified. |
scope | Scope without the leading @, if the package is scoped. |
fullName | Fully-qualified name: @scope/name when scoped, otherwise name. |
PackageRepository
Repository descriptor for a package. May be a structured object or a shorthand string.
type PackageRepository =
| {
readonly type: string;
readonly url: string;
}
| string;
PackagePerson
A person or entity associated with a package.
type PackagePerson =
| {
readonly name: string;
readonly email?: string;
readonly url?: string;
}
| string;
DomainValidationResult
Result returned by validateDomain.
interface DomainValidationResult {
readonly valid: boolean;
readonly domain: string;
}
| Field | Description |
|---|---|
valid | true when the input parsed into a URL with a usable hostname. |
domain | Lowercased hostname extracted from the URL, or empty string on failure. |
Internal But Reusable Exports
The following modules are used internally by NpmSafeEngine but are also exported for advanced use cases. Their APIs are stable within Phase 1.
Registry Client
Source: src/registry/client.ts
class NpmRegistryClient
HTTP client for the npm registry API v2. Thin wrapper around fetch with 10s timeout, exponential backoff retries (up to 3 attempts), and typed error handling.
Constructor
constructor(options?: {
readonly baseUrl?: string;
readonly userAgent?: string;
})
Defaults: baseUrl = 'https://registry.npmjs.org', userAgent = '@npm-safe/core (https://npmjs.org)'.
Methods
getPackageMetadata(name: string): Promise<PackageMetadata>
getVersionManifest(name: string, version: string): Promise<AbbreviatedVersion>
searchPackages(query: string, size?: number): Promise<SearchResult[]>
getPackageMetadata: Fetch the full packument for a package (GET /{name}).getVersionManifest: Fetch the abbreviated version manifest (GET /{name}/{version}).searchPackages: Search the registry (GET /-/v1/search?text={query}&size={size}).sizedefaults to 20.
All methods throw NpmRegistryError on failure.
Static Analyzer
Source: src/scanner/static-rules.ts
class StaticAnalyzer
const BUILTIN_RULES: readonly ScanRule[]
BUILTIN_RULES is an array of 10 built-in ScanRule implementations covering install script detection, eval/Function obfuscation, base64-encoded shell payloads, binary download links, typosquatting, secret exposure, child_process in browser packages, suspicious build metadata, homograph attacks, and registry mismatches.
Constructor
constructor(rules?: ScanRule[])
Accepts an optional custom rule array. Defaults to BUILTIN_RULES.
Methods
analyze(
readme: string,
packageJson?: Record<string, unknown>,
supplementalFindings?: readonly ScanFinding[],
contentScan?: ContentScanSummary,
): StaticScanReport
Runs all enabled rules against the given README and package.json. Scoring starts at 100 and subtracts per-finding weights (Critical -25, High -15, Medium -8, Low -3), clamped to [0, 100]. Overall level: >= 80 Safe, >= 50 Suspicious, >= 20 Dangerous, else Unknown.
Database Manager
Source: src/store/database.ts
class DatabaseManager
class DatabaseManagerError extends Error
Constructor
constructor(dbPath: string)
Opens (or creates) the SQLite database at dbPath, applies WAL-mode pragmas, and runs pending migrations. Throws DatabaseManagerError on failure.
Methods
getDb(): Database.Database
close(): void
isOpen(): boolean
getDb: Returns the underlyingbetter-sqlite3connection. Throws if the connection has been closed.close: Closes the database connection. Idempotent. ThrowsDatabaseManagerErrorif closing fails.isOpen: Returnstrueif the connection has not been closed.
DatabaseManagerError
class DatabaseManagerError extends Error {
readonly cause?: unknown;
constructor(message: string, cause?: unknown);
}
The name property is set to 'DatabaseManagerError'.
Cache Manager
Source: src/store/cache-manager.ts
class CacheManager
interface CacheManagerOptions {
readonly cacheTtlMs?: number;
}
Cache read/write layer backed by a DatabaseManager connection. All public methods return Promises for API consistency; the underlying better-sqlite3 calls are synchronous.
Constructor
constructor(database: DatabaseManager, options?: CacheManagerOptions)
cacheTtlMs defaults to 3_600_000 (1 hour). The constant DEFAULT_CACHE_TTL_MS is also exported.
Methods
getPackage(name: string): Promise<PackageMetadata | null>
setPackage(meta: PackageMetadata): Promise<void>
getSecurityReport(pkg: string, version: string): Promise<StaticScanReport | null>
setSecurityReport(report: StaticScanReport): Promise<void>
getWatchlist(): Promise<string[]>
addToWatchlist(name: string): Promise<void>
removeFromWatchlist(name: string): Promise<void>
getSetting(key: string): Promise<string | null>
setSetting(key: string, value: string): Promise<void>
getStalePackages(): Promise<string[]>
| Method | Description |
|---|---|
getPackage | Returns cached metadata if still fresh (TTL not elapsed), otherwise null. |
setPackage | Upserts a packument, stamping a fresh TTL. |
getSecurityReport | Returns the most recent static security report, or null. The SecurityLevel is reconstructed from the persisted numeric score. |
setSecurityReport | Upserts a static scan report (keyed by package_name + version + scan_type). |
getWatchlist | Returns all watched package names in insertion order. |
addToWatchlist | Adds a name (INSERT OR IGNORE, idempotent). |
removeFromWatchlist | Removes a name (no-op if absent). |
getSetting | Returns a setting value, or null if unset. |
setSetting | Upserts a setting (INSERT OR REPLACE). |
getStalePackages | Returns names of cached packages whose TTL has elapsed. |
Rate Limiter (Token Bucket)
Source: src/scheduler/rate-limiter.ts
class TokenBucket
Token bucket rate limiter using continuous refill. Tokens accumulate at a configurable rate up to a maximum burst capacity. consume() blocks until tokens are available, servicing concurrent callers in FIFO order.
Constructor
constructor(tokensPerSecond: number = 5, maxBurst: number = 10)
Both parameters must be positive finite numbers; throws RangeError otherwise.
Methods
consume(count?: number): Promise<void>
tryConsume(count?: number): boolean
getStats(): { available: number; maxBurst: number; rate: number }
dispose(): void
| Method | Description |
|---|---|
consume | Wait until count tokens are available (default: 1). Resolves immediately if tokens suffice, otherwise enqueues until the next refill tick. Rejects if called after disposal. |
tryConsume | Non-blocking variant. Returns true if tokens were consumed, false otherwise. Returns false after disposal. |
getStats | Returns current available tokens, max burst capacity, and refill rate. |
dispose | Stops the refill timer and rejects all pending consumers. Idempotent. |
Refresh Scheduler
Source: src/scheduler/refresh-scheduler.ts
class RefreshScheduler extends EventEmitter
Auto-refresh scheduler that periodically polls the npm registry for updated package metadata, re-caches it, and re-runs the static analyzer. Extends EventEmitter from node:events.
Constructor
constructor(
client: NpmRegistryClient,
cache: CacheManager,
limiter: TokenBucket,
analyzer: StaticAnalyzer,
)
Methods
start(intervalMs?: number): void
stop(): void
refreshPackage(name: string): Promise<boolean>
refreshAll(): Promise<boolean>
| Method | Description |
|---|---|
start | Start the periodic refresh loop. Kicks off an immediate background cycle, then repeats at intervalMs (default: 1 hour). Idempotent restart. |
stop | Stop the periodic refresh loop. Safe when not running. In-flight refreshes continue to completion. |
refreshPackage | Refresh a single package. Catches per-package errors, emits them as events instead of rejecting, and returns whether the refresh succeeded. |
refreshAll | Refresh every package with stale cache entries, processed sequentially, and returns whether all refreshes succeeded. |
Events
| Event | Payload | Description |
|---|---|---|
'refresh:start' | RefreshStartPayload | Emitted before each package refresh begins. |
'refresh:complete' | RefreshCompletePayload | Emitted after a package refresh succeeds. |
'refresh:error' | RefreshErrorPayload | Emitted when a package refresh fails. The scheduler does not throw on per-package failures; it emits the error and continues. |
Event Payload Interfaces
interface RefreshStartPayload {
readonly packageName: string;
}
interface RefreshCompletePayload {
readonly packageName: string;
readonly report: StaticScanReport;
}
interface RefreshErrorPayload {
readonly packageName: string;
readonly error: unknown;
}
Telemetry Manager
Source: src/telemetry/telemetry.ts
Opt-in, local-only usage telemetry used by the CLI. Disabled by default; nothing is ever sent anywhere.
class TelemetryManager {
constructor(filePath?: string); // default ~/.npm-safe/telemetry.json
isEnabled(): boolean;
enable(): void;
disable(): void;
record(event: TelemetryEvent): void; // no-op while disabled
getState(): TelemetryState;
reset(): void;
}
interface TelemetryEvent {
readonly event: string; // e.g. "check", "ci"
readonly timestamp: string;
readonly packageCount?: number;
readonly durationMs?: number;
readonly levels?: Readonly<Record<string, number>>;
readonly error?: string;
}
interface TelemetryState {
readonly enabled: boolean;
readonly since?: string;
readonly counts: Readonly<Record<string, number>>;
readonly totalPackagesScanned: number;
readonly levelTotals: Readonly<Record<string, number>>;
readonly totalErrors: number;
readonly recentEvents: readonly TelemetryEvent[]; // capped at 200
}
Validator Functions
Source: src/registry/validator.ts
Pure string-parsing validators with no network calls or side effects.
function validatePackageName(name: string): ValidationResult
function validateVersion(version: string): boolean
function validateDomain(url: string): DomainValidationResult
function isKnownRegistryDomain(domain: string): boolean
validatePackageName
Validates an npm package name according to the public registry's naming rules.
Rules enforced:
- Must be a non-empty string, maximum 214 characters.
- Must be entirely lowercase.
- Must not contain spaces.
- Must not begin with a dot (
.) or underscore (_). - Scoped names use
@scope/namesyntax; both segments are validated independently. - Rejects Unicode homograph look-alikes by restricting to ASCII lowercase letters, digits, hyphen, underscore, and dot.
Returns a ValidationResult with valid: boolean and an optional reason string.
validateVersion
Validates a string as a compliant semver 2.0.0 version (MAJOR.MINOR.PATCH with optional pre-release and build metadata). Numeric components with leading zeros are rejected. Returns boolean.
validateDomain
Parses a URL string and extracts its hostname. Prepends https:// when no scheme is present. Returns a DomainValidationResult with valid: boolean and domain: string (lowercased hostname, or empty string on failure).
isKnownRegistryDomain
Checks whether a domain is a known npm registry or source-control host. The whitelist includes npmjs.com, registry.npmjs.org, github.com, bitbucket.org, and gitlab.com. Comparison is case-insensitive and exact. Returns boolean.