{"_id":"@allanoricil/nrg-sentinel","_rev":"15-76348ec242875ec6ce86293c0d5b3b89","name":"@allanoricil/nrg-sentinel","dist-tags":{"latest":"1.3.0"},"versions":{"1.0.0":{"name":"@allanoricil/nrg-sentinel","version":"1.0.0","author":{"name":"AllanOricil"},"license":"SEE LICENSE IN LICENSE.md","_id":"@allanoricil/nrg-sentinel@1.0.0","maintainers":[{"name":"allanoricil","email":"allanoricilcos@outlook.com"}],"homepage":"https://nrg-sentinel.dev","bugs":{"url":"https://github.com/AllanOricil/nrg-sentinel-public/issues"},"bin":{"node-red":"bin/node-red.js"},"dist":{"shasum":"d14d77827889d2b68484576c42143fafe2219801","tarball":"https://registry.npmjs.org/@allanoricil/nrg-sentinel/-/nrg-sentinel-1.0.0.tgz","fileCount":14,"integrity":"sha512-D3sM5G2HEQOMNRjyzWKezR00PdnVFCACpQEdmeVX1u3JZbpeXuqW/NYB4s66g8x0FNJBnTlxVAWTAHwsdt57cA==","signatures":[{"sig":"MEUCIQDNWvsGAOjTQbHJ/h/W/COhIO4y0lDeeZ7YTD1L4I046AIgSAun9owhCprpOnWr6XRbRNfDlDU+pHq1S5v7bXNKg0E=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@allanoricil%2fnrg-sentinel@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1124482},"gitHead":"4167e3b8940a2dcd58dd47c1b864bbbd7c5ceef2","_npmUser":{"name":"allanoricil","email":"allanoricilcos@outlook.com"},"node-red":{"plugins":{"nrg-sentinel":"plugin.js"},"version":">=3.0.0"},"repository":{"url":"git+https://github.com/AllanOricil/nrg-sentinel-public.git","type":"git"},"_npmVersion":"10.9.4","description":"Node-RED Runtime Security Hardening","directories":{},"_nodeVersion":"22.22.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/nrg-sentinel_1.0.0_1773734724159_0.6894176205638303","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@allanoricil/nrg-sentinel","version":"1.0.1","author":{"name":"AllanOricil"},"license":"SEE LICENSE IN LICENSE.md","_id":"@allanoricil/nrg-sentinel@1.0.1","maintainers":[{"name":"allanoricil","email":"allanoricilcos@outlook.com"}],"homepage":"https://allanoricil.github.io/nrg-sentinel-public/","bugs":{"url":"https://github.com/AllanOricil/nrg-sentinel-public/issues"},"bin":{"node-red":"bin/node-red.js"},"dist":{"shasum":"18f58d31194e2f0566f52320274a0f02d27bc260","tarball":"https://registry.npmjs.org/@allanoricil/nrg-sentinel/-/nrg-sentinel-1.0.1.tgz","fileCount":14,"integrity":"sha512-9P8y3ROpm3nFmz621iieqUVywa96oIHVdLHYYfFNFIMUjITzPkaRDHl2nGE4DsY7RxtWbeTaPfQwDoJ/lP8E/w==","signatures":[{"sig":"MEUCIQC2YZXSjqCx9ufM/zzYMzBFgJWQ241M60f3jsaGbV9DNgIgdxpKpbMKvEDJHMKt9AfHCaYDk7n2prmO00F+J42XBIk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@allanoricil%2fnrg-sentinel@1.0.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1167294},"gitHead":"781991bad47eaa94ca689baa020e35615aaf6ebd","_npmUser":{"name":"allanoricil","email":"allanoricilcos@outlook.com"},"node-red":{"plugins":{"nrg-sentinel":"plugin.js"},"version":">=3.0.0"},"repository":{"url":"git+https://github.com/AllanOricil/nrg-sentinel-public.git","type":"git"},"_npmVersion":"10.9.4","description":"Node-RED Runtime Security Hardening","directories":{},"_nodeVersion":"22.22.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/nrg-sentinel_1.0.1_1773804063519_0.3423095162302141","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@allanoricil/nrg-sentinel","version":"1.0.2","author":{"name":"AllanOricil"},"license":"SEE LICENSE IN LICENSE.md","_id":"@allanoricil/nrg-sentinel@1.0.2","maintainers":[{"name":"allanoricil","email":"allanoricilcos@outlook.com"}],"homepage":"https://allanoricil.github.io/nrg-sentinel-public/","bugs":{"url":"https://github.com/AllanOricil/nrg-sentinel-public/issues"},"bin":{"node-red":"bin/node-red.js"},"dist":{"shasum":"facc655db2c01a862868b182b9660781b4aed785","tarball":"https://registry.npmjs.org/@allanoricil/nrg-sentinel/-/nrg-sentinel-1.0.2.tgz","fileCount":14,"integrity":"sha512-lokswTEBXYlxQz2vYEojLdoOFDPQoLoQPNz1RUGH2KkTYFat0YPAxRVowGDFJfvuk6rYSBkUvl8oT/iF7pLiGA==","signatures":[{"sig":"MEUCIQDRzAkMUQILZ6e5molVljxw14AB6r0Tysj6d+askj+9TgIgBosb0OmwHmlGIPA0oud2ulaP7LD+SAFuKIWyhc44Lok=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@allanoricil%2fnrg-sentinel@1.0.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1116903},"gitHead":"73a9d77016014765d5913c4f6ab309c0143ee76e","_npmUser":{"name":"allanoricil","email":"allanoricilcos@outlook.com"},"node-red":{"plugins":{"nrg-sentinel":"plugin.js"},"version":">=3.0.0"},"repository":{"url":"git+https://github.com/AllanOricil/nrg-sentinel-public.git","type":"git"},"_npmVersion":"10.9.4","description":"Node-RED Runtime Security Hardening","directories":{},"_nodeVersion":"22.22.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/nrg-sentinel_1.0.2_1773804485856_0.8026457151314659","host":"s3://npm-registry-packages-npm-production"}},"1.0.3":{"name":"@allanoricil/nrg-sentinel","version":"1.0.3","author":{"name":"AllanOricil"},"license":"SEE LICENSE IN LICENSE.md","_id":"@allanoricil/nrg-sentinel@1.0.3","maintainers":[{"name":"allanoricil","email":"allanoricilcos@outlook.com"}],"homepage":"https://allanoricil.github.io/nrg-sentinel-public/","bugs":{"url":"https://github.com/AllanOricil/nrg-sentinel-public/issues"},"bin":{"node-red":"bin/node-red.js"},"dist":{"shasum":"9fe5b747bcf38fa60bad1dc06cf1b57a27a8142a","tarball":"https://registry.npmjs.org/@allanoricil/nrg-sentinel/-/nrg-sentinel-1.0.3.tgz","fileCount":14,"integrity":"sha512-+/7Li4QOG9QRvUGWwyMkJ0wDLyfkGTyONSpOOG0+fBHUJIQ8Cx53HKQcMML7+GEDw/xOdGn5HNDg0jKJp2JL6A==","signatures":[{"sig":"MEQCICqXdcylRvj5LVtrF2gzP0ZbV6fDdCVFzKlwexWjv+vDAiBWNEbVJVsBFPCwLI4r3LZGhjf/MTP1I/onnH3MAy2R8w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@allanoricil%2fnrg-sentinel@1.0.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1176203},"gitHead":"e40097d41e1341c7a3cc54c208dc76cca234ec60","_npmUser":{"name":"allanoricil","email":"allanoricilcos@outlook.com"},"node-red":{"plugins":{"nrg-sentinel":"plugin.js"},"version":">=3.0.0"},"repository":{"url":"git+https://github.com/AllanOricil/nrg-sentinel-public.git","type":"git"},"_npmVersion":"10.9.4","description":"Node-RED Runtime Security Hardening","directories":{},"_nodeVersion":"22.22.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/nrg-sentinel_1.0.3_1773804758773_0.7991711500756822","host":"s3://npm-registry-packages-npm-production"}},"1.0.4":{"name":"@allanoricil/nrg-sentinel","version":"1.0.4","author":{"name":"AllanOricil"},"license":"SEE LICENSE IN LICENSE.md","_id":"@allanoricil/nrg-sentinel@1.0.4","maintainers":[{"name":"allanoricil","email":"allanoricilcos@outlook.com"}],"homepage":"https://allanoricil.github.io/nrg-sentinel-public/","bugs":{"url":"https://github.com/AllanOricil/nrg-sentinel-public/issues"},"bin":{"node-red":"bin/node-red.js"},"dist":{"shasum":"f9670e7009e7ddd8288018addcfc3d9e150d06f0","tarball":"https://registry.npmjs.org/@allanoricil/nrg-sentinel/-/nrg-sentinel-1.0.4.tgz","fileCount":14,"integrity":"sha512-hUWSDjyhp4Znl8nIKcmyzWkdTU7otZJBAePmjuh9L3qWBs9foMItzcmeo419JG3dCjXI9ewfZlbqtKGNgbQf4w==","signatures":[{"sig":"MEYCIQDbbCJ4H59qCOkBfkKVDJlLPTeuuBpJng5+wtGnWrf+2QIhAPf+JPXiSQEsmDxyZ0YR5g6JeU4O9vNyYtRj3P7m2i+T","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@allanoricil%2fnrg-sentinel@1.0.4","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1115616},"gitHead":"0cd9073c6e0111e4b62b9052a917d756533ebb7a","_npmUser":{"name":"allanoricil","email":"allanoricilcos@outlook.com"},"node-red":{"plugins":{"nrg-sentinel":"plugin.js"},"version":">=3.0.0"},"repository":{"url":"git+https://github.com/AllanOricil/nrg-sentinel-public.git","type":"git"},"_npmVersion":"10.9.4","description":"Node-RED Runtime Security Hardening","directories":{},"_nodeVersion":"22.22.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/nrg-sentinel_1.0.4_1773805400575_0.9918191691356","host":"s3://npm-registry-packages-npm-production"}},"1.0.5":{"name":"@allanoricil/nrg-sentinel","version":"1.0.5","author":{"name":"AllanOricil"},"license":"SEE LICENSE IN LICENSE.md","_id":"@allanoricil/nrg-sentinel@1.0.5","maintainers":[{"name":"allanoricil","email":"allanoricilcos@outlook.com"}],"homepage":"https://allanoricil.github.io/nrg-sentinel-public/","bugs":{"url":"https://github.com/AllanOricil/nrg-sentinel-public/issues"},"bin":{"node-red":"bin/node-red.js"},"dist":{"shasum":"69807cb4dd23c6d3f81ce8689a61e7bf7a4294f4","tarball":"https://registry.npmjs.org/@allanoricil/nrg-sentinel/-/nrg-sentinel-1.0.5.tgz","fileCount":14,"integrity":"sha512-YangCSu+Z4VMNcwWR9UbPs4z6BjpEZfWKQY7gZfMCmMm4lF/CgWMeol+YqCpSKGBS/gw49dYTm9FxSBUD04qtQ==","signatures":[{"sig":"MEUCIQCDxbJg6WZnudeK96qf4alHGULsnxMnm/8QhMTuH0Y91gIgQccxSHpXc+Z5cOx0OA6Nba88/Z4QPG9Y+WmkTF9nJxI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@allanoricil%2fnrg-sentinel@1.0.5","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1175190},"gitHead":"8cc1d31688840addb9cdaaef9b3bbe50589802d4","_npmUser":{"name":"allanoricil","email":"allanoricilcos@outlook.com"},"node-red":{"plugins":{"nrg-sentinel":"plugin.js"},"version":">=3.0.0"},"repository":{"url":"git+https://github.com/AllanOricil/nrg-sentinel-public.git","type":"git"},"_npmVersion":"10.9.4","description":"Node-RED Runtime Security Hardening","directories":{},"_nodeVersion":"22.22.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/nrg-sentinel_1.0.5_1774250977623_0.73013302565668","host":"s3://npm-registry-packages-npm-production"}},"1.0.6":{"name":"@allanoricil/nrg-sentinel","version":"1.0.6","author":{"name":"AllanOricil"},"license":"SEE LICENSE IN LICENSE.md","_id":"@allanoricil/nrg-sentinel@1.0.6","maintainers":[{"name":"allanoricil","email":"allanoricilcos@outlook.com"}],"homepage":"https://allanoricil.github.io/nrg-sentinel-public/","bugs":{"url":"https://github.com/AllanOricil/nrg-sentinel-public/issues"},"bin":{"node-red":"bin/node-red.js"},"dist":{"shasum":"a2cce78d69e0bcb6e3e424d7199d87e85e137af6","tarball":"https://registry.npmjs.org/@allanoricil/nrg-sentinel/-/nrg-sentinel-1.0.6.tgz","fileCount":14,"integrity":"sha512-TpFJPZbOmQPeiDJyPiJg1chtt80VwovLpvSXxbvbJy1GAFxbEyRLlNmVwV74MTU+/vw4/ZORFYjOroY+T+9A1w==","signatures":[{"sig":"MEUCIQCTSXT0WEJT5fnhGtqCnWt5P73TU4EQqnMRajmRQ9TMbAIgCl0eyoUFTwxl6wFCqi2hD93P6jLtcUrilvRe4QPAMWw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@allanoricil%2fnrg-sentinel@1.0.6","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1161007},"gitHead":"ccd8d6a20090f75bf9c74fef21708e96d5aa2bf3","_npmUser":{"name":"allanoricil","email":"allanoricilcos@outlook.com"},"node-red":{"plugins":{"nrg-sentinel":"plugin.js"},"version":">=3.0.0"},"repository":{"url":"git+https://github.com/AllanOricil/nrg-sentinel-public.git","type":"git"},"_npmVersion":"10.9.4","description":"Node-RED Runtime Security Hardening","directories":{},"_nodeVersion":"22.22.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/nrg-sentinel_1.0.6_1774407554830_0.9809335701642319","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@allanoricil/nrg-sentinel","version":"1.1.0","author":{"name":"AllanOricil"},"license":"SEE LICENSE IN LICENSE.md","_id":"@allanoricil/nrg-sentinel@1.1.0","maintainers":[{"name":"allanoricil","email":"allanoricilcos@outlook.com"}],"homepage":"https://allanoricil.github.io/nrg-sentinel-public/","bugs":{"url":"https://github.com/AllanOricil/nrg-sentinel-public/issues"},"bin":{"node-red":"bin/node-red.js"},"dist":{"shasum":"4ee9a53aea2ecaf698a3ecaff6c986e91200511c","tarball":"https://registry.npmjs.org/@allanoricil/nrg-sentinel/-/nrg-sentinel-1.1.0.tgz","fileCount":23,"integrity":"sha512-qxUlCAcNIO/4AQYT7CW6qJ+VOYLAuNKMY3VT8Vq7VBWK6oaEhVAqZbMBnlzuqALyGFsgPpaUu4i4sEUZkZy2bA==","signatures":[{"sig":"MEUCIA04/qJY2xjhxijI+YXrV2m0HvkvZSRfyMv2gQTtXC0oAiEA0ucUwS0QCyTubA5uoYHqxm5QU09rWMhIPShHAZLLlWk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@allanoricil%2fnrg-sentinel@1.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2394315},"gitHead":"5fef5c351d84671c9cdaaa82155aeea77637861c","_npmUser":{"name":"allanoricil","email":"allanoricilcos@outlook.com"},"node-red":{"plugins":{"nrg-sentinel":"plugin.js"},"version":">=3.0.0"},"repository":{"url":"git+https://github.com/AllanOricil/nrg-sentinel-public.git","type":"git"},"_npmVersion":"10.9.7","description":"Node-RED Runtime Security Hardening","directories":{},"_nodeVersion":"22.22.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/nrg-sentinel_1.1.0_1775829746158_0.596265037932306","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"name":"@allanoricil/nrg-sentinel","version":"1.1.1","author":{"name":"AllanOricil"},"license":"SEE LICENSE IN LICENSE.md","_id":"@allanoricil/nrg-sentinel@1.1.1","maintainers":[{"name":"allanoricil","email":"allanoricilcos@outlook.com"}],"homepage":"https://allanoricil.github.io/nrg-sentinel-public/","bugs":{"url":"https://github.com/AllanOricil/nrg-sentinel-public/issues"},"bin":{"node-red":"bin/node-red.js"},"dist":{"shasum":"309db5f78c961e2b8cdce2de5b74720a65d6d08e","tarball":"https://registry.npmjs.org/@allanoricil/nrg-sentinel/-/nrg-sentinel-1.1.1.tgz","fileCount":23,"integrity":"sha512-9JUOPosylipSyUq6OnoNTNNRGdKoLECgMDiLZatkSa6u5O1CBqI0YHpYZ6VQhKSYCJ84EFiFYPkUZWtgL9Gm9g==","signatures":[{"sig":"MEUCIQCtRmfspiOdbeF9wZKjDrfZVESy87rI+kPar+kcim4ZkwIgUli8HcgT4vaD7an9SjD406035awonUlvdbD+8zOdm2k=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@allanoricil%2fnrg-sentinel@1.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":2415105},"gitHead":"4bb8e8d5e6f7da686ff8daaefe9711ffd87a9d9f","_npmUser":{"name":"allanoricil","email":"allanoricilcos@outlook.com"},"node-red":{"plugins":{"nrg-sentinel":"plugin.js"},"version":">=3.0.0"},"repository":{"url":"git+https://github.com/AllanOricil/nrg-sentinel-public.git","type":"git"},"_npmVersion":"10.9.7","description":"Node-RED Runtime Security Hardening","directories":{},"_nodeVersion":"22.22.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/nrg-sentinel_1.1.1_1775831227289_0.8768832690982136","host":"s3://npm-registry-packages-npm-production"}},"1.3.0":{"name":"@allanoricil/nrg-sentinel","version":"1.3.0","description":"Node-RED Runtime Security Hardening","license":"SEE LICENSE IN LICENSE.md","author":{"name":"AllanOricil"},"homepage":"https://allanoricil.github.io/nrg-sentinel-public/","repository":{"type":"git","url":"git+https://github.com/AllanOricil/nrg-sentinel-public.git"},"node-red":{"version":">=3.0.0","plugins":{"nrg-sentinel":"plugin.js"}},"bin":{"node-red":"bin/node-red.js"},"publishConfig":{"access":"public"},"_id":"@allanoricil/nrg-sentinel@1.3.0","gitHead":"20b607bf2d316d22b27b70a5198dbc3a0ec91bda","bugs":{"url":"https://github.com/AllanOricil/nrg-sentinel-public/issues"},"_nodeVersion":"22.22.2","_npmVersion":"10.9.7","dist":{"integrity":"sha512-+Q4rQ/xnVjPzMZVSI8/2cT2ahws4DMHjdkymoIs3W138JcFl1S2O25H7ie0VE0au4KqETIgQYL+aV+PBZesTfg==","shasum":"a4704c0a3bd1b3ea3cb711f97afcde0d238e5c09","tarball":"https://registry.npmjs.org/@allanoricil/nrg-sentinel/-/nrg-sentinel-1.3.0.tgz","fileCount":23,"unpackedSize":2430702,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@allanoricil%2fnrg-sentinel@1.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDbhEF1lMu42XkQVcJj9r0W35lFsu9rf0V8u+597cTBJwIgczp6d7EZe9WAy7fxaTG3ZnM0i9tbcTJqcGi4MnXf0pY="}]},"_npmUser":{"name":"allanoricil","email":"allanoricilcos@outlook.com"},"directories":{},"maintainers":[{"name":"allanoricil","email":"allanoricilcos@outlook.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/nrg-sentinel_1.3.0_1776567946564_0.31155425611866594"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-17T08:05:24.048Z","modified":"2026-04-19T03:05:47.421Z","1.0.0":"2026-03-17T08:05:24.350Z","1.0.1":"2026-03-18T03:21:03.680Z","1.0.2":"2026-03-18T03:28:06.050Z","1.0.3":"2026-03-18T03:32:38.979Z","1.0.4":"2026-03-18T03:43:20.765Z","1.0.5":"2026-03-23T07:29:37.829Z","1.0.6":"2026-03-25T02:59:15.053Z","1.1.0":"2026-04-10T14:02:26.530Z","1.1.1":"2026-04-10T14:27:07.507Z","1.2.0":"2026-04-11T23:13:06.678Z","1.2.1":"2026-04-11T23:16:34.247Z","1.3.0":"2026-04-19T03:05:46.736Z"},"bugs":{"url":"https://github.com/AllanOricil/nrg-sentinel-public/issues"},"author":{"name":"AllanOricil"},"license":"SEE LICENSE IN LICENSE.md","homepage":"https://allanoricil.github.io/nrg-sentinel-public/","repository":{"type":"git","url":"git+https://github.com/AllanOricil/nrg-sentinel-public.git"},"description":"Node-RED Runtime Security Hardening","maintainers":[{"name":"allanoricil","email":"allanoricilcos@outlook.com"}],"readme":"<p align=\"center\">\n  <img alt=\"nrg-sentinel-icon\" src=\"https://gist.githubusercontent.com/AllanOricil/244c22dad889ed47ef6530e5bb605536/raw/b0b02ada070c2bc2d4970cf186866917a3af143b/nrg-sentinel-icon.svg\" style=\"width: 200px\"/>\n</p>\n<br/>\n<p align=\"center\">\n  <a href=\"https://github.com/AllanOricil/nrg-sentinel-public/actions/workflows/release.yml\"><img src=\"https://github.com/AllanOricil/nrg-sentinel-public/actions/workflows/release.yml/badge.svg\" alt=\"Release\"></a>\n  <a href=\"https://github.com/AllanOricil/nrg-sentinel-public/actions/workflows/docker.yml\"><img src=\"https://github.com/AllanOricil/nrg-sentinel-public/actions/workflows/docker.yml/badge.svg\" alt=\"Docker\"></a>\n</p>\n\n# NRG Sentinel\n\nA security layer for Node-RED that detects and blocks common attack vectors at runtime without modifying the Node-RED core.\n\n## E2E Test Results\n\nThe table below is updated automatically after each CI run on `main`.\n\n<!-- DEMO-TEST-RESULTS:START -->\n<details>\n<summary>✅ Node.js 18 &nbsp;—&nbsp; 58/58 passed</summary>\n\n| # | Demo | Result | Node-RED |\n|---|------|:------:|:--------:|\n| 01 | Monkey Patching | ✅ | `4.1.8` |\n| 02 | Hook Injection | ✅ | `4.1.8` |\n| 03 | Credential Theft | ✅ | `4.1.8` |\n| 04 | Wire Manipulation | ✅ | `4.1.8` |\n| 05 | Direct Receive Injection | ✅ | `4.1.8` |\n| 06 | Express Middleware | ✅ | `4.1.8` |\n| 07 | EventEmitter Hijack | ✅ | `4.1.8` |\n| 08 | Node Enumeration | ✅ | `4.1.8` |\n| 09 | Prototype Pollution | ✅ | `4.1.8` |\n| 10 | Flow File Tampering | ✅ | `4.1.8` |\n| 11 | Message Provenance | ✅ | `4.1.8` |\n| 12 | Settings.js Tampering | ✅ | `4.1.8` |\n| 13 | Sentinel Source Tampering | ✅ | `4.1.8` |\n| 14 | Express Route Backdoor | ✅ | `4.1.8` |\n| 15 | Config Node Z-Forgery | ✅ | `4.1.8` |\n| 16 | Symbol Property Bypass | ✅ | `4.1.8` |\n| 17 | EventEmitter Enumeration | ✅ | `4.1.8` |\n| 18 | Deep Stack Bypass | ✅ | `4.1.8` |\n| 19 | HTTP Route Deletion | ✅ | `4.1.8` |\n| 20 | Child Process Exec | ✅ | `4.1.8` |\n| 21 | SW Fetch Interception | — | — | browser-only — verify via `start-interactive.sh` |\n| 22 | FS Read | ✅ | `4.1.8` |\n| 23 | Process Env Exfiltration | ✅ | `4.1.8` |\n| 24 | Process Exit DoS | ✅ | `4.1.8` |\n| 25 | VM Sandbox Escape | ✅ | `4.1.8` |\n| 26 | Worker Thread Escape | ✅ | `4.1.8` |\n| 27 | Network Socket Exfiltration | ✅ | `4.1.8` |\n| 28 | Registry Type Hijack | ✅ | `4.1.8` |\n| 29 | Settings Mutation | ✅ | `4.1.8` |\n| 30 | Comms Publish Spoofing | ✅ | `4.1.8` |\n| 31 | Context Permissions | ✅ | `4.1.8` |\n| 32 | Flows Inject | ✅ | `4.1.8` |\n| 33 | Node Event Hijack | ✅ | `4.1.8` |\n| 34 | Config Node Credentials | ✅ | `4.1.8` |\n| 35 | Process Binding Bypass | ✅ | `4.1.8` |\n| 36 | ChildProcess Proto Bypass | ✅ | `4.1.8` |\n| 37 | UserDir Bypass | ✅ | `4.1.8` |\n| 38 | Exec Binary Allowlist | ✅ | `4.1.8` |\n| 39 | Native Addon Guard | ✅ | `4.1.8` |\n| 40 | Native WASM Guard | ✅ | `4.1.8` |\n| 41 | Module Cache Poison | ✅ | `4.1.8` |\n| 42 | HTTP Route Tamper | ✅ | `4.1.8` |\n| 43 | Symlink NPM Install | ✅ | `4.1.8` |\n| 44 | FS Read Cap Config | ✅ | `4.1.8` |\n| 45 | FS Write Cap Config | ✅ | `4.1.8` |\n| 46 | Flows File Protected | ✅ | `4.1.8` |\n| 47 | Per-Package Cap Config | ✅ | `4.1.8` |\n| 48 | Grants Merge | ✅ | `4.1.8` |\n| 49 | Network HTTP Allowlist | ✅ | `4.1.8` |\n| 50 | Exec Binary Union | ✅ | `4.1.8` |\n| 51 | Types Grant | ✅ | `4.1.8` |\n| 52 | Network TCP | ✅ | `4.1.8` |\n| 53 | Network UDP | ✅ | `4.1.8` |\n| 54 | Network DNS | ✅ | `4.1.8` |\n| 55 | Network WebSocket | ✅ | `4.1.8` |\n| 56 | Network Listen | ✅ | `4.1.8` |\n| 57 | HTTP Handler Hardening | ✅ | `4.1.8` |\n| 58 | FS Require Variants | ✅ | `4.1.8` |\n| 59 | ESM Process Exec | ✅ | `4.1.8` |\n</details>\n\n<details>\n<summary>✅ Node.js 22 &nbsp;—&nbsp; 58/58 passed</summary>\n\n| # | Demo | Result | Node-RED |\n|---|------|:------:|:--------:|\n| 01 | Monkey Patching | ✅ | `4.1.8` |\n| 02 | Hook Injection | ✅ | `4.1.8` |\n| 03 | Credential Theft | ✅ | `4.1.8` |\n| 04 | Wire Manipulation | ✅ | `4.1.8` |\n| 05 | Direct Receive Injection | ✅ | `4.1.8` |\n| 06 | Express Middleware | ✅ | `4.1.8` |\n| 07 | EventEmitter Hijack | ✅ | `4.1.8` |\n| 08 | Node Enumeration | ✅ | `4.1.8` |\n| 09 | Prototype Pollution | ✅ | `4.1.8` |\n| 10 | Flow File Tampering | ✅ | `4.1.8` |\n| 11 | Message Provenance | ✅ | `4.1.8` |\n| 12 | Settings.js Tampering | ✅ | `4.1.8` |\n| 13 | Sentinel Source Tampering | ✅ | `4.1.8` |\n| 14 | Express Route Backdoor | ✅ | `4.1.8` |\n| 15 | Config Node Z-Forgery | ✅ | `4.1.8` |\n| 16 | Symbol Property Bypass | ✅ | `4.1.8` |\n| 17 | EventEmitter Enumeration | ✅ | `4.1.8` |\n| 18 | Deep Stack Bypass | ✅ | `4.1.8` |\n| 19 | HTTP Route Deletion | ✅ | `4.1.8` |\n| 20 | Child Process Exec | ✅ | `4.1.8` |\n| 21 | SW Fetch Interception | — | — | browser-only — verify via `start-interactive.sh` |\n| 22 | FS Read | ✅ | `4.1.8` |\n| 23 | Process Env Exfiltration | ✅ | `4.1.8` |\n| 24 | Process Exit DoS | ✅ | `4.1.8` |\n| 25 | VM Sandbox Escape | ✅ | `4.1.8` |\n| 26 | Worker Thread Escape | ✅ | `4.1.8` |\n| 27 | Network Socket Exfiltration | ✅ | `4.1.8` |\n| 28 | Registry Type Hijack | ✅ | `4.1.8` |\n| 29 | Settings Mutation | ✅ | `4.1.8` |\n| 30 | Comms Publish Spoofing | ✅ | `4.1.8` |\n| 31 | Context Permissions | ✅ | `4.1.8` |\n| 32 | Flows Inject | ✅ | `4.1.8` |\n| 33 | Node Event Hijack | ✅ | `4.1.8` |\n| 34 | Config Node Credentials | ✅ | `4.1.8` |\n| 35 | Process Binding Bypass | ✅ | `4.1.8` |\n| 36 | ChildProcess Proto Bypass | ✅ | `4.1.8` |\n| 37 | UserDir Bypass | ✅ | `4.1.8` |\n| 38 | Exec Binary Allowlist | ✅ | `4.1.8` |\n| 39 | Native Addon Guard | ✅ | `4.1.8` |\n| 40 | Native WASM Guard | ✅ | `4.1.8` |\n| 41 | Module Cache Poison | ✅ | `4.1.8` |\n| 42 | HTTP Route Tamper | ✅ | `4.1.8` |\n| 43 | Symlink NPM Install | ✅ | `4.1.8` |\n| 44 | FS Read Cap Config | ✅ | `4.1.8` |\n| 45 | FS Write Cap Config | ✅ | `4.1.8` |\n| 46 | Flows File Protected | ✅ | `4.1.8` |\n| 47 | Per-Package Cap Config | ✅ | `4.1.8` |\n| 48 | Grants Merge | ✅ | `4.1.8` |\n| 49 | Network HTTP Allowlist | ✅ | `4.1.8` |\n| 50 | Exec Binary Union | ✅ | `4.1.8` |\n| 51 | Types Grant | ✅ | `4.1.8` |\n| 52 | Network TCP | ✅ | `4.1.8` |\n| 53 | Network UDP | ✅ | `4.1.8` |\n| 54 | Network DNS | ✅ | `4.1.8` |\n| 55 | Network WebSocket | ✅ | `4.1.8` |\n| 56 | Network Listen | ✅ | `4.1.8` |\n| 57 | HTTP Handler Hardening | ✅ | `4.1.8` |\n| 58 | FS Require Variants | ✅ | `4.1.8` |\n| 59 | ESM Process Exec | ✅ | `4.1.8` |\n</details>\n\n<details>\n<summary>✅ Node.js 24 &nbsp;—&nbsp; 58/58 passed</summary>\n\n| # | Demo | Result | Node-RED |\n|---|------|:------:|:--------:|\n| 01 | Monkey Patching | ✅ | `4.1.8` |\n| 02 | Hook Injection | ✅ | `4.1.8` |\n| 03 | Credential Theft | ✅ | `4.1.8` |\n| 04 | Wire Manipulation | ✅ | `4.1.8` |\n| 05 | Direct Receive Injection | ✅ | `4.1.8` |\n| 06 | Express Middleware | ✅ | `4.1.8` |\n| 07 | EventEmitter Hijack | ✅ | `4.1.8` |\n| 08 | Node Enumeration | ✅ | `4.1.8` |\n| 09 | Prototype Pollution | ✅ | `4.1.8` |\n| 10 | Flow File Tampering | ✅ | `4.1.8` |\n| 11 | Message Provenance | ✅ | `4.1.8` |\n| 12 | Settings.js Tampering | ✅ | `4.1.8` |\n| 13 | Sentinel Source Tampering | ✅ | `4.1.8` |\n| 14 | Express Route Backdoor | ✅ | `4.1.8` |\n| 15 | Config Node Z-Forgery | ✅ | `4.1.8` |\n| 16 | Symbol Property Bypass | ✅ | `4.1.8` |\n| 17 | EventEmitter Enumeration | ✅ | `4.1.8` |\n| 18 | Deep Stack Bypass | ✅ | `4.1.8` |\n| 19 | HTTP Route Deletion | ✅ | `4.1.8` |\n| 20 | Child Process Exec | ✅ | `4.1.8` |\n| 21 | SW Fetch Interception | — | — | browser-only — verify via `start-interactive.sh` |\n| 22 | FS Read | ✅ | `4.1.8` |\n| 23 | Process Env Exfiltration | ✅ | `4.1.8` |\n| 24 | Process Exit DoS | ✅ | `4.1.8` |\n| 25 | VM Sandbox Escape | ✅ | `4.1.8` |\n| 26 | Worker Thread Escape | ✅ | `4.1.8` |\n| 27 | Network Socket Exfiltration | ✅ | `4.1.8` |\n| 28 | Registry Type Hijack | ✅ | `4.1.8` |\n| 29 | Settings Mutation | ✅ | `4.1.8` |\n| 30 | Comms Publish Spoofing | ✅ | `4.1.8` |\n| 31 | Context Permissions | ✅ | `4.1.8` |\n| 32 | Flows Inject | ✅ | `4.1.8` |\n| 33 | Node Event Hijack | ✅ | `4.1.8` |\n| 34 | Config Node Credentials | ✅ | `4.1.8` |\n| 35 | Process Binding Bypass | ✅ | `4.1.8` |\n| 36 | ChildProcess Proto Bypass | ✅ | `4.1.8` |\n| 37 | UserDir Bypass | ✅ | `4.1.8` |\n| 38 | Exec Binary Allowlist | ✅ | `4.1.8` |\n| 39 | Native Addon Guard | ✅ | `4.1.8` |\n| 40 | Native WASM Guard | ✅ | `4.1.8` |\n| 41 | Module Cache Poison | ✅ | `4.1.8` |\n| 42 | HTTP Route Tamper | ✅ | `4.1.8` |\n| 43 | Symlink NPM Install | ✅ | `4.1.8` |\n| 44 | FS Read Cap Config | ✅ | `4.1.8` |\n| 45 | FS Write Cap Config | ✅ | `4.1.8` |\n| 46 | Flows File Protected | ✅ | `4.1.8` |\n| 47 | Per-Package Cap Config | ✅ | `4.1.8` |\n| 48 | Grants Merge | ✅ | `4.1.8` |\n| 49 | Network HTTP Allowlist | ✅ | `4.1.8` |\n| 50 | Exec Binary Union | ✅ | `4.1.8` |\n| 51 | Types Grant | ✅ | `4.1.8` |\n| 52 | Network TCP | ✅ | `4.1.8` |\n| 53 | Network UDP | ✅ | `4.1.8` |\n| 54 | Network DNS | ✅ | `4.1.8` |\n| 55 | Network WebSocket | ✅ | `4.1.8` |\n| 56 | Network Listen | ✅ | `4.1.8` |\n| 57 | HTTP Handler Hardening | ✅ | `4.1.8` |\n| 58 | FS Require Variants | ✅ | `4.1.8` |\n| 59 | ESM Process Exec | ✅ | `4.1.8` |\n</details>\n\n\n\n_Last updated: 2026-04-19T02:07:07Z_\n<!-- DEMO-TEST-RESULTS:END -->\n\n## Demos\n\nEach demo is a self-contained scenario that shows an attack against Node-RED and how Sentinel blocks it.\n\n| #   | Demo                                                               | Attack vector                                                                    |\n| --- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------- |\n| 01  | Monkey Patching                     | Overwrites Node-RED core functions at runtime                                    |\n| 02  | Hook Injection                       | Registers malicious `onSend`/`onReceive` hooks                                   |\n| 03  | Credential Theft                   | Reads decrypted credentials from live node instances                             |\n| 04  | Wire Manipulation                 | Rewires flow connections to exfiltrate data                                      |\n| 05  | Direct Receive Injection   | Bypasses auth chain via `node.receive()`                                         |\n| 06  | Express Middleware               | Installs rogue HTTP middleware on the admin API                                  |\n| 07  | EventEmitter Hijack             | Intercepts internal Node-RED events                                              |\n| 08  | Node Enumeration                   | Maps every node in the runtime via `eachNode()`                                  |\n| 09  | Prototype Pollution             | Pollutes `Object.prototype` to affect all objects                                |\n| 10  | Flow File Tampering                | Modifies the flows file on disk                                                  |\n| 11  | Message Provenance               | Detects and blocks injected messages via HMAC tagging                            |\n| 12  | Settings.js Tampering            | Modifies settings.js at runtime to inject capability grants                      |\n| 13  | Sentinel Source Tampering | Patches Sentinel's preload.js on disk to disable protection                      |\n| 14  | Express Route Backdoor       | Registers a hidden admin API route via `httpAdmin.get()`                         |\n| 15  | Config Node Z-Forgery         | Fakes config-node identity to bypass credential access rules                     |\n| 16  | Symbol Property Bypass       | Uses Symbol-keyed properties to evade proxy guard interception                   |\n| 17  | EventEmitter Enumeration   | Enumerates all `RED.events` listeners to map internal runtime wiring             |\n| 18  | Deep Stack Bypass                 | Chains anonymous wrappers to push the malicious frame outside the guard window   |\n| 19  | HTTP Route Deletion             | Deletes existing Express routes to disable authentication endpoints              |\n| 20  | Child Process Exec               | Spawns a shell command via `child_process` to execute arbitrary OS commands      |\n| 21  | SW Fetch Interception                      | Browser-only: editor script uses `fetch()` to exfiltrate data; Service Worker blocks it via the network-policy allowlist |\n| 22  | FS Read                                     | Reads `settings.js` via `require('fs')` to extract the credential secret         |\n| 23  | Process Env Exfiltration                | Reads `process.env` to harvest injected secrets and API keys                     |\n| 24  | Process Exit DoS                       | Calls `process.exit()` from a message handler to kill the runtime                |\n| 25  | VM Sandbox Escape                        | Uses `require('vm')` to run code outside Sentinel's `Module._load` hooks         |\n| 26  | Worker Thread Escape                  | Spawns a worker thread whose module loader is invisible to Sentinel              |\n| 27  | Network Socket Exfiltration          | Creates a raw TCP socket to bypass the HTTP URL allowlist                        |\n| 28  | Registry Type Hijack                | Calls `registerType('inject', ...)` to silently replace a built-in node type     |\n| 29  | Settings Mutation                 | Reads or writes `RED.settings` to extract the credential secret or add backdoors |\n| 30  | Comms Publish Spoofing                | Pushes fake notifications to the editor via `RED.comms.publish()`                |\n| 31  | Context Permissions             | Reads or writes another node's context store without a grant                     |\n| 32  | Flows Inject                           | Injects a malicious node into the running flow via the flows API                 |\n| 33  | Node Event Hijack                 | Spies on or silences another node's input handler via EventEmitter APIs          |\n| 34  | Config Node Credentials    | Interactive: explores open / restricted / locked config-node credential access   |\n| 35  | Process Binding Bypass              | Uses `process.binding('spawn_sync')` to spawn processes, bypassing JS-level guards |\n| 36  | ChildProcess Proto Bypass        | Calls `ChildProcess.prototype.spawn()` directly, bypassing module-export guards  |\n| 37  | UserDir Bypass                       | Launches Node-RED with `-u <path>` instead of env vars, so Sentinel defaults to `~/.node-red` and misidentifies all attacker frames as internal |\n\n## Capability grants\n\nBy default Sentinel blocks every privileged operation for every third-party package. A package that needs a capability must be explicitly granted it in `settings.js`.\n\nFor the complete capability reference — every capability string, what it gates, shorthand expansions, and known gaps — see **docs/capability-design.md**.\n\n### Capability quick-reference\n\n| Group | Atomic capabilities | Shorthands |\n|---|---|---|\n| `node:*` | `node:read`, `node:write`, `node:send`, `node:status`, `node:log`, `node:close`, `node:receive`, `node:events:subscribe`, `node:events:unsubscribe`, `node:list`, `node:wires:read`, `node:wires:write`, `node:credentials:read`, `node:credentials:write`, `node:credentials:delete`, `node:context:read`, `node:context:write` | `node:events`, `node:wires`, `node:credentials`, `node:context`, `node:all` |\n| `flows:*` | `flows:read`, `flows:write`, `flows:delete`, `flows:start`, `flows:stop` | `flows:all` |\n| `storage:*` | `storage:read`, `storage:write` | `storage:all` |\n| `registry:*` | `registry:write`, `registry:read` | `registry:all` |\n| `settings:*` | `settings:read`, `settings:write` | `settings:all` |\n| `hooks:*` | `hooks:on-send`, `hooks:pre-route`, `hooks:pre-deliver`, `hooks:post-deliver`, `hooks:on-receive`, `hooks:post-receive`, `hooks:on-complete`, `hooks:remove` | `hooks:message` (all pipeline hooks), `hooks:all` |\n| `http:*` | `http:admin` (prefixed), `http:admin:global` (original paths), `http:admin:global:middleware` (no-path/config), `http:node`, `http:node:global`, `http:node:global:middleware` | — |\n| `events:*` | `events:subscribe:<name>`, `events:subscribe`, `events:emit:<name>`, `events:emit`, `events:unsubscribe:<name>`, `events:unsubscribe` | — |\n| `process:*` | `process:exec`, `process:env:read`, `process:env:write`, `process:exit` | `process:env`, `process:all` |\n| `fs:*` | `fs:read`, `fs:write` | `fs:all` |\n| `network:*` | `network:http`, `network:tcp`, `network:udp`, `network:ws`, `network:dns`, `network:listen` | `network:all` |\n| `vm:*` | `vm:execute` | — |\n| `threads:*` | `threads:spawn` | — |\n| `native:*` | `native:addon`, `native:wasm` | — |\n| `comms:*` | `comms:publish` | — |\n| top-level | — | `all` (every cap — never use in production) |\n\nCapabilities that require `capabilityConfig` to be useful:\n\n| Capability | Config key | Effect when absent |\n|---|---|---|\n| `fs:read` | `sentinel.server.defaults.capabilityConfig[\"fs:read\"].allowedPaths` | All reads blocked |\n| `fs:write` | `sentinel.server.defaults.capabilityConfig[\"fs:write\"].allowedPaths` | All writes blocked |\n| `process:exec` | `sentinel.server.defaults.capabilityConfig[\"process:exec\"].allowedCommands` | All exec calls blocked |\n| `events:subscribe` | `sentinel.server.defaults.capabilityConfig[\"events:subscribe\"].allowedEvents` | All event subscriptions blocked |\n| `events:emit` | `sentinel.server.defaults.capabilityConfig[\"events:emit\"].allowedEvents` | All event emissions blocked |\n| `events:unsubscribe` | `sentinel.server.defaults.capabilityConfig[\"events:unsubscribe\"].allowedEvents` | All event unsubscriptions blocked |\n| `http:admin` | `sentinel.server.defaults.capabilityConfig[\"http:admin\"].allowedPaths` | All admin paths allowed (no path restriction) |\n| `http:admin:global` | `sentinel.server.defaults.capabilityConfig[\"http:admin:global\"].allowedPaths` | All admin paths allowed (no path restriction) |\n| `http:node` | `sentinel.server.defaults.capabilityConfig[\"http:node\"].allowedPaths` | All node paths allowed (no path restriction) |\n| `http:node:global` | `sentinel.server.defaults.capabilityConfig[\"http:node:global\"].allowedPaths` | All node paths allowed (no path restriction) |\n| `network:http` | `sentinel.server.defaults.capabilityConfig[\"network:http\"].allowedUrls` | All URLs allowed (guard inactive) |\n| `network:tcp` | `sentinel.server.defaults.capabilityConfig[\"network:tcp\"].allowedHosts` | All hosts allowed (guard inactive) |\n| `network:udp` | `sentinel.server.defaults.capabilityConfig[\"network:udp\"].allowedHosts` | All destinations allowed (guard inactive) |\n| `network:ws` | `sentinel.server.defaults.capabilityConfig[\"network:ws\"].allowedUrls` | All WebSocket URLs allowed (guard inactive) |\n| `native:addon` | `sentinel.server.defaults.capabilityConfig[\"native:addon\"].allowedFiles` | All addon loads blocked |\n| `native:wasm` | `sentinel.server.defaults.capabilityConfig[\"native:wasm\"].allowedModules` | All WASM compilation blocked |\n\n### Adding a grant\n\nGrants live in the `sentinel.server.packages` map inside `settings.js`. Each key is an **npm package name** exactly as it appears in `node_modules/`; the value is an object with a `capabilities` array and an optional `capabilityConfig`.\n\n#### Node-RED core nodes do not need grants\n\nSentinel only applies capability checks to packages loaded from the Node-RED **userDir** (`{userDir}/node_modules/` or `{userDir}/nodes/`). Node-RED's own built-in nodes (`inject`, `debug`, `function`, `http request`, etc.) are part of the Node-RED installation itself and live outside the userDir, so Sentinel never gates them. You only need to add grants for **third-party packages** that users install into their userDir.\n\n#### `registry:write` — required for every node package\n\nEvery node package must be granted `registry:write` so Sentinel allows it to call `RED.nodes.registerType()` at startup. Without this grant, Sentinel blocks the call, the node type is never registered, and Node-RED logs _\"Waiting for missing types\"_ indefinitely.\n\n```js\n// settings.js — minimal grant for a node package that needs no other privileges\nmodule.exports = {\n    sentinel: {\n        server: {\n            defaults: {\n                capabilityConfig: {},\n            },\n            packages: {\n                \"my-custom-node\": {\n                    capabilities: [\"registry:write\"],\n                    capabilityConfig: {},\n                },\n            },\n            types: {},\n        },\n    },\n};\n```\n\n#### Common grants\n\n```js\n// settings.js\nmodule.exports = {\n    sentinel: {\n        server: {\n            defaults: {\n                capabilityConfig: {},\n            },\n            packages: {\n                // A node that reads its own credentials (this.credentials) directly.\n                // See \"Credential access patterns\" below for config-node and cross-node cases.\n                \"node-red-contrib-influxdb\": {\n                    capabilities: [\"registry:write\", \"node:credentials:read\"],\n                    capabilityConfig: {},\n                },\n\n                // A flow-auditing plugin that needs to inspect the runtime topology.\n                \"node-red-contrib-flow-auditor\": {\n                    capabilities: [\n                        \"registry:write\",\n                        \"node:list\", // RED.nodes.eachNode()\n                        \"node:wires:read\", // read node.wires (output topology)\n                        \"flows:read\", // RED.runtime.flows.getFlows() / getFlow(id)\n                    ],\n                    capabilityConfig: {},\n                },\n\n                // A tracing / APM plugin that hooks the message pipeline.\n                // hooks:on-send fires before routing; hooks:post-deliver fires after delivery.\n                \"node-red-contrib-tracer\": {\n                    capabilities: [\"registry:write\", \"hooks:on-send\", \"hooks:post-deliver\"],\n                    capabilityConfig: {},\n                },\n\n                // A node that registers its own admin UI routes.\n                // http:admin covers httpAdmin; http:node covers httpNode.\n                \"node-red-contrib-dashboard\": {\n                    capabilities: [\"registry:write\", \"http:admin\", \"http:node\"],\n                    capabilityConfig: {},\n                },\n\n                // A node that genuinely needs to run OS commands.\n                \"node-red-contrib-exec\": {\n                    capabilities: [\"registry:write\", \"process:exec\"],\n                    capabilityConfig: {},\n                },\n\n                // A node that reads files from disk (e.g. a CSV reader).\n                \"node-red-contrib-file-in\": {\n                    capabilities: [\"registry:write\", \"fs:read\"],\n                    capabilityConfig: {},\n                },\n\n                // A node that makes outbound HTTP calls.\n                // network:http covers http.request/https.request.\n                // Add specific URLs to sentinel.client.network.allowlist to restrict further.\n                \"node-red-contrib-http-request\": {\n                    capabilities: [\"registry:write\", \"network:http\"],\n                    capabilityConfig: {},\n                },\n\n                // A plugin (no node types) that listens to runtime events.\n                // Plugins are registered via the node-red.plugins key in package.json\n                // and do not call registerType — no registry:write needed.\n                \"node-red-contrib-audit-logger\": {\n                    capabilities: [\"events:subscribe\"],\n                    capabilityConfig: {},\n                },\n            },\n            types: {},\n        },\n    },\n};\n```\n\nSentinel identifies the calling package at runtime by walking the call stack and extracting the `node_modules/<package>` segment from the nearest frame that does not belong to Node-RED or Sentinel itself. The match is against the npm package name exactly as it appears on disk.\n\n### Credential access patterns\n\nSentinel blocks all `.credentials` access by default — whether a node reads its own secrets or those of another node. The grant is explicit so the operator consciously approves it and it shows up in the config as an audit signal.\n\n#### Default: credentials are blocked\n\nWithout a grant, `.credentials` is `undefined` even for a node reading its own fields:\n\n```js\n// node-red-contrib-my-api/index.js\nmodule.exports = function (RED) {\n    function MyApiNode(config) {\n        RED.nodes.createNode(this, config);\n        var apiKey = this.credentials.apiKey; // → undefined without the grant\n    }\n    RED.nodes.registerType(\"my-api\", MyApiNode);\n};\n```\n\nThe same applies when reading `.credentials` from another node obtained via `RED.nodes.getNode()` — the accessing package needs the grant. Credential access via package grant is **scoped**: the package can only read credentials from node types it registered. To read credentials from a node type registered by a different package, a `types` entry on the target type is required (see below).\n\n#### The config-node pattern (preferred — no cap needed)\n\nMost packages only need their own config node's secret. The safe way to handle this is for the config node to copy the secret into a plain property during construction. Sentinel only proxies nodes returned by `getNode()`, not a node's own `this`, so this path is unguarded and requires no capability grant:\n\n```js\n// node-red-contrib-influxdb/index.js\nmodule.exports = function (RED) {\n    function InfluxConfigNode(config) {\n        RED.nodes.createNode(this, config);\n        this.token = this.credentials.token; // own this — not proxied, no cap needed\n        this.host  = config.host;\n    }\n    RED.nodes.registerType(\"influxdb-config\", InfluxConfigNode);\n\n    function InfluxWriteNode(config) {\n        RED.nodes.createNode(this, config);\n        var configNode = RED.nodes.getNode(config.configId);\n        this.on(\"input\", function (msg) {\n            writeToInflux(configNode.host, configNode.token, msg.payload); // plain property — no cap\n            this.send(msg);\n        });\n    }\n    RED.nodes.registerType(\"influxdb-write\", InfluxWriteNode);\n};\n```\n\n```js\n// settings.js — node:credentials:read not needed under this pattern\nsentinel: {\n    server: {\n        defaults: { capabilityConfig: {} },\n        packages: {\n            \"node-red-contrib-influxdb\": {\n                capabilities: [\"registry:write\"],\n                capabilityConfig: {},\n            },\n        },\n        types: {},\n    },\n}\n```\n\n#### Grant via `settings.sentinel.server.packages` (scoped to own node types)\n\n`node:credentials:read` grants a package access to `.credentials` only on node types **it registered**. It cannot read credentials from nodes registered by other packages — that requires an explicit `types` entry on the target type (described below).\n\n```js\n// settings.js\nsentinel: {\n    server: {\n        defaults: { capabilityConfig: {} },\n        packages: {\n            \"node-red-contrib-my-api\": {\n                capabilities: [\"registry:write\", \"node:credentials:read\"],\n                capabilityConfig: {},\n            },\n        },\n        types: {},\n    },\n}\n```\n\nWith this grant, `node-red-contrib-my-api` can read `.credentials` on any node whose type it registered. Calling `getNode(someOtherPkgNodeId).credentials` returns `undefined` even with this grant — the other package's node type is not in the allowlist.\n\n#### Grant via `.sentinel-grants.json`\n\nSentinel also reads `.sentinel-grants.json` from `userDir`. This is the file the **Sentinel editor panel** writes, letting operators manage grants through the browser without touching `settings.js` (which is typically mounted `:ro` in production).\n\n```json\n{\n    \"server\": {\n        \"defaults\": { \"capabilityConfig\": {} },\n        \"packages\": {\n            \"node-red-contrib-my-api\": {\n                \"capabilities\": [\"registry:write\", \"node:credentials:read\"],\n                \"capabilityConfig\": {}\n            }\n        },\n        \"types\": {\n            \"influxdb-config\": {\n                \"node:credentials:read\": {\n                    \"packages\": [\"node-red-contrib-influxdb\"],\n                    \"types\": []\n                }\n            }\n        }\n    }\n}\n```\n\n- **`packages`** — equivalent to `settings.sentinel.server.packages`; merged at runtime (union). For credential access, scoped to the caller's own registered node types.\n- **`types`** — target-side allowlist keyed on the node type being accessed. Each capability entry has a `packages` list (caller npm packages) and a `types` list (caller node types). This is the **only** way to grant cross-package credential access — a package grant alone is not sufficient when the target type belongs to a different package.\n\n##### `types` examples\n\nGrant a specific package access to a config node's credentials:\n\n```json\n{\n    \"server\": {\n        \"types\": {\n            \"mqtt-broker\": {\n                \"node:credentials:read\": {\n                    \"packages\": [\"node-red-contrib-mqtt-in\", \"node-red-contrib-mqtt-out\"],\n                    \"types\": []\n                }\n            }\n        }\n    }\n}\n```\n\nRestrict wire rewiring to a specific tool package:\n\n```json\n{\n    \"server\": {\n        \"types\": {\n            \"function\": {\n                \"node:wires:write\": { \"packages\": [\"node-red-contrib-flow-manager\"], \"types\": [] },\n                \"node:wires:read\":  { \"packages\": [\"node-red-contrib-flow-manager\", \"node-red-contrib-flow-auditor\"], \"types\": [] }\n            }\n        }\n    }\n}\n```\n\n### Grants are per package, not per node type\n\nA single npm package can register many node types, but all of them share the same package name in the call stack. Sentinel cannot distinguish `my-package/nodes/foo.js` from `my-package/nodes/bar.js` at the frame level — both resolve to `my-package`. This is intentional: the **package** is the unit you install, audit, and sign off on.\n\n### Fine-grained control with scoped child packages\n\nIf you need different capability levels for different node types, publish each trust boundary as its own scoped package and group them under a parent that users install as a single dependency.\n\n**Parent package** — a dependency aggregator with no node code of its own:\n\n```json\n{\n    \"name\": \"@my-company/nodes\",\n    \"version\": \"1.0.0\",\n    \"dependencies\": {\n        \"@my-company/node-data-formatter\": \"^1.0.0\",\n        \"@my-company/node-mqtt-enricher\": \"^1.0.0\",\n        \"@my-company/node-flow-auditor\": \"^1.0.0\"\n    }\n}\n```\n\n**Child packages** — each has its own `node-red` field and npm identity:\n\n```json\n{\n    \"name\": \"@my-company/node-mqtt-enricher\",\n    \"version\": \"1.0.0\",\n    \"node-red\": { \"nodes\": { \"mqtt-enricher\": \"index.js\" } }\n}\n```\n\nWhen a user runs `npm install @my-company/nodes`, npm (v7+) hoists the children to the top-level `node_modules/`. Node-RED discovers them directly because each has its own `node-red` field. Sentinel sees each child's package name independently, so grants can be applied at exactly the right granularity:\n\n```js\nsentinel: {\n    server: {\n        defaults: { capabilityConfig: {} },\n        packages: {\n            // formatter needs no privileged access — registry:write is enough\n            \"@my-company/node-data-formatter\": {\n                capabilities: [\"registry:write\"],\n                capabilityConfig: {},\n            },\n            // enricher reads credentials from a config node\n            \"@my-company/node-mqtt-enricher\": {\n                capabilities: [\"registry:write\", \"node:credentials:read\"],\n                capabilityConfig: {},\n            },\n            // auditor needs to walk the full node graph\n            \"@my-company/node-flow-auditor\": {\n                capabilities: [\"registry:write\", \"node:list\", \"node:wires:read\"],\n                capabilityConfig: {},\n            },\n        },\n        types: {},\n    },\n}\n```\n\nThis pattern is already established in the Node-RED ecosystem — `@node-red/nodes`, `@node-red/runtime`, and `@node-red/editor-api` are all separate packages under the `@node-red` namespace.\n\n### Service nodes as capability brokers\n\nSentinel resolves capabilities by looking at the **nearest** user-installed package in the call stack. This means a \"service\" package that wraps privileged operations acts as a capability broker: only the service needs the grant, not the packages that call into it.\n\n**How it works:** When package A calls a method in package B, and package B internally makes a privileged call (e.g. `fs.readFileSync`), the call stack looks like this:\n\n```\nfs.readFileSync          ← built-in (skipped)\nnode-red-contrib-file-service/index.js:55   ← nearest userDir frame → checked\nnode-red-contrib-my-processor/index.js:12   ← outer frame (not checked for this call)\n```\n\nSentinel finds `node-red-contrib-file-service` first and checks its grants. `node-red-contrib-my-processor` is not involved in the capability check at all.\n\n**In practice:** publish a service package that wraps privileged operations behind a controlled API, grant it the capabilities it needs, and let consumer packages call it freely:\n\n```js\n// node-red-contrib-file-service/index.js\n// This package holds fs:read — consumers don't need it.\nmodule.exports = function (RED) {\n    function FileServiceNode(config) {\n        RED.nodes.createNode(this, config);\n        // Exposed API — consumers call node.readConfig(), not fs directly.\n        this.readConfig = function (filePath) {\n            return require(\"fs\").readFileSync(filePath, \"utf8\");\n        };\n    }\n    RED.nodes.registerType(\"file-service\", FileServiceNode);\n};\n```\n\n```js\n// node-red-contrib-my-processor/index.js\n// No fs capability needed — reads files through the service node.\nmodule.exports = function (RED) {\n    function ProcessorNode(config) {\n        RED.nodes.createNode(this, config);\n        var service = RED.nodes.getNode(config.serviceId);\n        this.on(\"input\", function (msg) {\n            var data = service.readConfig(\"/data/config.json\"); // service makes the fs call\n            // ... process data\n            this.send(msg);\n        });\n    }\n    RED.nodes.registerType(\"my-processor\", ProcessorNode);\n};\n```\n\n```js\n// settings.js\nsentinel: {\n    server: {\n        defaults: { capabilityConfig: {} },\n        packages: {\n            // Only the service needs fs:read — it owns the privileged boundary.\n            \"node-red-contrib-file-service\": {\n                capabilities: [\"registry:write\", \"fs:read\"],\n                capabilityConfig: {},\n            },\n            // The consumer needs no capability beyond registering its node type.\n            \"node-red-contrib-my-processor\": {\n                capabilities: [\"registry:write\"],\n                capabilityConfig: {},\n            },\n        },\n        types: {},\n    },\n}\n```\n\nThis pattern is useful when multiple consumer packages need the same privileged operation: centralise it in one well-audited service package, grant only that package the capability, and consumers remain unprivileged. The service becomes the policy enforcement point — it decides what it exposes, and Sentinel enforces that nothing bypasses it.\n\n## Defense architecture\n\nSentinel runs inside the same Node.js process as every package it protects against — there is no sandbox, no separate process, and no OS-level isolation. Meaningful enforcement in that environment requires layered hardening techniques:\n\n| Layer | Technique | What it closes |\n|---|---|---|\n| 0 — Prototype hardening | `Object.preventExtensions` on all built-in prototypes | Prototype pollution before any third-party code runs |\n| 1 — Module interception | `Module._load` hook + non-configurable lock | `require()` of `fs`, `http`, `child_process`, `vm`, `worker_threads` |\n| 2 — Node isolation | ES6 `Proxy` on every `getNode()` return value | Property reads, writes, and `defineProperty` on live node instances |\n| 3 — Surface hardening | Guarded Express routing, `process.env` Proxy, router-stack Proxy | Post-init manipulation of the HTTP server and environment |\n| 4 — Network policy | Outbound HTTP/HTTPS/socket allowlist | Exfiltration paths not covered by the module gate |\n| Cross-cutting | Intrinsic capture, call-stack introspection, file integrity watchdog | Prototype mutation of guard helpers, call-identity forgery, on-disk tampering |\n\nAll built-in methods used by guard logic are pinned as standalone bound functions before the first `require()`, so a package that overwrites `String.prototype.includes` cannot blind the stack-frame checks. The `Module._load` hook is locked `configurable: false` immediately after installation so it cannot be stripped. Every node proxy intercepts `defineProperty` in addition to `get`/`set`, closing the bypass that would otherwise let a caller install a getter on a proxied node.\n\nFor the full reference — every technique explained with code examples and attack scenarios — see **[docs/defense-techniques.md](docs/defense-techniques.md)**.\n\n\n## Module access gates\n\nSentinel intercepts `require()` for dangerous built-in modules and blocks specific methods within them. When a call is blocked, Sentinel prints a warning to the Node-RED console and tells you exactly which grant to add.\n\nThe warning format is:\n\n```\n[@allanoricil/nrg-sentinel] BLOCKED fs.readFileSync() — my-custom-node lacks fs:read\n  Call stack:\n    at Object.<anonymous> (/data/node_modules/my-custom-node/index.js:42:5)\n  To allow, add to settings.js:\n    sentinel: { server: { packages: { \"my-custom-node\": { capabilities: [\"fs:read\"], capabilityConfig: {} } } } }\n```\n\nFor modules that are blocked entirely at `require()` time (like `vm` and `worker_threads`), the operation throws immediately:\n\n```\n[@allanoricil/nrg-sentinel] BLOCKED require('vm') — my-custom-node lacks vm:execute\n```\n\n### File system — `fs:read` and `fs:write`\n\nTriggered by `require('fs')`, `require('fs/promises')`, `require('node:fs')`, `require('node:fs/promises')`.\n\n| What you call                                                                                | Cap needed |\n| -------------------------------------------------------------------------------------------- | ---------- |\n| `readFile`, `readFileSync`, `readdir`, `createReadStream`, `stat`, `exists`, `watch`         | `fs:read`  |\n| `writeFile`, `writeFileSync`, `appendFile`, `createWriteStream`, `unlink`, `mkdir`, `rename` | `fs:write` |\n\n```js\n// Node-RED log when blocked:\n// [@allanoricil/nrg-sentinel] BLOCKED fs.readFileSync() — my-node lacks fs:read\n\nsentinel: {\n    server: {\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"fs:read\"],\n                capabilityConfig: {},\n            },\n        },\n    },\n}\n```\n\n> **Least-privilege default:** granting `fs:read` or `fs:write` without configuring `allowedPaths` **blocks every path**. The grant alone is not sufficient — you must explicitly list which paths are accessible.\n\n#### Path allowlist — `capabilityConfig[\"fs:read\"].allowedPaths` / `capabilityConfig[\"fs:write\"].allowedPaths`\n\nRestrict the package to a specific set of paths. Each entry is either a **directory** (all files under it are permitted) or a **glob pattern** (any path matching the pattern is permitted).\n\n```js\nsentinel: {\n    server: {\n        defaults: {\n            capabilityConfig: {\n                \"fs:read\": {\n                    allowedPaths: [\n                        \"/data/config\",          // all files under /data/config/\n                        \"/tmp/sentinel-*.json\",  // glob: JSON files in /tmp starting with sentinel-\n                        \"/etc/app/**/*.conf\",    // glob: any .conf under /etc/app/ at any depth\n                    ],\n                },\n                \"fs:write\": {\n                    allowedPaths: [\n                        \"/tmp\",                  // writes anywhere under /tmp/\n                        \"/data/output/*.csv\",    // glob: CSV files directly in /data/output/\n                    ],\n                },\n            },\n        },\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"fs:read\", \"fs:write\"],\n                capabilityConfig: {},\n            },\n        },\n    },\n}\n```\n\nGlob rules:\n- `*` matches any characters **within a single path segment** (does not cross `/`)\n- `**` matches **any number of path segments** (crosses `/`)\n- `?` matches a single character (not `/`)\n- `[abc]` / `[!abc]` — character classes with optional negation\n\n`fs:write` additionally has an **unconditional hard block on `node_modules`** — no package can write to `node_modules` regardless of `allowedPaths`.\n\n#### Per-package path restrictions\n\nThe global `sentinel.server.defaults.capabilityConfig` in `settings.js` applies the same `allowedPaths` to every package that holds the capability. For stronger isolation — where different packages need access to different path subtrees — use **per-package capabilityConfig** via the admin panel (`#/packages` → select a package → capability config section).\n\nPer-package config is stored in `.sentinel-grants.json` inside each package's `capabilityConfig` entry and overrides the global `sentinel.server.defaults.capabilityConfig` for that specific package:\n\n```json\n// .sentinel-grants.json (written by the admin panel)\n{\n  \"server\": {\n    \"defaults\": { \"capabilityConfig\": {} },\n    \"packages\": {\n      \"pkg-a\": { \"capabilities\": [\"fs:read\"], \"capabilityConfig\": { \"fs:read\": { \"allowedPaths\": [\"/data/pkg-a/**\"] } } },\n      \"pkg-b\": { \"capabilities\": [\"fs:read\"], \"capabilityConfig\": { \"fs:read\": { \"allowedPaths\": [\"/data/pkg-b/**\"] } } }\n    }\n  }\n}\n```\n\nWith this config, `pkg-a` can only read from `/data/pkg-a/` and `pkg-b` can only read from `/data/pkg-b/`, regardless of any global `allowedPaths`. Resolution order: **per-package config → global defaults → blocked**.\n\nThe same per-package scoping is available for `fs:write`, `process:exec`, `native:addon`, and `native:wasm`.\n\n### Outbound HTTP — `network:http`\n\nTriggered when a node calls `http.request()`, `https.request()`, `http.get()`, or the global `fetch()` (Node.js built-in fetch via undici). Both `http.request()` and `fetch()` are covered by a single `network:http` capability — there is no separate `network:fetch`.\n\n```js\n// Node-RED log when blocked:\n// [@allanoricil/nrg-sentinel] BLOCKED http.request() — my-node lacks network:http\n// NRG Sentinel: network:http not granted — my-node  (also for fetch())\n\nsentinel: {\n    server: {\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"network:http\"],\n                capabilityConfig: {},\n            },\n        },\n    },\n}\n```\n\nYou can restrict which URLs are reachable using `sentinel.server.defaults.capabilityConfig[\"network:http\"].allowedUrls`. This is the **global default** — it applies to every package holding `network:http` unless they have their own per-package entry that adds to it:\n\n```js\nsentinel: {\n    server: {\n        defaults: {\n            capabilityConfig: {\n                \"network:http\": {\n                    allowedUrls: [\n                        \"https://api.example.com/\",\n                        \"https://metrics.internal/\",\n                    ],\n                },\n            },\n        },\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"network:http\"],\n                capabilityConfig: {},\n            },\n        },\n    },\n}\n```\n\nA package can add its own extra URLs on top of the global default via its `capabilityConfig`:\n\n```js\nsentinel: {\n    server: {\n        defaults: {\n            capabilityConfig: {\n                \"network:http\": { allowedUrls: [\"https://shared.example.com/\"] },\n            },\n        },\n        packages: {\n            \"pkg-a\": {\n                capabilities: [\"registry:write\", \"network:http\"],\n                capabilityConfig: {\n                    \"network:http\": { allowedUrls: [\"https://pkg-a-only.internal/\"] },\n                },\n            },\n        },\n    },\n}\n```\n\nThe effective allowlist for `pkg-a` is the **union** of the global default and its own entry: `[\"https://shared.example.com/\", \"https://pkg-a-only.internal/\"]`. Packages with no per-package entry see only the global list.\n\nIf `allowedUrls` is not configured at either level the URL guard is inactive and the package may reach any host (subject to the capability gate).\n\n> **`sentinel.client.network.allowlist`** is a separate, browser-side policy fed to the Service Worker. It restricts which URLs the Node-RED editor UI itself may fetch — it does **not** affect server-side Node.js package HTTP calls.\n\n### Outbound TCP — `network:tcp`\n\nTriggered by `net.createConnection()`, `net.connect()`, and `tls.connect()` / `tls.createConnection()`. A package with only `network:http` cannot open raw TCP sockets. Use `network:tcp` for least privilege when you only need TCP (not UDP).\n\n```js\n// Node-RED log when blocked:\n// [@allanoricil/nrg-sentinel] BLOCKED net.createConnection() — my-node lacks network:tcp\n\nsentinel: {\n    server: {\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"network:tcp\"],\n                capabilityConfig: {\n                    \"network:tcp\": {\n                        allowedHosts: [\"api.example.com:443\", \"*.internal:*\"],\n                    },\n                },\n            },\n        },\n    },\n}\n```\n\n### UDP sockets — `network:udp`\n\nTriggered by `dgram.createSocket()`. The `allowedHosts` list (format: `host:port`) is enforced at `socket.send()` time, blocking sends to non-listed destinations even after the socket is created.\n\n```js\n// Node-RED log when blocked:\n// [@allanoricil/nrg-sentinel] BLOCKED dgram.createSocket() — my-node lacks network:udp\n\nsentinel: {\n    server: {\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"network:udp\"],\n                capabilityConfig: {\n                    \"network:udp\": {\n                        allowedHosts: [\"syslog.example.com:514\"],\n                    },\n                },\n            },\n        },\n    },\n}\n```\n\n### WebSocket connections — `network:ws`\n\nTriggered by HTTP requests containing an `Upgrade: websocket` header. The WebSocket protocol is built on HTTP transport — the guard detects the upgrade header inside the existing `http.request()` / `https.request()` guard and requires `network:ws` instead of `network:http`.\n\n```js\n// Node-RED log when blocked:\n// [@allanoricil/nrg-sentinel] BLOCKED http.request() — my-node lacks network:ws\n\nsentinel: {\n    server: {\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"network:ws\"],\n                capabilityConfig: {\n                    \"network:ws\": {\n                        allowedUrls: [\"wss://realtime.example.com/**\", \"ws://localhost:8080\"],\n                    },\n                },\n            },\n        },\n    },\n}\n```\n\n#### `network:ws` and `network:listen` — when do you need both?\n\n`network:ws` grants the ability to use the WebSocket protocol. Whether you also need `network:listen` depends on *how* you create the WebSocket server:\n\n- **WebSocket client (outbound connection)** — only `network:ws` needed\n- **WS server attached to Node-RED's existing HTTP server** — only `network:ws` needed (no new port is bound)\n- **Standalone WS server on its own port** — needs BOTH `network:ws` AND `network:listen`\n\n### Binding a port — `network:listen`\n\nTriggered by `net.createServer()`, `http.createServer()`, or `https.createServer()` binding a new TCP port. This is intentionally separate from outbound capabilities — starting a server exposes your Node-RED instance to inbound traffic on a new port.\n\n```js\n// Node-RED log when blocked:\n// [@allanoricil/nrg-sentinel] BLOCKED net.createServer() — my-node lacks network:listen\n\nsentinel: {\n    server: {\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"network:listen\"],\n                capabilityConfig: {},\n            },\n        },\n    },\n}\n```\n\nYou do **not** need `network:listen` if you are only making outbound connections (those use `network:http`, `network:tcp`, etc.).\n\n### DNS lookups — `network:dns`\n\nTriggered by `require('dns').lookup()`, `resolve()`, and all other dns methods, including `dns/promises` variants. DNS is a known data-exfiltration channel (subdomains can encode data to an attacker-controlled nameserver).\n\n```js\n// Node-RED log when blocked:\n// [@allanoricil/nrg-sentinel] BLOCKED dns.lookup() — my-node lacks network:dns\n\nsentinel: {\n    server: {\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"network:dns\"],\n                capabilityConfig: {},\n            },\n        },\n    },\n}\n```\n\n### Child processes — `process:exec`\n\nTriggered by `child_process.exec()`, `execSync()`, `spawn()`, `spawnSync()`, `execFile()`, `fork()`.\n\n```js\n// Node-RED log when blocked:\n// [@allanoricil/nrg-sentinel] BLOCKED child_process.execSync() — process:exec not granted for my-node\n\nsentinel: {\n    server: {\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"process:exec\"],\n                capabilityConfig: {},\n            },\n        },\n    },\n}\n```\n\n#### Command allowlist — `capabilityConfig[\"process:exec\"].allowedCommands`\n\nEven when `process:exec` is granted, you can restrict which commands the package may run to a pre-approved set identified by their **SHA-256 hash of the exact command string** evaluated at call time.\n\n```js\nsentinel: {\n    server: {\n        defaults: {\n            capabilityConfig: {\n                \"process:exec\": {\n                    // Only commands whose SHA-256 string hash appears here may run.\n                    // Compute with: printf '%s' 'whoami' | sha256sum | cut -d' ' -f1\n                    allowedCommands: [\n                        \"abc123...\",  // sha256 of \"whoami\"\n                        \"def456...\",  // sha256 of \"git status\"\n                    ],\n                },\n            },\n        },\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"process:exec\"],\n                capabilityConfig: {},\n            },\n        },\n    },\n}\n```\n\nWhen `allowedCommands` is configured:\n- The exact evaluated command string (e.g. `\"whoami\"`, `\"git status\"`) is hashed with SHA-256 and compared against the set. Any change to the command — different arguments, injected metacharacters — produces a different hash and is blocked.\n- A command not in the set is blocked with: `command hash not in allowedCommands`\n- Node-RED's **own built-in nodes** (e.g. the native `exec` node) are internal callers and are **exempt** — only external user-space packages with `process:exec` are subject to the hash check.\n- Omitting `allowedCommands` (from both `sentinel.server.defaults.capabilityConfig` and per-package `capabilityConfig`) blocks all commands — the least-privilege default requires an explicit allowlist even when `process:exec` is granted.\n\nTo compute hashes:\n```bash\n# Linux / macOS\nprintf '%s' 'whoami' | sha256sum | cut -d' ' -f1\nprintf '%s' 'git status' | sha256sum | cut -d' ' -f1\n```\n\n### Environment variables — `process:env:read`\n\nTriggered when a node reads `process.env.SOME_KEY`. This gates reads from the global `process.env` object.\n\n```js\n// Node-RED log when blocked:\n// [@allanoricil/nrg-sentinel] BLOCKED process.env.DATABASE_URL — my-node lacks process:env:read\n\nsentinel: {\n    server: {\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"process:env:read\"],\n                capabilityConfig: {},\n            },\n        },\n    },\n}\n```\n\n### VM contexts — `vm:execute`\n\nThe entire `require('vm')` call is blocked if the caller lacks this capability. Code run inside a `vm` context bypasses all `Module._load` hooks — Sentinel cannot see what it does.\n\n```js\n// Node-RED log when blocked (throws, does not just warn):\n// [@allanoricil/nrg-sentinel] BLOCKED require('vm') — my-node lacks vm:execute\n\nsentinel: {\n    server: {\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"vm:execute\"],\n                capabilityConfig: {},\n            },\n        },\n    },\n}\n```\n\n### Worker threads — `threads:spawn`\n\nThe entire `require('worker_threads')` call is blocked. Workers run in a separate V8 isolate whose module loader is invisible to Sentinel.\n\n```js\n// Node-RED log when blocked (throws, does not just warn):\n// [@allanoricil/nrg-sentinel] BLOCKED require('worker_threads') — my-node lacks threads:spawn\n\nsentinel: {\n    server: {\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"threads:spawn\"],\n                capabilityConfig: {},\n            },\n        },\n    },\n}\n```\n\n### Native addons — `native:addon`\n\nBlocked at two layers: `Module._extensions['.node']` (covers all `require()` paths) and `process.dlopen()` (covers direct low-level loads). Native addons run C++ code in-process and can bypass every JavaScript-level guard once loaded.\n\n```js\n// Node-RED log when blocked (throws):\n// [@allanoricil/nrg-sentinel] BLOCKED require('addon.node') — native:addon not granted for my-node\n\nsentinel: {\n    server: {\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"native:addon\"],\n                capabilityConfig: {},\n            },\n        },\n    },\n}\n```\n\n#### File allowlist — `capabilityConfig[\"native:addon\"].allowedFiles`\n\nEven when `native:addon` is granted, the addon's **SHA-256 content hash** must appear in `allowedFiles`. The hash check is **mandatory** — omitting `allowedFiles` blocks all native addon loads even when the capability is granted.\n\n```js\nsentinel: {\n    server: {\n        defaults: {\n            capabilityConfig: {\n                \"native:addon\": {\n                    // Only addons whose SHA-256 content hash appears here may load.\n                    // Compute with: sha256sum /path/to/addon.node | cut -d' ' -f1\n                    allowedFiles: [\n                        \"abc123...\",  // sha256 of the approved .node file\n                    ],\n                },\n            },\n        },\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"native:addon\"],\n                capabilityConfig: {},\n            },\n        },\n    },\n}\n```\n\nNode-RED's own built-in native addons (bcrypt, bufferutil, etc.) are **internal callers** and are always exempt.\n\n### WebAssembly — `native:wasm`\n\nGuards `WebAssembly.compile()`, `WebAssembly.instantiate()`, `new WebAssembly.Module()`, and the streaming variants. WASM executes arbitrary code outside `Module._load` and can exfiltrate data via any granted network capabilities.\n\n```js\n// Node-RED log when blocked:\n// [@allanoricil/nrg-sentinel] BLOCKED WebAssembly.compile() — native:wasm not granted for my-node\n\nsentinel: {\n    server: {\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"native:wasm\"],\n                capabilityConfig: {},\n            },\n        },\n    },\n}\n```\n\n#### Module hash allowlist — `capabilityConfig[\"native:wasm\"].allowedModules`\n\nEven when `native:wasm` is granted, the WASM bytes must hash to a value in `allowedModules`. The hash check is **mandatory** — omitting `allowedModules` blocks all WASM compilation even when the capability is granted.\n\n```js\nsentinel: {\n    server: {\n        defaults: {\n            capabilityConfig: {\n                \"native:wasm\": {\n                    // SHA-256 hash of the raw WASM bytes (not the .wasm file name).\n                    // Compute with: node -e \"var c=require('crypto'),b=require('fs').readFileSync('module.wasm');console.log(c.createHash('sha256').update(b).digest('hex'))\"\n                    allowedModules: [\n                        \"def456...\",  // sha256 of the approved WASM module bytes\n                    ],\n                },\n            },\n        },\n        packages: {\n            \"my-node\": {\n                capabilities: [\"registry:write\", \"native:wasm\"],\n                capabilityConfig: {},\n            },\n        },\n    },\n}\n```\n\n> **Streaming APIs** (`compileStreaming` / `instantiateStreaming`): bytes arrive from a network response and cannot be hashed at call time. These APIs are allowed when `allowedModules` is configured but are not hash-checked. To block streaming WASM entirely, do not grant `native:wasm`.\n\n### Module cache — automatic (no grant required)\n\n`require.cache` writes are guarded automatically. No configuration is needed. Sentinel blocks:\n\n- **Cache poisoning** — replacing another package's or Node-RED's cache entry.\n- **Cache eviction** — deleting a cache entry owned by a different package.\n- **Fake-entry injection** — pre-populating a path outside the attacker's own package directory.\n\nLegitimate operations (a package writing entries within its own package directory, reading any entry) are always permitted.\n\n### ESM `import()` and function-level guards\n\nNode.js ESM `import()` expressions bypass `Module._load` — they go through the ESM loader. Sentinel's response is to patch the **module objects themselves** at preload time, before any package code runs, rather than relying solely on intercepting the load call.\n\n`fs`, `net`, `dns`, `vm`, `worker_threads`, `http`, `https`, `dgram`, and `tls` are all patched at the function level on their cached module objects during Sentinel's initialisation. An `import('fs')` expression returns the same cached `fs` object with already-patched methods — the guards fire identically to `require('fs')`.\n\nThe installer bundler (rolldown) is used for **code splitting and singleton semantics** — it bundles each package's Node-RED entry points into ESM chunks with shared chunk extraction, so modules like datastores and event buses initialise exactly once across entries. This is not an ESM security measure; the function-level patches are what make the guards effective regardless of how a module is obtained.\n\n## Built-in Node-RED nodes and the trust boundary\n\nSentinel's capability gates — `process:exec`, `fs:read`, `network:http`, and every other grant — apply exclusively to **third-party npm packages** installed via the palette manager. They do not apply to Node-RED's own built-in nodes (`exec`, `function`, `http request`, `file in/out`, `tcp`, etc.).\n\nThis is intentional. Built-in nodes are installed by root into a read-only directory (`/usr/src/nodered/node_modules/@node-red/`). They cannot be modified at runtime by any package Sentinel guards against, and they are always visible in the flow graph — any use of them is fully auditable by the operator.\n\nThe questions below address common concerns about this boundary.\n\n---\n\n**\"Wait — I can drop an exec node into a flow and run arbitrary commands. Doesn't that mean Sentinel isn't working?\"**\n\nTo inject an exec node into a live deployment, an attacker must successfully deploy a flow change. Sentinel wraps every deployment in an approval queue. The review UI that approves or rejects deployments is served at a separate URL (`/nrg/plugins/sentinel/deployments/review`) in its own browser tab — completely outside the Node-RED editor's JavaScript context.\n\nA rogue palette node cannot tamper with that approval page from the client side: it runs in a different document with its own isolated JavaScript environment, and the Node-RED editor has no cross-origin access to it. On the server side, the approval API requires both admin authentication and the `X-Sentinel-Admin: 1` header. Sentinel's tamper detection in the editor (`plugin.html`) locks `fetch` and `XMLHttpRequest` against injected headers, so a rogue plugin in the editor context cannot forge that header.\n\nAn attacker who has already obtained legitimate admin credentials can deploy exec nodes — but they also have direct terminal access, file system access, and the ability to edit `settings.js`. At that point Sentinel is not the relevant defence; credential and access management is.\n\n---\n\n**\"A function node can read files and make HTTP requests. Is that a gap?\"**\n\nPartially, but it is constrained on multiple levels.\n\nFirst, deploying a flow that contains a malicious function node faces the same deployment queue barrier described above.\n\nSecond, function nodes cannot call `require('fs')` or `require('http')` unless `functionExternalModules: true` is explicitly enabled in `settings.js` and the specific module is listed in the node's setup. That option is off by default.\n\nThird, when `functionExternalModules` is enabled, function node code runs in an anonymous `eval` context. Sentinel's stack-frame analyser treats anonymous frames as unattributed external calls — the same anonymous-frame detection that blocks injection through `new Function()` wrappers. The call is blocked under the label `unknown` with no capability grant. This means a function node attempting `fs.readFileSync()` or `http.request()` through the guarded modules will be denied, even without a recognised package name.\n\n---\n\n**\"What about the HTTP request node — can't it exfiltrate data and bypass network:http?\"**\n\nThe HTTP request node is a built-in and is treated as an internal caller, so the `network:http` capability gate does not fire for it. The relevant control layer here is Node-RED's own authentication and access model: only operators with deploy rights can add an HTTP request node to a flow, and only flows that pass the Sentinel deployment queue are activated.\n\nIf your threat model requires restricting what HTTP requests the HTTP request node can make — for example in a multi-tenant environment where different operators manage different flows — the network policy allowlist (`sentinel.client.network.allowlist` in `settings.js`) applies globally to all outbound requests at the process level and is not limited to third-party packages. The service worker layer enforces the same allowlist for browser-originated requests.\n\n---\n\n**\"What if I write a custom node that uses the exec node — does it inherit the exec node's trust?\"**\n\nNo. A custom node cannot import the exec node as a library — Node-RED built-in nodes are not callable utilities, they are flow graph entities. The only way a custom node can leverage the exec node is by sending a message to an already-deployed instance via `RED.nodes.getNode(id).receive(msg)`.\n\nThat path is blocked at multiple points:\n\n- **The exec node must already be deployed.** For it to exist in the running flow it had to pass through Sentinel's deployment approval queue. A rogue package cannot add nodes to the flow at runtime — flow mutations go through Node-RED's storage layer, which is protected.\n- **The custom node must know the exec node's ID.** IDs are not predictable. Enumerating all nodes to find an exec instance requires the `node:list` capability grant, which the package must hold explicitly.\n- **The direct approach is already blocked.** If the custom node simply calls `child_process.spawn()` or `child_process.exec()` itself, Sentinel's `process:exec` guard fires immediately — the custom node is an external caller in `{userDir}/node_modules/` and the stack-frame check identifies it. No indirection through a built-in node is needed for that check to apply.\n\nIn short: calling through an exec node requires the exec node to already be trusted and deployed by an authenticated operator. At that point the exec node is part of the authorised flow, not an exploit path.\n\n---\n\n**\"So Sentinel only protects against palette nodes?\"**\n\nThat is the primary threat model: a developer installs a palette node from npm that looks legitimate but contains hidden malicious code. Without Sentinel, that code runs automatically the moment Node-RED loads the node, with full access to credentials, the file system, the network, and the ability to exec arbitrary processes — none of it visible in any flow graph.\n\nCore nodes are part of the operator's explicit flow design. They are chosen deliberately, they are visible, and they can only be activated through a deployment that the Sentinel approval queue has cleared. That is a fundamentally different attack surface from a supply-chain compromise hidden inside an npm package dependency.\n\n## Local / Host install\n\nInstall Sentinel into your Node-RED user directory:\n\n```bash\ncd ~/.node-red\nnpm install @allanoricil/nrg-sentinel\n```\n\nNode-RED auto-discovers plugins in `~/.node-red/node_modules/`, so the Sentinel sidebar and plugin features load automatically on the next restart. No extra configuration is needed for that.\n\nTo activate the **preload guard** (module-level interception), set `NODE_OPTIONS` before starting Node-RED:\n\n```bash\nNODE_OPTIONS=\"--require @allanoricil/nrg-sentinel/preload\" node-red\n```\n\nTo make this permanent, add it to your startup script, systemd unit, or shell profile:\n\n```bash\n# ~/.bashrc or ~/.zshrc\nexport NODE_OPTIONS=\"--require @allanoricil/nrg-sentinel/preload\"\n```\n\n> **Why not `./node_modules/.bin/node-red`?**\n> The `node-red` package itself is not installed inside `~/.node-red` — it lives in the global `node_modules`. The Sentinel wrapper binary handles both cases automatically: when `node-red` is co-installed in the same `node_modules` tree (Docker) it resolves the entrypoint directly; otherwise it finds `node-red` via PATH. Either way, the preload is injected via `NODE_OPTIONS`.\n\n## Docker\n\nThe [`Dockerfile`](Dockerfile) produces a hardened production image. The security model rests on three layers.\n\n### Filesystem layout\n\n```\n/usr/src/nodered   owned by root, chmod a-w   Node-RED + Sentinel install\n/etc/nodered       owned by root, chmod a-w   settings.js (read-only config)\n/data              owned by nodered           flows, credentials, c","readmeFilename":"README.md"}