- JavaScript 99%
- Shell 0.5%
- PEG.js 0.2%
- HTML 0.2%
* Native BinkP mailer: inbound server, outbound caller, ftn_bso integration
Adds a fully native BinkP (FidoNet mail exchange) implementation:
Core protocol (core/binkp/):
- frame.js / commands.js — wire framing and command constants
- cram.js — CRAM-MD5 challenge/response auth
- session.js — answering and originating session state machine
- bso_spool.js — BSO outbound spool reader/writer with BSY locking
- caller.js — callNode() / pollNodes() for outbound dialing
- binkp_poll_module.js — sysop menu module for on-demand polling (!BINKP)
Scanner/tosser (core/scanner_tossers/binkp.js):
- Inbound TCP server (port 24554 by default, configurable)
- Per-node CRAM-MD5 password lookup
- BSY lock management (sends M_BSY on collision)
- Scheduled outbound polling via later.js text schedule
- Emits NewInboundBSO after receiving files (triggers ftn_bso toss)
ftn_bso integration (core/scanner_tossers/ftn_bso.js):
- Subscribes to NewInboundBSO on startup; unsubscribes on shutdown
- Re-uses existing tryImportNow + 'importing' busy-guard so concurrent
triggers are safely coalesced
Infrastructure:
- core/system_events.js: new NewInboundBSO event key
- core/config.js: _pushTestConfig/_popTestConfig for test isolation
- core/config_default.js: documented binkp defaults block
- core/enigma_assert.js: lazy config access (safe before Config.create())
- misc/menu_templates/main.in.hjson: !BINKP sysop command
Tests (112 passing):
- test/binkp_frame.test.js, binkp_cram.test.js — unit tests
- test/binkp_session.test.js — session state machine
- test/binkp_bso_spool.test.js — spool read/write/lock
- test/binkp_server.test.js — live server accept/auth/file-receive/BSY
- test/binkp_caller.test.js — callNode/pollNodes against live server
- test/binkp_ftn_bso_integration.test.js — NewInboundBSO wiring + path compat
Docs:
- docs/_docs/messageareas/binkp.md: new operator setup guide
- docs/_docs/messageareas/bso-import-export.md: note native BinkP option
* binkp/bso_spool: drop double-dispose, GC all-tilded flow files
Two coupled cleanups in the post-send disposition path.
1) Double-dispose
BinkpSession._applyDisposition unlinks (or truncates) the queued file
based on its disposition before emitting 'file-sent'. The spool layer
was repeating that work in _applyFlowDisposition, which produced
spurious "Could not delete sent file" ENOENT warnings on every send
of a ^-prefixed (delete) flow entry — the exact spam visible against
fsxnet and spooknet sessions in production logs.
For direct-attach entries the same race was silent (the disposeFn's
.catch(() => {}) swallowed it) but equally redundant.
Fix: BinkpSession owns file lifecycle. The spool's disposeFn is now
purely flow-file bookkeeping — it tildes the line and nothing else
for direct-attach (where there's no flow file at all, disposeFn is
simply null and is no longer registered in the session's disposeMap).
2) Garbage-collect all-tilded flow files
_applyFlowDisposition rewrote sent lines with a leading ~ but never
removed the file when every line was processed. Quiet nodes
accumulated dead-marker .clo/.flo files indefinitely; busy nodes saw
ftn_bso append fresh entries to a file that already had pages of
stale ~-prefixed history.
Fix: after rewriting, if no live (non-empty, non-tilde) lines remain,
unlink the flow file. ftn_bso recreates it on the next outbound
queue for the same node.
Tests updated to match the new disposeFn contract (does not touch the
underlying file; session does), plus three new cases:
- direct-attach disposeFn is null and the file is left in place
- multi-entry flow file: GC happens only when the LAST live line is
tilded; partial state preserves the surviving entry
- concurrent-modification: disposeFn skips both rewrite and GC if the
flow file changed out from under the captured line
115 BinkP-suite tests passing (was 113 prior to this change).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: scope cross-file pollution from root hooks/stubs
Two pre-existing latent issues in the suite were masked by a third
that hid the symptom — together they caused 10 BinkP tests to fail
when run as part of the full suite (passing in isolation).
Root cause
----------
test/user_db.test.js stubbed Events.emit and Events.listenerCount at
*top level*:
Events.emit = () => {};
Events.listenerCount = () => 0;
Because that runs at file-load time, mocha installs the stubs the
moment it requires user_db.test.js — and they live on the Events
singleton, so they persist for the entire suite. Every subsequent
test that depends on Events.emit() (NewInboundBSO, NewOutboundBSO,
crashmail dispatch, ftn_bso's import-on-inbound listener) silently
saw nothing fire.
The user_db file similarly had top-level before()/after() hooks
stubbing StatLog functions, which mocha treats as *root* hooks that
wrap every test in every file.
Fixes
-----
- test/user_db.test.js: wrap the suite in a describe('user_db', …)
and move all stubbing (StatLog AND Events) into that describe's
before(), saving originals first. after() restores. The stubs are
now scoped to user_db's own tests.
- test/message_area.test.js: same shape — top-level before/after
was acting as a root hook. Wrapped in describe('message_area —
hideFromBrowse', …).
- core/events.js: look up logger.log on each call instead of
capturing it at module-load time. Tests that mock loggerModule.log
after events.js loads (i.e. all of them, since Log.init() is never
called in tests) used to crash with "Cannot read properties of
undefined (reading 'trace')" on the first Events.addListener.
Lazy lookup is also a more honest pattern — logger.log IS supposed
to be re-bindable via init().
- core/message_area.js: same lazy-Config-lookup change. Fixes a
test-only leak where the captured Config getter would freeze on a
stale fixture if the module had been reloaded via require.cache
invalidation.
Suite goes from 1228 passing / 10 failing to 1238 passing / 0
failing. No production behavior change — all four edits are either
lazy lookups (semantically identical to the eager capture in normal
runtime) or test-file scoping (no production effect at all).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* binkp: crashmail send-on-export, separate pull schedule
Replaces the single 5-minute "look at the spool, dial nodes that
have pending mail" timer with two distinct triggers — eliminating
the wait-to-send latency for outbound and the no-fetch silence for
quiet peers.
1) Crashmail (event-driven, automatic, no config)
ftn_bso.flowFileAppendRefs now takes a destAddress arg and emits
a new NewOutboundBSO system event (with payload { address }) on
successful append. scanner_tossers/binkp.js subscribes to that
event, debounces back-to-back exports for the same node into one
session (default 500 ms; tunable via crashmailDebounceMs), and
dispatches a targeted pollNodes([addr]) call.
Effect: a posted message is on the wire to the destination peer
within ~half a second instead of waiting up to 5 minutes for the
next scheduled poll.
2) Pull cycle (periodic, configured) for quiet peers
The previous binkp.schedule key (a 5-min timer that only dialed
nodes with pending mail) is replaced by binkp.pullSchedule. On
each tick the new behavior dials EVERY configured peer in
binkp.nodes regardless of pending state — so echo-mail flows in
from hubs that wait for the spoke to call. Per-node opt-out via
"pull": false in the node's config block.
Default pullSchedule: every 15 minutes. The old "won't dial
without pending mail" model meant spooknet/zer0net delivery only
happened when the local user happened to post into one of their
echos.
3) pollNodes(forceAddrs, cb) honors forceAddrs
Previous signature accepted but ignored 'args'. Now it unions
forceAddrs (Address instances or address strings) with the
spool's pending-mail set, dedupes by zone:net/node, and dials
each. Used by both new triggers above plus the existing sysop
"poll now" menu module (which still passes []).
4) Bonus fix: BinkpSession 'disconnect' propagation in callNode
When a remote drops the socket mid-handshake, BinkpSession emits
'disconnect' (not 'error'). The caller's session-result promise
now also listens for 'disconnect' and rejects, so callNode
returns promptly instead of hanging until the 5-min
SESSION_TIMEOUT. Also moves listener registration BEFORE the
awaited attachSpoolToSession so events that fire during that
await don't get missed.
Tests
-----
13 new tests across:
- test/binkp_caller.test.js
pollNodes forceAddrs: dials with no pending mail, accepts plain
address strings, ignores invalid strings, dedupes against pending.
- test/binkp_ftn_bso_integration.test.js
NewOutboundBSO emit: payload includes the destination address,
no emit when destAddress is omitted, no emit when the underlying
append fails.
- test/binkp_server.test.js
_pullAddresses: returns concrete addresses, omits pull:false,
skips wildcard patterns, returns [] when nodes is missing/empty.
Crashmail dispatch: registers/removes the NewOutboundBSO
listener on startup/shutdown, debounces same-address bursts to
one dial, dispatches per-distinct-address within the window,
discards events with missing address payloads.
These three test files also get their existing top-level
before/after hooks wrapped in describe blocks (the same pattern as
the test-isolation precursor commit) — defensive scoping so future
file additions can't easily reintroduce the kind of cross-file
pollution that ate the suite earlier.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Doc updates
* binkp: stale .bsy + inbound temp reapers, util consolidation
Operational hardening for the native BinkP mailer plus a small
cleanup pass over the call sites it grew during initial development.
Reapers (P0):
- Stale .bsy locks: bso_spool acquireLock now reaps an EEXIST
lock older than staleLockMaxAgeMs (default 30 min, 6× the
session timeout) and retries once. BinkpModule.startup also
sweeps every outbound directory unconditionally so a crashed
prior run doesn't leave nodes permanently un-pollable.
- Inbound temp files: BinkpSession tracks the binkp_in_*.dt
files it owns and unlinks any partials in _destroy() on
error/disconnect. BinkpModule.startup adds a startup sweep
of tempDir as a safety net for hard process kills, gated by
inboundTempMaxAgeMs (default 1 hr).
- Bonus fix: writeStream now has an error handler so
ERR_STREAM_DESTROYED from a destroy-during-write race no
longer escapes to uncaughtException.
Cleanup (P1):
- core/binkp/util.js consolidates localAddresses(config) and
addressKey(addr) — duplicates removed from caller.js and
scanner_tossers/binkp.js.
- Crashmail listener now validates address.isValid() before
queuing.
- Address#getMatchScore (previously commented out) is restored;
new findBestNodeMatch picks the most-specific matching pattern
so a "21:1/100" override always wins over a "21:*" catch-all
regardless of HJSON insertion order. Used by both nodeConfigFor
and _lookupPassword.
- Logger stub centralized in test/setup.js; removed the same
~10-line prelude from five test files.
Docs: docs/_docs/messageareas/binkp.md rewritten to cover
pullSchedule, crashmail, per-node pull:false, the new sweep
options, !BINKP sysop hotkey, and the pattern-specificity caveat
on nodes keys.
Tests: 1238 → 1266 (+28). Zero regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Pretty
* test: fix two binkp test flakes; ensure mocha exits cleanly
Three small fixes so `npm test` is deterministic and terminates.
binkp_caller "sends files from the outbound spool to the remote"
(pre-existing, ~10% flake): the test had the caller's localAddress
identical to the server's address, so both sides' spools — sharing
the same outbound dir — saw the same flow file and tried to send
it. The caller's delete-disposition unlinked the source mid-flight,
producing an intermittent file-received miss. Use distinct same-zone
addresses (caller=1:218/700, server=1:1/2) so each spool's queue
stays disjoint while keeping the outbound subdir as plain "outbound".
binkp_inbound_temp "unlinks a partial inbound temp file on _destroy()"
(introduced by the prior cleanup commit): the test waited one
setImmediate tick for fs.createWriteStream's open() to complete and
two more for _destroy()'s fire-and-forget unlink to land. Both races
become visible under load. Replace with poll-with-deadline helpers
(waitForFileExists / waitForFileGone) so timing is deterministic.
Add --exit to the npm test script. Some test fixture is leaking a
handle (likely a half-closed socket) at suite end, leaving mocha
running indefinitely after "1266 passing". --exit is the standard
mocha mitigation; it doesn't hide production-code leaks, only test
fixture ones, and avoids contributors thinking the suite has hung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Driveby AP fix
* Potential fix for pull request finding 'CodeQL / Unvalidated dynamic method call'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
|
||
|---|---|---|
| .devcontainer | ||
| .github | ||
| .husky | ||
| .vscode | ||
| art | ||
| config | ||
| core | ||
| dev_util | ||
| docker | ||
| docs | ||
| gopher | ||
| misc | ||
| mods | ||
| test | ||
| util | ||
| www | ||
| .dockerignore | ||
| .gitattributes | ||
| .gitignore | ||
| .lintstagedrc.json | ||
| .prettierignore | ||
| .prettierrc.json | ||
| autoexec.sh | ||
| CONTRIBUTING.md | ||
| DEV.md | ||
| eslint.config.mjs | ||
| LICENSE.TXT | ||
| main.js | ||
| mise.toml | ||
| mkdocs.yml | ||
| oputil.js | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| TROUBLESHOOTING.md | ||
| UPGRADE.md | ||
| watch.sh | ||
| WHATSNEW.md | ||
| yarn.lock | ||
ENiGMA½ BBS Software
ENiGMA½ is a modern BBS software with a nostalgic flair!
Features
Below are just some of the features ENiGMA½ supports out of the box:
- Multi platform — Anywhere modern Node.js runs likely works (known to work under Linux, FreeBSD, OpenBSD, macOS and Windows)
- Unlimited multi node support
- Highly customizable via HJSON based configuration, menus, and themes in addition to JavaScript based mods
- SQLite storage of users, message areas, etc.
- Strong PBKDF2 backed password encryption.
- Support for 2-Factor Authentication with One-Time-Passwords
- Structured Bunyan logging!
- Telnet, SSH, and both secure and non-secure WebSocket access built in! Additional servers are easy to implement
- Built-in web server with HTTP(S) support — powers temporary download URLs, file browsing, and more
- CP437 and UTF-8 output with wide character support — CJK, Hangul, fullwidth forms, and similar scripts display and edit correctly
- SyncTERM style font support. Display PC/DOS and Amiga style artwork as it should be! In general, ANSI-BBS / cterm.txt / bansi.txt are followed for expected BBS behavior.
- Baud emulation. View ANSI like the block gods intended.
- Full SAUCE support.
- Renegade style pipe color codes.
- MCI support for lightbars, toggles, input areas, and so on plus many other bells and whistles
- Message networks with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export, and MRC (Multi-Relay Chat)
- Native BinkP/1.1 mailer — built-in inbound listener and outbound caller for FTN networks. No external
binkdrequired; outbound mail ships immediately on export ("crashmail"), and a configurable pull cycle keeps echo mail flowing in from quiet hubs. - Internet mail — send and receive email directly from the BBS private message system via IMAP/SMTP. See Internet Mail
- ActivityPub / Fediverse (experimental) — federated messaging with WebFinger, NodeInfo2, actor profiles, and common ActivityPub object types; PNG avatars with auto-generated defaults
- Message bases exposed via Gopher and NNTP content servers
- Gazelle (🪦) inspired File Bases including fast fully indexed full text search (FTS), #tags, and legacy X/Y/Z modem support
- Upload processor supporting FILE_ID.DIZ and NFO extraction, year estimation, and more!
- Door support including common dropfile formats for legacy DOS doors. Built in BBSLink, DoorParty, and Exodus!
- Native x86/DOS door emulation via v86 — run classic DOS BBS doors (LORD, PimpWars, TradeWars, etc.) directly inside ENiGMA½ with zero external dependencies. No QEMU, DOSBox, or DOSEMU required. Includes
oputil fatfor managing FreeDOS disk images andoputil v86for an interactive browser-based DOS desktop. See Local Doors — v86. - Z-Machine interactive fiction — run Infocom classics (Zork, Colossal Cave, and hundreds more from the IF Archive) natively in Node.js. No external emulator required. Supports Z-Machine versions 3, 4, 5, and 8. See Z-Machine Door.
- Full Screen Editor (FSE) with ANSI art support, real-time cursor/mode indicators, inline find/search (
Ctrl-F), file upload to body, and a fully modernized view engine - A remote accessible Waiting For Caller (WFC)!
- Expandable achievement system — BBSing gamified!
...and much much more. Please check out the issue tracker and feel free to request features (or contribute!) features!
Documentation
Browse the docs online. Be sure to checkout the /docs/ folder as well for the latest and greatest documentation.
Installation
On most *nix systems simply run the following from your terminal:
curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/install.sh | bash
Please see Installation Methods for Windows, Docker, and so on...
Donating
If you feel the urge to donate, you can do so here
Support
- See Discussions and the issue tracker
- Discussion on a ENiGMA BBS! (see Boards below)
- Discord: https://discord.gg/ghx8Vxex
FSX_ENGon fsxNet available on many fine boards- Email: bryan -at- l33t.codes
- Facebook ENiGMA½ group
Terminal Clients
ENiGMA has been tested with many terminals. However, the following are suggested for BBSing:
Some Boards
- 💀 Xibalba - ENiGMA WHQ 💀 (ssh://xibalba.l33t.codes:44511 or telnet://xibalba.l33t.codes:44510)
- Undercurrents: (ssh://undercurrents.io)
Special Thanks
(in no particular order)
- Dave Stephens aka RiPuk for the awesome ENiGMA website and KICK ASS documentation, code contributions, etc.
- Daniel Mecklenburg Jr. for the awesome VTX terminal and general coding talk
- M. Brutman, author of mTCP (Interwebs for DOS!)
- M. Griffin, author of Enthral BBS, Oblivion/2 XRM and EtherTerm!
- Caphood, supreme SysOp of BLACK ƒlag BBS. May he rest in peace 🪦
- Luciano Ayres of Blocktronics, creator of the "Mystery Skulls" default ENiGMA½ theme!
- Sudndeath for Xibalba ANSI work!
- Jack Phlash for kick ass ENiGMA½ and Xibalba ASCII (Check out IMPURE60!!)
- Avon of Agency BBS and fsxNet for putting up with my experiments to his system and for FSX_ENG!
- Maskreet of Throwback BBS hosting DoorParty!
- Apam of Magicka
- nail/blocktronics for the sickmade Xibalba logo!
- Whazzit/blocktronics for the amazing Mayan ANSI pieces scattered about Xibalba BBS!
- Smooth/fUEL for lots of dope art. Why not snag a T-Shirt?
- Al's Geek Lab for the installation video and of course the Back to the BBS - Part one: The return to being online documentary!
- Alpha for the FTN-style configuration guide!
- Huge shout out to cognitivegears for the various fixes, improvements, and removing the need for cursor position reports providing a much better terminal experience!
- MeaTLoTioN for the MRC contributions!
...and so many others! This project would be nothing without the BBS and art scene communities!
License
Released under BSD 2-clause. See LICENSE.TXT
