|
| 1 | +# VERS |
| 2 | + |
| 3 | +**VE**rsion **R**ange **S**pecifiers — the pre-standard grammar for |
| 4 | +saying "any version of package X that matches this range" in a way |
| 5 | +every ecosystem can parse the same way. This doc covers what VERS is, |
| 6 | +how to read and write it, how this library implements it, and what |
| 7 | +"pre-standard" means for you as a consumer. |
| 8 | + |
| 9 | +## Who this is for |
| 10 | + |
| 11 | +Contributors who want to understand the `Vers` class before changing |
| 12 | +it, and callers who want to know whether VERS is stable enough to |
| 13 | +depend on in their own code. |
| 14 | + |
| 15 | +## What VERS is |
| 16 | + |
| 17 | +Today, every package ecosystem has its own version-range syntax: |
| 18 | + |
| 19 | +- **npm** uses `^1.2.3`, `~1.0`, `>=1.0 <2.0` (npm semver). |
| 20 | +- **pypi** uses `>=1.0,<2.0`, `~=1.0.0` (PEP 440). |
| 21 | +- **cargo** uses `^1.2.3` but with different prerelease rules. |
| 22 | +- **maven** uses `[1.0,2.0)` (interval notation). |
| 23 | +- **composer** uses `^1.2.3 || ~2.0`. |
| 24 | +- **nuget** uses `[1.0,2.0)` (similar to maven, different edges). |
| 25 | + |
| 26 | +A tool that consumes all of them — a vulnerability scanner, an |
| 27 | +SBOM tool, Socket.dev itself — has to implement and maintain eight |
| 28 | +different parsers just to answer "does version 1.4.2 satisfy this |
| 29 | +range?" |
| 30 | + |
| 31 | +VERS proposes a single grammar that any consumer can parse, with |
| 32 | +a `scheme` field telling you how to *compare* versions within the |
| 33 | +range (semver semantics, PEP 440 semantics, etc.): |
| 34 | + |
| 35 | +``` |
| 36 | +vers:<scheme>/<constraint>[|<constraint>…] |
| 37 | +``` |
| 38 | + |
| 39 | +Each `<constraint>` is a comparator (`=`, `!=`, `<`, `<=`, `>`, |
| 40 | +`>=`) followed by a version string, or the wildcard `*`. |
| 41 | + |
| 42 | +Constraints inside a single VERS are **ORed**. To express AND, use |
| 43 | +multiple VERS in your own logic (the spec is deliberately simple |
| 44 | +here — it doesn't try to encode every operator every ecosystem has). |
| 45 | + |
| 46 | +## Worked examples |
| 47 | + |
| 48 | +``` |
| 49 | +vers:npm/>=1.0.0|<2.0.0 |
| 50 | +``` |
| 51 | + |
| 52 | +"Any npm version ≥ 1.0.0, OR any npm version < 2.0.0." (OR semantics |
| 53 | +across constraints — note this matches almost everything; the example |
| 54 | +is intentionally showing the grammar, not a useful range.) |
| 55 | + |
| 56 | +``` |
| 57 | +vers:pypi/>=1.0,<2.0 |
| 58 | +``` |
| 59 | + |
| 60 | +Same shape, pypi semantics. The `scheme` (`pypi`) tells the parser |
| 61 | +how to compare "1.0.0a1" vs "1.0.0" (PEP 440: prereleases sort |
| 62 | +before release; semver: same, but "1.0.0-a" form). |
| 63 | + |
| 64 | +``` |
| 65 | +vers:cargo/^1.2.3 |
| 66 | +``` |
| 67 | + |
| 68 | +Cargo's caret — any version ≥ 1.2.3 and < 2.0.0. |
| 69 | + |
| 70 | +``` |
| 71 | +vers:semver/* |
| 72 | +``` |
| 73 | + |
| 74 | +Wildcard — matches any semver version. |
| 75 | + |
| 76 | +``` |
| 77 | +vers:npm/=1.2.3 |
| 78 | +``` |
| 79 | + |
| 80 | +Exact match — only version 1.2.3 satisfies. |
| 81 | + |
| 82 | +``` |
| 83 | +vers:npm/>=1.0.0|!=1.3.5|<2.0.0 |
| 84 | +``` |
| 85 | + |
| 86 | +"≥ 1.0.0 OR not 1.3.5 OR < 2.0.0" — again, grammar demo; the OR |
| 87 | +semantics make this permissive. Real policies usually fit in two |
| 88 | +constraints. |
| 89 | + |
| 90 | +## The pre-standard caveat |
| 91 | + |
| 92 | +VERS is **not finalized**. The spec lives at |
| 93 | +[package-url/vers-spec](https://github.com/package-url/vers-spec) |
| 94 | +with Ecma submission planned for **late 2026**. That means: |
| 95 | + |
| 96 | +- **Grammar may change.** The comparator set, wildcard semantics, or |
| 97 | + scheme names could shift before ratification. This library tracks |
| 98 | + the spec; we will land breaking changes in sync with the spec, |
| 99 | + not ahead of it. |
| 100 | +- **Some ecosystems aren't covered yet.** The library today |
| 101 | + implements the semver scheme and its common aliases (see below). |
| 102 | + Schemes like PEP 440, maven, and nuget are planned but not yet |
| 103 | + implemented — the grammar parses, but comparison under those |
| 104 | + schemes would throw. |
| 105 | +- **Use cautiously in hot paths.** If your product hinges on VERS |
| 106 | + behavior, review every release's changelog for spec-driven |
| 107 | + changes. We flag them prominently. |
| 108 | + |
| 109 | +If you need a stable version-range system *today*, use the native |
| 110 | +one for your ecosystem. VERS is for tooling that spans ecosystems |
| 111 | +and is willing to absorb some pre-standard churn in exchange for |
| 112 | +uniformity. |
| 113 | + |
| 114 | +## Supported schemes |
| 115 | + |
| 116 | +This library's `Vers` class currently supports the **semver |
| 117 | +comparison scheme** and its common aliases: |
| 118 | + |
| 119 | +| Scheme | Notes | |
| 120 | +|---|---| |
| 121 | +| `semver` | Reference semver 2.0.0 comparison | |
| 122 | +| `npm` | Same as semver (npm follows semver) | |
| 123 | +| `cargo` | Same as semver (cargo follows semver, with pre-release tail differences) | |
| 124 | +| `golang` | Same as semver | |
| 125 | +| `hex` | Same as semver (Elixir/Erlang) | |
| 126 | +| `pub` | Same as semver (Dart) | |
| 127 | +| `cran` | Same as semver | |
| 128 | +| `gem` | Same as semver | |
| 129 | +| `swift` | Same as semver | |
| 130 | + |
| 131 | +Unsupported-but-declared schemes (`pypi`, `maven`, `nuget`, `deb`, |
| 132 | +`rpm`, …) parse as VERS grammar but throw when a comparison is |
| 133 | +attempted. If you need one, open an issue or PR — the scheme table |
| 134 | +is a single-line addition plus the comparison function. |
| 135 | + |
| 136 | +## The `Vers` class |
| 137 | + |
| 138 | +Located at `src/vers.ts`. |
| 139 | + |
| 140 | +```typescript |
| 141 | +class Vers { |
| 142 | + readonly scheme: string |
| 143 | + readonly constraints: readonly VersConstraint[] |
| 144 | + |
| 145 | + static parse(versStr: string): Vers |
| 146 | + static fromString(versStr: string): Vers |
| 147 | + |
| 148 | + contains(version: string): boolean |
| 149 | + toString(): string |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +### Parsing |
| 154 | + |
| 155 | +Two synonymous entry points — `Vers.parse('vers:npm/>=1.0.0|<2.0.0')` |
| 156 | +or `Vers.fromString(...)`. Both: |
| 157 | + |
| 158 | +1. Verify the string starts with `vers:`. |
| 159 | +2. Split on `/` to extract the scheme. |
| 160 | +3. Split the constraint list on `|`. |
| 161 | +4. For each constraint, extract the comparator (longest-match greedy |
| 162 | + against the COMPARATORS table). |
| 163 | +5. Validate the comparator + version combination. |
| 164 | +6. Freeze the resulting `Vers` instance (immutable, per the |
| 165 | + hardening doctrine — see `docs/hardening.md`). |
| 166 | + |
| 167 | +Failure modes, all throwing `PurlError`: |
| 168 | + |
| 169 | +- Missing `vers:` prefix. |
| 170 | +- Empty scheme or empty constraints. |
| 171 | +- Unknown comparator. |
| 172 | +- Invalid version string for the scheme. |
| 173 | +- Too many constraints (capped at `MAX_CONSTRAINTS = 1000` to |
| 174 | + prevent resource-exhaustion inputs). |
| 175 | + |
| 176 | +### Matching |
| 177 | + |
| 178 | +`vers.contains(version)` returns `true` if at least one constraint |
| 179 | +in the VERS accepts the version. For the semver scheme that means: |
| 180 | + |
| 181 | +- `=` — `compareSemver(range.version, v) === 0` |
| 182 | +- `!=` — `compareSemver(range.version, v) !== 0` |
| 183 | +- `<` / `<=` / `>` / `>=` — the obvious comparisons |
| 184 | +- `*` — always true |
| 185 | + |
| 186 | +```typescript |
| 187 | +const range = Vers.parse('vers:npm/>=1.0.0|<2.0.0') |
| 188 | +range.contains('1.5.0') // true |
| 189 | +range.contains('2.5.0') // true (matches the >=1.0.0 constraint) |
| 190 | +range.contains('0.9.0') // true (matches <2.0.0) |
| 191 | +// (OR semantics — most versions satisfy this particular range) |
| 192 | +``` |
| 193 | + |
| 194 | +Prerelease ordering follows semver 2.0.0: |
| 195 | + |
| 196 | +```typescript |
| 197 | +compareSemver('1.0.0-alpha', '1.0.0') // -1 (alpha precedes) |
| 198 | +compareSemver('1.0.0-alpha.1', '1.0.0-alpha.2') // -1 (numeric compare) |
| 199 | +compareSemver('1.0.0-alpha.1', '1.0.0-alpha') // 1 (longer > shorter) |
| 200 | +``` |
| 201 | + |
| 202 | +Build metadata (`+xyz`) is ignored in comparisons, per semver. |
| 203 | + |
| 204 | +### Round-tripping |
| 205 | + |
| 206 | +`vers.toString()` reproduces a canonical string form: |
| 207 | + |
| 208 | +```typescript |
| 209 | +const v = Vers.parse('vers:npm/>=1.0.0|<2.0.0') |
| 210 | +v.toString() // 'vers:npm/>=1.0.0|<2.0.0' |
| 211 | +``` |
| 212 | + |
| 213 | +Round-tripping is lossless — `Vers.parse(v.toString())` always |
| 214 | +produces an equivalent VERS. (Not byte-identical if the input had |
| 215 | +redundant whitespace; the string form strips.) |
| 216 | + |
| 217 | +## Writing a VERS string by hand |
| 218 | + |
| 219 | +Cheat sheet for the most common patterns: |
| 220 | + |
| 221 | +| Intent | VERS | |
| 222 | +|---|---| |
| 223 | +| Exactly version X | `vers:npm/=X` | |
| 224 | +| Any version | `vers:npm/*` | |
| 225 | +| ≥ X | `vers:npm/>=X` | |
| 226 | +| Strict greater than X | `vers:npm/>X` | |
| 227 | +| Everything except X | `vers:npm/!=X` | |
| 228 | +| "X inclusive through Y exclusive" (intent: `[X,Y)`) | **Not expressible directly** — VERS uses OR between constraints; use your ecosystem's native range or pair a lower bound with a validator. | |
| 229 | + |
| 230 | +Note the last row — **VERS constraints OR together**, so writing |
| 231 | +`>=1.0|<2.0` does not mean "≥1.0 AND <2.0" (the intuition from npm |
| 232 | +semver), it means "≥1.0 OR <2.0" (everything). This is the biggest |
| 233 | +hazard in writing VERS by hand, and a source of "this range matches |
| 234 | +way more than I expected" bugs. |
| 235 | + |
| 236 | +If your use case needs AND-of-constraints, express it as multiple |
| 237 | +separate VERS on your end and AND them in your consumer code. |
| 238 | + |
| 239 | +## Why we implement VERS |
| 240 | + |
| 241 | +Socket consumes SBOMs from every ecosystem. Every new ecosystem- |
| 242 | +specific range syntax is new parser surface to write, test, and |
| 243 | +keep in sync with upstream rules. VERS is a bet that consolidating |
| 244 | +into one parser is worth the pre-standard risk. If VERS ratifies as |
| 245 | +Ecma-NNN, every range-aware tool gets one import instead of eight. |
| 246 | + |
| 247 | +We ship it under a `pre-standard` tag so callers know what they're |
| 248 | +signing up for. |
| 249 | + |
| 250 | +## Limits and hazards |
| 251 | + |
| 252 | +Read these before relying on VERS in production: |
| 253 | + |
| 254 | +- **MAX_CONSTRAINTS = 1000.** A VERS with more than 1000 `|`- |
| 255 | + separated constraints fails to parse. This is a hard cap to |
| 256 | + prevent resource-exhaustion inputs; if you have a legitimate use |
| 257 | + case for more, open an issue with the scenario. |
| 258 | +- **MAX string length: not enforced here.** Callers receiving VERS |
| 259 | + strings from the wire should size-limit at the boundary. |
| 260 | +- **OR semantics surprise.** As noted above — hand-written ranges |
| 261 | + with multiple constraints often mean what the author intended |
| 262 | + (AND) but match what they wrote (OR). A linter rule for |
| 263 | + "VERS with ≥2 constraints should be reviewed" is not a bad idea |
| 264 | + in consumer code. |
| 265 | +- **Scheme table is small.** We implement 9 semver-aliased schemes. |
| 266 | + Others parse but fail to compare. |
| 267 | +- **Exact-equality with prereleases:** `=1.0.0` does **not** match |
| 268 | + `1.0.0-alpha` under semver rules (the latter precedes the |
| 269 | + former). If you want prerelease-inclusive exact match, use |
| 270 | + `>=1.0.0-alpha` with a matching upper bound. |
| 271 | + |
| 272 | +## Further reading |
| 273 | + |
| 274 | +- [`docs/architecture.md`](./architecture.md) — where `vers.ts` |
| 275 | + fits in the module map. |
| 276 | +- [`docs/hardening.md`](./hardening.md) — why `Vers` instances are |
| 277 | + frozen and constraint-count capped. |
| 278 | +- [`src/vers.ts`](../src/vers.ts) — the implementation. |
| 279 | +- [package-url/vers-spec](https://github.com/package-url/vers-spec) |
| 280 | + — the upstream spec this library tracks. |
| 281 | +- [semver.org](https://semver.org/) — the version comparison |
| 282 | + semantics the default scheme follows. |
0 commit comments