The Nyora library (nyora-sdk) is a self-contained, in-process manga sources SDK
for Node.js. It embeds the JavaScript parser bundle inside a jsdom
window, so it needs no JVM helper: HTTP is handled by Node's native fetch
and HTML parsing by jsdom. The parser bundle and source catalog are kept current
through over-the-air (OTA) updates.
This guide documents every public export of src/index.ts — its classes,
functions, type interfaces, JSON factories, error classes, and constants — with
real signatures and runnable examples. Nothing here is invented: every symbol,
method, option, and field below exists in the published source.
nyora-sdk0.1.1>=18 (relies on global fetch)"type": "module")./dist/index.js · Types: ./dist/index.d.tsNyora classnpm install nyora-sdk
Because the package is ESM-only, import it with import (or a dynamic
await import(...) from CommonJS):
import { Nyora } from "nyora-sdk";
// or: import Nyora from "nyora-sdk"; // default export is the Nyora class
The full export surface:
import Nyora, {
// Client + services
Nyora as NyoraClient,
SourcesService,
MangaService,
sourceToHelperShape,
// OTA
OtaManager,
OTA_BASE,
// Runtime
ParserRuntime,
BROWSER_UA,
// Server
NyoraServer,
// Config / discovery
defaultPortFile,
readBaseUrlFromPortFile,
BASE_URL_ENV,
HELPER_PORT_FILE_ENV,
// Errors
NyoraError,
ParserRuntimeError,
HelperNotFoundError,
NyoraHTTPError,
// JSON factories
mangaPageFromJson,
mangaChapterFromJson,
mangaFromJson,
sourceFromJson,
searchPageFromJson,
mangaDetailsFromJson,
} from "nyora-sdk";
// Type-only exports:
import type {
RuntimeLike,
CallArgs,
ParserMethod,
JsonDict,
MangaPage,
MangaChapter,
Manga,
Source,
SearchPage,
MangaDetails,
OtaManifest,
OtaArtifact,
OtaUpdateResult,
OtaUpdateAvailability,
} from "nyora-sdk";
import { Nyora } from "nyora-sdk";
const client = new Nyora();
try {
// Resolve a source by id or name substring (case-insensitive)
const source = client.sources.find("asura");
// Browse popular manga
const page = await client.manga.popular(source.id);
const first = page.entries[0];
// Load full details + chapters
const details = await client.manga.details(source.id, first.url, {
title: first.title,
});
console.log(details.manga.title, "—", details.chapters.length, "chapters");
// Resolve the readable pages of the first chapter
const pages = await client.manga.pages(source.id, details.chapters[0].url);
console.log(pages.map((p) => p.url));
} finally {
client.close(); // release the embedded jsdom window
}
Always call
client.close()when you are done. The client owns a jsdom window whose resources are released byclose(). The call is safe to invoke more than once.
Nyora clientNyora is the default SDK entry point and the package's default export. It
drives an embedded ParserRuntime entirely in-process and exposes three things:
| Member | Type | Purpose |
|---|---|---|
client.sources |
SourcesService |
List and look up bundled sources |
client.manga |
MangaService |
Browse, search, details, pages |
client.ota |
OtaManager |
Over-the-air bundle/catalog updates |
new Nyora(options?: { ota?: OtaManager; runtime?: RuntimeLike })
options.ota — a pre-built OtaManager to share (e.g. with a custom cache
directory). When omitted, a default OtaManager is created.options.runtime — a pre-built runtime implementing RuntimeLike, used
instead of creating a jsdom-backed ParserRuntime. This is primarily for
testing (you can inject a stub that needs no jsdom window or network).import { Nyora, OtaManager } from "nyora-sdk";
const ota = new OtaManager({ cacheDir: "/tmp/nyora-cache" });
const client = new Nyora({ ota });
client.update(options?): Promise<OtaUpdateResult>Fetch the latest OTA parser bundle, then reload the embedded runtime so the new parsers take effect.
update(options?: { force?: boolean }): Promise<OtaUpdateResult>
options.force — re-download and reload even when the installed version is
already current.The returned OtaUpdateResult has updated set to false when the cache was
already up to date.
const result = await client.update();
if (result.updated) {
console.log("Updated to OTA version", result.version);
} else {
console.log("Already up to date (OTA version", result.version, ")");
}
client.checkUpdate(): Promise<OtaUpdateAvailability>Check whether a newer OTA bundle is available, without downloading anything. This
delegates to client.ota.isUpdateAvailable() and is network-error tolerant.
const { available, installed, latest } = await client.checkUpdate();
if (available) console.log(`Update: ${installed} -> ${latest}`);
client.close(): voidClose the embedded parser runtime and release its resources. Safe to call more than once.
SourcesServiceAccessible as client.sources, or constructable directly from any RuntimeLike:
new SourcesService(runtime: RuntimeLike)
list(): Source[]List every source available in the bundled catalog. Each raw catalog entry is
normalized through sourceToHelperShape and then sourceFromJson.
for (const s of client.sources.list()) {
console.log(`${s.id}\t${s.name}\t${s.lang}`);
}
find(query: string): SourceFind the first bundled source whose id or name contains query
(case-insensitive substring match). Throws a plain Error —
No bundled source matched '<query>' — when nothing matches.
const source = client.sources.find("asura"); // matches id or name
MangaServiceAccessible as client.manga, or constructable directly:
new MangaService(runtime: RuntimeLike)
All listing methods return a SearchPage whose hasNextPage is derived as
entries.length > 0 (i.e. a non-empty page is assumed to have a successor).
popular(sourceId, page?): Promise<SearchPage>popular(sourceId: string, page = 1): Promise<SearchPage>
Fetch a page of popular manga. page is one-based (default 1).
const page1 = await client.manga.popular(source.id);
const page2 = await client.manga.popular(source.id, 2);
console.log(page1.entries.length, "entries; more?", page1.hasNextPage);
latest(sourceId, page?): Promise<SearchPage>latest(sourceId: string, page = 1): Promise<SearchPage>
Fetch a page of the latest updated manga. Same shape and pagination as
popular.
const fresh = await client.manga.latest(source.id);
search(sourceId, query, page?): Promise<SearchPage>search(sourceId: string, query: string, page = 1): Promise<SearchPage>
Search a source for manga matching a free-text query.
const results = await client.manga.search(source.id, "frieren");
for (const m of results.entries) console.log(m.title, m.url);
details(sourceId, mangaUrl, options?): Promise<MangaDetails>details(
sourceId: string,
mangaUrl: string,
options?: { title?: string },
): Promise<MangaDetails>
Fetch full metadata and the chapter list for one manga. mangaUrl may be
source-relative or absolute. options.title is a known title passed through to
the parser to help it resolve the entry (it defaults to "").
The returned MangaDetails has both details.manga (a Manga) and
details.chapters (a MangaChapter[]). Chapters are extracted from the parser's
manga object when present.
const entry = results.entries[0];
const details = await client.manga.details(source.id, entry.url, {
title: entry.title,
});
console.log(details.manga.description);
console.log(details.chapters.map((c) => c.title));
pages(sourceId, chapterUrl, options?): Promise<MangaPage[]>pages(
sourceId: string,
chapterUrl: string,
options?: { branch?: string | null },
): Promise<MangaPage[]>
Resolve the readable image pages of a single chapter. chapterUrl may be
source-relative or absolute. options.branch selects a scanlation
branch/translation (defaults to null).
const chapter = details.chapters[0];
const pages = await client.manga.pages(source.id, chapter.url);
for (const p of pages) {
console.log(p.url, p.headers); // headers may include e.g. Referer
}
sourceToHelperShape(source): JsonDictsourceToHelperShape(source: Record<string, unknown>): JsonDict
Normalize a raw bundled source record (with id, title/name, domain,
locale, isNsfw fields) into the camelCase helper REST source shape. This is
the exact transform SourcesService.list() and the REST server's /sources
route apply. The output is suitable for sourceFromJson.
import { sourceToHelperShape } from "nyora-sdk";
sourceToHelperShape({ id: "ASURASCANS_US", title: "MangaDex", domain: "asura.org", locale: "en" });
// {
// id: "ASURASCANS_US",
// name: "MangaDex",
// lang: "en",
// baseUrl: "https://asura.org",
// engine: "JavaScript",
// contentType: "Manga",
// isInstalled: true,
// isPinned: false,
// isNsfw: false,
// canUninstall: false,
// }
ParserRuntime — the low-level engineParserRuntime is the no-helper execution engine that runs the bundled
parsers.bundle.js inside a jsdom window. The Nyora client wraps it; use it
directly when you want raw parser results or your own dispatch logic.
new ParserRuntime(options?: { ota?: OtaManager })
options.ota — an OtaManager to source the bundle text from. When omitted,
a default manager is created (cache first, then the bundled fallback asset).On construction the runtime builds a jsdom window, evaluates the bundle, and
captures the NyoraParsers global (which must expose getAllSources and
getParser); otherwise it throws a ParserRuntimeError.
sources(): Record<string, unknown>[]Return the catalog of sources as raw parser metadata objects (camelCase keys,
exactly as NyoraParsers.getAllSources() emits), or an empty array if the bundle
returns a non-array.
call(sourceId, method, args?): Promise<unknown>call(sourceId: string, method: ParserMethod, args?: CallArgs): Promise<unknown>
Invoke one parser method and return its decoded result (round-tripped through
JSON to detach from jsdom-backed objects). The sourceId may include a
parser: prefix, which is stripped. Throws a ParserRuntimeError when the
parser is missing, the method is unknown, or the parser/engine fails.
method is one of the ParserMethod values: "popular", "latest",
"search", "details", "pages".
import { ParserRuntime } from "nyora-sdk";
const rt = new ParserRuntime();
try {
const sources = rt.sources();
const popular = await rt.call("ASURASCANS_US", "popular", { page: 1 });
const results = await rt.call("ASURASCANS_US", "search", { query: "frieren" });
} finally {
rt.close();
}
reload(): voidRe-read the parser bundle via the OtaManager and rebuild the jsdom context from
scratch. Call this after an OTA update so the new parsers take effect.
close(): voidRelease the jsdom window. Safe to call more than once.
CallArgsThe argument bag accepted by ParserRuntime.call:
interface CallArgs {
page?: number; // one-based page for list methods
query?: string; // free-text query for "search"
url?: string; // manga/chapter URL for "details"/"pages"
title?: string; // known title passed to "details"
manga?: unknown; // pre-built manga object for "details"
branch?: string | null; // scanlation branch passed to "pages"
filter?: Record<string, unknown>; // extra filter for list methods
}
ParserMethodtype ParserMethod = "popular" | "latest" | "search" | "details" | "pages";
BROWSER_UAThe browser-like User-Agent string sent with every parser HTTP request:
export const BROWSER_UA =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
RuntimeLikeThe minimal runtime interface a Nyora client (and the services) depend on.
Implement it to inject a stub runtime for testing.
interface RuntimeLike {
sources(): Record<string, unknown>[];
call(sourceId: string, method: string, args: Record<string, unknown>): Promise<unknown>;
reload(): void;
close(): void;
}
import { Nyora, type RuntimeLike } from "nyora-sdk";
const stub: RuntimeLike = {
sources: () => [{ id: "FAKE", title: "Fake", domain: "fake.test", locale: "en" }],
call: async () => [],
reload: () => {},
close: () => {},
};
const client = new Nyora({ runtime: stub });
OtaManager — over-the-air updatesOtaManager keeps the parser bundle and source catalog current without a package
release. It fetches a manifest from the OTA feed, verifies each artifact by
SHA-256, and writes the bundle, catalog, and manifest atomically into a per-user
cache directory. When nothing is cached, reads fall back to the assets shipped
inside the package, so the SDK works fully offline on first run.
new OtaManager(options?: { cacheDir?: string; timeoutMs?: number })
options.cacheDir — directory for cached artifacts. Defaults to the per-user
cache directory .../nyora/ota.options.timeoutMs — HTTP timeout (ms) for manifest and artifact fetches.
Defaults to 30000.cacheDir (getter)The directory where OTA artifacts are cached.
const ota = new OtaManager();
console.log(ota.cacheDir);
fetchManifest(): Promise<OtaManifest>Download and parse the remote manifest from ${OTA_BASE}/manifest.json. Throws
a NyoraError if it cannot be fetched, is invalid JSON, or is not a JSON object.
installedVersion(): number | nullReturn the integer manifest version currently cached, or null when nothing is
cached or the cached manifest is unreadable.
isUpdateAvailable(): Promise<OtaUpdateAvailability>Check whether the remote feed offers a newer version. Network/manifest errors are treated as "no update available" rather than thrown, so it is safe to call opportunistically.
const { available, installed, latest } = await ota.isUpdateAvailable();
update(options?): Promise<OtaUpdateResult>update(options?: { force?: boolean }): Promise<OtaUpdateResult>
Download and cache the latest bundle and catalog. The manifest is fetched, each
artifact is SHA-256-verified, and all files are written atomically. When the
cache is already current and force is false, nothing is downloaded and
updated is false. Throws a NyoraError if the manifest or an artifact cannot
be fetched, or if an artifact fails SHA-256 verification.
const r = await ota.update({ force: true });
console.log(r.updated, r.version, r.bundlePath, r.sourcesPath);
Note:
OtaManager.updatedoes not reload any runtime. UseNyora.update()(which reloads the embedded runtime) or callParserRuntime.reload()yourself afterward.
readBundleText(): stringReturn the parser bundle source text — the cached copy, or the package-bundled fallback when no cache exists.
readSourcesText(): stringReturn the source catalog JSON text — the cached copy, or the package-bundled fallback when no cache exists.
OTA_BASEexport const OTA_BASE = "https://Hasan72341.github.io/nyora-ota-parsers";
The base URL of the public OTA parser feed. Artifacts are served at
manifest.json, parsers.bundle.js, and sources.json under this base.
NyoraServer — helper-compatible REST serverNyoraServer lets the JS SDK act as a Nyora helper. It serves the same camelCase
REST endpoints the JVM helper exposes — /health, /sources,
/sources/popular / /latest / /search, /manga/details, /manga/pages —
backed by an embedded ParserRuntime. Requests are serialized onto the single
jsdom-backed runtime via a promise chain, and every error is returned as clean
JSON rather than a 500 stack trace.
new NyoraServer(options?: {
host?: string;
port?: number;
runtime?: ServerRuntime;
writePortFile?: boolean;
})
options.host — interface to bind. Defaults to "127.0.0.1".options.port — port to bind, or 0 for a free ephemeral port. Defaults to
0.options.runtime — an existing runtime to serve. When omitted, a new
ParserRuntime is created and owned (and closed on stop()).options.writePortFile — whether to write the bound port to the standard
helper port file so other apps can discover this server. Defaults to true.start(): Promise<string>Start serving in the background and resolve to the base URL. Idempotent: calling
it again while running resolves to the existing URL. When writePortFile is
enabled, the bound port is persisted to the helper port file.
stop(): Promise<void>Close the listening socket and close an owned runtime. Safe to call when not running.
baseUrl (getter)The http://host:port base URL. Throws a NyoraError if the server has not been
started yet.
import { NyoraServer } from "nyora-sdk";
const server = new NyoraServer({ port: 0 });
const url = await server.start(); // e.g. "http://127.0.0.1:54187"
console.log("listening at", server.baseUrl);
const res = await fetch(`${url}/sources`);
const { sources } = await res.json();
await server.stop();
All responses are JSON. Runtime calls are serialized one at a time.
| Method | Path | Query params | Success body |
|---|---|---|---|
| GET | /health |
— | { "ok": true, "engine": "node-jsdom" } |
| GET | /sources |
— | { "sources": [ ...helper-shaped... ] } |
| GET | /sources/popular |
id (req), page=1 |
{ "entries": [...], "hasNextPage": bool } |
| GET | /sources/latest |
id (req), page=1 |
{ "entries": [...], "hasNextPage": bool } |
| GET | /sources/search |
id (req), q (req), page=1 |
{ "entries": [...], "hasNextPage": bool } |
| GET | /manga/details |
id (req), url (req), title? |
{ "manga": {...}, "chapters": [...] } |
| GET | /manga/pages |
id (req), url (req), branch? |
{ "pages": [...] } |
For the listing routes, hasNextPage is entries.length > 0.
| Status | When | Body |
|---|---|---|
400 |
Missing required param, or non-integer page |
{ "error": "Missing required query parameter: '<name>'" } / { "error": "Query parameter '<name>' must be an integer" } |
404 |
Unknown path | { "error": "Not found: <path>" } |
502 |
SDK/parser failure (NyoraError) |
{ "error": "<message>" } |
500 |
Any other unexpected error | { "error": "<Name>: <message>" } |
These helpers let a client discover a running Nyora helper (or an embedded
NyoraServer) without explicit configuration.
defaultPortFile(): stringReturn the platform-specific helper port-file path. Honors
NYORA_HELPER_PORT_FILE when set (a leading ~ is expanded to the home
directory); otherwise:
~/Library/Application Support/Nyora/helper.port%APPDATA%\Nyora\helper.port.../nyora/helper.portimport { defaultPortFile } from "nyora-sdk";
console.log(defaultPortFile());
readBaseUrlFromPortFile(portFile?): string | nullreadBaseUrlFromPortFile(portFile?: string): string | null
Read the port file (defaulting to defaultPortFile()), which holds a single
port number as plain text, and return http://127.0.0.1:<port>. Returns null
when the file does not exist or is empty.
import { readBaseUrlFromPortFile } from "nyora-sdk";
const baseUrl = readBaseUrlFromPortFile();
if (baseUrl) {
const res = await fetch(`${baseUrl}/health`);
}
export const BASE_URL_ENV = "NYORA_BASE_URL"; // explicit helper base URL
export const HELPER_PORT_FILE_ENV = "NYORA_HELPER_PORT_FILE"; // override port-file path
NYORA_BASE_URL names the env var for an explicit helper base URL (it
overrides port-file discovery in your own integration logic).NYORA_HELPER_PORT_FILE names the env var that overrides the port-file path;
defaultPortFile() honors it directly.The SDK defines a small error hierarchy so callers can catch failures
selectively. NyoraError is the common base; the others extend it.
class NyoraError extends Error {} // base
class ParserRuntimeError extends NyoraError {} // parser/runtime failure
class HelperNotFoundError extends NyoraError {} // no helper discoverable
class NyoraHTTPError extends NyoraError { // non-2xx from a helper
readonly statusCode: number; // HTTP status (>= 400)
readonly body: string; // raw response body
}
import { Nyora, ParserRuntimeError, NyoraError } from "nyora-sdk";
const client = new Nyora();
try {
await client.manga.popular("DOES_NOT_EXIST");
} catch (err) {
if (err instanceof ParserRuntimeError) {
console.error("parser failed:", err.message);
} else if (err instanceof NyoraError) {
console.error("sdk error:", err.message);
}
} finally {
client.close();
}
NyoraHTTPErrorexposesstatusCodeandbody. TheNyoraServer._handlepath maps a thrownNyoraErrorto an HTTP502.
Every data model has a tolerant *FromJson factory that accepts the raw
camelCase payloads emitted by the parser runtime (or the helper REST API) and
coerces field types defensively: missing or malformed fields fall back to
sensible defaults (empty strings, 0, empty arrays, null, false) instead of
throwing. An array expected where a non-array is found becomes []; an object
expected where a non-object is found becomes {}.
JsonDicttype JsonDict = Record<string, unknown>;
MangaPage · mangaPageFromJson(data)interface MangaPage {
url: string;
headers: Record<string, string>; // e.g. { Referer: "..." }
}
mangaPageFromJson(data) accepts a page object or a bare string (treated as
the URL). All header keys and values are stringified.
import { mangaPageFromJson } from "nyora-sdk";
mangaPageFromJson("https://img.example/1.jpg"); // { url: "...", headers: {} }
mangaPageFromJson({ url: "https://img.example/1.jpg", headers: { Referer: "https://example/" } });
MangaChapter · mangaChapterFromJson(data)interface MangaChapter {
id: string;
title: string;
number: number; // may be fractional
volume: number; // 0 if unknown
url: string;
scanlator: string | null;
uploadDate: number; // epoch milliseconds
branch: string | null; // scanlation branch/translation
pages: MangaPage[]; // resolved pages, when loaded
index: number; // position in the chapter list
}
Manga · mangaFromJson(data)interface Manga {
id: string;
title: string;
altTitles: string[];
url: string;
publicUrl: string;
rating: number; // -1 when unknown
isNsfw: boolean;
contentRating: string | null;
coverUrl: string;
largeCoverUrl: string | null;
state: string | null; // e.g. ongoing/finished
authors: string[];
source: JsonDict; // raw source metadata
sourceId: string;
description: string;
tags: JsonDict[];
chapters: MangaChapter[];
unread: number; // for library entries
progress: number; // read fraction, for library entries
}
Source · sourceFromJson(data)interface Source {
id: string;
name: string; // accepts name|title
lang: string; // accepts lang|locale
baseUrl: string; // accepts baseUrl|site
engine: string; // e.g. "JavaScript"
contentType: string; // e.g. "Manga"
isInstalled: boolean;
isPinned: boolean;
isNsfw: boolean;
isObsolete: boolean;
iconUrl: string;
version: string;
notes: string;
canUninstall: boolean; // defaults to true when absent
}
sourceFromJson accepts name/title, lang/locale, and baseUrl/site
aliases.
SearchPage · searchPageFromJson(data)interface SearchPage {
entries: Manga[];
hasNextPage: boolean;
}
MangaDetails · mangaDetailsFromJson(data)interface MangaDetails {
manga: Manga;
chapters: MangaChapter[];
}
interface OtaManifest {
version: number; // monotonic feed version
bundle?: OtaArtifact; // parser bundle descriptor
sources?: OtaArtifact; // source catalog descriptor
[key: string]: unknown; // additional fields preserved
}
interface OtaArtifact {
url: string; // absolute download URL
sha256?: string; // expected lowercase hex SHA-256
bytes?: number; // size in bytes, if advertised
}
interface OtaUpdateResult {
updated: boolean; // false when cache was already current
version: number; // version now installed
bundlePath: string; // cached bundle path
sourcesPath: string; // cached catalog path
}
interface OtaUpdateAvailability {
available: boolean;
installed: number | null; // null when nothing cached
latest: number | null; // null when feed unreachable
}
Using the factories on raw helper/parser payloads:
import { mangaFromJson, sourceFromJson, searchPageFromJson } from "nyora-sdk";
const res = await fetch(`${baseUrl}/sources/popular?id=ASURASCANS_US&page=1`);
const json = await res.json();
const page = searchPageFromJson(json); // page.entries: Manga[]
import { Nyora } from "nyora-sdk";
async function listChapterPages(sourceQuery: string, query: string) {
const client = new Nyora();
try {
const source = client.sources.find(sourceQuery);
const search = await client.manga.search(source.id, query);
if (search.entries.length === 0) return [];
const manga = search.entries[0];
const details = await client.manga.details(source.id, manga.url, {
title: manga.title,
});
if (details.chapters.length === 0) return [];
const firstChapter = details.chapters[0];
const pages = await client.manga.pages(source.id, firstChapter.url, {
branch: firstChapter.branch,
});
return pages.map((p) => p.url);
} finally {
client.close();
}
}
console.log(await listChapterPages("asura", "frieren"));
Nyora / ParserRuntime /
NyoraServer owns one jsdom window with the evaluated NyoraParsers global.NyoraServer, runtime calls are queued through a
promise chain and run one at a time. For true parallelism, run multiple
server/runtime instances."" on a transport failure, instead of throwing — this keeps
the widest range of sources working.readBundleText() /
readSourcesText() fall back to the assets shipped inside the package.The library depends on exactly three runtime packages:
@inquirer/prompts (^8.5.2) — interactive TUI promptsenv-paths (^3.0.0) — platform cache/config pathsjsdom (^24.1.0) — DOM parsing and the parser runtime environmentIt uses Node built-ins (node:fs, node:http, node:path, node:os,
node:crypto, node:url, node:net), the global fetch, and
crypto.createHash for SHA-256 — all available on Node 18+.
nyora-cli subcommands (sources, search, popular, latest,
details, pages, download, update, serve, version): see cli.md.server.md.ota.md.tui.md.