{"_id":"@ecosyste-ms/ecosystems-ts","_rev":"2-573af9133f4ac0cf3154aecb604c3b98","name":"@ecosyste-ms/ecosystems-ts","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@ecosyste-ms/ecosystems-ts","version":"0.1.0","keywords":["ecosyste.ms","purl","package-url","sbom","openapi","packages","advisories"],"license":"MIT","_id":"@ecosyste-ms/ecosystems-ts@0.1.0","maintainers":[{"name":"codeshark","email":"mail@codeshark.net"},{"name":"andrewnez","email":"andrewnez@gmail.com"}],"homepage":"https://github.com/ecosyste-ms/ecosystems-ts#readme","bugs":{"url":"https://github.com/ecosyste-ms/ecosystems-ts/issues"},"dist":{"shasum":"bd0a8d449418e05753d58aa08f045f5f1fd5703e","tarball":"https://registry.npmjs.org/@ecosyste-ms/ecosystems-ts/-/ecosystems-ts-0.1.0.tgz","fileCount":71,"integrity":"sha512-NBIf/A8Q9ZJGmPchLdcLKNd1tpZt5mb8P/EG9zWrjV+YGb2V5y4pTl793RRti68rse49dfgIM4VSTu6tD29dOA==","signatures":[{"sig":"MEUCIB98iXKiFqfiNzKf68ysX0rmXOtwGILeonCcdPYO4NiBAiEAiQbPcoaDKcuNpAC7JuSCeA04wZ3D2f1tm052nw/Hk/I=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":419150},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","browser":"./dist/browser-unsupported.js","default":"./dist/index.js"}},"gitHead":"66e2291e6e61a1cbf64c89373208811681349b7e","scripts":{"lint":"biome check . && tsc -p tsconfig.json --noEmit","test":"vitest run","build":"tsc -p tsconfig.build.json","clean":"node -e \"fs.rmSync('dist',{recursive:true,force:true})\"","format":"biome check . --write","generate":"node scripts/generate.mjs","typecheck":"tsc -p tsconfig.json --noEmit","sync-specs":"node scripts/sync-specs.mjs","prepublishOnly":"npm run build","sync-registries":"node scripts/sync-registries.mjs","test:integration":"vitest run --config vitest.integration.config.ts"},"_npmUser":{"name":"codeshark","email":"mail@codeshark.net"},"repository":{"url":"git+https://github.com/ecosyste-ms/ecosystems-ts.git","type":"git"},"_npmVersion":"11.18.0","description":"TypeScript client library for the ecosyste.ms APIs","directories":{},"_nodeVersion":"26.4.0","dependencies":{"openapi-fetch":"^0.17.0","packageurl-js":"^2.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"yaml":"^2.9.0","vitest":"^4.1.10","typescript":"^7.0.2","@types/node":"^26.2.0","@biomejs/biome":"^2.5.8"},"_npmOperationalInternal":{"tmp":"tmp/ecosystems-ts_0.1.0_1787343548838_0.5638975584136618","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@ecosyste-ms/ecosystems-ts","version":"0.2.0","description":"TypeScript client library for the ecosyste.ms APIs","license":"MIT","repository":{"type":"git","url":"git+https://github.com/ecosyste-ms/ecosystems-ts.git"},"homepage":"https://github.com/ecosyste-ms/ecosystems-ts#readme","bugs":{"url":"https://github.com/ecosyste-ms/ecosystems-ts/issues"},"publishConfig":{"access":"public"},"type":"module","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","browser":"./dist/browser-unsupported.js","default":"./dist/index.js"}},"main":"./dist/index.js","types":"./dist/index.d.ts","scripts":{"sync-specs":"node scripts/sync-specs.mjs","sync-registries":"node scripts/sync-registries.mjs","generate":"node scripts/generate.mjs","build":"tsc -p tsconfig.build.json","clean":"node -e \"fs.rmSync('dist',{recursive:true,force:true})\"","lint":"biome check . && tsc -p tsconfig.json --noEmit","test":"vitest run","test:integration":"vitest run --config vitest.integration.config.ts","prepublishOnly":"npm run build","format":"biome check . --write","typecheck":"tsc -p tsconfig.json --noEmit"},"dependencies":{"openapi-fetch":"^0.17.0","packageurl-js":"^2.0.1"},"devDependencies":{"@biomejs/biome":"^2.5.8","@types/node":"^26.2.0","typescript":"^7.0.2","vitest":"^4.1.10","yaml":"^2.9.0"},"keywords":["ecosyste.ms","purl","package-url","sbom","openapi","packages","advisories"],"gitHead":"463d80d9222abd0217bf914b27c9b851ce385b6d","_id":"@ecosyste-ms/ecosystems-ts@0.2.0","_nodeVersion":"26.7.0","_npmVersion":"12.0.2","dist":{"integrity":"sha512-BExYXdCZIicOwKWE3gVcEi764PqTcUn0nwaIWGG6Xg71AIb6s5DVo8hRYmKzWBQzFShR8HyYVc1WY4InzFyyMQ==","shasum":"b99ebe98bd9e202928ba00bb3be50fe23b56d817","tarball":"https://registry.npmjs.org/@ecosyste-ms/ecosystems-ts/-/ecosystems-ts-0.2.0.tgz","fileCount":71,"unpackedSize":427444,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ecosyste-ms%2fecosystems-ts@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDiWyTNxt7Zt8siHQeDLIl9B+9TDaF1N9M18sqyuWXnfgIgXnTiMPgxcfcM5//9nMVxPuI92fPyIyhvfdrTOiFiIQQ="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:e5e62f5b-6860-4b42-9215-e1414066b942"}},"directories":{},"maintainers":[{"name":"codeshark","email":"mail@codeshark.net"},{"name":"andrewnez","email":"andrewnez@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ecosystems-ts_0.2.0_1787482982201_0.5952944613963946"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-21T20:19:08.736Z","modified":"2026-08-23T11:03:02.689Z","0.1.0":"2026-08-21T20:19:08.985Z","0.2.0":"2026-08-23T11:03:02.351Z"},"bugs":{"url":"https://github.com/ecosyste-ms/ecosystems-ts/issues"},"license":"MIT","homepage":"https://github.com/ecosyste-ms/ecosystems-ts#readme","keywords":["ecosyste.ms","purl","package-url","sbom","openapi","packages","advisories"],"repository":{"type":"git","url":"git+https://github.com/ecosyste-ms/ecosystems-ts.git"},"description":"TypeScript client library for the ecosyste.ms APIs","maintainers":[{"name":"codeshark","email":"mail@codeshark.net"},{"name":"andrewnez","email":"andrewnez@gmail.com"}],"readme":"# ecosystems-ts\n\nTypeScript client library for the [ecosyste.ms](https://ecosyste.ms) APIs. See the\n[API documentation](https://ecosyste.ms/api) for details.\n\nA port of [ecosystems-go](https://github.com/ecosyste-ms/ecosystems-go). Two runtime\ndependencies:\n[`openapi-fetch`](https://openapi-ts.dev/openapi-fetch/) (6 kB) and\n[`packageurl-js`](https://github.com/package-url/packageurl-js).\n\n## Installation\n\n```bash\nnpm install @ecosyste-ms/ecosystems-ts\n```\n\nRequires Node 20+, or any server-side runtime with `fetch` (Deno, Bun, Cloudflare\nWorkers).\n\nNot browsers — the API hides the headers pagination depends on from cross-origin\nscripts, so importing this in a browser build throws. Call it from your server.\n\n## Usage\n\n```ts\nimport { EcosystemsClient } from \"@ecosyste-ms/ecosystems-ts\";\n\n// userAgent is required - identify your application\nconst client = new EcosystemsClient({ userAgent: \"my-app/1.0\" });\n\n// Bulk lookup packages by PURL\nconst { results, failures } = await client.bulkLookup([\n  \"pkg:gem/rails\",\n  \"pkg:npm/lodash\",\n  \"pkg:pypi/requests\",\n]);\n\n// Batches that failed, with the purls to retry\nconsole.log(failures.flatMap((f) => f.purls));\n\nfor (const [purl, pkg] of results) {\n  console.log(`${purl}: ${pkg.name} (${pkg.latest_release_number})`);\n}\n\n// Lookup a single package\nconst pkg = await client.lookup(\"pkg:gem/rake\");\nconsole.log(`rake has ${pkg?.versions_count} versions`);\n\n// Get a specific version\nconst version = await client.getVersion(\"rubygems.org\", \"rake\", \"13.0.0\");\nconsole.log(`rake 13.0.0 integrity: ${version?.integrity}`);\n\n// Get all versions\nconst versions = await client.getAllVersions(\"rubygems.org\", \"rake\");\nconsole.log(`rake has ${versions.length} versions`);\n\n// Get just the version numbers - one request, no Version objects\nconst numbers = await client.getVersionNumbers(\"rubygems.org\", \"rake\");\n\n// Every critical package across all registries (~9,600 today, ~96 requests)\nconst critical = await client.listCriticalPackages();\n```\n\n`lookup` is the single-package counterpart to `bulkLookup`: same PURL vocabulary, one\nrequest, one package. It takes a PURL string; `lookupPurl` takes a parsed `PackageURL`\n(see [PURL Helpers](#purl-helpers)).\n\n`bulkLookup` chunks its input at `batchSize` and sends the batches one after another, so\n5,000 PURLs is 50 round-trips. A failed batch does not fail the call — it lands in\n`failures` carrying its purls, so the other batches survive and you can retry the rest.\nNothing throws when every batch fails, so check `failures`.\n\nLookups that find nothing return `null` (or `[]` for lists) rather than throwing — \"not\nfound\" is a normal outcome for a lookup API, not an exception.\n\n## PURL Helpers\n\n```ts\nimport {\n  parsePurl,\n  purlToRegistry,\n  purlToName,\n} from \"@ecosyste-ms/ecosystems-ts\";\n\n// Parse a PURL string (handles with or without the pkg: prefix)\nconst purl = parsePurl(\"gem/rails@7.0.0\");\n\n// Convert PURL to an ecosyste.ms registry name\npurlToRegistry(purl); // \"rubygems.org\"\n\n// Convert PURL to the ecosyste.ms package name format\npurlToName(purl); // \"rails\"\n\n// Lookup using a PURL directly\nconst pkg = await client.lookupPurl(purl);\nconst version = await client.getVersionPurl(purl);\nconst versions = await client.getAllVersionsPurl(purl);\n```\n\nThe API speaks two vocabularies: bulk lookup takes PURLs, but the per-package endpoints\nare `/registries/{registry}/packages/{name}` — and a registry name is not a PURL type.\nThese helpers bridge them. The mapping is generated weekly from `GET /registries` and\ncovers all 51 ecosystems it serves; `client.listRegistries()` is authoritative at runtime.\n\n`purlToRegistry` returns `null` for types with no ecosyste.ms registry (`github`,\n`generic`, `oci`, `rpm`, …) rather than a name that 404s. `deb` resolves by vendor —\n`pkg:deb/ubuntu/curl` to the current Ubuntu LTS, `pkg:deb/debian/curl` to the current\nDebian stable, anything else to `null`.\n\n## Repository Metadata\n\n```ts\nconst repoUrl = \"https://github.com/rails/rails\";\n\nconst repo = await client.getRepository(repoUrl);\nconst packages = await client.lookupPackagesByRepositoryUrl(repoUrl, 25);\nconst advisories = await client.getAdvisoriesByRepoUrl(repoUrl, 100);\nconst commits = await client.getCommitsSummary(repoUrl);\nconst issues = await client.getIssuesSummary(repoUrl);\nconst dependents = await client.getDependentPackages(\"rubygems.org\", \"rails\", 30);\n```\n\nList methods follow `Link: rel=\"next\"` pagination and stop at the requested item cap when\none is provided. Reaching the page ceiling **throws** rather than silently returning a\nshort list.\n\nThat ceiling (`maxPages`, default 1000) exists to stop a server looping `rel=\"next\"`\nforever, not to bound result size - whole-collection crawls like `listCriticalPackages()`\nare expected to run to hundreds of pages. A call given an explicit item cap is not subject\nto it: the cap already bounds the work, so `getDependentPackages(reg, name, 500_000)` will\nnot fail at page 1000 for a limit it never set.\n\nThose methods hold every page in memory. To stream instead, `client.paginate` yields one\npage at a time and stops fetching the moment you stop consuming:\n\n```ts\nconst pages = client.paginate(() =>\n  client.packages.GET(\"/registries/{registryName}/packages/{packageName}/dependent_packages\", {\n    params: { path: { registryName: \"npmjs.org\", packageName: \"lodash\" } },\n  }),\n);\n\nfor await (const page of pages) {\n  for (const pkg of page) console.log(pkg.name);\n}\n```\n\nIt takes a thunk returning any `openapi-fetch` result, so it works on the endpoints this\nfacade wraps and on the many it does not. The page ceiling still applies and still throws,\nso a truncated stream can never be mistaken for a complete one.\n\n## Options\n\n```ts\nconst client = new EcosystemsClient({\n  userAgent: \"my-app/1.0\",       // required\n  from: \"you@example.com\",       // From header - see Rate limits below\n  apiKey: \"your-api-key\",        // Authorization: Bearer - see Rate limits below\n  batchSize: 50,                 // PURLs per bulk lookup request (max 100)\n  timeoutMs: 30_000,             // deadline per HTTP request, including its retries\n  maxPages: 1000,                // runaway-pagination guard; see Pagination below\n  retry: { attempts: 3 },        // or `false` to disable\n  fetch: myFetch,                // custom fetch (proxies, agents, tests)\n  servers: {                     // per-service base URL overrides; any of\n    packages: \"https://custom.packages.server\",  // packages, repos, advisories,\n  },                                             // commits, issues\n});\n```\n\nEvery method takes an optional trailing `{ signal }` for cancellation.\n\n```ts\nconst controller = new AbortController();\nconst pkg = await client.lookup(\"pkg:gem/rake\", { signal: controller.signal });\n```\n\n`timeoutMs` covers one HTTP request and its retries, so every bulk batch and every\nfollowed page gets its own budget. For a deadline over a whole operation — 50 batches of\na large `bulkLookup`, say — pass your own signal:\n\n```ts\nawait client.bulkLookup(purls, { signal: AbortSignal.timeout(120_000) });\n```\n\n## Rate limits\n\necosyste.ms runs two pools. Identifying yourself triples your quota:\n\n```ts\nnew EcosystemsClient({ userAgent: \"my-app/1.0\", from: \"you@example.com\" });\n```\n\nPassing `from` appends `(mailto:you@example.com)` to your User-Agent and sets the `From`\nheader. The User-Agent is what does the work. Measured 2026-08-16, every sample a\n`cf-cache-status: MISS`:\n\n| Identification | Tier | Limit |\n| --- | --- | --- |\n| nothing | `anonymous` | 5000/hr |\n| `From:` header | `anonymous` | 5000/hr |\n| email in the User-Agent | `polite` | 15000/hr |\n| `?mailto=` query parameter | `polite` | 15000/hr |\n\nThe `From` header alone does nothing. We send it as the polite HTTP convention, but never\nrely on it — put the email in the User-Agent, which is what `from` does for you.\n\n`?mailto=` works too and is deliberately not used: the CDN sends `Vary: Origin` only, so\na query parameter gives every email its own cache entry and fragments the shared cache.\nIdentifying through the User-Agent costs nothing.\n\n### API keys\n\nIf you have been issued a key, pass it as `apiKey` and it is sent as\n`Authorization: Bearer <key>` on every request, across all five services:\n\n```ts\nnew EcosystemsClient({ userAgent: \"my-app/1.0\", apiKey: process.env.ECOSYSTEMS_API_KEY });\n```\n\nKeys are not part of the public ecosyste.ms flow — there is no self-serve signup and the\nOpenAPI specs do not describe the scheme — so this is here for hosted or sponsor\ndeployments that do issue one, and for self-hosted instances behind a gateway. The tiers\nmeasured above are the ones an anonymous or polite caller sees; a key's quota is whatever\nthe issuing deployment sets. Identify through the User-Agent as well either way — it costs\nnothing and is what earns the polite pool when no key is in play.\n\n### Observing the limit\n\nThe most recent response's rate-limit headers are available read-only:\n\n```ts\nclient.rateLimit; // { limit, remaining, reset, tier, consumer } | null\n```\n\n**Do not build control flow on these.** Responses are CDN-cached, so on a cache hit you\nreceive the cached response's rate-limit headers rather than your own. For real\nbackpressure the client already reacts to 429 and `Retry-After`.\n\n## Beyond the wrapped methods\n\nThe methods above cover the common cases. The generated clients are exposed for\neverything else — the specs describe far more, including `/versions/lookup` (lookup by\nintegrity/sha256/sha1/sha512 hash), `/keywords`, `/registries/{r}/maintainers`, all of\n`/topics` and `/hosts/**`, and issue/commit detail endpoints.\n\n```ts\nconst { data, error, response } = await client.packages.GET(\"/versions/lookup\", {\n  params: { query: { sha256: \"abc123...\" } },\n});\n```\n\n`client.packages`, `.repos`, `.advisories`, `.commits` and `.issues` are typed\n`openapi-fetch` clients sharing the same identity, retry and rate-limit handling.\n\n## Generated Code\n\n`src/generated/` contains types generated from the vendored specs in `specs/`. To refresh:\n\n```bash\nnpm run sync-specs   # download the latest OpenAPI specs, record sha256 in specs/.lock.json\nnpm run generate     # regenerate types + server URLs\n```\n\nBoth the specs and the generated output are committed, and CI fails if they drift apart.\nBase URLs are read from each spec's `servers[0].url` rather than hardcoded.\n\nThe generator is not a dependency of this package. `npm run generate` runs\nopenapi-typescript from a throwaway `npx` install, pinned to exact versions at the top of\n`scripts/generate.mjs` and tracked by Renovate. It brings its own `typescript@5`, because\nopenapi-typescript emits through the TypeScript compiler API (`ts.factory`) and the\n`typescript@7` this package compiles with no longer ships one.\n\n## Testing\n\n```bash\nnpm test                  # unit tests (loopback HTTP servers, no network)\nnpm run test:integration  # integration tests (hits the live API)\n```\n\nSet `ECOSYSTEMS_FROM` to run the integration suite against the polite pool.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}