or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

examples

edge-cases.mdreal-world-scenarios.md
index.md
tile.json

validate-url.mddocs/reference/

URL Whitelisting

The ValidateUrl class validates that package URLs exactly match a whitelist of allowed URLs.

Capabilities

ValidateUrl Constructor

Creates a new URL validator instance.

/**
 * Creates a new URL whitelist validator
 * @param {Object} options - Validator configuration
 * @param {Object} options.packages - Package object from parsed lockfile
 * @throws {Error} If packages is not provided or not an object
 */
class ValidateUrl {
  constructor(options: {
    packages: Object;
  });
}

Requirements: The packages parameter must be an object (typically from ParseLockfile.parseSync().object). Throws an error if not provided or not an object.

Usage Examples:

const { ValidateUrl, ParseLockfile } = require('lockfile-lint-api');

// Parse lockfile first
const parser = new ParseLockfile({ lockfilePath: './package-lock.json' });
const lockfile = parser.parseSync();

// Create validator
const validator = new ValidateUrl({ packages: lockfile.object });

// Error cases
try {
  new ValidateUrl(); // Throws: 'expecting an object passed to validator constructor'
} catch (error) {
  // Handle error
}

try {
  new ValidateUrl({ packages: null }); // Throws: 'expecting an object passed to validator constructor'
} catch (error) {
  // Handle error
}

Validate All Packages

Validates that all packages in the lockfile resolve to whitelisted URLs.

/**
 * Validates all packages against allowed URL whitelist
 * @param {string[]} allowedUrls - Array of exact URL strings that are allowed
 * @param {Object} [options] - Reserved for future use (currently unused)
 * @returns {Object} Validation result with type ('success' or 'error') and errors array
 * @throws {Error} If allowedUrls is not an array
 */
validate(allowedUrls: string[], options?: Object): {
  type: 'success' | 'error';
  errors: Array<{
    message: string;
    package: string;
  }>;
}

Note: The options parameter is currently unused but reserved for future functionality. It exists for API consistency with other validators.

URL Matching:

  • Requires exact string match between package's resolved URL and whitelist
  • URLs must match completely including protocol, host, path, and query parameters
  • No pattern matching or wildcards supported
  • Case-sensitive comparison

Behavior:

  • Packages without a resolved field are automatically skipped (e.g., local filesystem packages)
  • URL parsing errors are silently ignored (package is considered valid)
  • Returns error result if any package URL is not in the whitelist

Usage Examples:

const { ValidateUrl, ParseLockfile } = require('lockfile-lint-api');

// Basic usage
const parser = new ParseLockfile({ lockfilePath: './package-lock.json' });
const lockfile = parser.parseSync();
const validator = new ValidateUrl({ packages: lockfile.object });

// Whitelist specific package URLs
const allowedUrls = [
  'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz',
  'https://registry.npmjs.org/express/-/express-4.18.2.tgz',
  'https://registry.npmjs.org/react/-/react-18.2.0.tgz'
];

const result = validator.validate(allowedUrls);

if (result.type === 'success') {
  console.log('All packages are from whitelisted URLs');
} else {
  console.error('URL validation failed:');
  result.errors.forEach(error => {
    console.error(`  ${error.package}: ${error.message}`);
  });
}

Extract URLs from Existing Lockfile:

const { ValidateUrl, ParseLockfile } = require('lockfile-lint-api');

// Create whitelist from existing trusted lockfile
function extractAllowedUrls(lockfilePath) {
  const parser = new ParseLockfile({ lockfilePath });
  const lockfile = parser.parseSync();

  const urls = [];
  for (const [packageName, metadata] of Object.entries(lockfile.object)) {
    if (metadata.resolved) {
      urls.push(metadata.resolved);
    }
  }

  return urls;
}

// Generate whitelist from trusted baseline
const allowedUrls = extractAllowedUrls('./baseline-package-lock.json');

// Validate new lockfile against baseline
const parser = new ParseLockfile({ lockfilePath: './package-lock.json' });
const lockfile = parser.parseSync();
const validator = new ValidateUrl({ packages: lockfile.object });
const result = validator.validate(allowedUrls);

if (result.type === 'error') {
  console.error('New packages detected:');
  result.errors.forEach(error => console.error(error.message));
}

CI/CD Strict Validation:

const { ValidateUrl, ParseLockfile } = require('lockfile-lint-api');
const fs = require('fs');

function strictUrlValidation(lockfilePath, whitelistPath) {
  try {
    // Load URL whitelist
    const allowedUrls = JSON.parse(fs.readFileSync(whitelistPath, 'utf-8'));

    const parser = new ParseLockfile({ lockfilePath });
    const lockfile = parser.parseSync();
    const validator = new ValidateUrl({ packages: lockfile.object });
    const result = validator.validate(allowedUrls);

    if (result.type === 'error') {
      console.error('Build failed: Unauthorized package URLs detected!');
      result.errors.forEach(error => {
        console.error(`  Package: ${error.package}`);
        console.error(`  ${error.message}`);
      });
      return false;
    }

    console.log('✓ All package URLs are whitelisted');
    return true;
  } catch (error) {
    console.error('Validation error:', error.message);
    return false;
  }
}

// whitelist.json contains array of approved URLs
if (!strictUrlValidation('./package-lock.json', './whitelist.json')) {
  process.exit(1);
}

Validate Single Package

Validates a single package against allowed URLs.

/**
 * Validates a single package against allowed URL whitelist
 * @param {string} packageName - Package key from the lockfile (e.g., 'lodash@4.17.21')
 * @param {string[]} allowedUrls - Array of exact URL strings that are allowed
 * @returns {boolean} True if package URL is in whitelist or package has no resolved URL, false otherwise
 */
validateSingle(packageName: string, allowedUrls: string[]): boolean;

Behavior:

  • Returns true if the package's resolved URL exactly matches any URL in the whitelist
  • Returns true if the package has no resolved field (e.g., local filesystem packages)
  • Returns false if the package's URL is not in the whitelist

Usage Examples:

const { ValidateUrl, ParseLockfile } = require('lockfile-lint-api');

const parser = new ParseLockfile({ lockfilePath: './package-lock.json' });
const lockfile = parser.parseSync();
const validator = new ValidateUrl({ packages: lockfile.object });

const allowedUrls = [
  'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz',
  'https://registry.npmjs.org/express/-/express-4.18.2.tgz'
];

// Validate specific packages
const isLodashValid = validator.validateSingle('lodash@4.17.21', allowedUrls);
const isReactValid = validator.validateSingle('react@18.2.0', allowedUrls);

if (!isLodashValid) {
  console.error('lodash URL is not whitelisted');
}

if (!isReactValid) {
  console.error('react URL is not whitelisted');
}

// Check critical packages
const criticalPackages = ['lodash@4.17.21', 'express@4.18.2'];
const unauthorized = criticalPackages.filter(
  pkg => !validator.validateSingle(pkg, allowedUrls)
);

if (unauthorized.length > 0) {
  console.error('Unauthorized critical packages:', unauthorized);
  process.exit(1);
}

Validation Result Structure

The validate() method returns a consistent result object:

interface UrlValidationResult {
  type: 'success' | 'error';
  errors: UrlValidationError[];
}

interface UrlValidationError {
  message: string;  // Detailed error message with allowed URLs and actual URL
  package: string;  // Package name that failed validation
}

Success Example:

{
  type: 'success',
  errors: []
}

Error Example:

{
  type: 'error',
  errors: [
    {
      message: 'detected invalid url(s) for package: unknown-package@1.0.0\n    expected: https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz\n    actual: https://malicious-registry.com/unknown-package/-/unknown-package-1.0.0.tgz\n',
      package: 'unknown-package@1.0.0'
    }
  ]
}

Security Use Cases

URL whitelisting provides the strictest form of package validation:

  1. Immutable Dependency Control: Ensure exact package versions and sources never change
  2. Supply Chain Lock-down: Prevent any unauthorized package additions
  3. Compliance Verification: Prove packages haven't changed for audit purposes
  4. Zero-Trust Package Management: Explicitly approve every package source
  5. Baseline Comparison: Detect any drift from approved baseline

High-Security Environment Example:

const { ValidateUrl, ParseLockfile } = require('lockfile-lint-api');
const crypto = require('crypto');
const fs = require('fs');

function generateUrlWhitelist(lockfilePath, outputPath) {
  const parser = new ParseLockfile({ lockfilePath });
  const lockfile = parser.parseSync();

  const whitelist = {
    generated: new Date().toISOString(),
    hash: '',
    urls: []
  };

  // Extract all URLs
  for (const metadata of Object.values(lockfile.object)) {
    if (metadata.resolved) {
      whitelist.urls.push(metadata.resolved);
    }
  }

  // Generate hash for integrity
  const whitelistJson = JSON.stringify(whitelist.urls);
  whitelist.hash = crypto.createHash('sha256').update(whitelistJson).digest('hex');

  fs.writeFileSync(outputPath, JSON.stringify(whitelist, null, 2));
  console.log(`Generated whitelist with ${whitelist.urls.length} URLs`);
  console.log(`Whitelist hash: ${whitelist.hash}`);
}

function validateAgainstWhitelist(lockfilePath, whitelistPath) {
  const whitelist = JSON.parse(fs.readFileSync(whitelistPath, 'utf-8'));

  // Verify whitelist integrity
  const whitelistJson = JSON.stringify(whitelist.urls);
  const currentHash = crypto.createHash('sha256').update(whitelistJson).digest('hex');

  if (currentHash !== whitelist.hash) {
    console.error('Whitelist integrity check failed!');
    return false;
  }

  const parser = new ParseLockfile({ lockfilePath });
  const lockfile = parser.parseSync();
  const validator = new ValidateUrl({ packages: lockfile.object });
  const result = validator.validate(whitelist.urls);

  return result.type === 'success';
}

// Generate baseline
// generateUrlWhitelist('./approved-package-lock.json', './url-whitelist.json');

// Validate against baseline
if (!validateAgainstWhitelist('./package-lock.json', './url-whitelist.json')) {
  console.error('Package URLs have changed from approved baseline!');
  process.exit(1);
}

Comparison with Other Validators

ValidateUrl is the most restrictive validator:

ValidateHost:

  • Validates hostname only
  • Allows any package from approved hosts
  • More flexible for package updates

ValidateScheme:

  • Validates protocol/scheme only
  • Allows any URL with approved protocol
  • Most flexible

ValidateUrl:

  • Validates complete URL
  • Requires exact match for every package
  • Most restrictive, highest security

Example Comparison:

const { ValidateUrl, ValidateHost, ParseLockfile } = require('lockfile-lint-api');

const parser = new ParseLockfile({ lockfilePath: './package-lock.json' });
const lockfile = parser.parseSync();

// ValidateHost: Allow any package from npm
const hostValidator = new ValidateHost({ packages: lockfile.object });
const hostResult = hostValidator.validate(['npm']);
// ✓ lodash-4.17.21 from npm - PASS
// ✓ lodash-4.17.22 from npm - PASS (newer version)

// ValidateUrl: Allow only specific URLs
const urlValidator = new ValidateUrl({ packages: lockfile.object });
const urlResult = urlValidator.validate([
  'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz'
]);
// ✓ lodash-4.17.21 from npm - PASS
// ✗ lodash-4.17.22 from npm - FAIL (different URL)

Error Handling

Constructor Errors:

// Throws: 'expecting an object passed to validator constructor'
const validator = new ValidateUrl();  // Missing packages
const validator = new ValidateUrl({ packages: null });  // Invalid type
const validator = new ValidateUrl({ packages: [] });  // Array instead of object

Validation Errors:

const validator = new ValidateUrl({ packages: lockfile.object });

// Throws: 'validate method requires an array'
validator.validate('https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz');  // String
validator.validate({ url: 'https://...' });  // Object

Implementation Notes

  1. Exact Matching: Uses strict string equality (===) for URL comparison
  2. No Pattern Matching: Does not support wildcards, regex, or glob patterns
  3. Case Sensitive: URLs are compared case-sensitively
  4. Local Packages: Automatically skipped (packages without resolved field)
  5. Error Tolerance: URL parsing errors are silently caught to avoid false positives

Practical Workflows

Generate Whitelist from Approved Lockfile

const { ParseLockfile } = require('lockfile-lint-api');
const fs = require('fs');

function generateWhitelist(approvedLockfile, outputFile) {
  const parser = new ParseLockfile({ lockfilePath: approvedLockfile });
  const lockfile = parser.parseSync();

  const urls = Object.values(lockfile.object)
    .filter(pkg => pkg.resolved)
    .map(pkg => pkg.resolved);

  fs.writeFileSync(outputFile, JSON.stringify(urls, null, 2));
  console.log(`Generated whitelist with ${urls.length} URLs`);
}

generateWhitelist('./approved-package-lock.json', './url-whitelist.json');

Validate with Generated Whitelist

const { ValidateUrl, ParseLockfile } = require('lockfile-lint-api');
const fs = require('fs');

function validateWithWhitelist(lockfilePath, whitelistPath) {
  const whitelist = JSON.parse(fs.readFileSync(whitelistPath, 'utf-8'));

  const parser = new ParseLockfile({ lockfilePath });
  const lockfile = parser.parseSync();
  const validator = new ValidateUrl({ packages: lockfile.object });

  return validator.validate(whitelist);
}

const result = validateWithWhitelist('./package-lock.json', './url-whitelist.json');

if (result.type === 'error') {
  console.error('Validation failed:');
  result.errors.forEach(error => console.error(error.message));
  process.exit(1);
}

Update Whitelist for New Packages

const { ParseLockfile } = require('lockfile-lint-api');
const fs = require('fs');

function updateWhitelist(lockfilePath, whitelistPath) {
  const parser = new ParseLockfile({ lockfilePath });
  const lockfile = parser.parseSync();

  const newUrls = Object.values(lockfile.object)
    .filter(pkg => pkg.resolved)
    .map(pkg => pkg.resolved);

  const existingUrls = JSON.parse(fs.readFileSync(whitelistPath, 'utf-8'));

  // Find new URLs
  const addedUrls = newUrls.filter(url => !existingUrls.includes(url));

  if (addedUrls.length > 0) {
    console.log(`Found ${addedUrls.length} new URLs:`);
    addedUrls.forEach(url => console.log(`  + ${url}`));

    // Merge and save
    const mergedUrls = [...new Set([...existingUrls, ...addedUrls])];
    fs.writeFileSync(whitelistPath, JSON.stringify(mergedUrls, null, 2));
    console.log('Whitelist updated');
  } else {
    console.log('No new URLs to add');
  }
}

updateWhitelist('./package-lock.json', './url-whitelist.json');