Ebb

Batch checking with checkAll

2 minutes read

checkAll(items) checks a batch of components synchronously on top of check and returns both positional outcomes and batch counts:

import { checkAll, isCheckAllError } from "@velella/ebb/check-all";

By design, checkAll never reads SBOM or lockfile files and never touches the network; only the first evaluation loads Ebb's active local rule index (later calls reuse the warm index). Map your SBOM components or lockfile entries into { purl, version } objects before calling it.

CycloneDX SBOM example

The runnable example below builds a CycloneDX 1.5 component list modeled on tests/check-all/fixtures/sbom.json, adding two invalid entries ("pkg:" and a component with no version) and keeping a duplicated dependency (request@2.88.2).

import { checkAll, isCheckAllError } from "@velella/ebb/check-all";

const sbom = {
  bomFormat: "CycloneDX",
  specVersion: "1.5",
  components: [
    { name: "express", version: "4.21.0", purl: "pkg:npm/express@4.21.0" },
    { name: "request", version: "2.88.2", purl: "pkg:npm/request@2.88.2" },
    { name: "node-sass", version: "9.0.0", purl: "pkg:npm/node-sass@9.0.0" },
    { name: "dotnet", version: "9.0.0", purl: "pkg:generic/dotnet@9.0.0" },
    { name: "nodejs", version: "22.12.0", purl: "pkg:generic/nodejs@22.12.0" },
    { name: "request", version: "2.88.2", purl: "pkg:npm/request@2.88.2" },
    { name: "broken-purl", version: "1.0.0", purl: "pkg:" },
    { name: "express", purl: "pkg:npm/express" },
    { name: "@types/node", version: "22.10.0", purl: "pkg:npm/%40types/node@22.10.0" },
  ],
};

const { results, summary } = checkAll(
  sbom.components.map((component) => ({
    purl: component.purl ?? component.name,
    version: component.version ?? "",
  })),
);

const findings = results.filter((entry) => !isCheckAllError(entry) && entry.status !== "ok");
const errors = results.filter(isCheckAllError);

console.log(JSON.stringify({ summary, findingCount: findings.length, errors }, null, 2));

Output:

{
  "summary": {
    "total": 9,
    "byStatus": {
      "ok": 2,
      "deprecated": 2,
      "unsupported": 1,
      "end-of-life": 1,
      "removed": 0,
      "scheduled": 1
    },
    "byEvent": {
      "deprecated": 5,
      "unsupported": 1,
      "end-of-life": 1,
      "removed": 0,
      "scheduled": 1
    },
    "errors": 2
  },
  "findingCount": 5,
  "errors": [
    {
      "status": "error",
      "index": 6,
      "code": "invalid-purl",
      "message": "PURL is missing a type: pkg:"
    },
    {
      "status": "error",
      "index": 7,
      "code": "invalid-version",
      "message": "Invalid version \"\"."
    }
  ]
}

Notice what happens in that run:

  • results.length === 9 and results[i] stays aligned with sbom.components[i] — nothing is sorted or dropped.
  • request@2.88.2 appears at indexes 1 and 5. (purl, version) is judged once, both positions receive the cached CheckResult, and both occurrences contribute to summary.
  • Index 6 ("pkg:") and index 7 (missing version) produce positional CheckAllError objects without aborting index 8 (@types/node@22.10.0).
  • byStatus counts each valid item once by its primary verdict (ok: 2, deprecated: 2, unsupported: 1, end-of-life: 1, scheduled: 1, total valid 7, plus errors: 2 equals total: 9). byEvent counts every matched rule event across valid items (request@2.88.2 matches two deprecated rules twice, and node-sass@9.0.0 matches both an end-of-life rule and a deprecated rule, giving byEvent.deprecated === 5).

Every valid entry in results has the schema documented in Result model:

{
  "schemaVersion": 1,
  "status": "end-of-life",
  "matches": [
    {
      "ruleId": "npm.package.node-sass.eol",
      "event": "end-of-life",
      "date": "2024-07-24",
      "daysLeft": null,
      "replacement": "sass",
      "sources": [
        {
          "title": "Node Sass is end-of-life",
          "url": "https://sass-lang.com/blog/node-sass-is-end-of-life/",
          "accessedAt": "2026-10-16"
        }
      ],
      "reviewedAt": "2026-10-14"
    },
    {
      "ruleId": "npm.package.node-sass.deprecated",
      "event": "deprecated",
      "date": null,
      "daysLeft": null,
      "replacement": "sass",
      "sources": [
        {
          "title": "node-sass package deprecation notice",
          "url": "https://www.npmjs.com/package/node-sass",
          "accessedAt": "2026-10-30"
        },
        {
          "title": "node-sass registry metadata",
          "url": "https://registry.npmjs.org/node-sass",
          "accessedAt": "2026-10-30"
        }
      ],
      "reviewedAt": "2026-10-30"
    }
  ],
  "version": "9.0.0"
}

Streaming iterables

checkAll accepts any Iterable<CheckAllItem>. Arrays use an indexed fast path with a pre-sized output array; generators, Sets, and other iterables stream in a single pass without building a second intermediate list first:

import { checkAll, isCheckAllError } from "@velella/ebb/check-all";

function* itemsFromLockfile() {
  yield { purl: "pkg:npm/express", version: "4.21.0" };
  yield { purl: "pkg:npm/node-sass", version: "9.0.0" };
  yield { purl: "pkg:npm/request", version: "2.88.2" };
}

const { results, summary } = checkAll(itemsFromLockfile());

console.log(
  JSON.stringify({
    statuses: results.map((entry) => (isCheckAllError(entry) ? entry.code : entry.status)),
    total: summary.total,
  }),
);

Output:

{"statuses":["ok","end-of-life","deprecated"],"total":3}

API and types

src/check-all.ts exports:

Symbol Role
checkAll(items) Evaluates Iterable<CheckAllItem> synchronously and returns CheckAllResult.
CheckAllItem { readonly purl: string; readonly version: string } — PURL or product name, plus required version string.
CheckAllResult { results: CheckAllEntry[]; summary: CheckAllSummary }
CheckAllEntry CheckResult | CheckAllError
isCheckAllError(entry) Type guard narrowing a CheckAllEntry to CheckAllError (entry.status === "error").
CheckAllSummary { total, byStatus, byEvent, errors }
CheckAllError { readonly status: "error"; readonly index: number; readonly code: CheckAllErrorCode; readonly message: string }
CheckAllErrorCode Reason code recorded when judging an individual item fails.

CheckAllItem requirements

Every item needs both purl and version. Even when a PURL from CycloneDX or SPDX already includes @version (pkg:npm/request@2.88.2), checkAll still evaluates against item.version ("2.88.2"). If a component has no version, item.version is empty and produces an "invalid-version" error entry at that index rather than guessing from the PURL.

CheckAllErrorCode values

Code When emitted
"invalid-purl" purl starts with pkg: but is malformed (PurlError / EbbError).
"invalid-version" version is empty/whitespace or invalid for the matched rule's version scheme (VersError).
"unknown-product" Bare product name (without pkg:) does not match any product in the active rules (CheckError).
"ambiguous-product" Bare product name ties across multiple ecosystems or packages (CheckError); pass a full pkg: PURL instead.
"invalid-data-version" Active data configuration pins an invalid data version (EbbError).
"invalid-date" Query supplied an invalid ISO date (EbbError).
"package-file-not-found" Required bundled package file could not be located (EbbError).
"internal" Unexpected internal error encountered while processing the item (EbbError).
"data-unavailable" Configured data bundle could not be loaded (EbbError).
"data-unavailable" Configured data bundle could not be loaded (EbbError).
"data-update-failed" Active data source failed verification or loading (EbbError).
"check-failed" Fallback when checking an item throws any other error.
View Markdown

Use with an agent
View Markdown

Open in