@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

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;
}
FieldTypeDefaultDescription
dbPathstring'./npm-safe.db'Filesystem path to the SQLite database file.
registryUrlstring'https://registry.npmjs.org'Base URL of the npm registry.
rateLimitnumber5Token bucket refill rate (tokens per second).
rateLimitBurstnumber10Maximum burst size for the token bucket.
cacheTtlMsnumber3600000Cache TTL for package metadata in milliseconds.
llmLlmProviderOptionsunsetOptional programmatic semantic security scan backed by any supported LLM provider.
llmConfigPathstring'~/.npm-safe/llm.json'Path to the LLM provider configuration JSON file.
rulesConfigPathstring'~/.npm-safe/rules.json'Path to the per-rule configuration JSON file.
rulesDirstring'~/.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;
}
FieldDescription
packageNameName of the checked package.
existsWhether the package exists on the registry.
latestVersionThe latest available version, or an empty string when exists is false.
security.overallLevelOverall security level derived from the combined scan.
security.overallScoreNumeric security score (0-100, higher is safer).
security.staticScanFull static scan report, or null if one is not available.
registryInfo.descriptionHuman-readable description of the package.
registryInfo.homepageURL to the package's homepage.
registryInfo.repositoryRepository descriptor as a string (e.g. "github:user/repo").
cachedAtISO-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;
}
FieldDescription
ruleIdStable identifier of the rule that produced this finding.
ruleNameHuman-readable name of the rule.
severityHow severe this finding is.
messageHuman-readable description of the issue.
codeSnippetOptional code snippet that triggered the finding.
lineNumberOptional 1-based line number where the issue was detected.
filePathOptional archive-relative path for a package-content finding.
recommendationOptional remediation guidance.
categoryCategory 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[];
}
FieldDescription
idStable unique identifier for the rule.
nameHuman-readable name of the rule.
descriptionDescription of what the rule detects.
severityDefault severity assigned to findings produced by this rule.
categoryCategory assigned to findings produced by this rule.
enabledWhether 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;
}
FieldDescription
packageNameName of the scanned package.
versionVersion of the scanned package.
overallLevelOverall security level derived from static findings.
scoreNumeric score from 0 to 100 (higher is safer).
findingsFindings produced by the static scan.
contentScanDeep-scan status and resource counters when content inspection was requested.
scannedAtISO 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;
}
FieldDescription
enabledWhether the LLM scan was actually performed.
reasonReason the LLM scan was skipped or disabled, if applicable.
summaryNatural-language summary of the LLM analysis.
functionalMatchWhether the package matches its declared functionality per the LLM.
suspiciousScoreSuspiciousness score assigned by the LLM (0-100, higher is more suspicious).
findingsFindings produced by the LLM scan.
scannedAtISO 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;
}
FieldDescription
packageNameName of the scanned package.
versionVersion of the scanned package.
staticScanResults of the static scan.
llmScanResults of the LLM scan, if performed.
overallLevelOverall security level combining static and LLM results.
overallScoreOverall numeric score (0-100, higher is safer).
scannedAtISO 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;
}
FieldDescription
packageNameName of the package.
latestVersionLatest known version of the package.
overallLevelOverall security level for the package.
overallScoreOverall numeric score (0-100, higher is safer).
findingCountTotal number of findings across all scans.
criticalCountNumber of findings with critical severity.
highCountNumber of findings with high severity.
mediumCountNumber of findings with medium severity.
lowCountNumber of findings with low severity.
lastScannedISO 8601 timestamp of the last scan, or null if never scanned.
cachedAtISO 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;
}
FieldTypeDefaultDescription
providerLlmProviderTypeLlmProviderType.OpenAiBackend to instantiate.
apiKeystringunsetAPI key. Falls back to a provider-specific environment variable when omitted.
baseUrlstringprovider defaultBase URL of the LLM API endpoint.
modelstringprovider defaultModel identifier to use for completions.
timeoutMsnumber30000Request timeout in milliseconds.
maxInputCharsnumber12000Maximum README characters to send to the model.
maxTokensnumber2000Anthropic-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.

VariableProviderPurpose
OPENAI_API_KEYOpenAI-compatibleAPI key.
OPENAI_BASE_URLOpenAI-compatibleAPI endpoint override.
OPENAI_MODELOpenAI-compatibleModel override (default gpt-4o-mini).
GEMINI_API_KEYGoogle GeminiAPI key.
GEMINI_BASE_URLGoogle GeminiAPI endpoint override.
GEMINI_MODELGoogle GeminiModel override (default gemini-2.0-flash).
ANTHROPIC_API_KEYAnthropic ClaudeAPI key.
ANTHROPIC_BASE_URLAnthropic ClaudeAPI endpoint override.
ANTHROPIC_MODELAnthropic ClaudeModel 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>>;
}
FieldDescription
namePackage name (scoped names include the leading @).
modifiedISO-8601 timestamp of the most recent modification.
dist-tagsDistribution tags keyed by tag name (e.g. latest) pointing to versions.
versionsAll published versions keyed by semver version string.
descriptionShort human-readable description.
homepageURL to the package's homepage.
repositorySource repository descriptor.
keywordsSearch/discoverability keywords.
authorOriginal package author.
maintainersCurrent maintainers of the package.
licenseSPDX license identifier or license text.
readmeFull readme contents.
readmeFilenameFilename of the readme (e.g. README.md).
timePublication 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;
  }>;
}
FieldDescription
integritySubresource Integrity (SRI) hash, e.g. sha512-....
shasumLegacy SHA-1 hex digest of the tarball.
tarballAbsolute URL to the downloadable .tgz tarball.
fileCountNumber of files contained in the tarball.
unpackedSizeUnpacked size of the tarball contents in bytes.
signaturesCryptographic 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;
}
FieldDescription
namePackage name without scope prefix.
versionSemver version string, if specified.
scopeScope without the leading @, if the package is scoped.
fullNameFully-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;
}
FieldDescription
validtrue when the input parsed into a URL with a usable hostname.
domainLowercased 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}). size defaults 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 underlying better-sqlite3 connection. Throws if the connection has been closed.
  • close: Closes the database connection. Idempotent. Throws DatabaseManagerError if closing fails.
  • isOpen: Returns true if 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[]>
MethodDescription
getPackageReturns cached metadata if still fresh (TTL not elapsed), otherwise null.
setPackageUpserts a packument, stamping a fresh TTL.
getSecurityReportReturns the most recent static security report, or null. The SecurityLevel is reconstructed from the persisted numeric score.
setSecurityReportUpserts a static scan report (keyed by package_name + version + scan_type).
getWatchlistReturns all watched package names in insertion order.
addToWatchlistAdds a name (INSERT OR IGNORE, idempotent).
removeFromWatchlistRemoves a name (no-op if absent).
getSettingReturns a setting value, or null if unset.
setSettingUpserts a setting (INSERT OR REPLACE).
getStalePackagesReturns 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
MethodDescription
consumeWait until count tokens are available (default: 1). Resolves immediately if tokens suffice, otherwise enqueues until the next refill tick. Rejects if called after disposal.
tryConsumeNon-blocking variant. Returns true if tokens were consumed, false otherwise. Returns false after disposal.
getStatsReturns current available tokens, max burst capacity, and refill rate.
disposeStops 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>
MethodDescription
startStart the periodic refresh loop. Kicks off an immediate background cycle, then repeats at intervalMs (default: 1 hour). Idempotent restart.
stopStop the periodic refresh loop. Safe when not running. In-flight refreshes continue to completion.
refreshPackageRefresh a single package. Catches per-package errors, emits them as events instead of rejecting, and returns whether the refresh succeeded.
refreshAllRefresh every package with stale cache entries, processed sequentially, and returns whether all refreshes succeeded.

Events

EventPayloadDescription
'refresh:start'RefreshStartPayloadEmitted before each package refresh begins.
'refresh:complete'RefreshCompletePayloadEmitted after a package refresh succeeds.
'refresh:error'RefreshErrorPayloadEmitted 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/name syntax; 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.