# `Lightning.Adaptors.NPM.Registry`
[🔗](https://github.com/OpenFn/lightning/blob/main/lib/lightning/adaptors/npm/registry.ex#L1)

NPM registry HTTP client for `Lightning.Adaptors.NPM`.

Talks to `registry.npmjs.org`. Responsible for the `list_adaptors/0`
scope listing and the packument endpoint `fetch_adaptor/1` uses.

`list_adaptors/0` deliberately merges two endpoints rather than calling
one:

  * `/-/user/openfn/package` is the authoritative name list for the
    `@openfn` org and the trust boundary, since it cannot return a
    name Lightning does not already trust. It has no version data.
  * `/-/v1/search` is only a cheap version lookup for whichever of those
    names it happens to cover. npm's relevance ranking demotes or
    excludes deprecated packages from search results even on an
    exact-name query, so search alone silently drops names. It is not a
    safe source of scope membership, only of version data for names
    already known to be in scope.

Any authoritative name missing from the search results falls back to a
per-name `get_packument/1` and `latest_version/1` call, bounded to the
handful of names search does not cover. Do not "simplify" this back to
a single search call or a pagination bump. Search's result count is
capped by npm's relevance ranking regardless of `size` and `from`, and
deprecated packages are excluded from ranking entirely, so no amount of
paging recovers them.

Base URL via `Lightning.Adaptors.Config.strategy_opts(Lightning.Adaptors.NPM)[:registry_url]`,
default `https://registry.npmjs.org`.

# `build_versions`

```elixir
@spec build_versions(map()) :: [map()]
```

Build the per-version `version_record` list from a packument.

# `deprecated?`

```elixir
@spec deprecated?(map(), String.t()) :: boolean()
```

Is the given `version` in `packument` flagged as deprecated?

# `get_packument`

```elixir
@spec get_packument(String.t()) ::
  {:ok, map()} | {:error, :not_found} | {:error, term()}
```

Fetch the full packument for a package.

# `latest_version`

```elixir
@spec latest_version(map()) :: {:ok, String.t()} | {:error, :no_latest_version}
```

Extract the `dist-tags.latest` version from a packument.

# `list_adaptors`

```elixir
@spec list_adaptors() ::
  {:ok, [%{name: String.t(), latest_version: String.t()}]} | {:error, term()}
```

Full `@openfn/language-*` scope listing, each with a real `latest_version`.

Merges `/-/user/openfn/package` (authoritative name list) with
`/-/v1/search` (cheap version lookup), falling back to a per-name
packument fetch for any name search doesn't cover. See the moduledoc for
why this isn't a single call.

The `@openfn` org holds hundreds of packages, so a registry answering
with none of them has not told us the adaptors are gone; it is a mirror
that has not synced the scope, or a mistyped URL. Rather than report an
empty catalogue and have the Scheduler act on it, an empty list is
`{:error, :empty_listing}` and a 200 whose body is not a map is
`{:error, :malformed_listing}`, both of which leave the stored rows alone.

# `repository_url`

```elixir
@spec repository_url(term()) :: String.t() | nil
```

Normalise the packument's `repository` field to a plain URL string.

# `require_tarball_url`

```elixir
@spec require_tarball_url(map(), String.t()) ::
  {:ok, String.t()} | {:error, :no_tarball_url}
```

Resolve the tarball URL for `version` in `packument`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
