Backlog
June 22, 2026 · View on GitHub
Items that are intentionally deferred. Pick up when the time is right.
Git Integration
AI
- AI-generated auto-commit messages — when auto-commit is enabled and a script is saved (or renamed/deleted), instead of the generic
"update <path>"message, ask the configured AI to generate a meaningful commit message based on the diff (git diff --cached). Should be best-effort: fall back to the generic message if the AI call fails or times out. Needs a budget-conscious prompt (diff only, no extra context) and a short timeout so saves don't feel sluggish.
Script Engine
-
Async callback safety — proper per-dispatch Promise wrapping — the global
unhandledRejectionhandler added in v1.13.0 prevents daemon crashes but cannot attribute the rejection to a specific script. The correct fix is to wrap every user callback dispatch site so that async errors are caught close to their source and logged with the script name. InstateChange,mqttEventCallbacks, and the sun/schedule dispatch, replacecallback(...)withPromise.resolve(callback(...)).catch(err => log.error(scriptLabel, 'async callback error:', err.message)). Requires touching ~6 dispatch sites but no API changes and no behaviour change for synchronous callbacks. Should also be applied instdlib.js(the timer/or/and/max/min helpers). Estimated effort: ~30 lines. -
Document async/await usage and constraints— DONE. Added "Async / await" section todoc/sandbox-api.md; fixed top-levelawaitinshe.configexample; updateddoc/examples/http-and-webhooks.mdto use try/catch throughout.Considered and rejected: auto-wrapping scripts in an async IIFE — wrapping every script source in
(async () => { ... })()at compile time would enable top-levelawaitwithout any API changes. Rejected because it introduces several subtle runtime problems specific to she's architecture: (a) script execution order breaks — everything after the firstawaityields to the event loop, so a later script can finish registering its callbacks before an earlier async script completes setup, destroying the predictable load order thatshe.globalsharing depends on; (b) the domain-based error handler does not catch Promise rejections, so all errors after the firstawaitbecome unhandled rejections unless the wrapping.catch()is explicitly routed back to the per-script logger via an injected VM variable; (c) thevm.Scriptinitial-runtimeoutoption only measures the synchronous preamble and becomes effectively useless; (d)returnat top level silently exits the IIFE instead of being aSyntaxError, masking bugs; (e) the wrapper shifts all line numbers by one, breaking stack-trace filtering and compile-error reporting.The recommended pattern when top-level
awaitis genuinely needed (e.g. fetch initial state before registering callbacks) is a user-written async IIFE:(async () => { const state = await she.http.fetch('http://...'); she.mqtt.sub('home/#', async (topic, val) => { try { /* ... */ } catch (err) { she.error(err); } }); })().catch(err => she.error('startup error:', err));This keeps line numbers correct, lets the user opt in deliberately, and makes the
.catch()explicit. Document this pattern alongside the constraints above. -
per-script resource limits / blocking callback detection — a script callback that runs a synchronous infinite loop (or any long-running blocking code) stalls the entire daemon: MQTT processing stops, all other scripts freeze, and there is no way to interrupt the blocking code from within the same thread. Two complementary improvements, in increasing complexity order:
-
vm.Scriptinitial-run timeout — pass atimeoutoption toscript.runInContext()(e.g. 5 s). This terminates the script if the top-level body runs synchronously for too long. Easy, ~5 lines. Does not protect against blocking callbacks. -
Event-loop heartbeat — DONE! schedule a
setImmediateevery 100 ms. If it fires more than ~500 ms late, the event loop was blocked. Track the "currently executing script" in a module-level variable (set when a sandbox callback fires, cleared in a follow-upsetImmediate); log a warning naming the script. This detects blocking callbacks and provides useful diagnostics, but cannot interrupt them — it only reports after the fact. -
Worker threads per script — the only way to achieve true isolation: each script runs in its own
worker_thread, so blocking one worker does not affect the daemon or other scripts. The worker can be killed and restarted on reload. Major architectural rewrite:she.mqtt.get()is currently synchronous — in a worker context it would needSharedArrayBuffer-based state sharing or become async (a breaking API change). All subscription callbacks would need to be dispatched across the thread boundary viapostMessage. Estimated effort: weeks. Only worth pursuing if the homelab grows large enough that a single misbehaving script is a real operational risk.
Practical path: implement 1 + 2 as a low-effort improvement now; defer 3 unless there is a concrete need.
-
-
graceful WebSocket shutdown — when
process.exit(0)is called (e.g. on restart request), connected WebSocket clients drop abruptly. Send a WS close frame first so the frontend can show a meaningful disconnect message. -
Safe mode — start without executing scripts — when a user script blocks the event loop permanently (infinite synchronous loop), the daemon becomes unresponsive: MQTT stops, the web UI is unreachable, and even
systemctl stophangs until systemd'sTimeoutStopSecSIGKILL fires. Safe mode initialises everything (MQTT, HTTP/WS server, file watcher, Config, Logs) but skips allloadScript()calls. The user can then open the web UI, identify and fix/delete the offending script, and restart normally.Activation — two independent mechanisms, both configurable:
-
CLI flag
--safe-mode— always enters safe mode regardless of any other state. Useful for a manual override (ExecStart=/usr/local/bin/she --safe-mode --data-dir /var/lib/sheas a one-off override viasystemctl edit --force). -
Auto-detect via sentinel file (configurable, default on) — on every normal startup, write a sentinel file
<dataDir>/.she-running. Delete it as the first synchronous action in theSIGTERMhandler (before any async cleanup) and also inprocess.on('exit', ...). On startup, if the file already exists, she was SIGKILL'd without a clean exit → enter safe mode automatically. Controlled by config keysafeMode.autoDetect(bool, defaulttrue). Expose as a toggle in the Config UI "Script engine" section alongside the heartbeat settings.
Getting out of safe mode: just use the existing "Restart daemon" button in the stats popup — no new UI flow needed. A clean restart without
--safe-modeand without a sentinel file present will start normally.Web UI: when
stats.safeMode === true, show a full-width red banner spanning the top of the app (above the nav bar): "⚠ SAFE MODE — scripts are not running. Edit or delete the problematic script, then restart the daemon." The Scripts tab remains fully functional for editing and deleting. No other UI changes are needed.Backend:
GET /she/statusgains asafeMode: booleanfield. Config keys:safeMode.autoDetect(bool, defaulttrue). The--safe-modeCLI flag (yargs boolean, defaultfalse) always wins.vm.Scriptinitial-run timeout (complementary): while implementing safe mode, also pass{ timeout: 5000 }toscript.runInContext(). This kills scripts whose top-level body blocks synchronously for >5s at load time, catching infinite loops at the point of loading rather than only after callbacks fire. On timeout, log an error with the script name and continue loading other scripts — same behaviour as a syntax error. Default 5s; make it configurable viasafeMode.scriptTimeout(ms,0= disabled). Note: this only applies to synchronous top-level code; callbacks are not affected.Known downsides and edge cases to handle carefully:
-
Interaction with
TimeoutStopSec=3(critical): the service file usesTimeoutStopSec=3. If she is doing legitimate slow graceful cleanup (MQTT disconnect, DB flush) and takes >3s, systemd SIGKILLs it. Sentinel remains → false-positive safe mode on next start. Fix: delete the sentinel synchronously at the very start of the SIGTERM handler, before any async cleanup begins. If SIGKILL fires 3s later the sentinel is already gone. Test this carefully. -
OOM killer / external
kill -9: any out-of-band SIGKILL (OOM, Docker, manual) leaves the sentinel and triggers safe mode on next start. This is arguably correct behaviour (something went wrong → investigate) but may surprise users who manage the process externally. -
Docker volumes: the sentinel file lives in
--data-dir. If the container is force-removed and recreated with the same volume, the sentinel persists and triggers safe mode on the first start of the new container. Document this; advise deleting the volume or settingsafeMode.autoDetect: falsein Dockerised setups. -
Development environments: developers who frequently
kill -9a local she instance will be constantly dropped into safe mode. ThesafeMode.autoDetect: falseconfig option is the escape hatch; mention it prominently in the docs. -
vm.Scripttimeout false positives: scripts that intentionally do heavy synchronous work at startup (parsing large JSON files, precomputing lookup tables) could be killed. The 5s default is generous for home-automation scripts; document thesafeMode.scriptTimeout: 0opt-out. -
Sentinel and restart-from-UI:
POST /she/restartcallsprocess.exit(0), which firesprocess.on('exit', ...)→ sentinel is deleted before the new process starts. No false positive. -
Multiple simultaneous starts: two she instances racing at startup could both see no sentinel, both write it, and both start normally. Edge case not worth special handling; process locking is overkill for this use case.
-
System scripts & cross-script event bus
Concept: two-tier script loading
Introduce a distinction between system scripts (ship with the daemon, in src/scripts/) and user scripts (the existing ~/.she/scripts/). Load order: system scripts first, then user scripts. This guarantees that any API a system script attaches to she.global is already available when the first user script runs.
System scripts are disabled by default. The user explicitly enables each one. Enabled state is tracked in ~/.she/system-scripts.json (a simple array of enabled script names, e.g. ["notify", "mqtt-to-influx"]). This file lives in the data dir — not config.json (config pollution) and not sheDB (avoids a dependency on --db-path being set).
Web UI: A small tab switcher appears above the file tree in the Scripts tab — Scripts (existing behaviour) | System. The System tab shows the contents of src/scripts/ as a read-only file tree. Clicking a script opens it in Monaco with readOnly: true and a "READ ONLY — system script" indicator in the toolbar. Each entry has an enable/disable toggle; toggling writes to system-scripts.json and immediately loads or unloads the script without a daemon restart.
Self-bootstrapping sheDB config
Each system script that needs configuration creates its own sheDB document with default values on startup, and recreates it with defaults if the document has been deleted. This means the user never has to manually create a config document to get started — the script is self-documenting and safe to run unconfigured. The user edits the document in the DB tab to supply real values (API keys, topic patterns, etc.).
// pattern used by every system script that needs sheDB config
const DOC_ID = 'system/notify';
const DEFAULTS = { service: 'pushover', appToken: '', userKey: '', defaultTitle: 'she' };
if (!she.db.get(DOC_ID)) she.db.set(DOC_ID, DEFAULTS);
const cfg = she.db.get(DOC_ID);
she.emit / events:: namespace — cross-script event bus
Currently scripts can share state via she.global (plain object, no callbacks) or via MQTT (requires broker, pollutes topic namespace, adds latency). A lightweight in-process event bus fills the gap.
Implemented as engine core (in index.js, always available, no system script needed) by extending the existing she.on pattern (mqtt::, var::, matter::) with a new events:: namespace and a new she.emit method:
// script-a.js — subscribe
she.on('events::alarm', ({ zone, severity }) => {
she.info('alarm in', zone, 'severity', severity);
});
// script-b.js — emit
she.emit('events::alarm', { zone: 'front-door', severity: 'high' });
events:: subscriptions are cleaned up on script unload just like var:: subscriptions — no leaks on hot-reload. she.emit is the only new sandbox method. Implementation: an in-memory Map<string, Set<{listener, _script}>> in index.js. On unload, remove all events:: listeners registered by that script. ~30 lines.
notify.js — Pushover notifications
Reads/bootstraps config from she.db.get('system/notify'):
{
"appToken": "",
"userKey": "",
"defaultTitle": "she"
}
Exposes she.global.notify.send(title, message, [opts]). If appToken or userKey are empty, logs a one-time warning and returns without sending. User scripts use she.global.notify?.send(...) — ?. makes it a silent no-op if the system script is disabled.
mqtt-to-influx.js — forward MQTT topics to InfluxDB
Reads/bootstraps config from she.db.get('system/mqtt-to-influx'):
{
"subscriptions": ["home/#"],
"measurement": "mqtt"
}
InfluxDB connection params come from the existing daemon-level --influx config (already available via she.influx.*). Subscribes to the configured topic patterns and writes each value change as an InfluxDB point using she.influx.write(). Useful for users who want time-series history of MQTT state without writing any script code — just enable the system script and configure which topics to forward.
⚠️ Open question: is the whole system scripts concept a good idea?
Arguments against, worth considering before implementing:
-
she's core value proposition is zero-boilerplate scripting — the
mqtt-to-influxuse case is already three lines of user script code. Shipping a system script for it adds a whole infrastructure layer (two-tier loading,system-scripts.json, read-only UI tab, self-bootstrapping sheDB docs) to solve something that is already trivially solvable in user space. Does the added complexity pay for itself? -
sheDB dependency for
notify.js— if--db-pathis not configured, sheDB is unavailable and the self-bootstrapping config pattern fails silently. Pushover credentials then have no home. The daemon-levelconfig.jsonis the more reliable place for API keys, but that conflicts with the "system scripts configure themselves" design. -
she.global.notifycouples user scripts to a system script — user scripts that callshe.global.notify?.send(...)now have an implicit dependency on a specific system script being enabled. If the system script is renamed, split, or the user switches notification service, every user script referencingshe.global.notifybreaks. This is exactly the kind of coupling she's philosophy discourages. -
mqtt-to-influxconflicts with existingshe.influx.*sandbox API — users who already useshe.influx.write()in their own scripts and also enable the system script end up with double-writes or competing subscription logic. The system script has no way to know what user scripts are already doing with InfluxDB. -
Maintenance surface — every system script is code that ships with every installation and must be maintained, tested, and documented. A poorly written
notify.js(e.g. no timeout on the HTTP request) can hang the event loop for every she user who enables it. The same bug in a user script only affects that one user. -
Alternative that avoids all of the above — instead of system scripts, ship a well-stocked
doc/examples.mdand a template library (see Script starter templates backlog item). Users copy a template, own the code, and there is no hidden layer. Theshe.emit/events::engine core stands on its own merits regardless of whether system scripts exist.
Consider whether she.emit alone (engine core, clearly useful, zero maintenance burden) is the right deliverable, and the system scripts concept is deferred or dropped entirely.
-
Script starter templates — when clicking
+ File, offer an optional template picker (dropdown or modal) with a handful of pre-filled starters: blank, motion-triggered light, daily schedule, MQTT bridge / link, HTTP fetch, sheDB subscriber. Templates can be drawn directly fromdoc/examples.md. Frontend-only change: a small UI addition to the new-file flow, no backend needed. -
Diff view before git commit — the git workflow currently goes: edit → commit dialog → write message → commit. Missing: a way to review what you are about to commit. Add a "Show diff" toggle in the commit dialog that renders
git diff HEAD(orgit diff --cachedafter staging) in a Monaco diff editor. Monaco has a built-increateDiffEditorAPI; the backend already hasGET /she/git/diffor can trivially add one. Makes the commit step much more intentional. -
Log export — the Logs tab holds a live ring buffer but provides no way to download it. A small export button (download as
.jsonlor plain text) would make bug reporting and offline log analysis straightforward. Backend: aGET /she/logs/exportendpoint that returns the buffer. Frontend: a button in the Logs toolbar. -
Find & Replace entry point — add a small Edit menu button in the editor toolbar, positioned between the
filenamespan and the git-status badges (current layout left-to-right:filename | [Edit▾] | git-status | Save | Delete | AI). Clicking it opens a compact dropdown with at minimum: Find (Ctrl+F) and Find & Replace (Ctrl+H). Could also include Go to line (Ctrl+G). Each item callseditor.getAction('<id>')?.run()on the Monaco instance. No Monaco context-menu changes; no new route. Frontend-only, ~20 lines + dropdown styling. -
file tree virtualization — the tree re-renders entirely on any change; with hundreds of scripts this becomes slow. Use a virtual list (
svelte-virtual-listor similar) to only render visible rows. -
Script-specific configuration UI ⚠️ questionable idea — needs more thought before implementing — scripts would call
she.config.define(schema)to declare typed config fields (string, number, boolean, select, topic, password) with labels and defaults. Config values would be stored as.<scriptname>.config.jsonnext to the script (dot-prefixed, hidden in file tree). The web UI would auto-generate a form from the schema — similar in spirit to Node-RED's node configuration panels.she.config.get()returns current values merged with defaults. A config change via the UI triggers a script hot-reload.Open questions that need resolving before this is worth implementing:
- Chicken-and-egg: schema is defined inside the script, but the script needs config values to run. On first run defaults are used — but if the script crashes before
she.config.define()is called, the UI has no schema to show. Static extraction (AST parse) avoids this but adds significant complexity. - Where does the config UI live? — ⚙ button in file tree per script? Panel in the editor when a schema is detected? Separate tab?
- Schema migration on hot-reload — what happens when
she.config.define()changes (fields added/removed)? UI needs to re-render and stored config needs to be migrated (drop removed keys, fill defaults for new ones). - Config files in file tree — hidden entirely, or visible but styled differently (greyed out, non-editable as text)?
- Multi-instance — the real power of Node-RED's model is running the same node type N times with different configs. With file-based scripts the natural mapping is one file = one config, which means you still need N script files for N instances. Whether a smarter multi-instance model is worth the complexity is unclear.
- Interaction with sheDB — sheDB already provides per-document storage that scripts can read. Is this feature adding enough over
she.db.get('config/myscript')to justify the complexity?
- Chicken-and-egg: schema is defined inside the script, but the script needs config values to run. On first run defaults are used — but if the script crashes before
MQTT
-
per-topic value history — the MQTT tab shows current state only. A configurable ring buffer (e.g. last 20 values with timestamps) per topic would be useful for debugging value changes over time.
-
multiple MQTT broker connections — allow connecting to more than one broker simultaneously. Config: replace the top-level
urlstring with abrokersmap where each key is a broker name and the value is the existing per-broker options object (url,username,password,ca,cert,key,mqttVersion). The existingurlkey stays supported as shorthand for a single default broker (backward compat). Script API:she.broker(name)returns a broker-scoped object exposing the fullmqttsub-API (sub,pub,get,link,or,and,max,min,timer,age,getProp).she.mqttremains a shorthand alias forshe.broker('default')(or the sole configured broker). State store keys becomemqtt::brokerName::topic. WSmqttevents carry an additionalbrokerfield. Each broker runs its own sentinel cycle on connect.link()and the stdlib helpers (or,and,max,min,timer) work within a single broker scope only in this first step — cross-broker bridging is a follow-up. Breaking change — requires a migration note when shipped.Config page UX — the broker configuration section uses a file-tab-style switcher (same visual language as the editor's script tabs) sitting above the broker form panel. One tab per configured broker; clicking a tab activates that broker's form. Each tab shows the broker name and a small status dot (green/yellow/red, same
nav-dotclasses). A+button at the right end of the tab bar adds a new broker with a generated name and empty fields. Each tab has a×delete button (with confirmation). The form fields are: name (editable — renaming updates the tab label and thebrokersmap key), url, username, password (masked, with show/hide toggle), ca, cert, key, mqttVersion, sentinelTimeout. When only one broker is configured the tab bar is still shown (consistent UX, and makes it obvious how to add a second one). The existing single-broker fields (url,mqttUsername, …) in the form are migrated to thebrokersmap on first save.MQTT page UX — when multiple brokers are configured, a broker filter tab bar appears above the topic list (styled like the Config broker tabs, "All" selected by default). Selecting a broker tab filters the list to that broker's topics only; the "All" tab shows every topic from every broker. In the "All" view each row shows a small broker badge (the broker name in a muted chip, similar to
pane-count) so the origin is visible at a glance; badges are hidden when only one broker is configured or a single broker tab is active. The publish form gains a broker selector dropdown (defaults to the first/only broker; hidden when only one broker exists). The nav dot on the MQTT tab reflects the worst-case state across all brokers: red if any broker is configured but disconnected, yellow/blinking if any broker is still waiting for the sentinel, green only when all brokers are fully ready. The dot's tooltip lists per-broker status.Optional follow-up — cross-broker references: allow
'brokerName##topic'as a topic string inlink()and stdlib helpers to reference a topic on a different broker.##is chosen as separator because a double hash can never appear in a valid MQTT topic (the#wildcard is only legal as a standalone final segment and publishing to a topic containing#is forbidden by spec), making it unambiguous to parse.
Matter
sheDB
Secrets
- secrets management — store secrets (named groups of arbitrary string fields, e.g.
{ "smtp": { "password": "…", "host": "…" } }) in a dedicated encrypted file (~/.she/secrets.enc) separate fromconfig.json, using AES-256-GCM via Node.js built-incrypto. Encryption key sourced from env varSHE_SECRETS_KEY(takes precedence) or key file~/.she/secrets.key(chmod 600). Access from scripts viashe.secrets.get('<name>/<field>'). Integrate as own section in config ui (show/hide values). Open questions to resolve before implementation: HTTP API exposure (security concern — avoid reading secret values over the network, or localhost-only), behavior when key is missing, hot-reload vs. startup-only, CLI subcommands for management, configurable secrets file path.
Broker Page — Raw config editor
- Monaco editor for
mosquitto.conf— thePUT /she/broker/config/rawendpoint and theputBrokerConfRaw()API helper already exist; only the frontend tab is missing. Add an Advanced sub-tab to the Broker page that renders the fullmosquitto.conftext in a Monaco editor (language: 'ini'). On save, callputBrokerConfRaw(content, checksum)— external-modify detection via checksum works the same way as the structured editor. The tab should also surface the backup list with a one-click restore (callsPOST /she/broker/config/restore). This gives power users a full escape hatch beyond what the structured Listeners / Global Config tabs expose.
Broker Page — Auth without dynsec
-
Password file & ACL file management — users who prefer static
passwd/aclfiles over the dynamic-security plugin have no UI support today. Needs to work in both local and SSH/remote mode (analogous to howmosquitto.confread/write already works viassh-deploy).Scope:
- Password file: read the file, list usernames, add a user (
mosquitto_passwd -b <file> <user> <pass>), change password, delete a user (mosquitto_passwd -D <file> <user>). In remote mode, run the command on the broker host viasshDeploy.runCommand(); in local mode useexecFile(). - ACL file: read/write the raw text (mosquitto acl format). Expose a Monaco editor for the raw file (like the Advanced config editor), plus optionally a structured UI for the common patterns (user-level
topic,readwrite,read,writelines andpatternlines). - Config integration: the global
password_fileandacl_filekeys inmosquitto.confalready flow through the managed-key system; listener-levelpassword_file/acl_fileare already parsed and serialised per-listener. The new UI should make it easy to point these at the managed files. - Remote path convention: document a recommended layout (e.g.
/etc/mosquitto/passwd,/etc/mosquitto/acl) so the SSH deploy path and themosquitto.confpath stay consistent.
- Password file: read the file, list usernames, add a user (
Broker Page — Certificates & mTLS
-
Client certificate management — deferred pending external CA strategy — The local CA and client cert issuance UI (Local CA section, Client Certs section, issue/revoke/download P12 flows) has been intentionally removed from the Security page UI. The backend API surface is preserved for automation. The right solution for client cert management in a homelab context is an external CA, not a she-managed one. Before re-introducing a client cert UI, the following questions need to be resolved:
- Which external CAs should be supported natively (step-ca, XCA, Let's Encrypt, ACME generic)?
- Should she support CSR-based enrolment for IoT devices ("poor man's CMP over MQTT" pattern — see former MQTT-based CSR renewal backlog item)?
- What is the right auto-renewal story for 24h-TTL CAs like step-ca?
- Should she offer a
broker.certRenewHookconfig field (a shell command executed after any cert change) as a generic post-renewal trigger forsystemctl reload mosquitto? ThePOST /she/broker/ca/certs,DELETE /she/broker/ca/certs/:serial,GET /she/broker/ca/certs/:serial/downloadand related CA endpoints remain available for scripted/API use.
-
step-ca integration (homelab PKI) — for users running Smallstep step-ca, the integration path is: (1) add the step-ca root/intermediate cert via the Trusted CAs section so mosquitto trusts step-ca-issued client certs; (2) issue mosquitto's server cert via
step ca certificateor use she's Generate CSR flow then upload the signed cert; (3) configure mosquittocertfile/keyfileto point at the cert. The missing piece is auto-renewal: step-ca issues 24h certs by default and providesstep ca renew --daemonfor background renewal. She needs to triggersudo systemctl reload mosquittoafter renewal. A config fieldbroker.certRenewHook(a shell command executed after any cert change) would cover this generically. Document the manual flow indoc/broker-management.mdwith copy-paste commands. -
Let's Encrypt server cert — use she's Generate CSR flow to produce the key and CSR, then use certbot or another ACME client to get it signed. Or use certbot standalone and import the resulting cert+key via the Import cert option. Auto-renewal hook:
broker.certRenewHook(see step-ca item). DNS challenge is the only approach that avoids port 80 conflicts with other services.
Operations
- health check endpoint —
GET /she/healthreturning 200 if the daemon is alive, configured mqtt broker is connected, ..., 503 otherwise. Useful for Docker health checks, nginx upstreams, and monitoring systems.
Security / Robustness
-
Script API endpoint authentication — when she auth is enabled (
auth: 'password'orauth: 'proxy'), all/she/*routes are protected byauthMiddleware. However, routes registered by scripts viashe.api.*andshe.http.sub()are explicitly excluded from this middleware (comment inserver.js: "user scripts control their own auth"). In practice, scripts rarely implement auth, there is no helper to do so, and the result is a false sense of security: a user who enables password auth believing their she instance is now protected is wrong — every script-registered route remains open to anyone who can reach the HTTP port.Root cause:
she.api.*andshe.http.sub()are conceptually different but share the same/api/prefix and the same absent auth policy:she.api.*— exposing script data or control endpoints, logically an extension of the she API → should inherit she-level authshe.http.sub()— receiving webhooks from external services (IFTTT, IoT devices, cloud hooks) that cannot present a session cookie → needs to remain reachable without she auth, optionally protected by a shared secret in the URL or body
Recommended fix: when auth mode is not
none, applyauthMiddlewareto/api/*by the same rule as/she/*. Giveshe.http.sub()an explicit{ public: true }option to bypass this for genuine external webhooks:// protected by she auth (inherits current mode): she.api.get('/status', () => she.mqtt.get('home/alarm')); // explicitly public — external caller can't log in: she.http.sub('/hook', body => she.info('received', body), { public: true });This is not a breaking change for deployments using the default
auth: 'none'— behaviour is identical. It only activates when the user has explicitly enabled auth, which is precisely when they expect protection.she.api.*routes without{ public: true }inherit she auth and require a valid session cookie (password mode) or proxy header (proxy mode). TheAuthorization: Bearer <token>header should also be accepted as an alternative to the session cookie so scripts can be called programmatically.Additionally: expose
she.checkAuth(req)as a convenience for scripts that intentionally handle auth themselves (e.g. check a per-script shared secret). Returnstrueif the request passes she-level auth, regardless of mode. -
path traversal via symlinks —
safePath()inscripts-api.jschecksstartsWith(root + sep)but does not resolve symlinks. A symlinked entry inside the script root could point outside it. Usefs.realpath()to resolve before checking. -
session persistence — login sessions are held in-memory and lost on every daemon restart, forcing all users to re-login. Persist session tokens (hashed) to a file in
--data-dir.
Testing
Unit tests
- Auth module unit tests —
POST /she/auth/setupchanges mode and password hash. Session TTL: a session createdSESSION_TTL_MSms ago is rejected.
Integration tests
-
Auth integration — all integration tests run with
auth: 'none'. Add a suite that starts the server withauth: 'password'and a hashed password: verify/she/scriptsreturns 401 without a cookie,POST /she/auth/loginwith correct credentials returns 200 and sets a cookie, the cookie grants access to/she/scripts, wrong password returns 401,GET /she/auth/modereturns 200 in all modes (public endpoint). Add totest/unit/server.test.js(it already spins up a real server) or a newtest/integration/auth.test.js. -
integration tests for
.shelib/.shedisablemarker hot-reload — the live unload/reload on marker changes has no test coverage. -
integration tests for rapid hot-reload — edge cases in subscription cleanup during rapid script updates (change → reload cycle) are not tested.