@knorby/eu-cosing-client
v0.2.0
Published
A universal TypeScript client for the European Commission's CosIng cosmetic ingredient database.
Readme
@knorby/eu-cosing-client
A universal TypeScript client for the European Commission's CosIng (Cosmetic Ingredient) database — cosmetic ingredient identity (INCI/CAS/EC), functions, and the regulatory Annexes of Regulation (EC) No 1223/2009, for Node tools and Expo/React Native apps.
This library is unofficial and not affiliated with, endorsed by, or supported by the European Commission or the European Union. CosIng is an informative, non-binding reference database: inclusion in CosIng does not establish that an ingredient is approved for cosmetic use, and the data this client returns is not medical advice. Only Regulation (EC) No 1223/2009 and its Annexes — not CosIng — establish whether and under what conditions a substance may be used in cosmetic products. Always consult the operative legislation and qualified professionals before making decisions based on this data.
About the data source
CosIng is the European Commission's cosmetic ingredient database. It contains ingredient names and CAS/EC identifiers, ingredient functions, chemical descriptions, scientific opinions (SCCS), and the restricted/prohibited substance lists from the Annexes of the Cosmetics Regulation. Entries are marked current or historical (status), and the database is updated regularly.
With gratitude to the European Commission for publishing and maintaining CosIng and for making its data available for reuse. EU-owned content on the Commission's websites is generally licensed under CC BY 4.0 — reuse is permitted with appropriate credit and indication of changes; asset-specific notices and exclusions may apply. Verify the current reuse notice before redistributing CosIng-derived data. This client's software is Apache-2.0 and does not relicense the upstream data.
Features
- Guided, fully typed API — search ingredients by free text, exact INCI, or wildcard CAS/EC, retrieve records by stable source ID, browse the ingredient function vocabulary, and acquire + parse the Annex II–VI regulatory exports as structured entries.
- Two layers — curated typed projections (
CosingIngredient,CosingFunction,AnnexEntry, …) plus a raw source-native access path for everything else. - Regulatory semantics preserved — Annex II prohibition, Annex III
conditional restriction, and unknown status stay distinct; concentration
bases, conditions, warnings, and SCCS references are kept verbatim. No
banned: booleanflattening, no invented fields. - Universal runtime — works in Node ≥ 18, browsers (where CORS allows),
and React Native/Expo; injectable
fetch; the only runtime dependency ispapaparsefor the annex CSV exports. - Predictable errors — typed error taxonomy separating timeouts, network failures, HTTP/API errors, parse failures, and configuration problems; empty results are empty arrays, never errors.
- Provenance on every record — source ID, source link, and retrieval time; source-supplied update dates are preserved and never replaced.
Install
npm install @knorby/eu-cosing-clientQuick start
import { CosingClient } from "@knorby/eu-cosing-client";
const client = new CosingClient();
// Free-text search (fuzzy) — 17 retinol-ish results:
const fuzzy = await client.ingredients.search({ text: "retinol" });
// Exact INCI match — 1 result:
const exact = await client.ingredients.search({ inciName: "RETINOL" });
// Retrieve by stable CosIng substance ID:
const retinol = await client.ingredients.get("37479");
// Functions assigned by the source, with vocabulary definitions:
const fns = await client.ingredients.getFunctions("37479");
// Resolve annex-linked substances (e.g. CMR entries) to full records:
const linked = retinol?.item.identifiedIngredientIds ?? [];
for (const substanceId of linked) {
const substance = await client.substances.get(substanceId);
// substance?.item.refNo, .annexNumbers, .wordingOfConditions, ...
}
// The full function vocabulary (async generator):
for await (const fn of client.functions.listAll()) {
console.log(fn.item.name);
}
// Regulatory annexes (no API key needed):
const annexII = await client.annexes.get("II");
annexII.entries[0]; // { kind: "prohibited", referenceNumber: "1", casNumbers: [...], ... }
const annexIII = await client.annexes.get("III"); // kind: "restricted"Search returns candidates with match metadata, so ambiguity is preserved:
const results = await client.ingredients.search({ text: "retinol" });
results.items[0].matchedOn; // "text" | "inciName" | "casNo" | ...
results.items[0].exact; // boolean — exact field match vs fuzzy/derivedAPI keys
Ingredient search uses the Commission's generic EU Search service, which
requires an API key — but not one you can register for. There is no signup
or developer portal: the only key is a static, public application key the
CosIng web app itself ships to every browser in its configuration
(env-json-config.json).
This client ships that key as a built-in default, so search works with no
configuration. The terms under which third parties may reuse that key are
not documented; annex export methods need no key at all.
The key can rotate when the web app is redeployed. If searches suddenly fail
with HTTP 400/500, pass a fresh value via new CosingClient({ apiKey }) —
maintainers can check for rotation with npm run check:search-key, and a
daily scheduled workflow runs the live test suite plus that check, opening a
tracking issue if either fails.
Requests embed the key in the URL query string; the client redacts the configured key from every error message and diagnostic it produces.
Universal runtime notes
| Runtime | Support |
| --- | --- |
| Node ≥ 18 | ✅ full |
| React Native / Expo | ✅ full (React Native provides global fetch; inject it if your environment lacks one) |
| Browsers | ⚠️ subject to upstream CORS — browser failure is not proof of native failure |
- The client uses global
fetchandAbortController, and injectsfetchwhere needed:new CosingClient({ fetch: myFetch }). - The search API requires
multipart/form-data; rather than relying on React Native's historically flakyFormData/Blobupload behavior, the client serializes multipart bodies manually as strings — this is invisible to consumers. - The core runtime imports no Node builtins (
node:*,fs,path, …).
Pagination
Search methods accept page and pageSize and return { items, page,
pageSize, total, hasMore }. ingredients.searchAll, substances.searchAll,
and functions.listAll return async generators yielding individual matches.
They stop when the source reports no more results (page * pageSize >= total)
or after the hard safety cap of 500 pages. Reaching that cap is not proof
that the complete inventory was retrieved.
Annex exports are complete single-file CSVs — no pagination.
Known source quirks
textvstermmatching — thetextparameter is fuzzy full-text search; exact criteria (INCI, status, function) are translated to the API'stermfilters.- CAS/EC lookup uses the source's wildcard query — the
textparameter does not index identifier fields at all (text=68-26-8returns zero results).ingredients.search({ casNo })instead sends the wildcard query clause the official web app's advanced search uses (*value*scoped to thecasNo/ecNofields, with Lucene special characters escaped), so partial values match multi-identifier strings. It is a substring match, so results carryexact: false. - Multi-value identifier strings — CAS/EC numbers for an entry are
stored as one
" / "-joined string (e.g."68-26-8 / 11103-57-4"). The curated types split these into arrays and preserve the raw string. - Array-valued metadata — every search metadata field is an array in the source; curated types unwrap singletons but the raw layer preserves the shape 1:1.
- Export endpoints only support GET — HEAD requests return 405.
- A private JSON API exists but requires internal EC auth — this client uses only the public search + export surfaces.
Testing
Deterministic offline tests run in CI with injected fetch mocks and captured fixtures. Live smoke tests against the real endpoints are opt-in and never run in CI:
COSING_LIVE_TESTS=1 npm run test:liveDevelopment
| Command | What it does |
| --- | --- |
| npm run build | Build the package (tsup + tsc — dual ESM/CJS output with .d.ts/.d.cts declarations) |
| npm run dev | Build in watch mode |
| npm run lint | Lint + formatting check with Biome (read-only) |
| npm run format | Format with Biome (writes changes) |
| npm run check | Lint + format in one pass (writes changes) |
| npm run typecheck | Type-check src/ + tests/ with tsc (no emit) |
| npm test | Run tests once (Vitest) |
| npm run test:live | Run live smoke tests against the real CosIng endpoints (opt-in, never in CI; COSING_API_KEY only overrides the built-in public search key) |
| npm run check:search-key | Compare the built-in public search key in src/constants.ts against the CosIng web app's live public config (also runs daily in the live-smoke workflow) |
| npx changeset | Create a changeset (required for changes affecting published output) |
See CONTRIBUTING.md for the full workflow.
License
Apache-2.0 © Kali Norby (@knorby). CosIng data is © the European Commission (see the reuse notice above); this license covers the client software only.
