Nyora (JavaScript) - v0.1.1
    Preparing search index...

    Programmatic SDK Guide

    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.

    • Package: nyora-sdk
    • Version: 0.1.1
    • License: Apache-2.0
    • Node.js: >=18 (relies on global fetch)
    • Module system: ESM only ("type": "module")
    • Main entry: ./dist/index.js · Types: ./dist/index.d.ts
    • Default export: the Nyora class

    npm 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 by close(). The call is safe to invoke more than once.


    Nyora 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 });

    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, ")");
    }

    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}`);

    Close the embedded parser runtime and release its resources. Safe to call more than once.


    Accessible as client.sources, or constructable directly from any RuntimeLike:

    new SourcesService(runtime: RuntimeLike)
    

    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 the first bundled source whose id or name contains query (case-insensitive substring match). Throws a plain ErrorNo bundled source matched '<query>' — when nothing matches.

    const source = client.sources.find("asura"); // matches id or name
    

    Accessible 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: 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: 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: 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: 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: 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: 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 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.

    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: 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();
    }

    Re-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.

    Release the jsdom window. Safe to call more than once.

    The 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
    }
    type ParserMethod = "popular" | "latest" | "search" | "details" | "pages";
    

    The 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";

    The 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 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.

    The directory where OTA artifacts are cached.

    const ota = new OtaManager();
    console.log(ota.cacheDir);

    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.

    Return the integer manifest version currently cached, or null when nothing is cached or the cached manifest is unreadable.

    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?: { 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.update does not reload any runtime. Use Nyora.update() (which reloads the embedded runtime) or call ParserRuntime.reload() yourself afterward.

    Return the parser bundle source text — the cached copy, or the package-bundled fallback when no cache exists.

    Return the source catalog JSON text — the cached copy, or the package-bundled fallback when no cache exists.

    export 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 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 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.

    Close the listening socket and close an owned runtime. Safe to call when not running.

    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.

    Return the platform-specific helper port-file path. Honors NYORA_HELPER_PORT_FILE when set (a leading ~ is expanded to the home directory); otherwise:

    • macOS: ~/Library/Application Support/Nyora/helper.port
    • Windows: %APPDATA%\Nyora\helper.port
    • Linux/other: the XDG config dir, .../nyora/helper.port
    import { defaultPortFile } from "nyora-sdk";
    console.log(defaultPortFile());
    readBaseUrlFromPortFile(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();
    }

    NyoraHTTPError exposes statusCode and body. The NyoraServer._handle path maps a thrown NyoraError to an HTTP 502.


    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 {}.

    type JsonDict = Record<string, unknown>;
    
    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/" } });
    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
    }
    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
    }
    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.

    interface SearchPage {
    entries: Manga[];
    hasNextPage: boolean;
    }
    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"));

    • Single jsdom window per instance. Each Nyora / ParserRuntime / NyoraServer owns one jsdom window with the evaluated NyoraParsers global.
    • Serialized requests. In NyoraServer, runtime calls are queued through a promise chain and run one at a time. For true parallelism, run multiple server/runtime instances.
    • Tolerant HTTP. Parser HTTP callbacks return the response body for any status code, and "" on a transport failure, instead of throwing — this keeps the widest range of sources working.
    • Offline first run. With an empty cache, 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 prompts
    • env-paths (^3.0.0) — platform cache/config paths
    • jsdom (^24.1.0) — DOM parsing and the parser runtime environment

    It 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+.


    • CLInyora-cli subcommands (sources, search, popular, latest, details, pages, download, update, serve, version): see cli.md.
    • Server — running the helper-compatible REST server: see server.md.
    • OTA — the over-the-air update feed: see ota.md.
    • TUI — the interactive terminal reader: see tui.md.