dsteem release 0.12.2 on npmjs @blazeapps/dsteem

in Steem Dev17 hours ago (edited)

Follow-up to yesterday's Browser Testing harness post: the modernized dsteem library is published on npm as @blazeapps/[email protected]. This is the wrap-up post for the entire eleven-day series — Phase 0's planning doc to today's npm publish. Twelve posts, eight phases, three milestone updates, one published package, and two same-day patches after a real Expo consumer surfaced bugs the harness couldn't catch. The honest version of how shipping software actually goes.

dsteem-v0.12-on-npm.png


Live Now

npm install @blazeapps/dsteem
SurfaceLink
npm packagehttps://www.npmjs.com/package/@blazeapps/dsteem
GitHub sourcehttps://github.com/blazeapps007/dsteem
API docshttps://blazeapps007.github.io/dsteem/
Browser test harnesshttps://blazeapps007.github.io/dsteem/harness/
Browser bundle (CDN)https://unpkg.com/@blazeapps/dsteem@^0.12/dist/dsteem.browser.global.js

Migration in one line: change every from 'dsteem' to from '@blazeapps/dsteem'. The public API is byte-identical to v0.11.3 — same classes, same methods, same return types. The legacy [email protected] on npm is left untouched; this is a clean parallel publish under a new scope.

Supported targets: Node.js 22+, modern browsers (via bundler or direct <script>), and React Native / Expo SDK 50–55 (Metro-ready as of v0.12.2 — Buffer + Node built-ins inlined inside the package; no consumer-side global.Buffer shim required).


v0.12.0 → v0.12.1 → v0.12.2 — Two Patches, One Real Bug

The first publish hit an Expo consumer within hours. The error:

_blazeappsDsteem.Client is not a constructor

Root cause: our package.json "browser" condition pointed at the IIFE bundle, which has no module-system exports — it assigns to var dsteem = (function(){...})() and is meant for <script src=""> consumption. Metro picked the IIFE thinking it was a module, then exposed undefined instead of Client.

v0.12.1 rerouted the browser/react-native conditions away from the IIFE and onto the Node CJS build (dist/index.cjs). That fixed the "not a constructor" error — but exposed a deeper bug:

Requiring unknown module "buffer"

The Node CJS build externalizes Node built-ins via esbuild's dynamic __require() shim. __require("buffer") works in Node (Node has buffer built in). It does not work in Metro, because Metro can't statically analyze indirect requires — it never bundles buffer, and the runtime blows up. Verified diagnosis:

$ grep -oE '__require[0-9]?\("(buffer|stream|util|assert)"\)' dist/index.cjs
__require("assert")
__require("buffer")
__require("stream")
__require("util")

Same for dist/index.mjs. Four ticking time bombs in the package's RN entry. The IIFE bundle had zero such leaks (the same grep returns empty against it — it's fully polyfilled via esbuild-plugin-polyfill-node), but it's not module-consumable. So we had a polyfilled bundle that bundlers couldn't use, and a module bundle that wasn't polyfilled. Neither one worked for RN.

v0.12.2 is the real fix. We added a third tsup build target — same source as the IIFE (src/index-browser.ts), same polyfillNode plugin, same noExternal: [/.*/] — but emitted as ESM + CJS modules instead of an IIFE. The new files dist/index.browser.{mjs,cjs} have:

  • All Node built-ins (buffer, stream, util, assert, process, crypto, …) inlined directly into the bundle (verified: grep -oE '__require\("(buffer|stream|util|assert)"\)' dist/index.browser.* returns zero matches)
  • Real named exports: export { Client, PrivateKey, … } in the .mjs; exports.Client = Client; in the .cjs
  • Same 22 named exports as the Node build, so the public surface is identical regardless of which entry the consumer's bundler picks

The exports map now routes react-native and browser conditions to dist/index.browser.mjs — the polyfilled module build. The Node entries (dist/index.{cjs,mjs}) stay unchanged for Node consumers (small, externalized, ~230 KB each). The IIFE bundle stays in dist/ for direct unpkg / <script> consumption. Three target classes, three correct entry points:

ConsumerResolves toWhy
Node importdist/index.mjssmall (229 KB), uses Node's native buffer/stream/util/assert
Node requiredist/index.cjssame, for CJS callers
React Native (Metro)dist/index.browser.mjsall Node built-ins inlined — no Metro "unknown module buffer"
Browser bundlers (webpack/vite/rollup)dist/index.browser.mjssame — works in any browser bundle target
Direct <script> / unpkgdist/dsteem.browser.global.jsIIFE, unchanged

The lesson: paste-and-test on a real RN app within the first hour caught what eight days of internal testing and the Browser Testing harness didn't catch. The harness validates op semantics in a normal browser — it doesn't exercise Metro's module-resolution path or its handling of indirect __require() calls. Two failure modes, one day apart, one real consumer made the difference. v0.12.1 was a partial fix shipped in good faith that exposed the deeper bug v0.12.2 actually patches.


The Whole Journey, in One Place

#DayPhaseWhat landed
12026-05-30Phase 0The plan + locked technical decisions + Phase 0 golden crypto fixtures captured
22026-05-31Phase 1GitHub Actions CI workflow (replaces CircleCI + Travis)
32026-06-01Phase 2tslint → ESLint 9 flat config + typescript-eslint 8
42026-06-02Phase 3TypeScript 3.1 → 5.6 + strict mode
52026-06-03Phase 4Native [email protected] (HIGH-severity CVE) → @noble/curves + @noble/hashes
62026-06-04Phase 5browserify+tsify+babelify+uglifyjs → tsup; dual ESM/CJS; 606 packages deleted
72026-06-05Phase 6mocha 5 → 11; nyc → c8 (70% gate); Karma+Sauce → Playwright headless
82026-06-06Phase 7typedoc 0.13 → 0.28; docs regenerated
92026-06-07MilestoneAll eight phases pushed as bef8443 to BlazeDevelopment; CI green on Node 22 + 24 + Playwright
102026-06-07Updatetypedoc site deployed to GitHub Pages (db9440e)
112026-06-08UpdateBrowser Testing harness deployed alongside docs (fa27358) — 47 ops × 10 tabs
122026-06-08Release@blazeapps/[email protected] → same-day patches 0.12.1 (RN exports rerouting, incomplete) → 0.12.2 (polyfilled browser/RN module build — the real RN fix)

Twelve posts. One publish. Two same-day patches. Plan locked in Phase 0, executed in order, only the patches were reactive.


What Actually Shipped to npm

$ npm pack --dry-run
@blazeapps/[email protected]
1.7 kB    LICENSE
9.1 kB    README.md
2.3 kB    package.json
359.4 kB  dist/dsteem.browser.global.js    (IIFE — for <script>/unpkg)
737.4 kB  dist/index.browser.cjs           (polyfilled CJS — browser/RN CJS bundlers)
736.9 kB  dist/index.browser.mjs           (polyfilled ESM — browser/RN/Metro modern path)
229.9 kB  dist/index.cjs                   (Node CJS — externalized built-ins)
228.9 kB  dist/index.mjs                   (Node ESM — externalized built-ins)
92.0 kB   dist/index.d.ts                  (types)
92.0 kB   dist/index.d.mts                 (types, dual-package)
+ source maps for each
─────────────────────────
15 files, 1.8 MB packed, 9.2 MB unpacked

Nothing else — no src/, no test/, no node_modules, no SteemitPosts. The "files": ["dist"] field in package.json is doing exactly what it says.

Why so big now? v0.12.1 packed at 795.7 kB. v0.12.2 packs at 1.8 MB. The extra ~1 MB is the inlined polyfill graph (buffer + stream + util + assert + process + events + …) baked into the two new index.browser.* files. The tradeoff is intentional: bundler consumers get a self-contained module that works on first install without any host-side shims. Node consumers still get the lean 230 KB build.


Headline Numbers

What changed underneath, in raw figures:

Metricv0.11.3 (Nov 2019)v0.12.2 (Jun 2026)Change
Node floorNode 8 eraNode 22 LTS+14 majors
TypeScript3.1.65.6.3+4 majors
Browser bundle782 KB351 KB (IIFE) + 737 KB (polyfilled module)IIFE −55%, module new
Crypto backendnative [email protected] (HIGH-severity CVE)@noble/curves + @noble/hashes (audited pure-JS)CVE eliminated, no native build
Linttslint (deprecated 2019)ESLint 9 flat config0 warnings, 0 errors
Test runnermocha 5mocha 11+6 majors
Coveragenycc8, 70% line gate enforced in CIreplaced
Browser testsKarma + Sauce LabsPlaywright headless Chromium/Firefox/WebKitreplaced
CICircleCI + TravisGitHub Actions matrix Node 22 + 24 + Playwrightreplaced
Build toolchainbrowserify + tsify + babelify + uglifyjs + dts-generator + Makefiletsup (esbuild)unified
node_modules entries(baseline)baseline −606 packagesmassively reduced
Dead polyfills purgedcore-js@2 + regenerator-runtime + whatwg-fetch + node-fetchall removeddead since 2019
Module formatCJS onlydual ESM + CJS + polyfilled-ESM/CJS + IIFE via exports mapadditive
React Native / Expounsupported (or required Metro hacks)supported out of the box via polyfilled module buildnew in 0.12.2
Production npm audit1 HIGH (secp256k1)0 vulnerabilitiesclean
Public API(baseline)identical — same classes, same methods, same return typespreserved
Tests passing(baseline)50 in 422 ms (offline slice)parity
Manual op coveragenone47 ops × 10 tabs in the browser harnessadded

The single decision that shaped everything else was preserve the public API exactly. That made each phase independently shippable, made the migration a one-line import rename, and made the v0.11.3 golden crypto fixtures load-bearing as the regression gate.


How a Consumer Migrates

Node, browsers (bundled), browsers (direct <script>)

Step 1: Update package.json:

-  "dsteem": "^0.11.3"
+  "@blazeapps/dsteem": "^0.12.2"

Step 2: Update every import:

- import {Client, PrivateKey} from 'dsteem'
+ import {Client, PrivateKey} from '@blazeapps/dsteem'

- const {Client} = require('dsteem')
+ const {Client} = require('@blazeapps/dsteem')

Step 3: Ensure your runtime is Node 22 LTS or newer.

That's it. Same Client.database.*, same client.broadcast.*, same PrivateKey.fromLogin, same cryptoUtils.

React Native / Expo

RN's JS runtime (Hermes/JSC) doesn't ship Web Crypto's getRandomValues, which @noble/curves needs for ECDSA entropy. As of v0.12.2, Buffer and all Node built-ins are inlined inside the package itself — no consumer-side global.Buffer = Buffer shim required. Only one polyfill is still needed:

npm install @blazeapps/dsteem react-native-get-random-values

At the very top of your app entry (index.js / App.tsx), before any @blazeapps/dsteem import:

import 'react-native-get-random-values'

Then everywhere else:

import {Client, PrivateKey} from '@blazeapps/dsteem'
const client = new Client('https://api.steemit.com')

Clear Metro's cache once after the upgrade:

npx expo start --clear

No metro.config.js tweaks needed. Verified on Expo SDK 50–55 / RN 0.83. If you were on v0.12.1: you can now delete the import {Buffer} from 'buffer' + global.Buffer = Buffer lines from your app entry — both are unnecessary in v0.12.2.


Security Outcome

The single most important reason for this work:

  • Eliminates the [email protected] high-severity advisory — the only known CVE in the v0.11.x production dependency tree. Replaced by @noble/curves (same library powering ethers / viem) and @noble/hashes — both audited, pure-JS, no native bindings.
  • Eliminates native-build attack surface. No more node-gyp, no prebuild binaries to verify, no platform-specific install failures.
  • Removes deprecated/unmaintained packages: tslint, core-js@2, dts-generator, the entire browserify toolchain.
  • All remaining production deps (bs58, bytebuffer, verror, @noble/*) are either current-maintenance or known-stable.

npm audit on the production tree of @blazeapps/[email protected]: 0 vulnerabilities.


The Three Surfaces

GitHub Pages serves the same workflow that publishes the npm package — typedoc docs at / and the manual op harness at /harness/. So as a consumer you have three independent ways to verify the package before trusting it with your account:

  1. npm install the actual artifact, run your test suite against it
  2. API docs — every public type, class, method documented at https://blazeapps007.github.io/dsteem/. New v0.12.0 additive exports (BroadcastAPI, CreateAccountOptions, the RC interfaces) are all in the sidebar
  3. Browser harness at https://blazeapps007.github.io/dsteem/harness/ — fill in a form for any of the 47 operations, sign with a pasted key (use a test account!), inspect the signed transaction, optionally broadcast. Build-only mode is the default; broadcast is per-op opt-in.

Full Series

Twelve posts, three same-day publishes (0.12.0 / 0.12.1 / 0.12.2), zero breaking changes for downstream consumers.


What's Next

This wraps the v0.12 release cycle. The repo and the package are stable at v0.12.2. The next things, when they happen, will be issue-driven rather than scheduled:

  • More bug reports — the same-day v0.12.1 + v0.12.2 patches are the model. Hit the package with your real app; if anything diverges from v0.11.x behaviour, that's a P0 release blocker, and it'll get patched fast.
  • v0.12.x patch releases — bug fixes only, additive-only changes. Same ^0.12 range stays compatible.
  • v0.13.0 — only when a meaningful set of new Steem-side features warrants it (hardforks, new operation types, etc.). Nothing planned right now.
  • Long-running CI — the Playwright browser smoke runs on every push, so regressions in the IIFE bundle are caught immediately. The Pages workflow republishes docs + harness on every push too, so the live surfaces never go stale. Open question: should CI also build the Metro target and try to resolve @blazeapps/dsteem from a minimal Expo fixture? That's the missing layer that would have caught the v0.12.0 → v0.12.1 → v0.12.2 chain on the first publish, not the first consumer. Likely a v0.12.3 follow-up.

The legacy [email protected] on npm stays where it is. Anyone with an existing "dsteem": "^0.11.x" keeps getting 2019-vintage behaviour until they explicitly opt into @blazeapps/dsteem.


How You Can Help

  • 🏗️ Adopt it: npm install @blazeapps/dsteem in your real app, swap your imports, run your tests
  • 📱 Try it on Expo / React Native: install + the single polyfill (react-native-get-random-values), drop your dsteem code in — and tell me if anything in the 47-op API misbehaves on the RN substrate. The v0.12.1/v0.12.2 patches only happened because a real consumer surfaced real bugs; that's the loop we need to keep running.
  • 🔬 Stress the harness: visit https://blazeapps007.github.io/dsteem/harness/, click through every tab, sign a few ops, broadcast a tiny custom_json from a test account
  • 🐛 File issues: https://github.com/blazeapps007/dsteem/issues — anything that diverges from v0.11.x is a release blocker
  • Star the repo — it makes the modernization more findable for the next person looking up "dsteem typescript"
  • 💬 Comment with your migration experience — what worked, what didn't, especially on React Native

🙏 Credits

Original dsteem by Johan Nordberg (2017–2019) — @blazeapps/dsteem is a modernization, not a replacement. Every architectural decision he made — the discriminated-union Operation type, the byte-exact FC serialization, the helper-class layering on Client, the canonical-signature retry loop — is preserved exactly. The work over the last eleven days was about replacing the substrate, never the design.


This work was developed with Claude AI assistance. All technical decisions reflect Steem ecosystem needs and the hard requirement of zero breaking changes.


Support Secure Steem Development

If you value proactive engineering, UX polish, and performance optimizations for the STEEM ecosystem, please consider supporting my witness: blaze.apps

🗳️ Vote Here:
Vote for blaze.apps Witness

Disclaimer: This post announces @blazeapps/[email protected] on the npm registry. Source: https://github.com/blazeapps007/dsteem. The public API matches [email protected] — migration is a one-line import rename. React Native consumers need react-native-get-random-values for ECDSA entropy (see "How a Consumer Migrates" above); everything else is inlined inside the package.