Upgrade Notes
Version-to-version notes to read before upgrading. This page is generated from UPGRADE.md in the repository. For the upgrade procedure itself, see Upgrading.
This document covers information for keeping your system updated through periodic upgrades as well as version-to-version upgrade notes. Be sure to read these notes for any upgrade!
Before Upgrading
Section titled “Before Upgrading”- Always back up your system! (See Administration - Backing Up Your System)
- Seriously, always back up your system!
- Review the version to version release notes within this document.
- Upgrade
The Upgrade Process
Section titled “The Upgrade Process”ENiGMA½ does not currently have much of a “release process” in that instead, it is expected that if you want new features, you will git pull them to your system.
Refer to Upgrading for details around this process.
Problems
Section titled “Problems”- Check TROUBLESHOOTING first.
- Report your issue on Xibalba BBS, or file a issue on GitHub!
Version to Version Notes
Section titled “Version to Version Notes”:warning: Be sure to inspect these notes during any upgrades!
0.5.1-beta to 0.6.0-beta
Section titled “0.5.1-beta to 0.6.0-beta”-
ATASCII (Atari) callers (new, early). SyncTERM and IcyTerm in their Atari modes, and anything on a Telnet listener marked
emulation: atascii, are now sent ATASCII in place of ANSI: text, cursor movement and keys are translated, in one colour. There are no ATASCII menus, art or theme yet, so such a caller sees your existing board translated and wrapped at 40 columns. See ATASCII (Atari) Callers.Action: none. Nothing changes for ANSI callers.
-
PETSCII (Commodore) callers (new, optional). ENiGMA½ can now talk to Commodore-style terminals: SyncTERM and IcyTerm in their C64 or C128 modes, and terminal programs on real hardware through a dedicated Telnet port. See PETSCII (Commodore) Callers.
What changes by itself: a caller whose terminal announces the Telnet terminal type
PETSCIIis now sent PETSCII. Before, such a caller was sent ANSI, which a PETSCII terminal prints as a screen of escape codes. They see your existing menus and art translated and wrapped at 40 columns: readable, not pretty. Nothing changes for ANSI callers, and nothing in your configuration, menus or themes is touched.Action: none, unless you want to offer PETSCII callers a proper board. Then either build your own menus, art and theme for them, or start from the PETSCII starter (a small 40 column menu tree with login, messages, who’s online and last callers, plus a theme of placeholder art). A new board is offered it by
oputil.js config new. On an existing board it is four manual steps, in An existing board: include the PETSCII menu file, add oneTE[petscii]branch totelnetConnected, copy the starter theme into your themes directory, and settheme.emulations.petscii. Do not copy a PETSCII theme into your themes directory without also routing PETSCII callers to menus made for it: such a theme is given to every PETSCII caller, and almost none of your stock menus have art in it.Keys: for every PETSCII session, F1 is Escape, F3 is Page Up and F5 is Page Down, because a Commodore keyboard has none of those keys. If you already run your own menus for PETSCII callers, those three function keys no longer arrive as themselves.
Also new and optional: additional Telnet listeners (
loginServers.telnet.listeners), each with its own port,firstMenuandemulation; theTE[...]ACS code for a session’s terminal emulation;info.emulationin a theme; andskipFileBasefor the new scan module. -
Amiga doors (new, optional). The
amiga_doormenu module runs AmiExpress (/X) XIM doors inside an embedded vAmiga emulator. It depends on the newenigma-vamiganpm package, which bundles the emulator core (WebAssembly) and the AROS replacement Kickstart, so no external emulator, Kickstart ROM or Workbench is needed.Action: run
npm installafter upgrading so the new dependency is present. Nothing else changes unless you add Amiga doors: see Amiga Doors for building a door image withmisc/amiga_door/image/make_image.shand thedoorMyGameAmigaexample inmisc/menu_templates/doors.in.hjson. The one new system setting,doors.amiga.snapshotCacheEntries(default 4), caps how many booted-machine snapshots are kept in memory; each is about 1.5 MB.From a development build with Amiga doors (enigma-vamiga 0.1.x). The module now needs enigma-vamiga 0.2.0 or later and refuses to start doors on 0.1.x, saying so in the log;
npm installbrings it in; restart ENiGMA afterwards, because the package is checked once, when the module loads. The emulator’s disk is now the door’s image file: what a door writes goes into the image as it writes it, after a hangup or a crash as much as after a clean exit, and the image is synced when the session ends. There is no copy-and-rename any more, so keep your own copy of an image whose data you would not want to lose.persistOnno longer does anything; a menu that has it still loads, with one warning per door in the log, and you can delete it.persist: falsestill keeps the image unchanged, but such a door no longer runs while a persisting door is using the same image (nor the other way round): callers get the usual busy message. Images must be named.hdf. Cached snapshots from 0.1.x are refused and rebuilt on their own. Expect much less memory per session with large images (about 60 MB whatever the image size; a 100 MB image cost about 340 MB) and about 40% more CPU per idle door (about 7% of a core instead of 5%).Door images keep the shim they were built with. An image made with an earlier shim than the one in
misc/amiga_door/bin/axshim(axshim 1.5 in this release: input held instead of dropped, RTS, thebaudsetting) keeps the old behaviour until it is refreshed: rebuild it withmake_image.sh, or replace just the shim with./oputil.js amiga-door add IMAGE misc/amiga_door/bin/axshim C/axshim. -
PM2: raise
kill_timeoutto 20000. ENiGMA½ now shuts down in order onSIGTERMas well asSIGINT, and takes up to about 3 seconds when Amiga doors are running (15 seconds at the very most, if a step hangs). PM2 sendsSIGKILL1600 ms after asking a process to stop, unless told otherwise, which can land in the middle of that.Action: if you run ENiGMA½ under
pm2, setkill_timeout: 20000in your ecosystem file, or start it withpm2 start main.js --kill-timeout 20000. The Docker image now setsPM2_KILL_TIMEOUT=20000forpm2-runtimeitself; give the container longer than that to stop (docker stop -t 30, orstop_grace_period: 30sin Compose), since Docker’s default is 10 seconds. Under systemd the defaultTimeoutStopSecof 90 seconds is enough. See Stopping. -
Docker image: new archivers, and RARLAB
unrarin place ofunrar-free(#905, #906). The image now has everything the default archivers and description tools call, includingzip/unzip,exiftool,xdmsand unarc, and reads Amiga LZX and Atari ATR with unarc through the newENIGMA_UNARC_ARCHIVERSvariable.unrar-freeis gone.Action: none, unless your
config.hjsonchanges theRararchiver to work withunrar-free(acmdofunrar-free, or your ownentryMatch): remove that, and the default, written for RARLAB’sunrar, applies. AnAtrorLzxentry of your own replaces the image’s unarc redirection outright, so it must be a complete archiver entry; a partial one such asLzx: { list: { cmd: "my-unlzx" } }leaves LZX listing broken. To re-describe files stored while the image lacked a tool, rescan their areas:docker exec ENiGMABBS ./oputil.js fb scan --full --update AREA_TAG. The rescan updates descriptions only; such files stay recorded as plain files. See Docker. -
enabled: falseon a server is now obeyed. It was ignored for Telnet: a board withloginServers.telnet.enabled: falsein itsconfig.hjsonstill accepted Telnet callers.Action: look for
enabled: falsedirectly insideloginServers.telnetin yourconfig.hjson. If it is there and you want Telnet, remove it or set it totrue, or Telnet stops after the upgrade. The same goes for anenabled: falseplaced directly insidecontentServers.web,contentServers.nntporscannerTossers.ftn_bso: these were ignored too and now turn that module off (the configuration check reports them as unknown keys). Theenabledflags one level down, such ascontentServers.web.http.enabledorloginServers.webSocket.ws.enabled, work as they always did. -
The message table is rebuilt once, at the first start (#958). Message IDs are no longer reused after the newest message is deleted, which SQLite can only do by recreating the
messagetable. ENiGMA½ does this by itself the first time it starts after the upgrade: every message ID, all message meta and the full-text index are kept, and the whole rebuild is one transaction, so if it fails the old table is left as it was and the log says so aterror; it is tried again at the next start. The log showsUpgrading the message tablewith the message count before it begins andUpgraded the message tablewith the time it took. A board with 36,000 messages took about a third of a second; a much larger one may take a little while to come up.Action: none. As always, back up
db/message.sqlite3first, and make sure there is free disk space for a second copy of the message table while this rebuild runs. -
The message search index is rebuilt once, at the first start (#964). Editing an ActivityPub post used to leave its old text in the full-text index, and
PRAGMA integrity_checkonmessage.sqlite3reports such an index as malformed. Right after the message table rebuild above, ENiGMA½ rebuilds the index from the messages themselves, once, in one transaction; if it fails the index is left as it was and the log says so aterror, and it is tried again at the next start. The log showsRebuilding the message search indexbefore it begins andRebuilt the message search indexwith the time it took. A board with 36,000 messages took about one and a half seconds.Action: none. The old index is kept until the new one is committed, so make sure there is free disk space for a new search index while it runs. The free space for a second copy of the message table, above, is for the #958 table rebuild only.
-
The ActivityPub actor cache is emptied once, at the first start. Cached remote actors could have been planted by another server, and such an entry cannot be told from a genuine one, so the
actorscollection indb/activitypub.sqlite3is cleared once and actors are fetched again as they are needed. Follower and following lists, posts and reactions are not touched. The log showsPurging the ActivityPub actor cacheandPurged the ActivityPub actor cache; 50,000 cached actors took under a second.Action: none. Delivery to followers is not interrupted: when a post, boost or like goes out, followers in the cache get it at once, and a follower that is not in the cache is fetched again in the background (four at a time across the board, once however many posts are waiting on it) and gets it once found, if it still follows. On a board with many followers, the first posts after the upgrade reach most followers a little later than usual, and expect more outbound fetching for a while. A follower that cannot be fetched is not tried again for six hours (this is kept in memory, so a restart tries it again).
-
ActivityPub: a fediverse server on your own network needs
allowedPrivateAddresses(#966). Outbound requests now refuse a name that resolves to a private address, so a server on your LAN whose name resolves privately can no longer federate until it is listed. Your board’s own URLs are exempt and need nothing.Action: if you federate with such a server, add its address or range to
contentServers.web.handlers.activityPub.allowedPrivateAddresses; see ActivityPub Web Handler. -
Event scheduler: new built-in tasks, more logging, and checked entries (#440). Four built-in events appear (
optimizeDatabases,checkpointDatabases,backupDatabases,tossInboundNow); none has a schedule, so none runs unless asked, and a startup line lists them.scheduleis now optional, so an entry with only anactionis accepted as a manual-only task instead of being rejected as invalid. Each event run now logs its start and end atinfo, a failing@methodlogs atwarninstead ofdebug, and a scheduled run skipped because the previous run is still going is logged atinfo. An@executeaction whose process is ended by a signal now counts as failed; before, only a non-zero exit code did.Action: none required.
oputil.js config validatenow knows the members an event entry may have (schedule,action,args,desc,confirm,manual,enabled,timeout) and warns about any other, so run it once: a warning on an entry of yours is most likely a misspelling. To switch off a built-in event, setenabled: falseon it. See Event Scheduler. -
The WFC task screen reaches new installations only (#440). Your
menu.hjsonis yours and is never rewritten, so T does nothing on an upgraded board until you add it.Action: to get the task screen, copy from
misc/menu_templates/main.in.hjsonintomainMenuWaitingForCallerin yourmenu.hjson: thet/shift + tkey on form0(action: @method:displayTasks), the new forms5(the task screen) and6(its confirm prompt), and thetasks: wfctasksandtaskPrompt: wfctaskpromptart keys. The art is optional: withoutwfctasksa plain text list is shown, and withoutwfctaskprompta plain[y/N]question. A theme of your own can add the formats listed under WFC: Tasks;luciano_blocktronicsships them. The activity ticker now shows failed and overdue tasks by default; settaskFailed: ""andtaskOverdue: ""underticker.eventsto turn that off. -
!BINKPis replaced by thebinkpPullNowWFC task (#440). Two built-in manual-only events are added,exportOutboundNowandbinkpPullNow, and appear on the WFC task screen and in the startup line naming manual-only events. The!BINKPcommand and thesysopBinkpPollNowmenu are removed frommisc/menu_templates/main.in.hjson. They never worked: the command’sacs: SUis not an ACS code, so it was refused for everyone (withInvalid ACS string!in the log). Thebinkp/binkp_poll_modulemodule is kept, but it now runs the full pull cycle (every nodebinkp.pullSchedulewould dial, plus any with mail waiting) instead of dialing only nodes with mail waiting.Action: none required. If your
menu.hjsonhas the!BINKPentry, delete it and thesysopBinkpPollNowmenu, and usebinkpPullNowon the WFC task screen instead; or, to keep a menu command, change itsacs: SUto a real sysop check such asGM[sysops](members of thesysopsgroup) orID1(the root user only). (SCis not one: it means a secure connection, so any caller on SSH or TLS would pass.) -
Reverse proxies on a public address must be listed in
trustedProxies(#981). The new top-leveltrustedProxiessetting names the proxies whoseX-Forwarded-For,X-Real-IPandX-Forwarded-Protoheaders are believed. Its default covers loopback and the private ranges (127.0.0.0/8,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7). The WebSocket login server withproxied: trueused to accept those headers from any peer; it now ignores them from a peer not in the list, so such a connection gets the proxy’s address and is not treated as secure. The web server is unchanged unless you set the newcontentServers.web.proxied: true, which makes its rate limits per visitor by using the forwarded address from a trusted proxy.Action: none if your proxy runs on the same host, in another container or on the LAN. If it connects from any other address, add that address to
trustedProxiesinconfig.hjson; the log names it atwarn(Ignoring forwarded headers from a WebSocket peer not in trustedProxies). SettingtrustedProxiesreplaces the default, so list the defaults you still need alongside it. Make sure the proxy setsX-Forwarded-Foritself (nginx:proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;; Caddy and Traefik do by default), since a header passed through unchanged was written by the client; without it every caller appears to come from the proxy, and the log says so once per proxy. A WebSocket caller that comes through a trusted proxy (withproxied: true) is no longer treated as local. See Trusted Proxies. -
With
--config, relative paths resolve beside yourconfig.hjson(#927).paths.config, which relative names such asgeneral.menuFile,general.achievementFileand the offline mail bulletins (messageNetworks.offlineMail) are taken from, now defaults to the directory the loadedconfig.hjsonis in, the same directoryincludeswere always read from. It used to default to the install’sconfig/directory whatever--configsaid. A board using the install’s ownconfig/config.hjsonsees no change, and neither does one that setspaths.config.Action: only if you start ENiGMA½ or
oputil.jswith--configpointing somewhere other than the install’sconfig/, and rely on relative paths in thatconfig.hjsonfinding files in the install’sconfig/directory: setpaths.configto that directory, for examplepaths: { config: "/enigma-bbs/config/" }(quoted: an unquoted HJSON value runs to the end of the line), or move the files beside yourconfig.hjson. The defaults count too:general.menuFileandgeneral.achievementFiledefault to the relative namesmenu.hjsonandachievements.hjson, so a board that never set them was reading the install’s copies. That includes boards made byoputil.js config newbefore this release, which wrote an absolutemenuFilebut noachievementFile(it now writes both). Without anachievements.hjsonto find, achievements are switched off and the log saysAchievements file not foundatwarn, with the path it tried; setgeneral.achievementFileto the install’s copy, for example"/enigma-bbs/config/achievements.hjson", or copy it beside yourconfig.hjson. Set it inconfig.hjsonitself: like any setting that has a default,paths.configin a file listed inincludesdoes not replace it. See Relative Paths.
0.5.0-beta to 0.5.1-beta
Section titled “0.5.0-beta to 0.5.1-beta”-
More archive formats, through unarc (optional). The file base can list and describe ARC, PAK, ZOO, ACE, SQZ, UC2,
.Z, xz, Zstandard and CAB archives, C64 Lynx, T64 and D64 files, and the files on Amiga DMS disks, using unarc. unarc is not shipped with ENiGMA½. Until it is installed these formats are handled as plain files, as before, and DMS keeps itsxdmsdescription.Action: none required. To use the new formats, install unarc as Archivers describes. It needs version 0.8.0 or later, now on crates.io:
cargo install --locked unarc-cli. The Docker image ships it from 0.6.0-beta on.-
Amiga LZX and Atari ATR still use
unlzxandatrby default, so a board with those keeps working unchanged. unarc reads both as well, including SpartaDOS ATR disks, whichatrcannot. Once it is installed you can drop the two tools by pointing their archivers at it inconfig.hjson:archives: {archivers: {Atr: "@reference:archives.archivers.Unarc"Lzx: "@reference:archives.archivers.Unarc"}} -
An upload whose archiver cannot list it is now handled as a plain file, where it used to keep an archive type with a listing that always failed. If an archiver you rely on is missing, those uploads now get a description from their file name until it is installed and the area is rescanned.
-
Long descriptions of Amiga uploads may change on rescan. Description-file patterns now match a literal dot, so an Amiga
.infoicon is no longer taken for an.NFO. DMS disks listed through unarc take their long description from a README or NFO on the disk, where they hadxdms’s table of track checksums.
-
-
REST API refresh cookie path changed to
/auth. Login and successful refresh expire the old cookie scoped to/auth/refreshand issue one that reaches both refresh and logout. Existing browser sessions must refresh once or log in again before logout can revoke their refresh token. Logout also clears the old cookie if it is still stored, but cannot revoke a token that the browser does not send. Client cookie jars must accept allSet-Cookieheaders in the response. Access tokens already issued remain valid for their normal 15-minute lifetime.Action: browser clients with existing sessions must refresh once or log in again before logging out. Non-browser clients must process every
Set-Cookieheader from login, refresh and logout. -
A door’s
io: socketserver now listens on loopback only (#885). The temporary TCP server anabracadabradoor connects back to used to be bound to every interface, and the first connection to arrive — from anywhere — was handed the caller’s session. It now binds127.0.0.1.Action: none if the door, launch script or emulator already connects to
localhostor127.0.0.1on the same machine, which covers the shipped examples, bivrost! and the documented QEMU and DOSEMU setups. Two cases need a change:-
A connect-back host that is not loopback. A script or emulator argument naming the machine’s LAN address, hostname, or a Docker/container address will no longer connect, and the door will hang waiting. Change it to
127.0.0.1— preferably spelled numerically, sincelocalhostcan resolve to::1first and a client that does not try every resolved address will miss the listener. -
An emulator or bridge on a different machine. Set
socketBindAddressin that door’sconfigblock to the address on this machine it should reach:io: socketsocketBindAddress: 10.0.0.5This reopens the exposure above to that network — any host that can reach the port can take over the caller’s session during the door’s start-up — so restrict the port with a host firewall. Every start of such a door logs a warning naming the address.
io: stdiodoors,v86_door, the telnet bridge and the door networks (BBSLink, DoorParty!, etc.) are unaffected. -
-
The Exodus door module has been removed (#857). The service is gone:
oddnetwork.org/exodus/and its sysop signup return 404, and both portsexodus.jsneeds — 1984 for a ticket and 22 for the session — refuse connections. There is no replacement host to point the module at, so it follows CombatNet out of the tree rather than staying as a module that cannot connect.The shipped templates no longer offer it, so a new installation is unaffected. Your
menu.hjsonis yours and is never rewritten, so a board that configured Exodus still names a module that no longer exists.Action: remove the Exodus menu and whatever points at it. If you used the shipped example, that is the
doorExodusCataclysmmenu and theEXODUSentry in your doors menu’ssubmitlist.You do not have to hunt for it.
oputil.js config validatenames it exactly, and startup reports the same thing:error menus.doorExodusCataclysm.modulemenu module "exodus" is not defined in the core modules -- no "exodus.js" there, nor "exodus/exodus.js" -
Nodemailer moved from 8.x to 10.x to clear a high severity advisory (see #860). Nothing in ENiGMA½’s own use of it changed — composing and sending produce byte-identical headers — but one upstream change can affect a sysop’s configuration.
Since nodemailer 9.0.0, HTTPS requests made while fetching remote content validate the server’s TLS certificate by default. That covers three things ENiGMA½ can be configured into:
- an OAuth2 token endpoint, if your
email.transportauthenticates that way, - an HTTP/HTTPS proxy reached with
CONNECT, - an attachment supplied by
hrefor a URLpath.
A host with a self-signed, expired, or hostname-mismatched certificate that worked before will now fail. This does not change how the SMTP connection itself is secured — only those content fetches.
Action: none for most boards. If you authenticate to your mail provider with OAuth2 through an internal endpoint, or send mail through a proxy with a private CA, and outbound mail stops working, set
tls: { rejectUnauthorized: false }in the relevant transport options — or, better, trust the CA properly. - an OAuth2 token endpoint, if your
-
Four editors were never migrated to the
%SB1footer —privateMailMenuCreateMessage,newUserFeedbackToSysOp,messageAreaReplyPostandpreAuthFeedbackdrawMSGEFTR, whose art has carried a single%SB1since 0.1.0, but never configured it. Three still had the retiredTLTLblock on form2, naming%TL1and%TL2views the art no longer has; the reply editor had no form2at all.core/fse.jsdrives that bar withsetPanel('pos')andsetPanel('mode'), andStatusBarView.setPanel()returns early unless the view was built with apanelsarray — which only a form2SB1block supplies. So those four editors have shown an empty footer: no cursor position, no INS/OVR indicator, nothing logged.oputil.js config validatecannot report it either, since MCI blocks belong to the modules that read them and are deliberately left unchecked.The shipped templates are fixed, so a new installation is correct. Your
menu.hjsonis yours and is never rewritten, so an existing board keeps the broken block until you replace it.Action: in each of those menus, replace form
2— or add one, if likemessageAreaReplyPostit has none — with:2: {mci: {SB1: {width: 9anchor: leftjustify: leftseparator: " "panels: [{ name: mode, width: 3, justify: right }{ name: pos, width: 5, justify: left }]}}}The other editors —
mainMenuFeedbackToSysOp,messageBaseNewPostandactivityPubCompose— were already correct and need nothing. If your theme still carries a form2TL1/TL2customization for any of the four above, it has been matching nothing and can go. -
Achievement totals earned before 0.5.1-beta may be too high (#843). Until the duplicate-award fix, crossing a new tier of a retroactive achievement re-counted every lower tier the user already held: the running
achievement_total_count/achievement_total_pointsproperties were incremented beforeINSERT OR IGNOREdropped the duplicate row. Theuser_achievementrecords were never wrong, only the running totals, and only upward. Boards that have been up for a while will see this in “Top Achievements” rankings and in the header of the Achievements screen, which can claim more achievements than the list below it shows.No new drift accrues once you are on this version.
Action: optional, and cosmetic — nothing is broken if you skip it. To correct the historical totals:
Terminal window # stop the BBS first: an online user holds their totals in memory, and the# next achievement they earn writes that stale figure back./oputil.js user fix-achievement-stats --dry-run # report only./oputil.js user fix-achievement-stats # writeBack up your user database first, and expect the headline numbers on your Top Achievements art to drop. The command is idempotent.
-
Daily time limits are off, and stay off (#795). No
users.timeLimitsships configured, so every user remains unlimited and nothing about your board changes. TheMLACS code, a stub that always returnedfalseuntil now, istruewhenever no limit applies — so anacscontainingMLthat had been silently failing will start passing. Nothing in the shipped menus or themes uses it.Action: none, unless you want limits. To opt in, see Time Limits.
If you do opt in, the
timeUpLogoffmenu is seeded frommisc/menu_templates/, whichoputil config newdeploys once; yourmenu.hjsonis yours and is never rewritten. Add it besideidleLogoff:timeUpLogoff: {art: TIMEUPnext: @systemMethod:logoff}Without it — and without a
TIMEUPart file, which no theme ships — a user whose time runs out gets a plain message and is disconnected. That is deliberate, not a fallback you need to avoid. -
U to upload a reply packet reaches new installations only (#810). Like every menu key, it is seeded from
misc/menu_templates/, whichoputil config newdeploys once; yourmenu.hjsonis yours and is never rewritten.Action: add the entry to the
submitlist ofmessageBaseMainMenuin yourmenu.hjson, and the menu it points at alongside your other message base menus:{value: { command: "U" }action: @menu:offlineMailImport}offlineMailImport: {desc: Offline Mail Importmodule: message_base_offline_import}The shipped
MSGMNU.ANSnames the key in its legend; a theme of your own needs the line adding to its own art.Nothing breaks if you leave the key out. A packet says it takes replies —
INF_HEADER.uses_upl_file— only when one of your menus carriesmessage_base_offline_import, so a board without the upload does not have callers write replies they cannot send. -
P on the message list reaches new installations only (#213). This is the list of messages you reach with L, not the message menu, which has offered P to post for as long as it has existed. The new key is seeded from
misc/menu_templates/, whichoputil config newdeploys once; your existingmenu.hjsonis yours and is never rewritten, so the key does not appear on an upgraded board until you add it.Action: add the binding to
messageBaseMessageListandmessageBaseMyMessagesListin yourmenu.hjson. Each already has anactionKeysentry;@reference:common.quitToPrevsubstitutes the whole value, so the quit binding is written out alongside the new one rather than extended:actionKeys: [{keys: [ "escape", "q", "shift + q" ]action: @systemMethod:prevMenu}{keys: [ "p", "shift + p" ]action: @method:postNewMessage}]New installations now seed a second form of the shared binding for exactly this case —
common.quitToPrevEntry, the entry without the array around it — which a menu can list beside its own keys instead of copying it:actionKeys: [@reference:common.quitToPrevEntry{keys: [ "p", "shift + p" ]action: @method:postNewMessage}]Your
menu.hjsonpredates it, so the copy above is what to write unless you addquitToPrevEntryto your owncommonsection first. Either is fine; the copy costs you a board that no longer tracks changes to the shared binding, which for a binding that has not changed in years is a small thing.The shipped
MSGLIST.ANSandMYMSGLST.ANSname the key in their legend; a theme of your own needs the line adding to its own art. -
opVisibility: falseon the WFC now does what it says — this one affects you whether or not you change anything.wfc.mdhas documented the option as a boolean since it existed, while the code matched only the strings'hidden'and'visible'— soopVisibility: falsefell through the switch and did nothing, and that is the form people wrote. Booleans are now accepted, which means an +op who has always stayed visible despite the setting will start being hidden on the nextgit pull.That is more than cosmetic: an invisible +op is excluded from
getActiveConnections({visibleOnly: true}), so they receive no broadcast notifications at all — no global achievements, no-ALL-node messages.Action: none required if you meant it. If you did not, either remove the setting or write the string form, which stays canonical:
mainMenuWaitingForCaller: {config: {// or: visible | hiddenopVisibility: current}} -
The WFC’s message keys and its
wfcNodeMessagemenu reach new installations only (#438, #217). The routing itself works on an upgraded board — node messages stop painting over the dashboard the moment you pull — but there is no way to read them until you add the keys, because yourmenu.hjsonis yours and is never rewritten. Without this the messages accumulate in the inbox unread, and the status bar count is the only sign they exist.Action: three additions to
mainMenuWaitingForCallerin yourmenu.hjson. First, the two keys on form0, alongside the ones already there:{keys: [ "m", "shift + m" ]action: @method:displayMessages}{keys: [ "s", "shift + s" ]action: @method:sendNodeMessage}Second, a new form
4for the viewer itself:4: {mci: {VM1: { focus: true }MT2: {acceptsFocus: falseacceptsInput: false}}actionKeys: [{keys: [ "r", "shift + r" ]action: @method:replySelectedMessage}{keys: [ "d", "shift + d", "del" ]action: @method:dismissSelectedMessage}{keys: [ "escape", "q", "shift + q" ]action: @method:exitMessages}]}Third, the menu S and R go to. It is the same module and art as
nodeMessage, as its own entry so the sysop path can be rethemed without changing what callers see:wfcNodeMessage: {desc: Node Messagingmodule: node_msgart: NODEMSGconfig: {cls: trueart: {header: NODEMSGHDRfooter: NODEMSGFTR}}form: {0: {mci: {SM1: {argName: node}ET2: {argName: messagesubmit: true}}actionKeys: @reference:common.escToPrevsubmit: {*: [{value: {message: null}action: @method:sendMessage}]}}}}Set
nodeMessageMenuNameon the WFC if you would rather point S and R at your existingnodeMessageand skip the separate entry. -
A theme of your own needs a
wfcNodeMessageblock, or its node list will look wrong. Theme customization is keyed by menu name, andwfcNodeMessageis a new name that inherits none ofnodeMessage’s formatting. Without a block the node selector renders as a bare2instead ofnode 2 (Umbra). Nothing errors and nothing is logged — it just looks wrong, and only if you happen to look at it.luciano_blocktronicshas been given a matching block, and two tests now guard the pattern so the next one is caught by CI: menu names that a module falls back to in code are checked against the shipped templates, and a sysop-side variant is checked against its original for the same module and the same MCI codes.Action: copy your theme’s
nodeMessageblock towfcNodeMessage. If you customize neither, there is nothing to do. -
New WFC art is optional, and the features degrade rather than break without it. Three additions, none required:
wfcmessagesart with%VM1(message list) and%MT2(detail). Without it M shows a plain text list instead, which works — but the fallback has no selection, so R and D do nothing until the art exists.%TK6in your WFC art turns on the activity ticker. Without it the feed accumulates, bounded byticker.maxItems, and nothing renders.%SB5gives you a status bar. It must be in panel mode: every named panel is filled fromstatusBar<Name>Format, withstatusBar<Name>ActiveFormatlighting up themessagesandpagescounts while they are non-zero. The menu template now ships a minimalSB5with just those two panels, so amenu.hjsonthat predates it needs one added to form 0 ofmainMenuWaitingForCaller— or let your theme supply it.
luciano_blocktronicsnow ships theSB5andTK6config, the status bar formats, atickerblock and message viewer item formats, replacing the oldmainInfoFormat10header line. A theme’s MCI definitions reach your board without touchingmenu.hjson, and the matching art ships with them:wfc.ansputs%SB5and%TK6on its top two rows,wfcmessages.ansgives M a proper viewer, andwfchelp.anslists the new keys. If you keep your own copy of the luciano WFC art, add%SB5and%TK6to it, or put amainInfoFormat10back in your theme for your%TL10.A theme of your own without its own
wfc.ansnow gets the new layout. Art a theme lacks comes from the default theme,luciano_blocktronicsunless you changed it, but the MCI config still comes from your theme. So your dashboard picks up the new%SB5and%TK6without luciano’s settings for them. The status bar still shows the old header line plus the message and page counts, throughstatusBarFormat. The ticker, though, is 15 columns wide with default colours until you configure it, and amainInfoFormat10you had for%TL10no longer has a view to fill. Action: addSB5andTK6blocks to your theme’smainMenuWaitingForCaller(copy luciano’s as a start), or copy the previouswfc.ansinto your theme to keep the old layout. If you addedart.messagesfor the message viewer, also give form 4’sVM1andMT2a height and width, because luciano’swfcmessages.anscomes without luciano’s sizes.Those view IDs are not interchangeable: the WFC looks the ticker up as ID 6 and the status bar as ID 5. ID 1 is already
%VM1, the node status list, andViewControllerkeys views by ID with a plain assignment — so%TK1or%SB1in WFC art would replace the node list rather than quietly do nothing. -
Eight customizations in the
luciano_blocktronicstheme were named wrongly and have been corrected.ThemeManagerstarts from yourmenu.hjsonand looks each menu up in the theme, so a customization naming a menu that does not exist is never consulted — it silently does nothing. Seven menu blocks and one prompt block in the theme we ship named nothing at all. Three were renamed to the menus they were meant for and four were removed.This only affects you if you use
luciano_blocktronicsunmodified and yourmenu.hjsonpredates the menu renames. If so, these blocks stop applying to your board:Was Now If your menu.hjson still has the old name messageSearchmessageBaseSearchrename the menu, or copy the block into your own theme messageBaseReplyPostmessageAreaReplyPostsame messageMenuCommand(prompt)messageBaseMenuPromptsame qwkExportPacketCurrentConfig,fileBase,irc,activityPubPostPublicMessageremoved copy the block into your own theme if you have such a menu The safest check is to compare the theme’s menu names against your own: anything in
customization.menusthat is not a key undermenusin yourmenu.hjsonwas already doing nothing, before or after this change.One correction is a fix rather than a removal:
MSGPMPT.ANScontains a%TL1placeholder and the theme supplies its text, but under the wrong name — so the message area command prompt has been missing its hint line. It now renders. If you have themedmessageBaseMenuPromptyourself, check it still looks right.If you maintain your own theme, nothing here affects you.
-
Configuration problems are now reported at startup (#281). A mistyped key in
config.hjsonhas never been reported — because your file is merged into the defaults, a typo lands quietly beside the correct key and the setting you wrote is simply never read. Those are now listed on the console at startup and to the log on a hot reload.Expect to see output you have not seen before, possibly naming real mistakes that have been silently inert for years. This now covers
menu.hjsonand every theme as well asconfig.hjson, so a board with a long-customised menu file or a hand-edited theme should expect the most to say. Nothing blocks: a configuration with problems is applied exactly as before, and the board starts. Run./oputil.js config validatefor the full report, and setgeneral.configValidationtooffto silence the startup output — the command keeps working either way. There is deliberately no strict mode. -
A TIC whose file has not arrived yet is now held instead of rejected (#735). A
.ticand the file it announces routinely arrive in separate mailer sessions, minutes or hours apart. Until now the announcement was processed the instant it landed, failed because the file was not there, and was archived topaths.reject— so when the file did arrive there was nothing left to pair it with, and it sat in the inbound forever. For a large file from a given peer this could fail every single time. Such a TIC is now kept and retried on later import passes, bounded by the newscannerTossers.ftn_bso.tic.holdMaxAgeMs(default 48 hours;0holds indefinitely). No action is required.Check your inbound for files this already stranded. Anything in
mail/ftn_secin/(ormail/ftn_in/) that is not a.pkt, a bundle or a.ticis very likely an orphaned TIC payload. The matching announcement is inmail/reject/asreject-tic--<timestamp>-<name>.tic; the simplest recovery is to move both back into the secure inbound and let the next import pass pick them up, now that the pairing works. Re-announcement by the peer is not needed.Three related changes ride along, none of which need configuration:
- The announced filename is now matched case-insensitively. A TIC naming
NODELIST.Z34finds a deliverednodelist.z34, which on a case-sensitive filesystem it previously never did. This alone could account for a file that “never once imported”. - A file still being written is no longer destroyed. A payload shorter than the TIC’s
Size, or one failing its CRC, used to be archived as a reject and unlinked — which, with a mailer that writes straight into the inbound, could delete a transfer in progress. It is now held and re-checked, and only given up on once the hold expires. - The TIC password is compared without regard to case, matching what other FTN software does and what ENiGMA½ already did for packet passwords. A TIC previously refused purely over case will now be accepted.
- The announced filename is now matched case-insensitively. A TIC naming
-
A TIC could write its announced file outside the file base (#735). The
Filefield was checked for path traversal; theLfileandFullnamefields, which are what a file is actually stored under, were not. A peer whose TIC otherwise validated — a configured node, a mappedticAreasentry, a matching password if one is set, and a correct CRC — could sendLfile ../../../some/pathand have ENiGMA½ write peer-supplied content to any path the BBS user could write. All three fields are now validated and such a TIC is rejected.No action is required beyond upgrading, and no configuration changes. If you carry TIC areas and want to be sure, check
paths.rejectfor entries you do not recognise and look for unexpected files near yourfileBase.areaStoragePrefix. Note that thesecureInOnlychange below independently narrows who could reach this at all. -
scannerTossers.ftn_bso.tic.secureInOnlyis now enforced (#735). It has been documented and defaulted totruesince TIC support landed, but was never actually read: TIC files in the unsecure inbound were imported into the file base on the strength of an unauthenticatedFromline alone. They are now ignored — left in place, neither imported nor deleted — and the fact is logged once.Action: only if you deliberately accept TICs from the unsecure inbound. Set
tic.secureInOnlytofalseto keep that behaviour. Note that doing so accepts files from any node matching yournodesconfiguration without the peer having authenticated, so prefer atic.passwordif you go this route. -
NetMail no longer needs a
netMail.routesentry to reach a node you have already configured (#739). A destination listed inscannerTossers.ftn_bso.nodesis now delivered direct; routes are still consulted first and behave exactly as before, so no action is required and every existing configuration resolves identically.Two cases do get stricter, both of which previously “succeeded” into a dead end:
- NetMail addressed to your own FTN address used to be accepted and filed into your own outbound, where nothing would ever send it. It is now refused unless you list your own address in
nodes, and the sender is told. - A route whose
networknames something not inmessageNetworks.ftn.networksis refused when the route is chosen, rather than a step later with an error about the network. The message is the same failure either way; only the wording and timing change.
Worth a look if you have more than one network in the same zone: with no
networknamed on the route or the node, the zone alone cannot tell them apart and the tie goes todefaultNetwork. Addnetworkto thenodesentry to be explicit — the wrong choice means mail sent from the wrong address. - NetMail addressed to your own FTN address used to be accepted and filed into your own outbound, where nothing would ever send it. It is now refused unless you list your own address in
-
Cross-zone routed NetMail queued before this release will not have been delivered, and needs moving by hand (#734). NetMail routed through an uplink in a zone other than the recipient’s was filed in the outbound directory for the recipient’s zone rather than the uplink’s, where no mailer would ever find it. The fix applies to newly exported mail only — it does not relocate what is already queued.
Action: only if you route NetMail across zones. Look for a zone-suffixed outbound directory that should not hold mail for that uplink — for a
1:154/10uplink carrying zone 2 mail, that isoutbound.002/009a000a.*(009a= net 154,000a= node 10) plus the.pktfiles its flow file names. Move both the flow file and the packets into the uplink’s own outbound directory (outbound/when the uplink is in your network’s default zone). The stored paths inside the flow file do not need editing — a reference that no longer resolves is now also looked for by name in the flow file’s own directory. If you would rather not sort it out, deleting the stray directory’s contents loses the affected messages and nothing else.Two related changes are worth knowing about even if none of this applies to you: a flow file entry whose file is missing is now logged rather than silently skipped, and a node whose entries all point at missing files is no longer reported as having pending mail — so it stops being dialled every poll cycle with nothing to send.
-
Two TIC parsing defects with real consequences are fixed (#743). No action is required beyond upgrading, and no configuration changes.
A NUL byte in a TIC’s
Filefield stalled the whole import pass until the watchdog fired, skipping every other TIC in that run; and the path of an announced file was built without a safety check, so a TIC naming../../../some/filecaused that file to be archived topaths.rejectand then deleted when the TIC was rejected. Neither needed the TIC to come from a node you have configured.Worth a look if you carry TIC areas: check
paths.rejectfor archived entries whose names you do not recognise, which is where anything affected would have landed. -
Who may import into a TIC area can now be restricted (#743). No action is required —
scannerTossers.ftn_bso.tic.requireAreaAuthorizationdefaults tofalse, which is exactly today’s behaviour.Worth knowing what today’s behaviour is, though: authentication is not per-area. Any node in
nodesmay announce a file into any area you carry, because the sender is checked against your node list and the area against the areas you carry, with nothing correlating them. With the setting enabled, the sender must be listed in that area’suplinks.It is off by default because no existing configuration contains the information the check needs. While it is off, startup tells you which areas would stop importing if you enabled it, so the change can be planned rather than discovered. A forwarding hub already has the
uplinkslists and can usually turn it on for free. -
TIC file echoes can now be forwarded to downlinks (#743). Previously ENiGMA½ was always a leaf node for file echoes. No action is required: forwarding happens only for a
ticAreasentry that namesdownlinks, and an area without them behaves exactly as before.If you want to carry an echo for others, add
uplinks,downlinksandnetworkto the area and give each downlink anodesentry with its owntic.password.uplinksnames who is allowed to publish into the echo and is required for forwarding — an area with downlinks and no uplinks forwards nothing, and says so at startup. It exists because authentication is not per-area: without it, any node you have configured for any reason could announce into that echo and have you relay it under your own address. See TIC Support. Two things to know before you point a downlink at it:- A node with no
tic.passwordis never authenticated — the password check is skipped entirely when none is configured — so files received from such a node are not forwarded. This is deliberate: importing affects only your own file base, while forwarding makes other systems receive traffic yourPathandSeenbylines vouch for. Set a password per node, or settic.allowUnverifiedForwardif you accept the risk. - TICs from the unsecure inbound are never forwarded, even where you have set
tic.secureInOnlytofalsein order to import from there.
One behaviour changes whether or not you forward anything: a TIC addressed to a system that is not you — one being routed through you — is still imported as before, but is now logged as not forwarded. Routing such files onward is not implemented; if you see this, that echo is not reaching whoever is downstream of you and never was.
- A node with no
-
A hub with more than one AKA now signs each downlink from the nearest one (#757). This changes behaviour without any configuration change on your part. TIC forwarding previously took the network from the first downlink’s zone and signed every downlink with it, so a system carrying an echo to links on two networks announced the wrong identity to some of them.
Each downlink is now addressed from your closest AKA by zone and net, and
Seenbylists every AKA in play rather than only one. If you have a single address, nothing changes.Action: none, unless you were relying on the old behaviour — in which case pin it explicitly, either per area or per link:
ticAreas: {fsx_gen: {// every downlink of this areanetwork: fsxnet}}nodes: {"21:1/100": {// just this linktic: {network: fsxnet}}}The outbound directory is deliberately unaffected and stays the area’s: the per-node
.bsylock is derived from the address by zone, and filing a flow file anywhere else would mean the tosser and a live BinkP session take different locks. -
Hatching and passthrough areas are both new and both off (#751, #753). Nothing changes on an existing board.
oputil.js fb hatchis there when you want it, and aticAreasentry carries files in transit only once you setpassthrough: true.passthroughis required explicitly and is never inferred from a missingareaTag. That is deliberate and worth knowing: aticAreaskey is matched againstfileBase.areastoo, so an entry without anareaTagmay well be a working stored area — inferring passthrough would have turned it into one that deletes its files after the last downlink collects them. -
Queueing outbound now respects the FTS-5005
.bsylock, as it always should have. ENiGMA½ appended to BSO flow files without taking the per-node lock, so an append landing while the BinkP side was rewriting the same file was silently lost and that file never shipped. No action is required and no configuration changes; the newscannerTossers.ftn_bso.flowLockTimeoutMsdefaults to 5 seconds.Worth knowing if you run an external mailer such as Binkd against the same spool. This is the same
NNNNnnnn.bsylock your mailer already takes, which is precisely why honouring it makes the two interlock correctly — but it also means ENiGMA½ now waits on a lock your mailer holds. Two consequences:- A long session with a node briefly defers queueing for that node only, logged as
Flow file busy; outbound not queued this pass. The next export cycle queues it. Seeing this occasionally is normal; seeing it constantly means raisingflowLockTimeoutMs. - A stale
.bsyleft behind by a mailer that crashed mid-session will now hold up queueing for that node until it is reaped by age (binkp.staleLockMaxAgeMs, 30 minutes by default). If you find one node’s outbound sitting still, check its outbound directory for an orphaned.bsyand remove it.
- A long session with a node briefly defers queueing for that node only, logged as
-
The most specific
nodes{}and NetMailroutes{}pattern now wins, rather than the first one written. Where only one pattern matched an address nothing changes. Where several matched, the entry that applied used to depend on the order of yourconfig.hjson.Action: review your configuration if it mixes a wildcard with more specific entries that also match. Two cases change behaviour, both towards what the specific entry says: a
"21:*"node block written above"21:1/100"no longer shadows that node’s own settings — includingpacketPassword, which means a packet password you configured and believed to be in force may only now start being enforced — and a"*"NetMail route written above"21:*"no longer claims mail the narrower route was meant to take.The same rule can also relax a packet password, so check this direction too. A matching entry is used whole; its fields are not merged with those of the wildcard it beat. So where the wildcard carries the password and the specific entry does not:
nodes: {"21:*" : { packetPassword: MUHPA55 }"21:1/100" : { archiveType: ZIP } // no packetPassword}packets from
21:1/100used to be password checked by way of the wildcard and now are not, because the specific entry wins and sets no password. If any of your specific node blocks omit apacketPasswordthat a broader entry supplies, add it to them. -
The
downloadfile area ACS is now actually enforced. It previously applied only to the REST API; over telnet and SSH any user who could browse an area could also download from it. If you set adownloadACS on any file area expecting it to restrict downloads, it was not doing so until now.Action: review your
fileBase.areasACS blocks. Areas with nodownloadACS are unaffected — the default (GM[users]) matches thereaddefault, so nothing changes for them. Areas where you did setdownloadwill now restrict downloads to what you configured, which may be tighter than what users have actually been getting. Note that an entry whose area is no longer inconfig.hjsonfails closed and can no longer be downloaded. -
Automatic message area creation is available, and off (#241). EchoMail for an FTN area tag you have not configured can now create that area instead of being skipped and lost. No action is required: with no
autoAreasblock in your configuration nothing changes, and with no network enabled the feature does no work at all.If you want it, run
./oputil.js mb auto-areas initonce and then add anautoAreasblock per network — see FTN. Two things are worth knowing before you turn it on:- The
initcommand addsauto-areas.hjsontoincludesin yourconfig.hjson. Do not remove that file while it is still listed — a file listed inincludesthat does not exist stops the board from starting. To back the feature out, remove theincludesentry first, then the file. - Created areas are read-only by design: no
uplinks, so nothing exports, plus a write-denyacsso nothing can be posted into them. If you want to actually link one, define it in your ownconfig.hjsonwithuplinksand your ownacs—config.hjsonwins over the generated file, so you do not need to edit or remove anything there.
Turning it on adds a read-only scan of your inbound directories at the start of each import cycle. For loose
.pktfiles that is a second parse; for bundles it is a second extraction, since the import removes each bundle as it finishes with it. Neither happens while the feature is off. - The
-
oputil mb import-areasis stricter about what it will import (#733). It previously imported;and%comment lines as message areas, and imported a FILEBONE file echo list — which most networks ship named*.na, alongside their message list — as a set of areas all taggedArea. It now works the format out from the file’s content.Action: if you have imported a
.nafile in the past, checkconfig.hjsonfor areas tagged;,%orAreaand remove them. Going forward, three things that used to “succeed” now stop with an explanation instead:- A FILEBONE list is refused and points you at
oputil.js fb import-areas. - A list with the columns reversed (description first, tag last) is refused — SpookNet ships one, and the old parser accepted 35 nonsense areas from it.
- A file in no recognised format is refused rather than half-imported.
Lines that are skipped for their own reasons are now listed before the confirmation prompt.
AREAS.BBShandling is unchanged.oputil fb import-areasshares the same parser. FileGate.ZXXand FILEBONE.NAfiles import exactly as before — verified byte for byte against real network packs — and a file echo list shipped as a plainTAG Descriptionlist now imports rather than reporting “Nothing to import”. Be aware that a plain list looks identical to a message echo list;import-areaswarns before the confirmation prompt, so read the area listing before answering yes, especially with--create-dirs. - A FILEBONE list is refused and points you at
-
A port conflict now stops startup instead of being ignored (#547). Previously a server that could not bind its port left the startup sequence hanging with no message and no exit; NNTP specifically logged a warning and carried on, so the board ran with NNTP silently absent. Bind failures are now reported on the console and are fatal for every server, and ENiGMA½ exits with a non-zero status.
Action: if you run NNTP, Gopher, or the web server on a port something else on the host also uses, a start that previously “worked” (minus that service) will now stop with an explicit error. Fix the conflicting port in
config.hjson, or disable the service withenabled: false. -
BinkP sessions were left to time out rather than closing cleanly. ENiGMA½ ended after the first binkp/1.1 batch, so the peer waited for an
M_EOBthat never arrived. Mail transferred correctly, but the remote logged the session as failed, and while ours sat waiting it held that node’s lock — so crashmail queued for the node during the window was skipped until the next scheduled poll. No action is required; if a peer has been reporting failed sessions with your system despite the mail arriving, this is why. -
BinkP files compressed with GZ were rejected by every other mailer (#723). ENiGMA½ wrapped compressed data in the gzip container where FTS-1029 — and binkd, Mystic and the rest — use the zlib one. The receiving mailer rejected the stream immediately; our side logged nothing and re-sent the file on every poll. Nothing was lost and no configuration change is required: the affected files stayed in the outbound spool and go out on the next poll.
Only nodes with no
archiveTypewere affected, since ArcMail bundles are already compressed and never carried GZ.If you peer with a system still running a pre-fix ENiGMA½, be aware the incompatibility is now reversed in that one direction: they cannot decode what we send, though we can decode what they send. Either coordinate the update, or disable compression for that node until they do:
nodes: {"21:1/100": {host: "bbs.example.com"gz: false}}Only an explicit
falsedisables it. A failed decompression is also no longer fatal: ENiGMA½ asks for the file again uncompressed (anNZrequest, per FTS-1029) and skips it rather than stalling if that does not work either. -
BinkP sessions hung to the five minute timeout against peers using NR mode (#724). If a peer asked for non-reliable mode, ENiGMA½ offered each file correctly but then sent its data without the
M_FILEthat FTS-1026 requires after anM_GET. A conforming receiver had already closed the file and discarded the bytes without complaint, so both sides waited until the session timed out. Nothing was lost — the affected files stayed in the outbound spool and will go out on the next poll. No configuration change is required.binkd asks for NR mode whenever a sysop has set
-nror-ndon your node, which is why this could appear against one peer and not another regardless of version.One behaviour change worth knowing about: ENiGMA½ no longer asks peers to send to it in NR mode by default. That request costs a round trip per file, and FTS-1028 is explicit that it “should be used only if absolutely necessary” — binkd likewise only asks when configured to. If you peer over a link that drops often enough that restarting transfers from zero is a real cost, opt back in for that node:
nodes: {"21:1/100": {host: "bbs.example.com"requestNR: true}}Honouring a peer that asks us for NR mode is unconditional and needs no configuration, as the spec requires.
-
EchoMail exported to a node with no
archiveTypewas silently never delivered (#722). A node with noarchiveTypeconfigured exports bare packets instead of ArcMail bundles, and those were written to the outbound spool with a malformed name — the dot before the extension was missing (43792ae5cutrather than43792ae5.cut). No mailer could match such a file, so the message stayed in the spool indefinitely with no error at any log level. This is fixed; un-archived packets now ship as flow file references like everything else, and no configuration change is required. SettingarchiveTypeis no longer a workaround for anything, though it is still recommended for bandwidth.Action required if you run a node without
archiveType: stranded files are not migrated automatically, because their names carry no destination address. Find them:Terminal window # the malformed direct-attach namesfind mail/ftn_out -type f -regextype posix-extended \-regex '.*/[0-9a-fA-F]{8}(out|cut|iut|hut|dut)' -print# and, if you use fileCase: 'upper', names where the temp extension was never strippedfind mail/ftn_out -type f -iname '*.pk_*' -printEach one is a complete FTS-0001 packet. The directory it sits in identifies the network and zone, and the destination net/node is in the packet header — normally just the uplink that area exports to, but you can read it directly:
Terminal window node -e 'const b = require("fs").readFileSync(process.argv[1]);console.log(`dest = ${b.readUInt16LE(22)}/${b.readUInt16LE(2)}`)' \mail/ftn_out/outbound/43792ae5cutTo deliver one, give it a
.pktextension and reference it from that node’s flow file, whose name is 4 hex digits of the destination net followed by 4 of its node:Terminal window mv mail/ftn_out/outbound/43792ae5cut mail/ftn_out/outbound/43792ae5.pktecho "^$(pwd)/mail/ftn_out/outbound/43792ae5.pkt" >> mail/ftn_out/outbound/00640000.cloIf the messages are old enough not to be worth recovering, deleting the files is safe — they are copies; the originals remain in your message base.
-
Outbound files are now matched case-insensitively. If you set
fileCase: 'upper'on a node, the native BinkP mailer previously reported that node as having mail and then queued none of it — dialing every poll cycle and shipping nothing. The mailer now handles both cases, as FTS-5005.003 §2 asks. No action required; anything queued in upper case starts shipping on the next poll. This also applies to an outbound directory inherited from a DOS-era mailer, where upper case names are the norm. -
Mail to point addresses is now shipped by the native BinkP mailer.
ftn_bsohas always written point mail to theNNNNnnnn.pntsubdirectory the spec requires, but the mailer did not look there, so it was never sent. If you carry points and have been running the built-in mailer, expect a backlog to go out on the next poll — check that the accumulated volume is something you are happy to send before starting up, and delete anything stale frommail/ftn_out/**/*.pnt/first if not. -
Direct-attach netmail packets are renamed when sent. A
NNNNnnnn.?utfile in the outbound is now transmitted as a uniqueNNNNNNNN.pkt, per FTS-5005.003 §3.1. Previously it went out under its.?utname and the receiving system’s tosser ignored it. This only affects.?utfiles placed in your outbound by something other than ENiGMA½ — a netmail tracker, another mailer, or by hand. No action required. -
Multi-network BSO outbound directories are now consistent between
ftn_bsoand the native BinkP mailer (#719). The scanner/tosser and the mailer each used their own rule for deciding which FTN network owns the bareoutbound/directory, and the two disagreed whenever more than one network was configured. Both now use the documented rule:scannerTossers.ftn_bso.defaultNetworkwhen set, otherwise the first network listed inmessageNetworks.ftn.networks.If you have two or more FTN networks configured and have not set
defaultNetwork, the first-listed network’s outbound now lands inmail/ftn_out/outbound/rather thanmail/ftn_out/<networkName>/.-
Built-in BinkP mailer — no action required. Mail already queued under the previous layout is still found and sent; once that directory drains you may delete it. A startup log entry appears while this applies.
-
External mailer (Binkd, Mystic, etc.) — action required. Either update the mailer’s outbound path for that network to
outbound/, or keep the previous layout by explicitly declaring that there is no default network:scannerTossers: {ftn_bso: {// no default network; every network uses its own subdirectorydefaultNetwork: null}}
Either way, explicitly setting
defaultNetworkto a network name is recommended for multi-network systems: it pins the layout so that adding or reordering entries inmessageNetworks.ftn.networkscan never relocate a spool directory. See BSO Import / Export. -
-
Network names are now matched case-insensitively when resolving outbound directories. Systems using a mixed-case key in
messageNetworks.ftn.networks(e.g.fsxNet) on a case-sensitive filesystem could have outbound mail written to a directory the mailer never scanned. No action required.
0.4.0-beta to 0.5.0-beta
Section titled “0.4.0-beta to 0.5.0-beta”-
No breaking changes or required migrations.
-
DORINFO graphics field changed from
1to2— ENiGMA½ now writes2(ANSI color) in the DORINFO graphics field instead of1(IBM high-bit chars). RBBS-mode doors such as TradeWars 2002 require2to enable ANSI color; other doors treat any non-zero value as graphics-capable and are unaffected. No configuration change required. -
Optional: enable OSC 8 hyperlinks in message viewers — clickable URL support is now available for
%MTviews inprevieworread-onlymode. The default menu templates already includehyperlinks: trueon the appropriate views. If you maintain a custom menu config, addhyperlinks: trueto any message-body or NFO viewer%MTview where you want URL detection:MT1: {mode: previewhyperlinks: true}No effect on terminals that do not support OSC 8 — it degrades silently to plain text.
-
Optional: expose the new
user_status_configmodule — a new module lets users toggle their own availability and visibility. To add it to your menus, wire up a command or menu entry pointing to@menu:userStatusConfig. A minimal menu block is required in your menu config:userStatusConfig: {desc: User Statusmodule: user_status_configconfig: {art: {menu: user_status_config}enabledIndicator: "√"disabledIndicator: XmenuInfoFormat10: "{availableIndicator}"menuInfoFormat11: "{visibleIndicator}"}form: {0: {mci: {TL10: {}TL11: {}}actionKeys: [{ keys: [ "a", "shift + a" ] action: @method:toggleAvailable }{ keys: [ "v", "shift + v" ] action: @method:toggleVisible }{ keys: [ "escape", "q", "shift + q" ] action: @systemMethod:prevMenu }]}}}You will also need to create an art file named
user_status_config(.ans,.asc, etc.) containingTL10andTL11MCI codes for the availability and visibility indicators respectively. ThemenuInfoFormat10/menuInfoFormat11format strings support{availableIndicator},{visibleIndicator},{isAvailable}, and{isVisible}tokens and can be styled with pipe codes in your theme. -
Recommended: review any secrets currently stored as plain text in
config.hjson(privateKeyPass, SMTP/IMAP passwords, BinkPsessionPassword, FTNpacketPassword, TICpassword,jwtSecret, door service credentials) and consider moving them to@file:or@environment:references. This is optional but strongly encouraged — existing plain-text values continue to work unchanged. See Security for examples.
0.3.0-beta to 0.4.0-beta
Section titled “0.3.0-beta to 0.4.0-beta”N/A
0.2.0-beta to 0.3.0-beta
Section titled “0.2.0-beta to 0.3.0-beta”ActivityPub data may need purged if you have utlized it. Easiest to just delete your activitypub.db file and start anew.
0.1.1-beta to 0.2.0-beta
Section titled “0.1.1-beta to 0.2.0-beta”N/A
0.1.0-beta to 0.1.1-beta
Section titled “0.1.0-beta to 0.1.1-beta”N/A
0.0.14-beta to 0.1.0-beta
Section titled “0.0.14-beta to 0.1.0-beta”-
We are nearing 1.0! Version numbers have changed.
-
⚠️ FSE editor footer art and menu config have changed. The full-screen editor’s
footerEditorform (form2) previously used two separate%TL(Text Label) views —%TL1for cursor position and%TL2for INS/OVR mode — driven by aTLTLform config block. These have been replaced by a single%SB1(StatusBarView) with named panels.If you have a custom
MSGEFTRart file or a customizedcreateMessageEditor/readMessageEditormenu config:-
Art file (
MSGEFTR.ANS) — Replace any%TL1+%TL2pair with a single%SB1. Position it where you want the combined status indicator to appear (the default theme places it near the right side of the footer line). Remove any%TL2entirely. -
Menu config — In the
createMessageEditorandreadMessageEditor(or equivalent) menu entries, replace the old form2block:// OLD — remove this:2: {TLTL: {mci: {TL1: { width: 5 }TL2: { width: 4 }}}}with the new panel-mode
%SB1config:// NEW — use this:2: {mci: {SB1: {width: 9anchor: leftjustify: leftseparator: " "panels: [{name: modewidth: 3justify: right}{name: poswidth: 5justify: left}]}}}
In all cases, diff the template against your existing config before applying changes — your config likely contains other customizations you don’t want to lose:
Terminal window diff ./misc/menu_templates/message_base.in.hjson ./config/menus/your_board-message_base.hjsonApply only the form
2changes shown above. See Configuration Files for details. -
-
⚠️ New FSE keyboard shortcuts and find overlay require menu config updates. The following changes apply to all FSE menu entries (
messageBaseNewPost,messageAreaViewPost,messageAreaReplyPost, and their private-mail equivalents).New art file —
MSGFND: A find-prompt footer art file is now required. Reference it in each FSE menu’sconfig.artblock:config: {art: {// ... existing art keys ...footerFind: MSGFND}}The art file must contain a single
%ET1(EditTextView) input field. Create and theme it like your other footer art files.Header form (form
0) — Escape key handler change: Replace@systemMethod:prevMenuwith@method:headerEscapePressedin the header form’sactionKeys. This is required so thatCtrl-A(“change subject”) can return focus to the body without exiting the FSE:// OLD — remove:{ keys: [ "escape" ], action: @systemMethod:prevMenu }// NEW — replace with:{ keys: [ "escape" ], action: @method:headerEscapePressed }Body editor (form
1) — new action keys: Add to theactionKeysarray in the body (MT1) form:{ keys: [ "ctrl + f" ], action: @method:editModeFind } // open find prompt{ keys: [ "ctrl + n" ], action: @method:editModeFindNext } // find next match{ keys: [ "ctrl + p" ], action: @method:editModeFindPrev } // find previous match{ keys: [ "ctrl + a" ], action: @method:editModeChangeSubject } // edit subject inlineView mode (form
4) — new action keys: Add to the view footeractionKeysarray:{ keys: [ "ctrl + f" ], action: @method:viewModeFind }{ keys: [ "ctrl + n" ], action: @method:viewModeFindNext }{ keys: [ "ctrl + p" ], action: @method:viewModeFindPrev }New form
6— find footer form: All FSE menu entries need a form6definition. The simplest approach is to use the shared reference added to the template:6: @reference:common.fseFindFooterFormYou must also add the
fseFindFooterFormfragment to your menu file’scommon:section. Copy it verbatim frommisc/menu_templates/message_base.in.hjson(it is anET1input with Enter to submit and Escape to cancel).Editor command menu (form
3) — upload item: The ESC command menu (HM1) now includes an"upload"item allowing users to upload a file as the message body (ANSI art or plain text). Add"upload"to theitemsarray and the corresponding submit action:HM1: { items: [ "save", "discard", "help", "upload" ] } // postHM1: { items: [ "save", "discard", "quote", "help", "upload" ] } // replyWith matching submit entries:
{ value: { 1: N }, action: @method:editModeMenuUpload }Where
Nis the zero-based index of"upload"in your items list. Upload access defaults toGM[users]; override per-menu viaconfig.uploadAcs.As always, diff the template against your config before applying changes:
Terminal window diff ./misc/menu_templates/message_base.in.hjson ./config/menus/your_board-message_base.hjson -
nodemailer upgraded to v8. If you have
email.transportconfigured with AWS SES, you will need to update your transport config to use the SESv2 SDK — see the nodemailer SES docs. All other transports (SMTP, etc.) require no changes. -
Pause prompt and TickerView enhancements — new
pause: pageBreakpagination mode,pausePrompt,pausePosition,continuousKey/quitKey, and TickerView (%TK) support in pause prompts. Existingpause: trueconfigs continue to work unchanged. See What’s New and Pause Prompts for details.
0.0.13-beta to 0.0.14-beta
Section titled “0.0.13-beta to 0.0.14-beta”- A new ActivityPub menu template has been created. Upgrades will not have this file present so you will need to copy the template to your
config/menusdirectory and rename it appropriately (it must match theincludestatement in your mainmenu.hjsonfile). Example:
cp ./misc/menu_templates/activitypub.in.hjson ./config/menus/my_board_name-activitypub.hjson`This will expose the default ActivityPub setup. Enabling ActivityPub functionality requires the web server enabled and ActivityPub itself enabled in your config.hjson. See Configuration Files Include Statements for more information on using include.
- ⚠ The menu flag
noHistoryhas been revamped to work as expected. Some menu entires now need this flag. Look for any “NoResults” entries and removemenuFlags. For example, here is the (updated) defaultfileBaseListEntriesNoResultsmenu:
fileBaseListEntriesNoResults: { desc: Browsing Files art: FBNORES config: { pause: true // no menuFlags here }}See also: Menu Modules.
- Due to changes to supported algorithms in newer versions of openssl, the default list of supported algorithms for the ssh login server has changed. There are both removed ciphers as well as optional new kex algorithms available now. NOTE: Changes to supported algorithms are only needed to support keys generated with new versions of openssl, if you already have a ssl key in use you should not have to make any changes to your config.
- Removed ciphers: ‘blowfish-cbc’, ‘arcfour256’, ‘arcfour128’, and ‘cast128-cbc’
- Added kex: ‘curve25519-sha256’, ‘curve25519-sha256@libssh.org’, ‘curve25519-sha256’, ‘curve25519-sha256@libssh.org’, ‘ecdh-sha2-nistp256’, ‘ecdh-sha2-nistp384’, ‘ecdh-sha2-nistp521’
0.0.12-beta to 0.0.13-beta
Section titled “0.0.12-beta to 0.0.13-beta”- To enable the new Waiting for Caller (WFC) support, please see WFC.
- :exclamation: The SSH server’s
ssh2module has gone through a major upgrade. Existing users will need to comment out two SSH KEX algorithms from theirconfig.hjsonif present else clients such as NetRunner will not be able to connect over SSH. Comment outdiffie-hellman-group-exchange-sha256anddiffie-hellman-group-exchange-sha1 - Gopher configuration change. See WHATSNEW
- All features and changes are backwards compatible. There are a few new configuration options in a new
termsection in the configuration. These are all optional, but include the following options in case you use them:
{ term: { // checkUtf8Encoding requires the use of cursor // position reports, which are not supported on all terminals. // Using this with a terminal that does not support cursor // position reports results in a 2 second delay during the // connect process, but provides better auto configuration of utf-8 checkUtf8Encoding: true
// Checking the ANSI home position also requires the use of // cursor position reports, which are not supported on all /// terminals. Using this with a terminal that does not support // cursor position reports results in a 3 second delay during // the connect process, but works around positioning problems with // non-standard terminals. checkAnsiHomePosition: true }}In addition to these, there are also new options for term.cp437TermList and term.utf8TermList. Under most circumstances these should not need to be changed. If you want to customize these lists, more information is available in config_default.js
0.0.11-beta to 0.0.12-beta
Section titled “0.0.11-beta to 0.0.12-beta”- Be aware that
masteris now mainline! This means allgit pull’s will yield the latest version. See WHATSNEW for more information. - BREAKING CHANGE There is no longer a
prompt.hjsonfile. Prompts are now simply part of the menu set in thepromptssection. If you have an existing system you will need to add yourprompt.hjsonto yourmenu.hjson’sincludessection at a minimum. Example:
{ includes: [ my-prompts.hjson // ref your old prompts here ]}- A set of database fixes were made that cause some records to be properly cleaned up when e.g. deleting a file. Existing
file.dbdatabases will need to be updated manually. Note that this applies to users upgrading within 0.0.12-beta as well:
- Make a backup of your file.db!
- Shut down ENiGMA.
- From the enigma-bbs directory:
sqlite3 db/file.sqlite3 < ./misc/update/tables_update_2020-11-29.sql0.0.10-alpha to 0.0.11-beta
Section titled “0.0.10-alpha to 0.0.11-beta”- Node.js 12.x LTS is now in use. Follow standard Node.js upgrade procedures (e.g.:
nvm install 12 && nvm use 12).
0.0.9-alpha to 0.0.10-alpha
Section titled “0.0.9-alpha to 0.0.10-alpha”- Security related files such as private keys and certs are now looked for in
config/securityby default. - Default archive handler for zip files has switched to InfoZip due to a bug in the latest p7Zip packages causing “volume not found” errors. Ensure you have the InfoZip
zipandunzipcommands in ENiGMA’s path. You can switch back to 7Zip by overridingarchiveHandlerforapplication/zipin yourconfig.hjsonunderfileTypesto7Zip.
0.0.8-alpha to 0.0.9-alpha
Section titled “0.0.8-alpha to 0.0.9-alpha”- Development is now against Node.js 10.x LTS. Follow your standard upgrade path to update to Node 10.x before using 0.0.9-alpha!
- The property
justifyfound on various views previously hadleftandrightvalues swapped (oops!); you will need to adjust any customtheme.hjsonthat use one or the other and swap them as well. - Possible breaking changes in FSE: The MCI code
%TL13for error indicator is now%TL4. This is part of a cleanup and standardization on “custom ranges”. You may need to update yourtheme.hjsonand related artwork. - Removed view width auto-size: Some views still can auto-size their height, but in general you should be explicit in your themes
- More standardization using “custom ranges” and
itemFormat/focusItemFormatsemantics. Update your themes! - In addition to using
itemFormat, theonelinerzmodule usesuserNamevsusername(note the case) to match other modules loginServers.webSocketconfiguration block has changed to be more consistent with other servers. Example:
webSocket: { ws: { enabled: true } wss: { enabled: true port: 1234 } proxied: true // X-Forwarded-Proto: https support}- The module export
registerEventshas been deprecated. If you have a module that depends on this, use the new more genericmoduleInitializeexport instead. - The
system.dbuser_event_logtable has been updated to include a unique session ID. Previously this table was not used, but you will need to perform a slight maintenance task before it can be properly used. After updating to0.0.9-alpha, please run the following:sqlite3 db/system.db DROP TABLE user_event_log;. The new table format will be created and used at startup. - If you have art configured for message conference or area selection via the
artconfiguration value, you will need to include ashow_artmenu reference. Defaulted tochangeMessageConfPreArtfor conferences andchangeMessageAreaPreArtfor areas & included in the examplemenu.hjson. - Config
defaultssection was theme related and as such, has been renamed totheme.defaults.themeis nowtheme.default, andpreLoginThemeis nowtheme.preLogin. Seeconfig.jsif this isn’t clear as mud. - Similar to the last item,
defaults.general.passwordCharintheme.hjsonis now justdefaults.passwordChar.
0.0.7-alpha to 0.0.8-alpha
Section titled “0.0.7-alpha to 0.0.8-alpha”ENiGMA 0.0.8-alpha comes with some structure changes:
- Configuration files are defaulted to
./config. Related, the--configoption now points to a configuration directory ./mods/arthas been moved to./art/general./modsis now reserved for actual user addon modules- Themes have been moved from
./mods/themesto./art/themes
With the change to the ./mods directory, @systemModule is now implied for module declarations in menu.hjson. To use a user module in ./mods you must specify @userModule!
With the above changes, you’ll need to to at least:
- Move your
~/.config/enigma-bbs/config.hjsonto./config/config.hjsonor utlize the--configoption. - Move your
prompt.hjsonandmenu.hjson(e.g.myboardname.hjson) to./config - Move any non-theme art files, and theme directories to their appropriate locations mentioned above
- Move any module directories such as
message_post_evtto./mods/ - Move any certificates, pub/private keys, etc. from
./miscto./config - Specify user modules as
@userModule:my_module_name
0.0.6-alpha to 0.0.7-alpha
Section titled “0.0.6-alpha to 0.0.7-alpha”No issues
0.0.5-alpha to 0.0.6-alpha
Section titled “0.0.5-alpha to 0.0.6-alpha”No issues
0.0.4-alpha to 0.0.5-alpha
Section titled “0.0.4-alpha to 0.0.5-alpha”No issues
0.0.1-alpha to 0.0.4-alpha
Section titled “0.0.1-alpha to 0.0.4-alpha”Node.js 6.x+ LTS is now required
Section titled “Node.js 6.x+ LTS is now required”You will need to upgrade Node.js to 6.x+. If using nvm (you should be!) the process will go something like this:
nvm install 6nvm alias default 6Newly written code will use ES6 and a lot of code has started the migration process. Of note is the MenuModule class. If you have created a mod that inherits from MenuModule, you will need to upgrade your class to ES6.
Manual Database Upgrade
Section titled “Manual Database Upgrade”A few upgrades need to be made to your SQLite databases:
rm db/file.sqltie3 # safe to delete this time as it was not used previouslysqlite3 db/message.sqlitesqlite> INSERT INTO message_fts(message_fts) VALUES('rebuild');Archiver Changes
Section titled “Archiver Changes”If you have overridden or made additions to archivers in your config.hjson you will need to update them. See Archive Configuration and core/config.js
File Base Configuration
Section titled “File Base Configuration”As 0.0.4-alpha contains file bases, you’ll want to create a suitable configuration if you wish to use the feature. See File Base Configuration.