Release Notes
Major changes and additions, newest first. This page is generated from WHATSNEW.md in the repository. For the full detail behind any entry, see the linked issue or GitHub.
This document attempts to track major changes and additions in ENiGMA½. For details, see GitHub.
0.5.1-beta
Section titled “0.5.1-beta”-
oputil.js config validatechecks your configuration before you restart (#281) — a mistyped key inconfig.hjsonhas never been reported. Because your file is merged into the defaults, a typo does not replace anything:outbundlands quietly besideoutbound, the setting you wrote is never read, and the board goes on using the default. Nothing is logged, and the file looks correct.The new command reports those, along with values whose type disagrees with the default they override:
config.hjson: 2 issues (1 error, 1 warning)error general.maxConnectionsexpected number, got stringwarning scannerTossers.ftn_bso.paths.outbundunknown key "outbund" -- did you mean "outbound"?It exits non-zero when there are errors, so it can be used from a script or a systemd
ExecStartPre. Warnings alone are not a failure: an unrecognised key may perfectly well belong to a mod. Sections you add yourself, and the tags and names you choose for areas, networks and nodes, are never reported.--check-envadditionally reports@environment:,@file:and@reference:specs that do not resolve. That one is opt-in because resolution depends on the shell it runs in: checked by default, a perfectly good configuration would look broken purely because it was validated from the wrong place.It also checks names that have to line up across sections — a file area naming a storage tag that was renamed, a
ticAreasentry pointing at a file area that does not exist, an echo or a NetMail route naming a network spelled differently where it is defined, an NNTP conference or area exposed publicly that is not there. None of those produce an error today: the file quietly never lands, the echo is quietly never exported, and nothing in the log says why.A theme or a first menu that is not there is now reported too.
theme.default,theme.preLoginand each login server’sfirstMenuname things that live outsideconfig.hjson— a directory underpaths.themes, an entry inmenu.hjson— so until now the only way to find a typo in one was to restart and watch what happened. A misnamed theme quietly falls back to whichever theme loads first; a misnamed first menu breaks that login server for everyone using it.error theme.defaulttheme "lucian_blocktronics" is not defined in paths.themes -- did you mean "luciano_blocktronics"?Checked at startup, once the themes and menus are loaded, and by
oputil.js config validate— which gathers both sets itself, so it can tell you before the restart rather than after.*still means “pick one at random” and is left alone, and a login server you have switched off is not checked at all, since a menu for a server nobody can reach is not a problem. If either set cannot be read, nothing is reported rather than everything.Values are now checked, not just key names. A port outside 1-65535, a
logging.rotatingFile.levelbunyan has never heard of, a negative idle timeout, a two factor method that is not implemented, a TICfileCasethat is neitherlowernorupper— each of these is reported with the values that would have worked. Every one of them was taken from the code that reads the setting, and a test asserts that no constraint excludes the value ENiGMA½ itself ships, because a validator that fails a correct board is worse than none.achievements.hjsonis checked too — at startup, on reload, and byoputil.js config validate, which now reports on both files and names each one. Astatnamewritten forstatNamehas always meant the achievement is silently discarded at load and simply never fires, with nothing logged either way. The same goes for atypethat is not one of the three the code implements, or a tier whosepointsare a string. Anachievements.hjsonthat cannot be read at all is reported as a warning rather than a failure, because that is what the board itself does: it logs and carries on without achievements.Checking it turned up one more defect: art and date formats set on an achievement were silently ignored. They are meant to be overridable at three levels — the tier, the achievement, then the file’s top level — but the middle one read the wrong object and so never matched. Tier-level and file-level overrides worked; the one in between did nothing.
menu.hjsonand your themes are checked too. They carry the same silent failure asconfig.hjson, and one of them is worse:ThemeManagerstarts from your menu file and looks each menu up in the theme, so acustomizationnaming a menu that does not exist is never consulted — the theming simply does not happen, with nothing logged. The theme ENiGMA½ ships had eight such blocks; they are fixed, and a check now reports any that remain as a warning.A menu naming a module that cannot be loaded is reported too, for both built-in modules and your own under
paths.mods. That found the sysop!BINKPcommand pointing atbinkp_poll, which has never existed — the module isbinkp/binkp_poll_module— so the command has failed since native BinkP support landed.Menu files get their structure checked — a misspelled
modulformodule, anacswhere onlyconfig.acsis read — along with every menu name they reference, wherever it appears:next, a submit handler, an action key. That last one immediately found theScommand in the ActivityPub menu pointing at a menu that has never existed.config{},form{}and MCI blocks are deliberately not checked: those belong to the modules that read them and to the art they position, and claiming to know them would report most menus on most boards.A machine-readable JSON Schema for
config.hjsonis now published atmisc/config.schema.json, generated from the same source the validator uses bynpm run build:schema. It is there for the configuration editor asked for in #133 and for anything else that wants to know what a setting is called, what type it takes and what it defaults to, without readingconfig_default.js. A test fails if it is ever out of date. Note it describes the configuration after@reference:,@environment:and@file:specs are resolved, and that HJSON editors do not consume JSON Schema — this is not editor tooling.Writing it turned up a false positive in the validator itself: an archiver or file transfer protocol of your own could be told a legitimate key was a typo. The shapes of those blocks are inferred from the examples ENiGMA½ ships, and only the outermost level of an inferred shape was left open. So
entryGroupOrder— which the shippedArjarchiver uses, but the first archiver in the list does not — was reported as an unknown key inside anyone else’slist{}, as wasprocessIACsinside a custom protocol’sexternal{}. Inferred shapes are now open at every level.The same problems are now also reported at startup and when you edit a running board — on the console at startup, since the log is not open yet, and to the log on a hot reload. Neither ever stops anything: a configuration with problems is still applied, exactly as before. Set
general.configValidationtooffto stay silent;oputil.js config validatekeeps working either way. There is deliberately no strict mode. -
TIC-announced files were lost whenever the file arrived after its announcement (#735) — a
.ticcontrol file and the file it announces routinely arrive in separate mailer sessions; a peer running HTick was observed announcing a full Zone 1 nodelist 15–20 minutes ahead of the payload. ENiGMA½ processed the.ticthe moment it landed, could not find the file, archived the announcement toreject/and unlinked it. The file then arrived with nothing left to pair it with and sat in the secure inbound indefinitely. For that peer and that file it failed every single time, and recovery meant finding the orphan by hand and forcing a rescan.A TIC whose file is not here yet is now held and retried on later import passes — the same disposition HTick uses (
TIC_NotRecvd, “has not been received, waiting”) — rather than rejected. Because an import pass already runs the instant a BinkP session delivers files, the pairing normally completes within seconds of the file landing. A TIC that is never satisfied is given up on aftertic.holdMaxAgeMs(48 hours by default) and rejected as before, so nothing accumulates forever.Four more defects in the same code path came out with it:
-
The announced name was matched case-sensitively. A TIC naming
NODELIST.Z34never found a deliverednodelist.z34. Other FTN software normalises this (HTick callsadaptcase()); ENiGMA½ now resolves the name case-insensitively against the inbound. -
A file still being written could be deleted. A payload shorter than the TIC’s
Size, or one failing its CRC, was archived as a reject and unlinked — which, with a mailer that writes directly into the inbound, could destroy a transfer in progress. Short and mismatched payloads are now held and re-checked instead. -
A
.ticwith noFilefield crashed the import pass. Building the payload path threw aTypeErrorthat escaped into a filesystem callback, leaving the pass hung until the five-minute watchdog fired — so every remaining TIC, and the other inbound directory, were skipped. Such a TIC is now simply rejected and the pass continues. -
tic.secureInOnlydid nothing. Documented and defaulted totruesince TIC support landed, it was never read, so TIC files in the unsecure inbound were imported on the strength of an unauthenticatedFromline. It is now enforced. See UPGRADE if you relied on the old behaviour.
-
-
A TIC could write its file anywhere the BBS user could write (#735) — the
Filefield was checked for path traversal, butLfile/Fullnamewere not, and it is the long name a file is stored under:paths.join(areaStorageDir, ticFileInfo.longFileName). A peer whose TIC otherwise validated could therefore sendLfile ../../../somewhere/eviland place arbitrary content outside the file base. All three fields are now checked, andlongFileNamediscards an unsafe candidate rather than returning it. Found by a security review of the work above and confirmed against a live instance.Two smaller things fall out of the same work: the TIC password is now compared without regard to case, as other FTN software does and as ENiGMA½ already did for packet passwords; and the packet and bundle import stages, which run over the same directory first, no longer consume a TIC-announced file whose name happens to match their patterns.
-
Two more ways a TIC file could hurt you, both found by fuzzing the parser (#743) — the same shape as the traversal above, reachable again by different routes.
-
A single NUL byte in the
Filefield stalled the entire import pass. A name likeNODE<NUL>LIST.Z21is not a path separator, is not..and is not absolute, so every safety check passed it — and the filesystem call that followed threw synchronously, escaping the asynchronous machinery so the import never finished. Every remaining TIC and the other inbound directory were skipped until the five-minute watchdog fired. Control characters are now rejected in an announced file name. -
A TIC could cause an arbitrary file to be copied and then deleted. The path of the announced file was built without a safety check, so
File ../../../etc/passwdresolved outside the inbound. Such a TIC is rejected — but rejecting is what does the damage: the reject path archives the announced file topaths.rejectand then unlinks it. This needed neither a known node nor an area you carry, because theFilefield is read several steps before those checks.
Nine further robustness defects came out of the same work, none individually dramatic: a legal 2-dimensional
Seenbywas written back asundefined:1/50to every downlink; a peer’sSeenbylist of a few thousand entries could stall the event loop for seconds; aCrcof00000000— the checksum of an empty file — was misread as a missing field and the TIC rejected; and a bare CR inside a value could smuggle an extra keyword line into TICs forwarded under your own name. -
-
NetMail could not be sent to a node you had configured, without also adding a route for it (#739) —
netMail.routesis meant to say where mail goes when it is not going direct, but in practice it was mandatory for every NetMail destination. A message to an uplink sitting right there innodes{}was refused withNo NetMail route for …, and the sender got a delivery failure notice.The network was being resolved with a helper that compares an address against your own
localAddress— the right question when importing a packet addressed to you, and one a correspondent can never answer yes to. So the unrouted path could only ever succeed for mail addressed to yourself.Destinations are now settled as the code always described: a
routesentry if one matches, otherwise anodesentry for the recipient itself, otherwise refused. Nothing that worked before changes — routes are still consulted first and handled exactly as they were.Two smaller things fall out of the same rework:
-
networkis now optional on a route. Omitting it used to produce the sameNo NetMail routeerror even though a route had matched. It is filled in from the zone of the node being dialed, which is what picks the outbound directory anyway, so the from-address and the spool path cannot disagree. Name anetwork— on a route or on anodesentry — only when two of your networks share a zone. -
Bad configuration is reported for what it is. A route naming a network that does not exist, or carrying an address that will not parse, now says so. The first was reported a step later as a problem with the network rather than the route; the second threw.
This also means automatic AreaFix rescan requests (#241) reach an uplink without a
routesentry having to cover it. -
-
Routed cross-zone NetMail was filed where nothing would ever look for it (#734) — NetMail routed through an uplink in a different zone than the recipient (the standard FidoNet zone gate: zone 2 mail leaving through a zone 1 hub) was written to the outbound directory of the recipient’s zone instead of the uplink’s. A mailer only looks in the zone of the node it is calling, so the session to the uplink completed cleanly having transferred nothing and the message sat on disk indefinitely.
Nothing surfaced it. The packet’s flow file was named for the uplink but filed under the recipient’s zone, so the pending-mail scan — which reads a node’s address from the directory’s zone plus the flow file’s name — reported a node that does not exist (
2:154/10, for an uplink of1:154/10), which the poller then skipped atdebuglevel. Every visible signal said success.Packets are now filed for the node that will actually be dialled. Three related changes mean this shape of failure is noisy rather than silent if it ever recurs:
- A flow file entry whose file is missing is logged rather than skipped in silence.
- A node whose live flow entries all point at missing files is no longer reported as having pending mail. That state used to put the poller in a dial-every-cycle-and-transfer-nothing loop.
- A reference is also looked for by name in the flow file’s own directory. Flow files store absolute paths, so mail that has been moved — including anything misfiled by this bug before upgrading — now ships by moving the files, with no hand-editing of flow files. See UPGRADE.
Each routed NetMail also logs where it was routed, so the effective route can be confirmed rather than inferred.
-
TIC-announced files can now be forwarded to downlinks (#743) — ENiGMA½ could only ever be a leaf node for FTN file echoes. A file received from an uplink was imported into the file base and then deleted from the inbound; it was never passed on. There was no way to declare downlinks, no TIC was ever generated, and the
Path/Seenbyfields were read but never written. Anyone downstream had to be fed by some other file processor, which in practice meant ENiGMA½ could not be used as a file-echo hub at all — a notable gap for a package that otherwise does full EchoMail and NetMail hubbing.Declare downlinks on the TIC area and it forwards, generating a TIC for each:
ticAreas: {fsx_gen: {areaTag: fsxGeneralnetwork: fsxnetuplinks: [ "21:1/100" ] // who may publish into itdownlinks: [ "21:1/200", "21:2/150" ] // who receives it}}Off unless you configure it. An area with no
downlinksbehaves exactly as before. See TIC Support.-
uplinksis an access control, not a formality. Authentication is not per-area: every node innodesis equally able to send you a TIC for any area you carry. Withoutuplinks, a node configured for some entirely unrelated reason could announce a file into an echo it has no rights to and have you relay it to that echo’s subscribers under your own address,PathandSeenby. An area withdownlinksand nouplinksforwards nothing and says so at startup. HTick performs the same check before forwarding. -
The loop guard is the part that matters. A downlink already listed in the TIC’s
Seenbyis skipped, as are the sender, the TIC’sTo, the file’sOrigin, and your own addresses on any network. Addresses are compared allowing for the 2D–5D differences that are ordinary in FTN control data — FSC-0087 lets each hop rewrite them — because a strict comparison would fail to notice a system had already seen a file and send it round again. -
Forwarding is gated harder than importing. Importing affects your own file base; forwarding makes other systems receive traffic your
PathandSeenbyvouch for. A TIC from the unsecure inbound is never forwarded even if you import from there, and neither is one from a node with notic.password— such a node is never actually authenticated, because the check is simply skipped when no password is configured. A TIC addressed to somebody else is imported but not re-announced. -
Replacesdequeues. When a replacement supersedes a file still queued for a downlink that has not collected it, the old file and its TIC are removed from that downlink’s outbound. Otherwise a downlink polling weekly would receive both — and the older one has by then been deleted locally, so it would receive neither.
Problems that would otherwise be silent are reported at startup: an area with downlinks but no resolvable network, a downlink missing from
nodes, or one with no TIC password.- A system carrying file echoes and no message areas can now import at all. The import pass was gated on having
messageNetworks.ftn.areasconfigured, which TIC processing has nothing to do with — so a file-echo-only system logged “EchoMail export disabled” and then silently did nothing with every file its uplink sent. That configuration was barely reachable before this feature existed; it is the obvious one to try now.
-
-
Outbound could be dropped on the floor while a mail session was running — ENiGMA½ appended reference records to BSO flow files without taking the flow file’s
.bsylock, while the BinkP side rewrites those same files as entries are sent: it reads the whole file, marks a line done, and writes it back. An append landing between that read and that write was silently discarded. No error, no log — the queued file simply never shipped, and with nothing left referencing it, its packet sat in the outbound indefinitely. The busier the system, the more often it happened.FTS-5005.003 §5.1 puts the requirement on software, not on mailers: “A bsy is a main control file that must be used by any software dealing with flow files in BSO […] If a bsy file exists all changes are prohibited in any corresponding flow files.” The tosser queueing outbound is exactly that. Both BinkP session paths already took the lock, so the writer was the only unprotected party.
Because
NNNNnnnn.bsyis the standard name, honouring it also interlocks correctly with external mailers — Binkd takes the very same lock, so neither side needs to know about the other. (The pre-existingenigma.bsyflag could never serve this purpose: it is a non-FTS-5005 name in the outbound root, so no external mailer has reason to interpret it.) The protocol now lives in one place and is shared by the writer and the mailer rather than reimplemented on each side.When the lock cannot be taken the outbound is not queued that pass and says so — not writing is what the spec requires, and the next export cycle picks it up.
scannerTossers.ftn_bso.flowLockTimeoutMs(default 5 seconds) bounds the wait; see BinkP.A second, related race was found by running two instances against each other: when one session ships more than one file to the same node, each file’s completion rewrote the whole flow file independently, so all but one entry silently lost its “sent” mark. For mail the entry is deleted after sending, so the leftover reference merely dangles; for a forwarded file echo — where the file stays in your file base and must not be deleted — it looked unsent and went out again on every subsequent session. Those rewrites are now serialised.
-
A wildcard could shadow a more specific entry in
nodes{}and NetMailroutes{}— where several patterns matched an address, which one applied depended on nothing but the order they were written inconfig.hjson. A"*"route above"21:*"took every message; a"21:*"node block above"21:1/100"meant that node’spacketPasswordwas never the one checked. The most specific match now wins regardless of order, which is what the native BinkP mailer already did for its ownbinkp.nodes{}.routes{}is still consulted beforenodes{}, so a catch-all"*"route claims all NetMail — AreaFix to your uplinks on other networks included. With ordering no longer deciding the outcome, a more specific route can now reliably send those direct. See NetMail. -
The
downloadfile area ACS was enforced over the REST API and nowhere else — a security control that silently did not work. A file area can reasonably let everyone browse while restricting who may actually pull bytes down:someArea: {acs: {read: GM[users]download: GM[donors]}}readwas honoured everywhere, so browsing looked correctly gated and the configuration appeared to work.downloadwas checked only bycore/rest/routes/files.js— over telnet and SSH, where essentially every user is, anyone who could see a file could queue and download it. It was the only ACS scope in the system with no terminal-side enforcement.The check is now applied at every path that can move a file to a user:
- Queueing —
DownloadQueue.add()refuses an entry the user has nodownloadright to, and the file area list shows a distinct indicator (noDownloadAccessIndicator,n/ain the shipped theme) rather than letting the queue key look broken. - Protocol downloads — the send queue is filtered at transfer time. A download queue persists in a user property across sessions, so an item queued while permitted can outlive the permission; being removed from a group now takes effect immediately.
- Web download links — both single and batch link generation are gated in
FileAreaWeb, which also covers the browse screen’s “generate web link” action. That path never went through the download queue at all.
Temporary session downloads — QWK packets, FSE attachments — are generated for the user on request, carry no ACS of their own, and are unaffected.
- Queueing —
-
Message areas can now be created automatically for EchoMail you are not configured for (#241) — mail arriving for an unknown FTN area tag was skipped with a warning, and since the packet was then removed, skipped meant lost. ENiGMA½ can now create those areas instead. Off unless you configure it; a system with no
autoAreasblock behaves exactly as before, and with no network enabled nothing extra runs at all.Setup is one command —
./oputil.js mb auto-areas init— and then a per-networkautoAreasblock. See FTN.-
Created areas are read-only, and that means both halves. They carry no
uplinks, so nothing is exported. On its own that is not read-only: a local user could still post into an area that looks live and goes nowhere. So they also carry a write-deny ACS. To adopt one, define it inconfig.hjsonwith your ownuplinksandacs—config.hjsonalways wins over the generated file. -
Nothing is ever written to your
config.hjson. Created areas go to a generatedauto-areas.hjsonpulled in throughincludes, which merges such that your own file wins. That file can therefore be rewritten on every pass without ever touching a description you wrote by hand. -
Real names and descriptions come from the network info pack. The pack arrives by TIC like any other file; ENiGMA½ looks for it in the file area you name rather than waiting to be told, so one that landed before you enabled the feature counts too. It only renames areas you already carry — a pack lists what the network carries, which would otherwise leave you with hundreds of permanently empty areas.
-
Collisions are refused, not merged. An FTN tag lower cases straight onto a local area tag, and
PRIVATE_MAILwould land an echo in everyone’s mailbox. A tag already used in any conference, or carried by another network, is refused too, and the reason is logged. -
ignoreandmaxAutoCreate. Adding a tag toignoreremoves it if it was already created, so it is a real un-create.maxAutoCreatecaps the total ever created for a network rather than the number per run — a per-run cap compounds. -
AreaFix rescan is optional, off, and has no default command. FSC-0057 specifies
=TAG R=nand Mystic and CrashMail II implement it, but husky and SBBSecho do not: they read it as a request to add an area with a garbage tag, and a husky hub withforwardRequestsmay pass that upstream. There is no portable form, so you state the one your uplink speaks or nothing is sent. Replies are matched to requests actually sent and summarised to you in NetMail.
-
-
oputil mb import-areasimported comment lines as message areas (#733) — a leading;or%line became an area whose tag was the comment character. Measured against real network info packs, importing fsxNet’s file echo list produced 18 of them and AgoraNet’s 14, written straight intoconfig.hjsonfor you to find and remove by hand.The area list parser is now shared between
oputiland automatic area creation, and it works out the format from the file’s content rather than its extension:;,%and#comment lines are skipped. fsxNet used%in its 2018 pack and;in the current one, so both are needed.- A FILEBONE file echo list — which six of eight surveyed networks ship named
*.na, alongside their message list — is recognised and sent tofb import-areasinstead of being imported as areas all taggedArea. - A list with the columns reversed, description first, is recognised and refused. SpookNet ships one, and the old parser took 35 entries from it with tags like
Aliens,— nothing on those lines is malformed enough to reject one at a time, so the decision has to be made about the file as a whole. - Lines that survive but cannot be used are listed for you before the confirmation prompt instead of being silently dropped.
AREAS.BBSis unchanged: it cannot be told from a plain area list by shape, so it is still only assumed from the.bbsextension or--type bbs. -
oputil fb import-areasshares that parser, so a FILEBONE list is recognised by its content rather than its name, and a file echo list shipped as a plainTAG Descriptionlist — ArakNet’s, among others — imports rather than reporting “Nothing to import”. FileGate.ZXXand FILEBONE.NAfiles produce byte-identical results to before; a multi-digit area level and flags other than!and*&are now accepted as well. Since a plain list is indistinguishable from a message echo list,import-areassays so before the confirmation prompt. -
A missing
includes:file said the wrong thing on startup — a file listed inincludesthat does not exist stops the board from starting, correctly, but the message advised running./oputil.js config newas thoughconfig.hjsonitself were missing, and printed the placeholder'{configFile}'instead of a path. Both now name the file that is actually missing and say what to do about it. -
A second instance bound nothing, said nothing, and kept running (#547) — starting ENiGMA½ while another instance already held its ports produced no error, no
System started!, and no exit. The process simply sat there, serving no one.Node’s
server.listen(port, host, cb)registerscbas a one-shot'listening'listener: it never receives an error argument, and on a failed bind it never fires at all. Every server treated it as an error-first callback, soEADDRINUSEleft the startup series inlistening_server.jswaiting for a callback that would never come. Telnet caught the event but logged it atinfo— and the default logging config writes to a rotating file with no console stream, so nothing reached the terminal. The remaining servers had no server-level'error'handler at all, so the event fell through to the process-leveluncaughtExceptionnet, which by design keeps the process alive.Every login, content, and chat server now binds through a shared helper that listens for both
'listening'and'error'and calls back exactly once, with an error an operator can act on —EADDRINUSE,EACCESon a privileged port, andEADDRNOTAVAILeach get an explanation rather than a bare code. A bind failure is now fatal for every server, including NNTP; see UPGRADE.md.-
Startup output now reads in the order things happen — the banner, then any configuration warnings, then the server list, then
System started!. The banner was previously printed at the very end, underneath everything it was supposed to introduce. -
Startup now reports what actually bound. One line per server with its address and port, so a misconfigured or missing service is visible at a glance instead of only in the log. Colour is used only when stdout is a TTY and
NO_COLORis unset, so piped output and journals stay clean. -
Startup failures always exit non-zero. An error that had already been displayed previously fell through the final handler and the process wound down with status
0, so service managers and install scripts saw a clean exit from a failed start.
-
-
BinkP sessions ended on a timeout instead of a clean close — binkp/1.1 runs a session as a series of batches: another follows any batch that carried more than the two
M_EOBs, and only an empty batch ends the session. ENiGMA½ stopped after the first one, leaving a conforming peer waiting for anM_EOBthat never came.No mail was lost — it had already transferred by then — but the peer sat until its own timeout (five minutes, for binkd) and then recorded an otherwise successful session as
failed. With binkd’s shippedtry/holdsettings, enough of those in a row will put us on hold and stop it calling for a while.The cost was ours too: an inbound session parked like that still holds the node’s lock, so crashmail queued for that node inside the window is skipped and waits for the next scheduled poll — quietly defeating the point of crashmail. Sessions against binkd now finish in milliseconds and log
done (..., OK, ...).Peers below binkp/1.1, and any that never identify their version, are treated exactly as before.
-
BinkP GZ compression used the wrong container (#723) — FTS-1029 specifies zlib’s
compress()/compress2(), which produce the RFC 1950 container: two header bytes and an Adler-32 tail. ENiGMA½ emitted RFC 1952 (gzip) instead — the same deflate payload behind a different header — so every conforming peer rejected it on sight. binkd logsDecompress <file> error -3and abandons the transfer; the sending side saw nothing wrong and re-offered the same file on every poll, for days. Confirmed here against binkd 1.1a-115 and reported against Mystic 1.12A48.Only nodes without
archiveTypewere affected: ArcMail bundles and archives are already compressed, so GZ is skipped for them entirely. That is the same set of systems as the packet-naming bug in #722.- Both containers are accepted on receive. The GZ option carries no version, so a pre-fix and a post-fix ENiGMA½ cannot tell each other apart. Inbound data is now sniffed and either container decoded, which keeps a half-upgraded link working in that direction. Only what we send changed.
- A failed decompression recovers instead of stalling. FTS-1029 lets a receiver switch compression off mid-session by answering with an
M_GETcarrying anNZtoken; ENiGMA½ now does that and takes the file again in the clear. If that fails too it sendsM_SKIP, so the batch finishes rather than hanging to a timeout. Previously the error was logged atwarnand the session simply stopped. - Compression can be turned off per node with
gz: false, for a peer whose decompressor cannot be fixed.
-
BinkP outbound stalled against any peer that asked for NR mode (#724) — a peer sending
OPT NRobliges us to offer each file with an offset of-1and wait for it to name the offset it wants (FTS-1028). We did that, then began sending file data straight away. FTS-1026 requires the answer toM_GETto “proceed with transmission of the file requested starting with an appropriateM_FILE”, and a conforming receiver has already closed the file by the time it sendsM_GET— binkd discards every data frame that arrives with nothing open, silently. Both sides then waited: ours for the acknowledgement, theirs for the re-announcement, until the five minute session timeout. No mail moved and nothing was logged abovedebug. Note that this had nothing to do with the peer’s version: binkd asks for NR whenever a sysop sets-nror-ndon a node, and-ndis common. Fixed, along with four related defects in the same code:-
M_SKIPwedged the batch — a skipped file stayed in the in-flight slot forever, producing the same silent stall. binkd sendsM_SKIPin place ofM_GETwhenever an inbound skip mask matches, so this was a second route to an identical symptom. Skips are now honoured as FTS-1026 intends: non-destructive, the file kept for a later session, the batch carrying on. -
M_GETfor an already-sent file was ignored — FTS-1026 requires recognising a request naming “a file that have been transmitted, but we are still waiting an M_GOT acknowledge for it” — the race it explicitly warns implementations about. We logged it and moved on, leaving the remote waiting. -
Inbound NR started each file twice — after answering an offset request we left the receive open, so the sender’s required re-announcement began a second receive of the same file. A FREQ arriving that way pinned the end-of-batch hold above zero and the session never sent
M_EOB. -
OPT NRis no longer sent by default — the frame asks the remote to hand us files an offset at a time, costing a round trip each; FTS-1028 says it “should be used only if absolutely necessary”. It is now opt-in per node viarequestNR. Honouring a peer’s request remains unconditional, as the spec requires.
Also removed: a denylist of binkd 0.9.x version strings that switched NR off. It was transcribed from binkd’s source, where the identical list drives the opposite workaround.
-
-
BSO outbound correctness & spec compliance — a set of fixes to the FidoNet-style outbound spool shared by
ftn_bsoand the native BinkP mailer, each checked against FTS-5005.003 (Advanced BinkleyTerm Style Outbound flow and control files). All four caused mail to sit in the spool undelivered with nothing logged at any level, includingtrace. See UPGRADE.md for recovery of anything already stranded.-
EchoMail export to a node without
archiveType(#722) — a node with noarchiveTypeexports bare packets rather than ArcMail bundles. Those were renamed onto a BSO netmail flow file name with no separating dot (e.g.43792ae5cutinstead of43792ae5.cut), which nothing — this mailer or any other — could ever match. The basename was also a message serial number rather than the destination’s net/node, so simply restoring the dot would have decoded it as a phantom address. Un-archived packets now ship as flow file references (NNNNnnnn.?lo), exactly as ArcMail bundles and NetMail already did. This is also what the spec requires: §3.1 gives a netmail flow file a one-to-one correspondence with its destination, so a node can hold exactly one at a time, while a single export routinely produces several packets. -
Direct-attach packets are renamed on the wire — §3.1 requires that a netmail flow file (
NNNNnnnn.?ut) “must be dynamically renamed at the moment of sending to a remote system with a unique name and extensionpkt”. ENiGMA½ sent it under its.?utname, so although the transfer succeeded, the receiving system’s tosser did not recognize what arrived as a packet. Such files — which other software, or a sysop, may legitimately place in the outbound — are now sent as a uniqueNNNNNNNN.pkt. -
Upper case outbound files are now found — §2 asks that software “be able to handle both” upper and lower case BSO filenames. The mailer’s pending-mail scan was case-insensitive but its per-node lookup was not, so a node with upper case flow files (from
fileCase: 'upper', or an outbound inherited from a DOS-era mailer) was reported as having mail, dialed every poll cycle, and then sent nothing. Lookups,.bsylocks, and.pntsubdirectories are now all matched case-insensitively. -
Point addresses are now polled and shipped — §2 puts a point’s flow and control files in a
NNNNnnnn.pntsubdirectory of the boss node’s outbound.ftn_bsohas always written that layout, but the native BinkP mailer had no knowledge of it: point mail was never sent, and polling a point address served it the boss node’s mail instead. Points are now scanned, shipped, and locked in their own subdirectory.
-
-
Multi-network BSO outbound fix —
ftn_bso(the scanner/tosser) and the native BinkP mailer used two independent rules to decide which FTN network owns the bareoutbound/spool directory. With two or more networks configured and no explicitscannerTossers.ftn_bso.defaultNetwork, the two disagreed: outbound NetMail and EchoMail for the first-listed network was written tomail/ftn_out/<network>/but looked for inmail/ftn_out/outbound/, so it was never sent — and never logged an error. Both sides now share a single resolver (defaultNetworkwhen set, otherwise the first listed network), network names are matched case-insensitively, and mail already queued under the previous layout is picked up and sent automatically. See UPGRADE.md for details.
0.5.0-beta
Section titled “0.5.0-beta”-
NetRunner over SSH sizing fix — NetRunner connects via stock cryptlib, which hardcodes the terminal size it reports as 80x48, throwing art positioning off by a row on an 80x25 screen. That reported size is now skipped; ENiGMA½ queries the terminal instead, which NetRunner answers correctly. SyncTERM is unaffected — Synchronet’s cryptlib fork reports a real size and identifies itself distinctly. Controlled by
loginServers.ssh.untrustedTermSizeClients. -
v86 multi-node shared disk — concurrent door sessions for the same disk image now share a single in-memory buffer (SharedArrayBuffer), giving all nodes a live view of the same disk — exactly as they would on a real BBS with a shared drive. Games that rely on file/record locking (e.g. TradeWars 2002) work correctly when
SHARE.COMis loaded fromrunBatch. Single-player doors are unchanged; usenodeMax: 1as before. The buffer is flushed back to the image file after each session exits, serialized so concurrent exits never race at the OS level. -
OSC 8 clickable hyperlinks — URLs in message bodies, file NFO/readme viewers, and file download listings are now rendered as clickable hyperlinks on terminals that support the OSC 8 standard: IcyTerm, SyncTERM, VTX, and modern *nix terminals (foot, Alacritty, GNOME Terminal, kitty, WezTerm, Windows Terminal, and others). Sysops enable hyperlinks per-view by adding
hyperlinks: trueto any%MTview in preview or read-only mode. The default menu templates and ActivityPub viewer have this enabled out of the box. See Multi Line Edit Text View for details. -
User status config module — users can now toggle their own availability and visibility via the new
user_status_configmodule (commandSTATUSfrom the main menu in the default template). Availability controls whether the user can be paged/messaged; visibility controls whether they appear in who’s-online and last-callers lists. Sysops expose the module by wiring@menu:userStatusConfiginto their menu config. The module supportsenabledIndicator/disabledIndicatorconfig overrides andTL10/TL11custom-format views ({availableIndicator},{visibleIndicator}) for full theme control. -
Pre-auth feedback to sysop — visitors can now send a private message to the sysop directly from the login matrix, before logging in. The sender types a free-text name (not resolved to any user account) and composes a message using the full FSE editor. Replies to these ghost-sender messages are blocked at the inbox with a clear notice rather than failing silently. See the Sysop Chat & Contact doc for configuration details.
-
Secret file injection (
@file:) — any string-valued config key that holds a secret (SSH host key passphrase, SMTP/IMAP passwords, BinkP session passwords, FTN packet passwords, JWT signing secret, door service credentials, etc.) can now be kept out ofconfig.hjsonentirely by using the new@file:directive. Point it at any readable file — absolute or relative to the config directory — and ENiGMA½ reads and trims the value at startup. Docker/Podman secrets (/run/secrets/…) andchmod 600files both work. The existing@environment:directive is unchanged. See Configuration Files — Secret Files and Security for examples covering every secret-bearing config key. -
REST API v1 — a JSON REST API is now available under
/_enig/api/v1/when the web server is enabled. Endpoints cover system info, message conferences and areas (read/post/delete), file areas (list/metadata/download/upload), and user profiles. Two auth schemes are supported: short-lived JWT Bearer tokens (obtained viaPOST /auth/login) and long-lived API keys managed withoputil rest api-key. Public access to specific message and file areas can be configured without requiring authentication. See REST API for full documentation.
0.4.0-beta
Section titled “0.4.0-beta”-
Security hardening — several security improvements across the system:
-
Stronger password hashing — PBKDF2 iteration count raised from 1,000 to 210,000 (OWASP 2024 recommendation for PBKDF2-SHA512). Existing user passwords are transparently re-hashed to the new parameters on next successful login — no user action or migration script required. Hash parameters are now stored per-user in
pw_hash_paramsso future algorithm changes can be made without a flag day. -
Password reset token TTL — reset tokens now expire after a configurable period (default 1 hour). Configure via
contentServers.web.resetPassword.tokenTtlMinutesinconfig.hjson. -
Web endpoint rate limiting — per-IP sliding-window rate limits are now enforced on the password reset and 2FA/OTP registration endpoints (GET and POST), returning HTTP 429 when exceeded. Limits are configurable under
contentServers.web.rateLimits(pwResetGet,pwResetPost,otpRegGet,otpRegPost); defaults are 5–10 requests per 10-minute window. -
Command injection fix — the
oputil ssh-keygenerator now passes the key passphrase as a discrete process argument rather than interpolating it into a shell string, eliminating a shell injection vector. -
Archive util filename injection fix — filenames in
compressToare now passed as discrete arguments rather than space-joined, preventing argument injection via crafted filenames. -
Static file symlink escape fix — the web server’s static file resolver now dereferences symlinks via
fs.realpathbefore the path boundary check, preventing a symlink placed insidestaticRootfrom escaping it. -
Config file security note — see Security for guidance on protecting
config.hjson(which contains secrets such asprivateKeyPass) with appropriate file permissions.
-
-
Native BinkP / FTN mailer — built-in BinkP/1.1 implementation (inbound listener + outbound caller) that integrates directly with the existing BSO scanner/tosser. No external
binkdor cron-driven poll script required for FidoNet-style networks. Two complementary triggers replace the old single-timer model:- Crashmail — when ftn_bso writes a flow file, the destination peer is dialed within ~½ second (debounced; tunable via
crashmailDebounceMs). No more waiting for the next scheduled tick to ship outbound. - Pull schedule —
binkp.pullSchedule(defaultevery 15 minutes) dials every configured peer inbinkp.nodesregardless of pending mail, so echo mail flows in from hubs that wait for the spoke to call. Per-node opt-out viapull: false. - Configure inbound port, per-node hosts/passwords (CRAM-MD5), and the schedules under
scannerTossers.ftn_bso.binkpinconfig.hjson.
- Crashmail — when ftn_bso writes a flow file, the destination peer is dialed within ~½ second (debounced; tunable via
-
Internet Mail (send & receive) — users can now send and receive internet email directly from the BBS private message system, via a new
emailscanner/tosser module. See Email Configuration and Internet Mail.- Send: private messages addressed to
user@domain.comare delivered through your configured SMTP transport (Nodemailer-compatible — any provider, or service shortcut like Zoho/Fastmail). - Receive: inbound email is pulled from a single IMAP mailbox (polling or
IMAP IDLE) and routed to the local user whose name matches the To: local-part. Successfully imported messages are marked\Seenand optionally moved toinbound.imap.processedFolder. Unmatched / unparseable mail is preserved as.emlinmail/email/failed/, marked\Seento prevent re-fetch loops, and optionally moved toinbound.imap.failedFolder. ENiGMA½ never deletes mail from your IMAP server — retention is up to you or your provider. - Per-user
From:header — whenemail.outbound.fromDomainis set, outbound mail is sent as"UserName" <sanitized@fromDomain>instead of the staticdefaultFrom. The SMTPSender:header and envelopeMAIL FROMare kept as the authenticated mailbox so bounces stay deliverable and receivers display the standard “on behalf of” attribution. Local-part derivation respectsusers.badUserNames— reserved names fall back todefaultFrom. Replacement char for invalid username characters is configurable viaemail.outbound.usernameReplaceChar(default_). - Signature / pipe-code stripping on export — outbound message bodies are run through the same ANSI + MCI pipe-code stripping pipeline used by the NNTP export, so signatures render cleanly in external mail clients.
- Send: private messages addressed to
-
Wide Character (CJK/UTF-8) Support — full-width Unicode characters (CJK ideographs, Hangul, Hiragana, Katakana, fullwidth forms) are now handled correctly throughout the view and word-wrap layers
- All display-width measurements use
wcwidth(3)semantics — wide characters count as 2 terminal columns, combining marks as 0 EditTextViewandMultiLineEditTextViewcursor navigation, scrolling, and line-wrap all operate on display columns rather than string indices; the cursor cannot land inside the phantom second column of a wide character- Word-wrap (
word_wrap.js) andLineBufferwrap at display-column boundaries — a wide character is never split across lines renderStringLength,ansiRenderStringLength, and the new exportedrenderSplitPosinstring_util.jsall account for wide characters; pipe codes and ANSI cursor-forward sequences are handled correctly in bothgetText()onLineBuffercorrectly round-trips CJK text — no spurious space is inserted at character-boundary soft-wrap points
- All display-width measurements use
-
UTF-8 Art Variants (
.utf8ans) — place aFOO.UTF8ANSalongsideFOO.ANSin any art or theme directory; UTF-8-capable users automatically receive the UTF-8 variant while CP437 users see the standard file. No menu or theme configuration is required — selection is automatic based on the negotiated terminal encoding. See General Art Information.- Opt-in upward UTF-8 probe — set
term.probeUtf8Encoding: trueinconfig.hjsonto enable a CPR-based check that upgrades CP437-identified terminals (e.g.ansi,syncterm) to UTF-8 output when the terminal actually supports it. Uses the same cursor-advance technique ascheckUtf8Encoding. Default:false.
- Opt-in upward UTF-8 probe — set
-
Full log viewer from WFC. Defaults to
lkey. -
Major FTN compatibility fixes, especially for those wanting to run a point.
-
Newscan enhancements — users now have direct control over their newscan experience:
-
User-configurable scan areas — new
configure_newscanmodule (keyNfrom the message base menu) lets users toggle individual message areas on or off for newscan, or toggle all at once. Selection is persisted immediately as a JSON array in thenewscan_area_tagsuser property. Areas not selected are skipped entirely during the scan. -
Newscan floor date — a new
newscan_min_timestampuser property acts as a non-destructive lower bound on the scan. For each area, the effective scan start isMAX(per-area last-read pointer, first message at floor date). Unlike the existing Set Newscan Date feature (which rewrites per-area pointers), the floor is a persistent filter that never touches read state. Set or clear it via theGkey in the configure screen, which navigates to a floor-date entry form. The existingset_newscan_datemodule is unchanged and kept for explicit pointer repositioning. -
New-user backlog protection —
newscan_min_timestampis automatically set to the account creation timestamp for all new accounts, so users joining a busy BBS with years of message history only see posts from their join date onward. -
set_newscan_dategainstarget: floor— sysops can wire a menu entry that writesnewscan_min_timestampinstead of moving per-area pointers, using the same date-input UI. -
Count/list consistency fix — the new message count check and the message list passed to
msg_listnow both use the same floor-adjusted effective last-read ID, eliminating the possibility of navigating to an area that shows zero qualifying messages.
-
0.3.0-beta
Section titled “0.3.0-beta”Various fixes
-
ActivityPub MAJOR updates have landed.
-
Server-side baud rate emulation —
baudRatein a menu’sconfigblock now throttles art display on the server rather than delegating to a SyncTERM-specific terminal escape sequence. Emulation now works with every terminal client. The previous approach was sticky (rate persisted across menus until explicitly cleared); the new approach is scoped precisely to each art display and resets automatically. ExistingbaudRateconfig values require no changes. -
SQLite driver migrated to
better-sqlite3— This is an internal change with no impact on existing data or configuration. Results in some major DB performance gains. -
Z-Machine Interactive Fiction Door — new
zmachine_doormodule runs Z-Machine IF games (Zork, Colossal Cave Adventure, Photopia, Anchorhead, Lost Pig, and hundreds more) natively in Node.js. No external emulator, no serial bridge, no drop file — a cross-platform pure-JavaScript path for text-adventure games.- Backed by ifvms.js (the Z-Machine interpreter used by Parchment) and glkote-term, run in a dedicated worker thread per session for isolation.
- Supports Z-Machine versions 3, 4, 5, and 8 — covers all classic Infocom titles, the original Crowther/Woods Adventure port, and the vast majority of modern Inform games from the IF Archive.
0.1.1-beta
Section titled “0.1.1-beta”- NNTP server improvements — several protocol compliance and reliability fixes:
- Article posting now correctly detects end-of-post and handles CRLF line endings
AUTHINFO USERis now advertised inCAPABILITIESso clients know to authenticate before postingXrefheader is now generated, improving cross-session read tracking in NNTP clients- Newsgroups header parsing is more robust (null-safe, whitespace-tolerant)
- Group message cache TTL increased from 30s to 5 minutes
0.1.0-beta
Section titled “0.1.0-beta”-
Sysop Chat / Break Into Chat — real-time split-screen chat between sysop and user
- Sysop can break into chat with any node directly from WFC (
Bkey on selected node) - Users can page the sysop via the
pageSysopmenu entry; includes per-user rate limiting and BEL + interrupt notification to all online sysops (sysops at WFC see it directly in the node list) - If no sysop is available, users are offered the option to send their message as private mail instead
- Both parties share the same
sysopChatmodule with role-based panel routing (sysop messages top, user messages bottom) - Status line uses the standard custom-range token system (
chatInfoFormat10, etc.) — fully themable - WFC node list gains a
{pageIndicator}token per row — non-empty when that node has a pending page; configurable viapageIndicatorin the WFCconfigblock - WFC custom tokens
{pendingPageCount},{pendingPageUser},{pendingPageNode},{pendingPageMessage}for surfacing page queue state in art prefixFormatproperty onEditTextView— set per-view intheme.hjsonto display a role-specific prefix before the input (e.g."|15{userName}|07> "); pipe codes render live as the user types; cursor and scroll account for the prefix width automatically
- Sysop can break into chat with any node directly from WFC (
0.1.0-beta
Section titled “0.1.0-beta”-
Pause Prompt Improvements — see Pause Prompts for the full reference
pause: pageBreak— art is paginated and displayed screen-by-screen with a prompt between pages; detects absolute-positioning ANSI and falls back to single-page display automaticallypause: '<promptId>'— shorthand: end-mode pause using the named prompt; equivalent topause: true+pausePrompt: <promptId>pausePrompt— per-menu override of the prompt name used for end-of-art and/or page-break pauses; accepts a string (same prompt for both) or{ end, page }object for independent controlpausePosition— per-menu{ row, col }override to force the pause prompt to a specific screen positioncontinuousKey/quitKey— configurable keys on thepausePageprompt to skip remaining page breaks or abort all remaining pages entirelypausePagesystem prompt — add this alongsidepausein yourpromptsblock to customise page-break behavior; supports all MCI views including%TK(TickerView) for animated instructions- Pipe color codes in TickerView
textare now preserved across all non-dynamic motion styles (bounce,reveal,typewriter,fallLeft/fallRight) — color survives scrolling - Module developers:
displayThemedPause/displayThemedPrompt(whenpause: true) callbacks now receive a third argumentpressedKey: { ch, key }. Existing callers that ignore extra arguments are unaffected.
-
New MCI View Types
- TickerView (
%TK) — animated single-line marquee with a two-axis model; works in any context including pause prompts (see above):- Motion styles:
left,right,bounce,reveal,typewriter,fallLeft,fallRightfallLeft/fallRight: characters spread across the window with increasing inter-char gaps toward the source edge, then all slide at 1 col/tick and stack against the target edge — a “stack of bricks” effect
- Effects: text-style effects (
upper,lower,title,l33t,mixed, and more) baked at set-time; dynamic per-tick effects (rainbow,scramble,glitch) - Text-style and dynamic effects are independent axes and can be freely combined (e.g.
l33t+rainbow) scramblerenders each character’s noise in its own pipe color with reverse-video;glitchusesstyleSGR2for corruption color- Redraw optimization: ticks where the rendered output hasn’t changed (e.g.
bounceat rest, hold phases) are skipped entirely — no unnecessary cursor movement - All configuration via
mciblock inmenu.hjson/theme.hjson— no inline MCI args needed destroy()clears timers; view teardown inViewControllernow callsdestroy()on all views, fixing timers surviving menu transitions
- Motion styles:
- StatusBarView (
%SB) — single-line view with two modes:- Single mode: auto-refreshing text label that re-renders a format template on a configurable
refreshInterval; skips redraws when text hasn’t changed - Panel mode (
panelsarray): divides the view into independently-addressable named slots, each with its own width, alignment, color, fill character, and optional auto-refresh template. Panels are updated from code viasetPanel(name, value)/setPanels(updates)without touching adjacent slots. A panel’stextproperty (withoutrefreshInterval) sets a static initial value evaluated once at init — useful for fixed label prefixes configured entirely frommenu.hjson.
- Single mode: auto-refreshing text label that re-renders a format template on a configurable
- FSE editor footer now uses a single
%SB1in panel mode (replacing the old separate%TL1/%TL2views) — displays cursor position and INS/OVR mode side-by-side, updated live as the cursor moves. See UPGRADE if you have custom FSE art or menu config.
- TickerView (
-
View System Modernization
- Converted the entire view system from
util.inherits/prototype patterns to ES6 classes:View,TextView,EditTextView,MaskEditTextView,ButtonView,MenuView,HorizontalMenuView,VerticalMenuView,FullMenuView,ToggleMenuView,SpinnerMenuView,MultiLineEditTextView,ViewController - Numerous bug fixes applied during conversion (position defaults, SGR field aliasing,
key_entry_view.jsboolean logic,color_codes.jsWWIV/CNET capture groups,horizontal_menu_view.jsheight,multi_line_edit_text_view.jstabStopsbinding) - New LineBuffer — isolated, view-dependency-free line storage using
Uint32per-character attribute words (fg, bg, bold, blink, underline, italic, strikethrough, color source, true-color flags); soft/hard EOL tracking; word-boundary wrap with character-break fallback - EditTextView and MaskEditTextView are now backed by
LineBuffer: cursor-aware insert/delete at any position, left/right/home/end movement with scroll-window tracking, forward-delete, fixed partial-fillgetData()bug inMaskEditTextView client_term.js:beginWrite()/commitWrite()with nesting support — all writes within a keypress or focus switch are buffered and flushed as a single socket write, eliminating intermediate cursor flicker in terminals
- Converted the entire view system from
-
oputil userSSH Key Managementoputil.js user import-ssh-key USERNAME KEYFILE— imports a SSH public key for a user from a file, validates the key, and stores it for SSH key-based loginoputil.js user remove-ssh-key USERNAME— removes a user’s stored SSH public keyoputil.js user info USERNAMEnow displays SSH key info (algorithm, SHA256 fingerprint, comment) when a key is on file
-
File Base: Wildcard/Recursive Storage Tags (#194)
Appending
/*to a storage tag path enables recursive scanning of all subdirectories:storageTags: {scene_files: "/path/to/scene/*" // walks all subdirs}- Files found in subdirectories are indexed with their
relPath(e.g.2024/April) stored in the database, so same-named files in different subdirectories are tracked as distinct entries. - When an area mixes flat and wildcard tags, flat tags are scanned first and their directories are excluded from wildcard scans to prevent double-indexing.
.enigmaignorefiles (gitignore syntax) can be placed anywhere in a wildcard tree to exclude files or directories from scanning.- Startup warns on malformed wildcard patterns (e.g. a bare
*not at the trailing/*position). - New database column
storage_tag_rel_path; automatically added to existing installations on first startup.
- Files found in subdirectories are indexed with their
-
Bug Fixes & Stability
0.0.14-beta
Section titled “0.0.14-beta”-
ActivityPub & Mastodon Support (Experimental)
-
A new ActivityPub Web Handler has been added.
-
⚠️ WARNING: ActivityPub is disabled by default. There may be security implications, federation may be unstable, and some parts may not work yet. Use at your own risk!
-
Provides groundwork for federated features: WebFinger discovery, NodeInfo2, actor profiles/avatars, inbox/outbox/shared inbox, and handling of common ActivityPub object types (
Note,Accept,Undo, followers/following). -
WebFinger and NodeInfo2 handlers are also disabled by default. These may be useful for inter-BBS or other integrations, but note: WebFinger may still “advertise” ActivityPub endpoints even if AP itself is off.
-
Cool new functionality arrives with or without AP enabled:
-
PNG Avatars: users now get avatars (including auto-generated defaults) that can be served via the web frontend.
-
Message editor and timeline improvements:
- Recognition of
@user@domainaddressing (Fediverse general) - Unicode → ASCII transliteration for federated messages (via AnyAscii). …but we can use it for any <-> web!
- Recognition of
-
Better routing for web handlers and
.well-knownpaths. -
Dedicated web logging under
contentHandlers.web.logging. -
TONS of fixes and improvements to the code base
The fate of full ActivityPub support in ENiGMA is till up in the air…
-
-
-
Web Server Changes (⚠️ some may be breaking):
/static/prefixes are no longer required (ugly hack removed).- Internal routes (e.g. password reset) now live under
/_enig/. - File base routes now default to
/_f/instead of/f/. If yourconfig.hjsonstill uses/f/, update it. - The system will now search for
index.htmlthenindex.htmif a suitable route cannot be found. - Web Handler modules are now easier to add; several exist by default.
-
Other Additions & Changes
- New users now have randomly generated avatars assigned (served via System General Web Handler).
- CombatNet has shut down; the module (
combatnet.js) has been removed. - New
NewUserPrePersistsystem event available for developers to hook into account creation. viewValidationListenercallback signature has changed: now(err, newFocusId). To ignore a validation error, call withnullforerr.- The Menu Flag
popParenthas been removed;noHistoryhas been updated to work as expected. See UPGRADE. - Various New User Application (NUA) properties are now optional. Remove optional fields from NUA artwork if you wish to collect less information (stored as empty string). Optional properties: Real name, Birth date, Sex, Location, Affiliations (Affils), Email, Web address.
- Art handling now respects art width from SAUCE metadata when terminal width is greater, fixing display issues on wide UTF-8 terminals.
0.0.13-beta
Section titled “0.0.13-beta”- Note for contributors: ENiGMA has switched to Prettier for formatting/style. Please see CONTRIBUTING and the Prettier website for more information.
- Removed terminal
cursor position reportsfrom most locations in the code. This should greatly increase the number of terminal programs that work with Enigma 1/2. For more information, see Issue #222. This may also resolve other issues, such as Issue #365, and Issue #320. Anyone that previously had terminal incompatibilities please re-check and let us know! - Bumped up the minimum Node.js version to v14. This will allow more expressive Javascript programming syntax with ECMAScript 2020 to improve the development experience.
- New Waiting For Caller (WFC) support via the
wfc.jsmodule. - Added new configuration options for
term.checkUtf8Encoding,term.checkAnsiHomePosition,term.cp437TermList, andterm.utf8TermList. More information on these options is available in UPGRADE. - Many new system statistics available via the StatLog such as current and average load, memory, etc.
- Many new MCI codes:
MB,MF,LA,CL,UU,FT,DD,FB,DB,LC,LT,LD, and more. See MCI. - SyncTERM style font support detection.
- Added a system method to support setting the client encoding from menus,
@systemMethod:setClientEncoding. - Many additional backward-compatible bug fixes since the first release of 0.0.12-beta. See the project repository for more information.
- Deprecated Gopher’s
messageConferencesconfiguration key in favor of a easier to deal withexposedConfAreasallowing wildcards and exclusions. See Gopher. - NNTP write (aka POST) access support for authenticated users over TLS.
- Advanced MCI formatting!
- Additional options in the
abracadabramodule for launching doors. See Local Doors
0.0.12-beta
Section titled “0.0.12-beta”- The
masterbranch has become mainline. What this means to users isgit pullwill always give you the latest and greatest. Make sure to read Upgrading and keep an eye onWHATSNEW.md(this file) and UPGRADE! See also ticket #276. - Development now occurs against Node.js 14 LTS.
- The default configuration has been moved to config_default.js.
- A full configuration revamp has taken place. Configuration files such as
config.hjson,menu.hjson, andtheme.hjsoncan now utilize includes via theincludesdirective, reference ‘self’ sections using@reference:and import environment variables with@environment. - An explicit prompt file previously specified by
general.promptFileinconfig.hjsonis no longer necessary. Instead, this now simply part of thepromptssection inmenu.hjson. The default setup still creates a separate prompt HJSON file, but it isincludesed inmenu.hjson. With the removal of prompts thePromptsChangedevent will no longer be fired. - New
PVACS check for arbitrary user properties. See ACS for details. - The
messagearg used bymsg_listhas been deprecated. Please starting usingmessageIndexfor this purpose. Support formessagewill be removed in the future. - A number of new MCI codes (see MCI)
- Added ability to export/download messages. This is enabled in the default menu. See
messageAreaViewPostin the default message base template and look for the download options (@method:addToDownloadQueue, etc.) for details on adding to your system! - The Gopher server has had a revamp! Standard
gophermapfiles are now served along with any other content you configure for your Gopher Hole! A default gophermap can be found in the misc directory that behaves like the previous implementation. See Gopher docs for more information. - Default file browser up/down/pageUp/pageDown scrolls description (e.g. FILE_ID.DIZ). If you want to expose this on an existing system see the
fileBaseListEntriesin the defaultfile_base.in.hjsontemplate. - File base search has had an improvement to search term handling.
./oputil user group -groupto now accepts~groupremoving the need for special handling of the ”-” character. #331- A fix has been made to clean up old
file.dbentries when a file is removed. Previously stale records could be left or even recycled into new entries. Please see UPGRADE.md for details on applying this fix (look fortables_update_2020-11-29.sql). - The onelinerz module can have
dbSuffixset in it’sconfigblock to specify a separate DB file. For example to use as a requests list. - Default hash tags can now be set in file areas. Simply supply an array or list of values in a file area block via
hashTags. - Added ability to pass an
envvalue (map) toabracadabradoors. See Local Doors. dropFileTypeis now optional when launching doors withabracadabra. It can also be explicitly set tonone.- FSE in view mode can now stylize quote indicators. Supply
quoteStyleLevel1in theconfigblock. This can be a single string or an array of two strings (one to style the quotee’s initials, the next for the ’>’ character, and finally the quoted text). See themessageAreaViewPostmenuconfigblock in the defaultluciano_blocktronicstheme.hjsonfile for an example. An additional level style (e.g. for nested quotes) may be added in the future. - FSE in view mode can now stylize tear lines and origin lines via
tearLineStyleandoriginStyleconfigvalues in the same manor asquoteStyleLevel.
0.0.11-beta
Section titled “0.0.11-beta”- Upgraded from
alphatobeta— The software is far along and mature enough at this point! - Development is now against Node.js 12.x LTS. Other versions may work but are not currently supported!
- QWK support
oputil fb scan *areaTagWildcard*scans all areas in which wildcard is matched.- The archiver configuration
escapeTelnethas been renamedescapeIACs. Support for the old value will be removed in the future.
0.0.10-alpha
Section titled “0.0.10-alpha”oputil.js user rename USERNAME NEWNAMEmy_messages.jsmodule (defaulted to “m” at the message menu) to list public messages addressed to the currently logged in user. Takes into account their username andreal_nameproperty.- SSH Public Key Authentication has been added. The system uses a OpenSSH style public key set on the
ssh_public_keyuser property. - 2-Factor (2FA) authentication is now available using RFC-4266 - HOTP: HMAC-Based One-Time Password Algorithm), RFC-6238 - TOTP: Time-Based One-Time Password Algorithm, or Google Authenticator. QR codes for activation are available as well. One-time backup aka recovery codes can also be used. See Security for more info!
- New ACS codes for new 2FA/OTP:
ARandAF. See ACS for details.
oputil.js user 2fa USERNAME TYPEenables 2-factor authentication for a user.
oputil.js user info USERNAME --securitycan now display additional security information such as 2FA/OTP.oputil.js fb scan --quickis now the default. Override with--full.- ACS checks can now be applied to form actions. For example:
{ value: { command: "SEC" } action: [ { // secure connections can go here acs: SC action: @menu:securityMenu } { // non-secure connections action: @menu:secureConnectionRequired } ]}idleLogoutSecondsandpreAuthIdleLogoutSecondscan now be set to0to fully disable the idle monitor.- Switched default archive handler for zip files from 7zip to InfoZip (
zipandunzip) commands. See UPGRADE. - Menu submit
action’s can now in addition to being a simple string such as@menu:someMenu, or an array of objects with ACS checks, be a simple array of strings. In this case, a random match will be made. For example:
submit: [ { value: { command: "FOO" } action: [ // one of the following actions will be matched: "@menu:menuStyle1" "@menu:menuStyle2" ] }]- Added
read(list/view) andwrite(post) ACS support to message conferences and areas. - Many new built in modules adding support for things like auto signatures, listing “my” messages, top stats, etc. Take a look in the docs for setting them up!
- Built in MRC support!
- Added an customizable achievement system!
0.0.9-alpha
Section titled “0.0.9-alpha”- Development is now against Node.js 10.x LTS. While other Node.js series may continue to work, you’re own your own and YMMV!
- Fixed
justifyproperties:leftandrightvalues were formerly swapped (oops!) - Menu items can now be arrays of objects not just arrays of strings.
- The properties
itemFormatandfocusItemFormatallow you to supply the string format for items. For example if a menu object is{ "userName" : "Bob", "age" : 35 }, aitemFormatmight be|04{userName} |08- |14{age}. - If no
itemFormatis supplied, the default formatter is{text}. - Setting the
datamember of an object will cause form submissions to use this value instead of the selected items index. - See the default
luciano_blocktronicsmatrixmenu for example usage.
- The properties
- You can now set the
sortproperty on a menu to sort items. Iftrueitems are sorted bytext. If the value is a string, it represents the key in menu objects to sort by. - Hot-reload of configuration files such as menu.hjson, config.hjson, your themes.hjson, etc.: When a file is saved, it will be hot-reloaded into the running system
- Note that any custom modules should make use of the new Config.get() method.
- The old concept of
autoScalehas been removed. See https://github.com/NuSkooler/enigma-bbs/issues/166 - Ability to delete from personal mailbox (finally!)
- Add ability to skip file and/or message areas during newscan. Set config.omitFileAreaTags and config.omitMessageAreaTags in new_scan configuration of your menu.hjson
{userName}(sanitized) and{userNameRaw}as well as{cwd}have been added to param options when launching a door.- Any module may now register for a system startup initialization via the
initializeModules(initInfo, cb)export. - User event log is now functional. Various events a user performs will be persisted to the
system.sqlite3user_event_logtable for up to 90 days. An example usage can be found in the updatedlast_callersmodule where events are turned into Ami/X style actions. Please seeUPGRADE.md! - New MCI codes including general purpose movement codes. See MCI codes
install.shwill now attempt to use NPM’s--build-from-sourceoption when ARM is detected.oputil.js config newwill now generate a much more complete configuration file with comments, examples, etc.oputil.js config catdumps your current config to stdout.- Handling of failed login attempts is now fully in. Disconnect clients, lock out accounts, ability to auto or unlock at (email-driven) password reset, etc. See
users.failedLogininconfig.hjson. - NNTP support! See NNTP docs for more information.
oputil.js user rmandoputil.js user infoare in! See oputil CLI.- Performing a file scan/import using
oputil.js fb scannow recognizes variousFILES.BBSformats. - Usernames found in the
config.users.badUserNamesare now not only disallowed from applying, but disconnected at any login attempt. - Total minutes online is now tracked for users. Of course, it only starts after you get the update :)
- Form entries in
menu.hjsoncan now be omitted from submission handlers usingomit: true
0.0.8-alpha
Section titled “0.0.8-alpha”- Mystic BBS style extended pipe color codes. These allow for example, to set “iCE” background colors.
- File descriptions (FILE_ID.DIZ, etc.) now support Renegade |## pipe, PCBoard, and other less common color codes found commonly in BBS era scene releases.
- New menu stack flags:
noHistorynow works as expected, and a new addition ofpopParent. See the defaultmenu.hjsonfor usage. - File structure changes making ENiGMA½ much easier to maintain and run in Docker. Thanks to RiPuk (Dave Stephens)! See UPGRADE.md for details.
- Switch to pure JS xxhash instead of farmhash. Too many issues on ARM and other less popular CPUs with farmhash (Dave Stephens)
- Native CombatNet support! (Dave Stephens)
- Fix various issues with legacy DOS Telnet terminals. Note that some may still have issues with extensive CPR usage by ENiGMA½ that will be addressed in a future release.
- Added web (http://, https://) based download manager including batch downloads. Clickable links if using VTXClient!
- General VTX hyperlink support for web links
- DEL vs Backspace key differences in FSE
- Correctly parse oddball
INTL,TOPT,FMPT,Via, etc. FTN kludge lines - NetMail support! You can now send and receive NetMail. To send a NetMail address a external user using
Name <address>format from your personal email menu. For example,Foo Bar <123:123/123>. The system also detects other formats such asaName @ address(Foo Bar@123:123/123) oputil.js: Addedmb areafixcommand to quickly send AreaFix messages from the command line. You can manually send them from personal mail as well.oputil.js fb rm|remove|del|deletefunctionality to remove file base entries.oputil.js fb descfor setting/updating a file entry description.- Users can now (re)set File and Message base pointers
- Add
--updateoption tooputil.js fb scan - Fix @watch path support for event scheduler including FTN, e.g. when looking for a
toss!.nowfile produced by Binkd.
…LOTS more!
Pre 0.0.8-alpha
Section titled “Pre 0.0.8-alpha”See GitHub