npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@syall/lsm-recovery-version-api-js

v0.3.0

Published

TypeScript client for the LSM Text Only Holy Bible Recovery Version API.

Readme

@syall/lsm-recovery-version-api-js

TypeScript client for the LSM Text Only Holy Bible Recovery Version API.

Documented vs. observed API behavior: several things in this README (and in this package's own doc comments) reflect live, verified behavior of api.lsm.org that differs from — or simply isn't mentioned in — LSM's own published docs (https://api.lsm.org/recver/txo-docs.htm). See DIFFERENCES.md for the full catalogue with examples. This package follows the live behavior wherever the two disagree.

Looking for the verse-reference string builder? It's a separate, independent package: @syall/verse-reference-builder (source). This package has no dependency on it — see Building reference strings below.

Install

npm install @syall/lsm-recovery-version-api-js

Ships both ESM (import) and CommonJS (require) builds, plus type declarations for both — works with Node, Bun, Deno, and any bundler (webpack, esbuild, Vite, Rollup, etc.) targeting either module system. Node and Bun/Deno compatibility are both verified in CI (see Runtime compatibility below), not just claimed.

Requires Node 22+ — Node 20 reached end-of-life on 2026-04-30 and is no longer covered by CI. Every supported line has a global fetch; for older runtimes or non-Node environments without one, pass a fetch polyfill via LsmClientConfig.fetch (e.g. via undici or node-fetch).

// ESM / TypeScript
import { LsmRecoveryVersionClient } from "@syall/lsm-recovery-version-api-js";
// CommonJS
const { LsmRecoveryVersionClient } = require("@syall/lsm-recovery-version-api-js");

Browser <script> tag (no bundler)

A dependency-free, minified UMD-style build is also published under dist/umd, exposing a LsmRecoveryVersionApi global — usable directly from a CDN with no build step:

<script src="https://unpkg.com/@syall/lsm-recovery-version-api-js/dist/umd/lsm-recovery-version-api-js.min.js"></script>
<script>
  const { LsmRecoveryVersionClient } = LsmRecoveryVersionApi;

  // No appId/token needed to try it out — see "Authentication" below.
  const client = new LsmRecoveryVersionClient();

  client.getVerses({ string: "John 1:14" }).then((result) => console.log(result));
</script>

(package.json's unpkg/jsdelivr fields point at this same file, so https://cdn.jsdelivr.net/npm/@syall/lsm-recovery-version-api-js also resolves to it.)

Authentication

LSM's own docs describe exactly one auth scheme: HTTP Basic Auth with an appId/token pair you register at api.lsm.org. Live testing found a second, undocumented way the API actually accepts requests — a public file= query-parameter site key, which is what the production text.recoveryversion.bible widget itself sends (see DIFFERENCES.md). This package supports both, chosen automatically from what you configure:

import { LsmRecoveryVersionClient } from "@syall/lsm-recovery-version-api-js";

// 1. No credentials (the default) — uses the built-in public file=
//    token. Works immediately, no registration needed. This key isn't
//    issued to this package specifically, so LSM could revoke or
//    rate-limit it without notice — fine for trying things out or
//    casual/low-volume use, not guaranteed for production traffic.
const quickStart = new LsmRecoveryVersionClient();

// 2. Your own registered credentials — sent as HTTP Basic Auth, LSM's
//    documented scheme. Both fields are required together.
const production = new LsmRecoveryVersionClient({
  appId: process.env.LSM_APP_ID,
  token: process.env.LSM_TOKEN,
});

// 3. A different file= token, if LSM issues you one directly, or the
//    built-in default is ever revoked.
const customFileToken = new LsmRecoveryVersionClient({ fileToken: "..." });

Supplying only one of appId/token is always a configuration mistake and throws IncompleteCredentialsError synchronously — there's no partial-credentials mode to fall into.

Usage

import {
  LsmRecoveryVersionClient,
  IncompleteCredentialsError,
  InvalidInputError,
  UnauthorizedError,
  NetworkError,
} from "@syall/lsm-recovery-version-api-js";

const client = new LsmRecoveryVersionClient({
  appId: process.env.LSM_APP_ID,
  token: process.env.LSM_TOKEN,
});

try {
  const result = await client.getVerses({
    string: "Prov. 29:18; Acts 26:19; Eph. 4:4-6; Rev. 21:2, 9-10",
    lang: "eng",
  });

  for (const verse of result.verses) {
    console.log(`${verse.ref} — ${verse.text}`);
  }

  // Required by LSM's Terms of Use: display this alongside the verses.
  console.log(result.copyright);
} catch (err) {
  if (err instanceof InvalidInputError) {
    console.error("Bad reference string:", err.body);
  } else if (err instanceof UnauthorizedError) {
    console.error("Check your app id / token.");
  } else if (err instanceof NetworkError) {
    console.error("Couldn't reach the LSM API:", err.message);
  } else {
    throw err;
  }
}

Constructing the client itself can throw IncompleteCredentialsError — that's a configuration mistake (exactly one of appId/token set, see Authentication above), not an API-call failure, so it's usually left uncaught rather than handled alongside the errors above:

new LsmRecoveryVersionClient({ appId: "only-this-one" });
// throws IncompleteCredentialsError

Word search mode

GetVersesParams.string doesn't have to be a citation. If the API doesn't recognize it as a reference, it falls back to a full-text search over the whole Bible, and result.searchType tells you which happened — this is confirmed live but completely undocumented by LSM (see DIFFERENCES.md):

const result = await client.getVerses({ string: "eternal life" });
result.searchType; // "words" — no citation was recognized
result.verses;      // every verse containing "eternal" and "life"

const ref = await client.getVerses({ string: "John 3:16" });
ref.searchType; // "references"

A few things confirmed about word-search mode:

  • A multi-word query matches verses containing all the words, not necessarily as an exact adjacent phrase — e.g. "eternal life" also matches Daniel 12:2's "life eternal" (reversed) and Matthew 18:8's "eternal fire" (same words, unrelated phrase), not just the exact phrase "eternal life".
  • Exact-phrase matches are returned first; looser word-only matches are appended at the end — so result order is relevance-ranked, not Bible order, unlike reference-lookup mode.
  • Very common connecting words (e.g. "the") appear to be effectively ignored rather than narrowing the match set.
  • It also fires — silently, with an empty result rather than an error — for input the docs claim should be rejected outright (disallowed characters). See DIFFERENCES.md for the exact example.

Edge cases in string

Two more confirmed-live behaviors worth knowing about before you build string dynamically (e.g. from user input) — neither is validated for you by this package, since it never inspects string before sending it:

  • Empty string (string: "") doesn't return a verses-shaped response at all — LSM's API returns its own HTML documentation landing page instead, mislabeled content-type: application/json. This package can't parse that as JSON, so it surfaces as a plain LsmApiError ("The API response could not be parsed as JSON") with the HTML dumped into err.body, rather than any more specific error. If you build string from user input, guard against an empty (or whitespace-only) value yourself before calling getVerses().

  • "No such reference" phantom verses: a syntactically well-formed but non-existent reference (an unrecognized book, or a real book with an out-of-range chapter/verse) doesn't error either — it's a normal 200 with one fake Verse standing in for the real result: { "ref": " 99:99", "urlpfx": "", "text": "No such reference" }. getVerses() returns it as an ordinary entry in result.verses, since the request succeeded from the API's own point of view. Detect it by checking verse.text === NO_SUCH_REFERENCE_TEXT (exported by this package) or an empty verse.urlpfx:

    import { NO_SUCH_REFERENCE_TEXT } from "@syall/lsm-recovery-version-api-js";
    
    const result = await client.getVerses({ string: "Zzz 99:99" });
    const real = result.verses.filter((v) => v.text !== NO_SUCH_REFERENCE_TEXT);

    See DIFFERENCES.md for the full write-up of both cases.

Language support

LSM's docs only document lang: "eng" (the default) and lang: "spa". Live testing found the API also accepts three more values — confirmed by getting back correctly translated reference labels and verse text, not just a 200:

await client.getVerses({ string: "John 3:16", lang: "por" }); // Portuguese
await client.getVerses({ string: "John 3:16", lang: "zho" }); // Chinese
await client.getVerses({ string: "John 3:16", lang: "tag" }); // Tagalog

These aren't guessed ISO codes — they're the exact internal language keys LSM's own reader site uses (see DIFFERENCES.md). Other guesses for the same three languages ("chi", "zh", "pt", "tl") — and any other value — get a 500 from the API, same as an invalid Lang always has. This package doesn't special-case that 500 — it's not confirmed to mean only "unrecognized lang," so it surfaces as a plain LsmApiError like any other non-200 status, rather than a dedicated error class that could misattribute some other server-side 500.

lang doesn't just format the response — it also affects how string itself gets parsed, and this is where it gets risky. Localized book abbreviations generally work as string input, but only paired with the matching lang:

await client.getVerses({ string: "Jn. 3:16", lang: "spa" }); // John 3:16, in Spanish
await client.getVerses({ string: "太 1:1", lang: "zho" });    // Matthew 1:1, in Chinese

The same abbreviation under a mismatched (or default "eng") lang doesn't reliably fail — it can silently resolve to a completely different, wrong book, with no error and no indication anything went wrong:

// "Jo" is John's Portuguese abbreviation, but nothing here mentions
// Portuguese — this silently returns a real verse from Joshua 3:16.
await client.getVerses({ string: "Jo 3:16", lang: "eng" });

One specific Chinese abbreviation is broken even with the matching lang: John's single-character abbreviation "約" doesn't resolve to John under lang: "zho" at all — it prefix-matches Joshua's full Chinese name instead once a chapter number is appended. The book's actual Chinese name ("約翰" / "約翰福音") resolves correctly. This was only spot-checked across a sample of abbreviations, not verified exhaustively for all 66 books × 5 languages — see DIFFERENCES.md for the full findings table. If you accept a book abbreviation from user input, always pass the lang it actually belongs to, and treat any localized abbreviation as input with some caution.

Building reference strings

GetVersesParams.string is a plain string — this package doesn't know or care how you produce it, and has no dependency on any builder. Hand-write it per LSM's documented grammar (api.lsm.org/recver/txo-docs.htm), or use the separate, independent @syall/verse-reference-builder package for a type-safe (and, for runtime-known data, dynamic/runtime-checked) builder, and call .build() yourself:

import { LsmRecoveryVersionClient } from "@syall/lsm-recovery-version-api-js";
import { VerseReferenceBuilder } from "@syall/verse-reference-builder";

const client = new LsmRecoveryVersionClient({ appId: "...", token: "..." });

const result = await client.getVerses({
  string: new VerseReferenceBuilder()
    .verse("John", 1, 14)
    .verse("Ephesians", 4, { from: 4, to: 6 })
    .verse("Jude", 9) // single-chapter book — no chapter argument
    .build(),
  // "John 1:14; Ephesians 4:4-6; Jude 9"
});

See that package's own README for the full builder API. Note this package does not support LSM's documented OSIS input mode (In=osis) — only the default reference-string grammar.

Notes

  • Attribution: LSM's Terms of Use require displaying the copyright string from every response alongside any verses shown to end users.
  • No offline storage: don't persist verse text returned by this API.
  • 50-verse limit: requests beyond 50 verses return a partial result with a truncation notice in message.
  • Errors can arrive as HTTP 200: LSM's docs don't specify HTTP status codes for any error condition, and the live API has been observed reporting an unauthorized request as HTTP 200 with an empty verses array and a message starting with "Error: ..." rather than a 401 — confirmed identical whether credentials are omitted entirely or a bogus appId/token pair is sent. getVerses() checks for this in addition to the HTTP status, so UnauthorizedError is still thrown correctly — you don't need to inspect message yourself. See DIFFERENCES.md for what this package could not confirm (a genuine non-authorization input error).

Development

npm install
npm run typecheck
npm test
npm run test:browser      # loads the built UMD bundle in a real browser (see below)
npm run test:runtime:bun  # optional — requires Bun installed (see "Runtime compatibility")
npm run test:runtime:deno # optional — requires Deno installed
npm run build             # emits dist/esm, dist/cjs, and dist/umd

Run these from this package directory (packages/api-client), or from the repo root via npm run <script> --workspace=packages/api-client. See the repo root README for the monorepo layout and shared tooling.

npm run build compiles src/ twice — once as ESM into dist/esm, once as CommonJS into dist/cjs — and stamps a minimal package.json ({"type": "module"} / {"type": "commonjs"}) into each so Node resolves every file's module format correctly regardless of the root package's own "type" field. It also bundles a browser <script>-tag build into dist/umd via esbuild (see ../../scripts/build-umd.mjs, shared across every package in this monorepo). npm run build:esm / build:cjs / build:umd run any one part individually.

prepublishOnly re-runs the full test suite and a clean build before npm publish, and prepare builds automatically when this package is installed directly from its git repository (so a git-URL install works without a separate manual build step).

Browser smoke test

npm test runs src/'s unit tests under Node, so it never actually executes the dist/umd bundle the way a <script src="..."> consumer would. npm run test:browser closes that gap: it builds the UMD bundle, loads it into a real headless browser via playwright-core, stubs window.fetch (no network access needed), and confirms window.LsmRecoveryVersionApi is exposed and getVerses() works end to end — for both the default (no-credentials, file-token fallback) and Basic Auth paths (see test/browser-smoke.mjs and the shared ../../scripts/test-browser.mjs harness).

By default it drives whatever Chrome or Edge is already installed on your machine — playwright-core doesn't download its own browser — so it works locally with no extra setup as long as one of those is installed. If neither is found, either install one, or run npx playwright install chromium once to fetch a managed Chromium and re-run the command. To point at a specific browser binary instead, set PLAYWRIGHT_CHROMIUM_EXECUTABLE=/path/to/browser.

Set PLAYWRIGHT_ENGINE=firefox or PLAYWRIGHT_ENGINE=webkit to run the same smoke test against Firefox or WebKit instead of the default Chromium — CI does this for all three (see below) — after first fetching a managed copy with, e.g., npx playwright install firefox.

CI

.github/workflows/ci.yml (at the repo root) defines four independent jobs, each running against both packages in this monorepo:

  • test — runs test (which typechecks first via the pretest script) and build for both packages, across a matrix of Node 22/24/26 on Ubuntu, Windows, and macOS (9 combinations).
  • browser — runs test:browser for both packages, matrixed by both OS and browser engine (chromium/firefox/webkit × 3 OSes = 9 combinations), with no Node-version axis at all (one Node version is enough to run the driving script). chromium drives the system's preinstalled Chrome/Edge (every GitHub-hosted runner has one); firefox/webkit aren't preinstalled, so the job fetches a managed copy first via npx playwright@<version> install, where <version> is read directly from the root playwright-core devDependency (not hardcoded) so it can never drift out of sync.
  • runtime-bun / runtime-deno — build the ESM output for both packages, then run each package's test/runtime-smoke.mjs (via the shared ../../scripts/smoke-test-esm.mjs harness) under Bun and Deno respectively, on a single OS each.

CI does not publish anything; publishing runs through the separate, manually-triggered .github/workflows/publish.yml workflow instead (see Releasing below).

Runtime compatibility

Beyond Node (tested across three versions × three OSes in the test job above), this package claims to work with Bun and Deno too — both support the plain ESM output directly. test/runtime-smoke.mjs (run via the shared ../../scripts/smoke-test-esm.mjs harness) verifies that claim: it imports the built dist/esm output (as a real consumer would), exercises getVerses() against a stubbed fetch for both the Basic Auth and file-token-fallback paths, and checks the IncompleteCredentialsError constructor validation — written with only standard Web/JS APIs so the identical file runs unmodified under Node, Bun, or Deno. Run it locally with (each script builds dist/esm itself first, so no separate build step is needed):

npm run test:runtime:bun   # requires Bun installed
npm run test:runtime:deno  # requires Deno installed

Releasing (Changesets)

Version bumps and CHANGELOG.md are managed with Changesets, independently per package in this monorepo. For any PR that changes published behavior:

npx changeset       # records the change + bump type (patch/minor/major)

Commit the generated .changeset/*.md file with your PR. See the repo root's .changeset/README.md for more. npm run version-packages (changeset version, run from the repo root) is still a manual step, but publishing itself is automated: dispatching the "Publish" GitHub Actions workflow (.github/workflows/publish.yml) builds, tests, and publishes via npm Trusted Publishing, pushes the release tag, and opens a GitHub Release — gated behind a required-reviewer approval on the npm-publish environment.

License

This package's code is MIT licensed — see LICENSE. The verse text and other content returned by the LSM API it talks to is not covered by that license and remains subject to LSM's own Terms of Use — see NOTICE.md and the Attribution note above.