Output alignment overhaul, a new --cert-delete --purge flag, a full code/security audit pass (HIGH/MED/LOW fixes), performance work, and a GitHub Actions CI (ShellCheck + shfmt + Gitleaks).
✨ New Features
--cert-delete <id|domain> --purge— New optional--purgeflag. After the API soft-delete (NPM setsis_deleted=1, which only removes the cert from the list/UI),--purgealso deletes the leftover on-disk files:custom_ssl/npm-<id>,letsencrypt/live|archive/npm-<id>andletsencrypt/renewal/npm-<id>.conf. RequiresNGINX_PATH_DOCKERto be set innpm-api.conf; without it (or if the path is not accessible)--purgeis a safe no-op with a clear message. The DB row is intentionally left untouched to avoid referential-integrity risk and DB-schema drift between NPM versions.--user-create … --admin— New optional--adminflag. Users are now created as standard (non-admin) by default; pass--adminto grant the admin role. Previously every created user was forced toroles: ["admin"].--cert-show-all— Now works as an alias of--cert-list(it was shown in usage text but had no parser case).
💅 Output / Display
host_list()— long & multiple domains — Proxy hosts with several domains (or a very long one) no longer break column alignment. The first domain stays on the main row; the remaining domains are listed below, indented under the DOMAIN column. Newtruncate_pad()helper truncates over-long values with an ellipsis (…) instead of overflowing, applied to the DOMAIN and TARGET columns.redirect_host_list()— same alignment fix — Applied the identical treatment to the Redirection Hosts table (it had the samepad-without-truncation overflow bug):truncate_pad()on the DOMAIN and FORWARD DOMAIN columns, plus multi-line display for hosts with multiple domains.show_help()— aligned description column — Newhelp_row()helper measures visible width (ignoring ANSI color codes and counting wide emoji such as 🆔 as 2 cells) and aligns every description to a fixed column. Lines whose left part is wider than the column (e.g.--cert-download) wrap their description onto the next line, still aligned. All option lines converted tohelp_row.
🐛 Bug Fixes
list_cert_all()— wrong Valid/Expired statistics — The stats usedselect(.expires_on > now), comparing an ISO date string to the numericnow; in jq every string sorts greater than every number, so "Valid" always equalled the total and "Expired" was always 0. Now uses the API's own.expiredboolean (select(.expired == true)/!= true).cert_delete()— no check that the certificate is still in use — Before deleting, the script now lists the proxy/redirection hosts that still reference the certificate and warns that they will be left with a danglingcertificate_id. When combined with--purge, the purge is now refused while the cert is still referenced (its on-disk files are in use), to avoid breaking live TLS.--access-list-create— command ran during argument parsing; bottom dispatch was dead code —ACCESS_LIST_CREATEwas never set, so the dispatcher branch was unreachable and the function executed mid-parse. Now aligned with--access-list-update: arguments are stashed (ACCESS_LIST_CREATE_ARGS) and the function runs from the dispatch block.--access-list-create --pass-auth— asymmetric parsing — The create parser set--pass-authwith no value while--access-list-update(and the documented usage--pass-auth true) expects atrue|falseargument, so--pass-auth truefailed with "Unknown option true". Create now consumes and validates the value like update.host_acl_enable/host_acl_disable— error handling — Read the error from the wrong jq path (.messageinstead of.error.message) and never checked the HTTP status. Now captureHTTPSTATUS, treat non-200 as failure (exit 1), and read.error.message.host_show()— wrong fields — Read.websockets_enabled(always null) instead of the real field.allow_websocket_upgrade, and passed.forward_scheme(http/https) throughcolorize_boolean(mislabelled). Both fixed.user_create— no longer writes the raw API response to/tmp/npm_debug.log(a world-readable path).cert_delete— declining the confirmation now exits 0 (deliberate user choice) instead of 1.- Exit codes —
host_enable/host_disableandredirect_host_enable/redirect_host_disablenowreturn 1on every failure path (previously they printed the error but still returned success). full_backup—success_countdouble-counted — Each backed-up certificate incremented the success counter twice, inflating the final summary. Fixed to count once.
🛡️ Robustness
--infodispatch — Added a dedicatedelif [ "$INFO" = true ]branch so--infoworks even when combined with other flags (it previously only ran as the no-argument fallback).set -usafety — InitializedCERT_ID,GENERATED_CERT_ID,USER_IDandsearch_termin the top variable block (they were referenced in the dispatcher before being set on some code paths).set -esafety — Guarded the 11[ "$HTTP_STATUS" -eq … ]checks with${VAR:-0}so an empty/garbled response falls into the error branch instead of aborting the script, and converted the 10((var++))counters tovar=$((var + 1))(the((x++))form returns exit status 1 whenxis 0, which aborts underset -e).- Consistent boolean defaults —
HTTP2_SUPPORT,SSL_FORCED,HSTS_ENABLEDandHSTS_SUBDOMAINSnow default tofalseinstead of0(same class as the Issue #23 fix), and the duplicateAUTO_YES=falsedeclaration was removed.
⚡ Performance
- Token read once per run — The API token was re-read from disk with
$(cat "$TOKEN_FILE")on every single request (~81 call sites). It is now cached in a global$TOKEN(populated bycheck_token_notverbose); headers use${TOKEN:-$(cat "$TOKEN_FILE")}so an empty cache still falls back safely to reading the file. host_list— one certificate fetch instead of one per host — The CERT DOMAIN column previously triggered aGET /nginx/certificates/<id>call for every proxy host that had SSL. The full certificate list is now fetched once and resolved locally through anid → domainsmap, turning N API round-trips into 1.
♻️ Code Quality / Cleanup
- Factored the duplicated certificate formatter — The three near-identical
jqcert-formatting one-liners (incert_showID/domain branches andlist_cert_all) now share a singleCERT_JQ_FMTformat string and acert_colorizehelper; this also fixes the inconsistentProvider :vsProvider:label. access_listtable — Guardedproxy_host_countagainstnull(// 0), truncated long names to the column width viatruncate_pad, and made the "Proxy Hosts" cell fixed-width so a 2-digit count no longer breaks the box border.- Consistent confirmation prompts — All
(y/n)prompts now accept^[Yy]$(the two French leftovers acceptingO/owere aligned). - English usage text — Translated the remaining French usage/error strings (
--host-update,--access-list-show) to match the rest of the CLI. - Removed dead code — Dropped the unimplemented
-O/-Jflag stubs (they only printedtodo) and a duplicatedGET /nginx/certificatesblock incert_generate.
🆕 Documentation / Help
--host-list-full— Now shown in--help(was a functional but hidden/commented command). Lists all proxy hosts with full details (JSON).--cert-generate— Help now lists the new NPM v2.15.0 Certbot DNS plugins:hostinger,rcodezero,hoster.by.- Minor help wording fixes:
Show Default…(double space) andCheck Check current token info(duplicated word) cleaned up.
🔎 Compatibility
- Reviewed against NPM v2.15.0: no proxy-host/certificate data-model changes, version is read dynamically, and DNS provider names are passed through to the API — so no breaking changes for the script.
Full changelog: CHANGELOG.md