{"_id":"@rodit/rodit-auth-be","_rev":"472-6297e69daaa0b29f7a66ba887004e158","name":"@rodit/rodit-auth-be","dist-tags":{"latest":"9.16.2"},"versions":{"9.11.14":{"name":"@rodit/rodit-auth-be","version":"9.11.14","keywords":["authentication","express","middleware","jwt","rodit"],"author":{"name":"Discernible IO"},"license":"UNLICENSED","_id":"@rodit/rodit-auth-be@9.11.14","maintainers":[{"name":"rodit","email":"npmjs+npmjs@discernible.io"}],"homepage":"https://github.com/discernible-io/rodit-auth-be#readme","bugs":{"url":"https://github.com/discernible-io/rodit-auth-be/issues"},"dist":{"shasum":"1624229124160da7f0d0bef8d1447d74ae18a675","tarball":"https://registry.npmjs.org/@rodit/rodit-auth-be/-/rodit-auth-be-9.11.14.tgz","fileCount":28,"integrity":"sha512-oqian+PpFN+7YzqAPrFD8aO2e/32yZdXNMquRX/2h99D9isah5fJrrb+Oux//YOvGPs9yRVRXUHVVnQrgsnu2Q==","signatures":[{"sig":"MEUCIQDcdHod8QEaEsSoBs0tiRm/FDPOeItiwO4poNa9cVYIfAIgU1QzPefXy3obuDpJiClaCJyiSKrJkyqHWWG3cCtiSZI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":735198},"main":"index.js","engines":{"node":">=18.0.0"},"exports":{".":"./index.js"},"gitHead":"0045fc5631caa662ff0ed81cc401ee383b1d977f","private":false,"scripts":{},"_npmUser":{"name":"rodit","email":"npmjs+npmjs@discernible.io"},"repository":{"url":"git+https://github.com/discernible-io/rodit-auth-be.git","type":"git"},"_npmVersion":"10.9.7","description":"RODiT-based authentication system for Express.js applications","directories":{},"_nodeVersion":"22.22.2","dependencies":{"jose":"^5.6.3","ulid":"^3.0.2","undici":"^6.0.0","tweetnacl":"^1.0.3","tweetnacl-util":"^0.15.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{},"peerDependencies":{"bs58":">=5 <6","borsh":"^2.0.0","config":"^4.0.0","express":">=4.18 <6","winston":"^3.0.0","node-vault":"^0.10.0","express-session":"^1.17.0 || ^1.18.0","express-rate-limit":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/rodit-auth-be_9.11.14_1780498015115_0.31098931778625594","host":"s3://npm-registry-packages-npm-production"}},"9.11.16":{"name":"@rodit/rodit-auth-be","version":"9.11.16","keywords":["authentication","express","middleware","jwt","rodit"],"author":{"name":"Discernible IO"},"license":"UNLICENSED","_id":"@rodit/rodit-auth-be@9.11.16","maintainers":[{"name":"rodit","email":"npmjs+npmjs@discernible.io"}],"homepage":"https://github.com/discernible-io/rodit-auth-be#readme","bugs":{"url":"https://github.com/discernible-io/rodit-auth-be/issues"},"dist":{"shasum":"4a7ac484ae76830b9e064445e5b9cbcf06605634","tarball":"https://registry.npmjs.org/@rodit/rodit-auth-be/-/rodit-auth-be-9.11.16.tgz","fileCount":28,"integrity":"sha512-u5sFRnI/AO/+M61xDNzVHPu4rWF0nNQGWdkkwFnG1OzIKfXI981gq4J88uiPb+yVblT+ZIujxXEINnUBJycVbw==","signatures":[{"sig":"MEYCIQC1idtJ6o6xisytsFuQCLvN12vxF4zOh75JHI7MfpBExgIhALNvil4hwy44We6qJX+OSQTsIO376QLA+omq58fQhnV+","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":736147},"main":"index.js","engines":{"node":">=18.0.0"},"exports":{".":"./index.js"},"gitHead":"a6cae9491f80e9b3013915b1a1fb6e1a4c837974","private":false,"scripts":{},"_npmUser":{"name":"rodit","email":"npmjs+npmjs@discernible.io"},"repository":{"url":"git+https://github.com/discernible-io/rodit-auth-be.git","type":"git"},"_npmVersion":"10.9.7","description":"RODiT-based authentication system for Express.js applications","directories":{},"_nodeVersion":"22.22.2","dependencies":{"jose":"^5.6.3","ulid":"^3.0.2","undici":"^6.0.0","tweetnacl":"^1.0.3","tweetnacl-util":"^0.15.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{},"peerDependencies":{"bs58":">=5 <6","borsh":"^2.0.0","config":"^4.0.0","express":">=4.18 <6","winston":"^3.0.0","node-vault":"^0.10.0","express-session":"^1.17.0 || ^1.18.0","express-rate-limit":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/rodit-auth-be_9.11.16_1780756011042_0.668850767430651","host":"s3://npm-registry-packages-npm-production"}},"9.11.20":{"name":"@rodit/rodit-auth-be","version":"9.11.20","keywords":["authentication","express","middleware","jwt","rodit"],"author":{"name":"Discernible IO"},"license":"UNLICENSED","_id":"@rodit/rodit-auth-be@9.11.20","maintainers":[{"name":"rodit","email":"npmjs+npmjs@discernible.io"}],"homepage":"https://github.com/discernible-io/rodit-auth-be#readme","bugs":{"url":"https://github.com/discernible-io/rodit-auth-be/issues"},"dist":{"shasum":"63e556128167e8849533d8c54db60f9ad2281cff","tarball":"https://registry.npmjs.org/@rodit/rodit-auth-be/-/rodit-auth-be-9.11.20.tgz","fileCount":28,"integrity":"sha512-0L4UCD4p8NpZ+lVyY7wv8aPa0fijDqisjIYZRJboOM4NyWBdmTEEQ/z+4yKZHox4ZGAGckUSLy3xZv1jdtdxVw==","signatures":[{"sig":"MEYCIQDMY1829gJyrQOmzUpuxZS7ScRViE3DIvklW2iedgrVCgIhANLE2ZvGCwaxvxonA9h4rWJshgGvarX/mKr2OE9R3d6q","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":741026},"main":"index.js","engines":{"node":">=18.0.0"},"exports":{".":"./index.js"},"gitHead":"a6cae9491f80e9b3013915b1a1fb6e1a4c837974","private":false,"scripts":{},"_npmUser":{"name":"rodit","email":"npmjs+npmjs@discernible.io"},"repository":{"url":"git+https://github.com/discernible-io/rodit-auth-be.git","type":"git"},"_npmVersion":"10.9.7","description":"RODiT-based authentication system for Express.js applications","directories":{},"_nodeVersion":"22.22.2","dependencies":{"jose":"^5.6.3","ulid":"^3.0.2","undici":"^6.0.0","tweetnacl":"^1.0.3","tweetnacl-util":"^0.15.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{},"peerDependencies":{"bs58":">=5 <6","borsh":"^2.0.0","config":"^4.0.0","express":">=4.18 <6","winston":"^3.0.0","node-vault":"^0.10.0","express-session":"^1.17.0 || ^1.18.0","express-rate-limit":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/rodit-auth-be_9.11.20_1780765291457_0.1437524391250795","host":"s3://npm-registry-packages-npm-production"}},"9.12.0":{"name":"@rodit/rodit-auth-be","version":"9.12.0","keywords":["authentication","express","middleware","jwt","rodit"],"author":{"name":"Discernible IO"},"license":"UNLICENSED","_id":"@rodit/rodit-auth-be@9.12.0","maintainers":[{"name":"rodit","email":"npmjs+npmjs@discernible.io"}],"homepage":"https://github.com/discernible-io/rodit-auth-be#readme","bugs":{"url":"https://github.com/discernible-io/rodit-auth-be/issues"},"dist":{"shasum":"8ee89fc7ab9eefec834c1b4a36534cf2df949986","tarball":"https://registry.npmjs.org/@rodit/rodit-auth-be/-/rodit-auth-be-9.12.0.tgz","fileCount":28,"integrity":"sha512-Z1pfvJ3lHvF8QThY/29i/ITxAwbtfW4m507oC7/8LDb2vO13VIYXRkDDRnZoKy+lQiOSb02QoMcBn2cMLrdM/w==","signatures":[{"sig":"MEUCIGcIdJyE1OJIKFjLrRO5wnLDknkV7VEuhDOipjD4dzb2AiEA7b+GyfViM8tp5PGRsg1WUGoCXeM1Br/HeCNVlGSLCYQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":768568},"main":"index.js","engines":{"node":">=18.0.0"},"exports":{".":"./index.js"},"gitHead":"40e9c4d4ae58b076e69f66a9bf6cd21d14d60fc1","private":false,"scripts":{},"_npmUser":{"name":"rodit","email":"npmjs+npmjs@discernible.io"},"repository":{"url":"git+https://github.com/discernible-io/rodit-auth-be.git","type":"git"},"_npmVersion":"10.9.7","description":"RODiT-based authentication system for Express.js applications","directories":{},"_nodeVersion":"22.22.2","dependencies":{"jose":"^5.6.3","ulid":"^3.0.2","undici":"^6.0.0","tweetnacl":"^1.0.3","tweetnacl-util":"^0.15.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{},"peerDependencies":{"bs58":">=5 <6","borsh":"^2.0.0","config":"^4.0.0","express":">=4.18 <6","winston":"^3.0.0","node-vault":"^0.10.0","express-session":"^1.17.0 || ^1.18.0","express-rate-limit":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/rodit-auth-be_9.12.0_1783076237126_0.2838678536584227","host":"s3://npm-registry-packages-npm-production"}},"9.13.0":{"name":"@rodit/rodit-auth-be","version":"9.13.0","keywords":["authentication","express","middleware","jwt","rodit"],"author":{"name":"Discernible IO"},"license":"UNLICENSED","_id":"@rodit/rodit-auth-be@9.13.0","maintainers":[{"name":"rodit","email":"npmjs+npmjs@discernible.io"}],"homepage":"https://github.com/discernible-io/rodit-auth-be#readme","bugs":{"url":"https://github.com/discernible-io/rodit-auth-be/issues"},"dist":{"shasum":"b08321fc462050bd1dbe768d7d103ad819994fad","tarball":"https://registry.npmjs.org/@rodit/rodit-auth-be/-/rodit-auth-be-9.13.0.tgz","fileCount":28,"integrity":"sha512-/YQzAC7b2xPhzOtDUl9iDu9h+HvMBV2CgCrs+BxknIHjFee5GzU9pCVVlr/Jku8fOK33pB9WqOsyg/3I0AWp2Q==","signatures":[{"sig":"MEYCIQD4JWlgmmeCUUz5YUZTSLjC1VpHCnbXwyBF4YqIV0/OggIhAIytBtMJyRuwrHiETEYVGABAQU1l7CB/GIxYNKqAdNng","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":752826},"main":"index.js","engines":{"node":">=18.0.0"},"exports":{".":"./index.js"},"gitHead":"504f3600ceb4f7b78b510f5e241ae0beecfe76cd","private":false,"scripts":{},"_npmUser":{"name":"rodit","email":"npmjs+npmjs@discernible.io"},"repository":{"url":"git+https://github.com/discernible-io/rodit-auth-be.git","type":"git"},"_npmVersion":"10.9.7","description":"RODiT-based authentication system for Express.js applications","directories":{},"_nodeVersion":"22.22.2","dependencies":{"jose":"^5.6.3","ulid":"^3.0.2","undici":"^6.0.0","tweetnacl":"^1.0.3","tweetnacl-util":"^0.15.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{},"peerDependencies":{"bs58":">=5 <6","borsh":"^2.0.0","config":"^4.0.0","express":">=4.18 <6","winston":"^3.0.0","node-vault":"^0.10.0","express-session":"^1.17.0 || ^1.18.0","express-rate-limit":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/rodit-auth-be_9.13.0_1784151175832_0.9302444795009304","host":"s3://npm-registry-packages-npm-production"}},"9.14.0":{"name":"@rodit/rodit-auth-be","version":"9.14.0","keywords":["authentication","express","middleware","jwt","rodit"],"author":{"name":"Discernible IO"},"license":"UNLICENSED","_id":"@rodit/rodit-auth-be@9.14.0","maintainers":[{"name":"rodit","email":"npmjs+npmjs@discernible.io"}],"homepage":"https://github.com/discernible-io/rodit-auth-be#readme","bugs":{"url":"https://github.com/discernible-io/rodit-auth-be/issues"},"dist":{"shasum":"589b7c87ff75d0b0ac6f8d583931d9fb39486bc1","tarball":"https://registry.npmjs.org/@rodit/rodit-auth-be/-/rodit-auth-be-9.14.0.tgz","fileCount":28,"integrity":"sha512-YK5m2hhdvFz3zvrrPv8z/gXIr1nEIdm1L4kw4jn5VzxkoCV45XAEhGn3Y4++RRopZjEpzvfgPdTU0RW3N6KcaA==","signatures":[{"sig":"MEQCIANbfj3Z9Q5SB/AFrP4VxfRCMl9TAz0E5h5ONfW1eFUJAiBxCVBOxmTIgsyYq7Ryg5OLVHkvdWKvFMcf3hZhr/wJoA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":751870},"main":"index.js","engines":{"node":">=18.0.0"},"exports":{".":"./index.js"},"gitHead":"b422a536aaf59e32be3a4532fb0412b2c657c1a2","private":false,"scripts":{},"_npmUser":{"name":"rodit","email":"npmjs+npmjs@discernible.io"},"repository":{"url":"git+https://github.com/discernible-io/rodit-auth-be.git","type":"git"},"_npmVersion":"10.9.7","description":"RODiT-based authentication system for Express.js applications","directories":{},"_nodeVersion":"22.22.2","dependencies":{"jose":"^5.6.3","ulid":"^3.0.2","undici":"^6.0.0","tweetnacl":"^1.0.3","tweetnacl-util":"^0.15.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{},"peerDependencies":{"bs58":">=5 <6","borsh":"^2.0.0","config":"^4.0.0","express":">=4.18 <6","winston":"^3.0.0","node-vault":"^0.10.0","express-session":"^1.17.0 || ^1.18.0","express-rate-limit":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/rodit-auth-be_9.14.0_1784193242982_0.7324713720019407","host":"s3://npm-registry-packages-npm-production"}},"9.15.0":{"name":"@rodit/rodit-auth-be","version":"9.15.0","keywords":["authentication","express","middleware","jwt","rodit"],"author":{"name":"Discernible IO"},"license":"UNLICENSED","_id":"@rodit/rodit-auth-be@9.15.0","maintainers":[{"name":"rodit","email":"npmjs+npmjs@discernible.io"}],"homepage":"https://github.com/discernible-io/rodit-auth-be#readme","bugs":{"url":"https://github.com/discernible-io/rodit-auth-be/issues"},"dist":{"shasum":"b6357c6a881f4e11bfebda75cb106b58e1c420bb","tarball":"https://registry.npmjs.org/@rodit/rodit-auth-be/-/rodit-auth-be-9.15.0.tgz","fileCount":29,"integrity":"sha512-XNH8Pqunpuzo/4SDLPM29kncON5EGPxbpa72z8t0WieNufD12Mu59YgS40EfXgMSk5rAzSUlQqMY+G1votWeWw==","signatures":[{"sig":"MEUCIQCoQjjkupSKOJXJ+5ZyJ+XcbVquNGjQJHjSjTOnyDuGyQIgJI0stqNaOgDhyVUsKG5ZJ04qpNEVKTUjktBgGvDUnT4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":774745},"main":"index.js","engines":{"node":">=18.0.0"},"exports":{".":"./index.js"},"gitHead":"6ca6267536f2ac2182ac85f16d40fe227bb5e913","private":false,"scripts":{},"_npmUser":{"name":"rodit","email":"npmjs+npmjs@discernible.io"},"repository":{"url":"git+https://github.com/discernible-io/rodit-auth-be.git","type":"git"},"_npmVersion":"10.9.8","description":"RODiT-based authentication system for Express.js applications","directories":{},"_nodeVersion":"22.23.1","dependencies":{"jose":"^5.6.3","ulid":"^3.0.2","undici":"^6.0.0","tweetnacl":"^1.0.3","tweetnacl-util":"^0.15.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{},"peerDependencies":{"bs58":">=5 <6","borsh":"^2.0.0","config":"^4.0.0","express":">=4.18 <6","winston":"^3.0.0","node-vault":"^0.10.0","express-session":"^1.17.0 || ^1.18.0","express-rate-limit":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/rodit-auth-be_9.15.0_1786433894817_0.7148350573800188","host":"s3://npm-registry-packages-npm-production"}},"9.16.0":{"name":"@rodit/rodit-auth-be","version":"9.16.0","keywords":["authentication","express","middleware","jwt","rodit"],"author":{"name":"Discernible IO"},"license":"UNLICENSED","_id":"@rodit/rodit-auth-be@9.16.0","maintainers":[{"name":"rodit","email":"npmjs+npmjs@discernible.io"}],"homepage":"https://github.com/discernible-io/rodit-sdk/tree/main/rodit-auth-be#readme","bugs":{"url":"https://github.com/discernible-io/rodit-sdk/issues"},"dist":{"shasum":"783209e611c6ea49b8efeb7f7ca2bcc2473e4c3a","tarball":"https://registry.npmjs.org/@rodit/rodit-auth-be/-/rodit-auth-be-9.16.0.tgz","fileCount":29,"integrity":"sha512-ZM1m2FvP03LBgVUHtC1vlOfe1YKbORB3NUIqhntsMqlzmpbgqims8osZ69bqMYb0vIe7FI2NBEKmz4NhiQeYCg==","signatures":[{"sig":"MEUCIH1x56u/nCElMC9uEgAwN0Qzd2q57a0z6RVHhXqkV4rUAiEAhpDnn/Jb9E9X2S9Vw4H1AN/mtqnOTclrpq3gCrBxUbY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIA0DsfzgCsEE8qrolHxCD+4iuzxEAv8KAffuyr1y6nTvAiEA9wMeImmF1laXNXNwKOym6KnpCGY9ZcutF5d8KgO/Jl0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":770569},"main":"index.js","engines":{"node":">=18.0.0"},"exports":{".":"./index.js"},"gitHead":"afd6b4bb8aaa4249a560d0e060d8869fe004c5e1","private":false,"scripts":{},"_npmUser":{"name":"rodit","email":"npmjs+npmjs@discernible.io"},"repository":{"url":"git+https://github.com/discernible-io/rodit-sdk.git","type":"git","directory":"rodit-auth-be"},"_npmVersion":"10.9.8","description":"RODiT-based authentication system for Express.js applications","directories":{},"_nodeVersion":"22.23.1","dependencies":{"jose":"^5.6.3","ulid":"^3.0.2","undici":"^6.0.0","tweetnacl":"^1.0.3","tweetnacl-util":"^0.15.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{},"peerDependencies":{"bs58":">=5 <6","borsh":"^2.0.0","config":"^4.0.0","express":">=4.18 <6","winston":"^3.0.0","node-vault":"^0.10.0","express-session":"^1.17.0 || ^1.18.0","express-rate-limit":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/rodit-auth-be_9.16.0_1789370986115_0.766538599320125","host":"s3://npm-registry-packages-npm-production"}},"9.16.2":{"_id":"@rodit/rodit-auth-be@9.16.2","bugs":{"url":"https://github.com/discernible-io/rodit-sdk/issues"},"dist":{"shasum":"fc6e432e0f52238dca201ca074b9978c9ab9816e","tarball":"https://registry.npmjs.org/@rodit/rodit-auth-be/-/rodit-auth-be-9.16.2.tgz","fileCount":30,"integrity":"sha512-4SHuk0VxtpvoKeRH/4nTCk/yJFPeqIVGExwvTa+VkGod7UcbIsylfrm2z8sSVvDjNwfxMcvwS3Vwqv8ssEMjNQ==","signatures":[{"sig":"MEUCIQDVnLHb5Yvz0OKoJcIqcCctbtjFwSTLQ9W/geZDJvoTdAIgRiDx3bK7fTBs/xzJ7cczyt8bO1DpGy0JQqRONg3hu4o=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCOn00y8F41+KFMRaiNkGFQP9f1qEIiVc9/zmxalniVDAIhALYVfk86UkDK7NMTWH/tg1n6Dlv9xLDarfRkmsnGmNWu"}],"unpackedSize":781530},"main":"index.js","name":"@rodit/rodit-auth-be","author":{"name":"Discernible IO"},"engines":{"node":">=18.0.0"},"exports":{".":"./index.js"},"gitHead":"ee240a29e346334678eddbc29d00779ee045dc07","license":"UNLICENSED","private":false,"scripts":{},"version":"9.16.2","_npmUser":{"name":"rodit","email":"npmjs+npmjs@discernible.io"},"homepage":"https://github.com/discernible-io/rodit-sdk/tree/main/rodit-auth-be#readme","keywords":["authentication","express","middleware","jwt","rodit"],"repository":{"url":"git+https://github.com/discernible-io/rodit-sdk.git","type":"git","directory":"rodit-auth-be"},"_npmVersion":"10.9.8","description":"RODiT-based authentication system for Express.js applications","directories":{},"maintainers":[{"name":"rodit","email":"npmjs+npmjs@discernible.io"}],"_nodeVersion":"22.23.2","dependencies":{"jose":"^5.6.3","ulid":"^3.0.2","undici":"^6.0.0","tweetnacl":"^1.0.3","tweetnacl-util":"^0.15.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{},"peerDependencies":{"bs58":">=5 <6","borsh":"^2.0.0","config":"^4.0.0","express":">=4.18 <6","winston":"^3.0.0","node-vault":"^0.10.0","express-session":"^1.17.0 || ^1.18.0","express-rate-limit":"^8.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/rodit-auth-be_9.16.2_1789810337481_0.012505119089830208"}}},"time":{"created":"2025-08-31T09:55:28.598Z","modified":"2026-09-19T09:32:17.777Z","1.0.1":"2025-08-31T09:55:29.000Z","1.0.2":"2025-08-31T11:04:00.388Z","1.0.3":"2025-08-31T11:13:52.488Z","1.0.4":"2025-08-31T11:24:13.913Z","1.0.5":"2025-09-01T11:57:36.068Z","1.0.6":"2025-09-01T12:12:36.691Z","1.0.8":"2025-09-01T19:33:12.049Z","1.0.9":"2025-09-02T08:56:00.935Z","1.0.10":"2025-09-02T09:45:34.823Z","1.0.11":"2025-09-02T10:19:17.643Z","1.0.12-x":"2025-09-02T12:34:59.082Z","1.0.20":"2025-09-03T10:38:22.644Z","1.0.21":"2025-09-04T09:42:52.062Z","1.0.22":"2025-09-04T09:52:37.958Z","1.0.23":"2025-09-04T10:22:26.005Z","1.0.24":"2025-09-04T10:43:23.122Z","1.0.25":"2025-09-05T07:40:16.413Z","1.0.26":"2025-09-05T10:22:43.268Z","1.0.27":"2025-09-05T10:25:38.110Z","1.0.28":"2025-09-05T10:55:27.354Z","1.0.29":"2025-09-08T13:59:48.660Z","1.0.31":"2025-09-08T14:41:56.068Z","1.0.32":"2025-09-09T10:10:58.897Z","1.0.33":"2025-09-09T11:17:15.850Z","1.0.34":"2025-09-09T14:46:58.026Z","1.0.35":"2025-09-12T15:44:12.308Z","1.0.36":"2025-09-12T16:12:30.890Z","1.0.37":"2025-09-13T06:40:49.103Z","1.0.38":"2025-09-13T08:05:01.191Z","1.0.39":"2025-09-13T10:16:41.287Z","1.0.40":"2025-09-13T16:33:27.201Z","1.1.0":"2025-09-14T10:39:29.426Z","1.1.1":"2025-09-14T10:46:26.356Z","1.1.3":"2025-09-16T08:59:29.085Z","1.1.4":"2025-09-17T13:55:31.140Z","1.1.5":"2025-09-17T14:23:15.598Z","1.1.6":"2025-09-17T14:33:22.415Z","1.1.7":"2025-09-17T15:09:59.012Z","1.1.8":"2025-09-17T15:56:35.156Z","1.2.0":"2025-09-18T12:50:37.555Z","1.2.1":"2025-09-18T13:38:10.771Z","1.2.2":"2025-09-18T13:54:57.873Z","1.2.3":"2025-09-18T16:42:05.056Z","1.2.31":"2025-09-18T16:55:38.893Z","1.2.33":"2025-09-19T11:20:11.419Z","1.3.0":"2025-09-19T11:46:44.284Z","1.3.1":"2025-09-19T12:55:12.356Z","1.4.0":"2025-09-19T13:28:17.732Z","1.4.4":"2025-09-19T14:52:53.527Z","1.4.5":"2025-09-19T14:58:41.149Z","1.4.6":"2025-09-19T15:08:23.787Z","1.4.7":"2025-09-19T15:49:59.793Z","1.4.8":"2025-09-19T15:56:23.710Z","1.5.0":"2025-09-19T16:17:05.309Z","1.5.1":"2025-09-19T16:29:40.636Z","1.5.2":"2025-09-19T17:19:24.292Z","2.0.0":"2025-09-19T18:55:55.964Z","2.0.1":"2025-09-20T05:24:21.633Z","2.0.2":"2025-09-20T08:09:50.705Z","2.0.3":"2025-09-20T08:59:45.971Z","2.0.4":"2025-09-20T10:58:51.928Z","2.0.5":"2025-09-20T12:00:59.395Z","2.0.6":"2025-09-20T12:18:08.833Z","2.0.7":"2025-09-20T14:19:06.590Z","2.0.8":"2025-09-21T08:34:25.319Z","2.0.9":"2025-09-21T13:15:06.831Z","2.0.10":"2025-09-22T12:27:40.383Z","2.0.11":"2025-09-23T13:56:34.016Z","2.0.12":"2025-09-24T11:57:08.111Z","2.1.0":"2025-09-24T12:43:21.527Z","2.1.1":"2025-09-24T13:10:22.175Z","2.1.2":"2025-09-24T13:32:31.847Z","2.1.3":"2025-09-24T15:05:38.690Z","2.1.4":"2025-09-24T15:37:46.448Z","2.1.5":"2025-09-25T05:08:01.446Z","2.1.6":"2025-09-25T06:22:01.264Z","2.1.7":"2025-09-25T17:48:22.758Z","2.1.8":"2025-09-26T10:53:38.845Z","2.1.9":"2025-09-26T11:23:22.991Z","2.2.0":"2025-09-26T15:53:07.002Z","2.2.1":"2025-09-26T17:24:46.571Z","2.2.2":"2025-09-26T17:44:13.330Z","2.2.3":"2025-09-27T07:14:40.478Z","2.2.4":"2025-09-27T07:28:41.522Z","2.2.5":"2025-09-27T08:00:41.155Z","2.2.6":"2025-09-27T08:15:44.020Z","2.2.7":"2025-09-27T08:47:52.573Z","2.2.8":"2025-09-27T13:18:02.030Z","2.2.9":"2025-09-28T08:12:51.096Z","2.2.10":"2025-09-28T08:26:07.908Z","2.2.11":"2025-09-28T09:01:52.833Z","2.3.0":"2025-09-28T10:19:14.170Z","2.3.1":"2025-09-28T10:46:19.191Z","2.3.2":"2025-09-28T10:55:11.857Z","2.3.3":"2025-09-28T11:03:20.348Z","2.3.4":"2025-09-28T11:13:14.457Z","2.3.5":"2025-09-28T13:08:59.955Z","2.3.6":"2025-09-28T13:28:48.980Z","2.4.1":"2025-09-28T14:01:48.158Z","2.4.2":"2025-09-28T14:09:09.596Z","2.4.3":"2025-09-28T14:22:39.537Z","2.4.4":"2025-09-28T16:36:07.906Z","2.4.5":"2025-09-28T16:48:23.133Z","2.4.6":"2025-09-29T08:03:22.023Z","2.4.7":"2025-09-29T08:51:15.262Z","2.4.8":"2025-09-29T09:01:03.978Z","2.4.9":"2025-09-29T09:28:45.815Z","2.4.10":"2025-09-29T09:50:40.308Z","2.4.11":"2025-09-29T10:14:24.239Z","2.4.12":"2025-09-29T10:26:54.941Z","2.4.13":"2025-09-29T11:25:52.308Z","2.4.14":"2025-09-29T11:47:51.046Z","2.4.15":"2025-09-29T12:09:03.151Z","2.5.1":"2025-09-29T13:22:17.184Z","2.5.2":"2025-09-29T13:39:50.836Z","2.5.3":"2025-09-29T14:45:57.844Z","2.5.4":"2025-09-29T15:12:38.696Z","2.5.6":"2025-09-29T15:23:54.584Z","2.5.7":"2025-09-29T15:57:43.792Z","2.6.0":"2025-09-30T07:27:40.459Z","2.6.1":"2025-09-30T08:19:15.725Z","2.6.2":"2025-09-30T08:22:43.059Z","2.6.4":"2025-09-30T12:03:01.854Z","2.6.5":"2025-09-30T12:49:44.878Z","2.6.6":"2025-09-30T13:00:32.044Z","2.6.7":"2025-09-30T13:39:10.707Z","2.6.8":"2025-09-30T14:25:17.032Z","2.6.9":"2025-10-01T12:07:46.327Z","2.6.11":"2025-10-01T12:34:36.724Z","2.6.12":"2025-10-01T13:31:46.221Z","2.6.13":"2025-10-01T15:32:54.107Z","2.6.14":"2025-10-01T16:58:23.090Z","2.6.15":"2025-10-02T10:13:13.246Z","2.6.16":"2025-10-02T12:30:33.878Z","2.6.17":"2025-10-02T12:52:28.490Z","2.6.18":"2025-10-07T14:27:15.401Z","2.6.19":"2025-10-08T11:31:04.142Z","2.6.20":"2025-10-09T19:36:40.128Z","2.7.0":"2025-10-09T20:26:16.035Z","2.7.1":"2025-10-09T20:35:33.018Z","2.7.3":"2025-10-11T13:45:19.166Z","2.7.4":"2025-10-12T13:50:20.553Z","2.7.5":"2025-10-13T15:48:02.992Z","2.7.6":"2025-10-20T09:13:09.144Z","2.7.7":"2025-10-20T12:14:25.468Z","2.7.8":"2025-10-20T12:21:48.707Z","2.7.9":"2025-10-20T13:26:56.541Z","2.7.10":"2025-10-20T13:42:45.674Z","2.8.0":"2025-10-26T09:46:23.702Z","2.9.0":"2025-11-01T08:38:52.001Z","3.0.0":"2025-11-02T11:18:35.277Z","3.0.1":"2025-11-02T11:33:17.055Z","3.0.2":"2025-11-02T12:03:51.138Z","3.0.3":"2025-11-07T16:23:29.646Z","3.0.4":"2025-11-07T16:56:18.289Z","3.0.5":"2025-11-07T17:03:42.186Z","4.0.0":"2025-11-08T17:13:09.038Z","4.0.1":"2025-11-08T17:15:00.098Z","4.0.2":"2025-11-08T17:22:16.468Z","3.0.6":"2025-11-10T09:47:51.372Z","3.0.8":"2025-11-13T15:22:04.610Z","3.0.9":"2025-11-14T06:20:42.475Z","3.1.0":"2025-11-14T12:03:01.456Z","4.0.3":"2026-04-02T15:07:32.809Z","4.0.4":"2026-04-05T14:24:24.057Z","4.0.5":"2026-04-06T08:36:18.062Z","4.0.6":"2026-04-06T09:01:11.289Z","4.0.7":"2026-04-10T08:08:06.558Z","4.0.8":"2026-04-10T08:21:21.211Z","4.0.9":"2026-04-10T08:29:14.453Z","4.1.0":"2026-04-11T07:31:18.633Z","4.1.1":"2026-04-11T07:43:33.123Z","5.0.1":"2026-04-11T10:40:57.500Z","5.1.0":"2026-04-13T12:46:40.639Z","5.2.0":"2026-04-14T10:07:21.064Z","5.2.1":"2026-04-15T06:36:16.998Z","5.2.2":"2026-04-15T07:32:48.513Z","6.0.0":"2026-04-15T09:20:34.886Z","6.0.1":"2026-04-17T14:43:34.327Z","6.0.2":"2026-04-17T15:08:35.903Z","6.0.3":"2026-04-22T05:43:20.545Z","6.0.4":"2026-04-23T14:02:12.203Z","6.1.0":"2026-04-24T13:33:29.063Z","6.2.0":"2026-04-24T14:34:18.183Z","6.2.1":"2026-04-27T17:21:11.380Z","6.3.0":"2026-04-28T05:47:28.424Z","6.3.1":"2026-04-28T06:03:18.477Z","6.4.0":"2026-04-30T16:34:14.724Z","7.0.0":"2026-05-01T08:41:54.588Z","7.0.1":"2026-05-01T09:12:02.054Z","8.0.0":"2026-05-02T18:27:47.747Z","8.1.0":"2026-05-02T19:41:12.164Z","8.1.1":"2026-05-02T19:45:17.063Z","8.1.2":"2026-05-03T08:42:38.720Z","8.1.3":"2026-05-05T09:04:00.917Z","8.1.4":"2026-05-05T09:17:49.245Z","8.1.5":"2026-05-05T09:42:32.531Z","8.1.6":"2026-05-05T09:52:23.305Z","8.6.6":"2026-05-05T11:06:52.950Z","8.6.7":"2026-05-05T11:29:01.786Z","9.0.0":"2026-05-05T11:51:17.977Z","9.0.1":"2026-05-05T11:59:08.895Z","9.0.2":"2026-05-05T13:53:42.431Z","9.0.3":"2026-05-05T14:44:52.884Z","9.0.5":"2026-05-05T21:36:30.242Z","9.8.0":"2026-05-06T06:04:03.589Z","9.8.1":"2026-05-06T07:07:26.679Z","9.8.2":"2026-05-06T07:45:29.448Z","9.8.3":"2026-05-06T11:40:12.570Z","9.8.4":"2026-05-06T15:52:28.872Z","9.8.5":"2026-05-06T18:26:12.426Z","9.9.0":"2026-05-06T19:12:44.760Z","9.9.9":"2026-05-06T19:24:04.432Z","9.9.10":"2026-05-06T19:31:12.464Z","9.9.11":"2026-05-06T19:54:54.710Z","9.9.13":"2026-05-07T04:31:49.386Z","9.9.14":"2026-05-07T09:01:55.512Z","9.9.15":"2026-05-07T13:27:35.746Z","9.9.16":"2026-05-10T07:29:50.583Z","9.9.17":"2026-05-10T09:16:22.696Z","9.9.18":"2026-05-10T09:31:01.943Z","9.9.20":"2026-05-10T09:57:33.694Z","9.9.21":"2026-05-10T10:11:53.319Z","9.10.1":"2026-05-15T13:01:58.129Z","9.10.2":"2026-05-18T10:50:54.298Z","9.10.4":"2026-05-20T10:22:24.832Z","9.10.6":"2026-05-22T10:07:50.809Z","9.10.8":"2026-06-01T14:37:22.061Z","9.11.12":"2026-06-01T20:48:43.933Z","9.11.14":"2026-06-03T14:46:55.344Z","9.11.16":"2026-06-06T14:26:51.225Z","9.11.20":"2026-06-06T17:01:31.640Z","9.12.0":"2026-07-03T10:57:17.328Z","9.13.0":"2026-07-15T21:32:56.071Z","9.14.0":"2026-07-16T09:14:03.148Z","9.15.0":"2026-08-11T07:38:14.962Z","9.16.0":"2026-09-14T07:29:46.203Z","9.16.2":"2026-09-19T09:32:17.611Z"},"bugs":{"url":"https://github.com/discernible-io/rodit-sdk/issues"},"author":{"name":"Discernible IO"},"license":"UNLICENSED","homepage":"https://github.com/discernible-io/rodit-sdk/tree/main/rodit-auth-be#readme","keywords":["authentication","express","middleware","jwt","rodit"],"repository":{"url":"git+https://github.com/discernible-io/rodit-sdk.git","type":"git","directory":"rodit-auth-be"},"description":"RODiT-based authentication system for Express.js applications","maintainers":[{"name":"rodit","email":"npmjs+npmjs@discernible.io"}],"readme":"# @rodit/rodit-auth-be\n\nNode.js / Express SDK for RODiT-based mutual authentication, authorization, session management, and webhooks.\n\n**npm:** [https://www.npmjs.com/package/@rodit/rodit-auth-be](https://www.npmjs.com/package/@rodit/rodit-auth-be)  \n**Monorepo:** [discernible-io/rodit-sdk](https://github.com/discernible-io/rodit-sdk) (this directory)  \n**Version:** 9.16.2 · **License:** Proprietary · **Author:** Discernible IO\n\n## Production deployments\n\nThis package is the server runtime behind RODiT-powered APIs, including:\n\n| Deployment | Role |\n|------------|------|\n| [api.identyclaw.com](https://api.identyclaw.com) | IdentyClaw platform API ([source: api-idc](https://github.com/discernible-io/api-idc)) |\n| [api.lastcradle.io](https://api.lastcradle.io) | Last Cradle family API |\n| Custom / federated APIs | Same SDK; sibling APIs in one token family support [federated login](#federated-login-same-family-different-api-url) |\n\nBrowser and wallet clients for these APIs use the companion package [`@rodit/rodit-auth-fe`](https://www.npmjs.com/package/@rodit/rodit-auth-fe).\n\n**Login `POST` /api/login:** Use **`accountid`** (or **`roditid`**), **`timestamp`**, and **`base64url_signature`**. Sign UTF-8 bytes of `identifier + timestamp_iso`, and reject deprecated keys such as **`signature`** and **`account_id`**. See [CHANGELOG.md](./CHANGELOG.md).\n\n**9.15.0:** Outbound `send_webhook` SSRF controls (private/metadata blocklist, `webhook_cidr` enforcement, DNS pin); login success exposes verified peer id as JSON `roditid` and `req.authenticatedRoditId`; optional `enforceRateLimitFromClaims()` for per-route claim quotas. See [CHANGELOG.md](./CHANGELOG.md).\n\n**9.13 federated login:** A client RODiT issued for API A can log into API B in the same SR/CR family via `login_server({ apiEndpoint })`. See [Federated login](#federated-login-same-family-different-api-url).\n\n## Table of Contents\n\n- [Quick Start](#quick-start)\n- [Core Concepts](#core-concepts)\n- [Installation & Setup](#installation--setup)\n- [Authentication](#authentication)\n  - [Login Mode Control](#login-mode-control)\n  - [Federated login](#federated-login-same-family-different-api-url)\n- [Authorization & Permissions](#authorization--permissions)\n- [Session Management](#session-management)\n  - [Session lifetime and TTL](#session-lifetime-and-ttl)\n- [Configuration](#configuration)\n  - [Environment Variables](#environment-variables)\n  - [Dynamic Rate Limiting](#dynamic-rate-limiting)\n  - [Session Storage Configuration](#session-storage-configuration)\n  - [Configuration Priority](#configuration-priority)\n- [Logging & Monitoring](#logging--monitoring)\n- [Performance Tracking](#performance-tracking)\n- [Webhooks](#webhooks)\n- [Advanced Usage](#advanced-usage)\n  - [Portal Authentication](#portal-authentication-server-to-server)\n  - [SignPortal URL Configuration](#signportal-url-configuration)\n  - [CRUDA Operations Example](#cruda-operations-example)\n- [API Reference](#api-reference)\n- [Best Practices](#best-practices)\n- [Troubleshooting](#troubleshooting)\n\n## Quick Start\n\n### Installation\n\n```bash\nnpm install @rodit/rodit-auth-be\n```\n\n### Basic Server Setup\n\n```javascript\nconst express = require('express');\nconst { RoditClient } = require('@rodit/rodit-auth-be');\nconst { setExpressSessionStore } = require('@rodit/rodit-auth-be/lib/auth/sessionmanager');\nconst { ulid } = require('ulid');\nconst session = require('express-session');\nconst SQLiteStore = require('connect-sqlite3')(session);\n\nconst app = express();\n\n// Configure session storage BEFORE initializing RoditClient\nconst sessionStore = new SQLiteStore({\n  db: 'sessions.db',\n  dir: './data',\n  table: 'sessions',\n});\nsetExpressSessionStore(sessionStore);\n\napp.use(express.json());\napp.use(express.urlencoded({ extended: false }));\napp.use((req, res, next) => {\n  req.requestId = req.headers['x-request-id'] || ulid();\n  req.startTime = Date.now();\n  next();\n});\n\nasync function startServer() {\n  try {\n    const roditClient = await RoditClient.create('server');\n    app.locals.roditClient = roditClient;\n\n    const logger = roditClient.getLogger();\n    app.use(roditClient.getLoggingMiddleware());\n\n    const authenticate = (req, res, next) => roditClient.authenticate(req, res, next);\n    const authenticateLogout = (req, res, next) =>\n      roditClient.authenticateForLogout(req, res, next);\n    const authorize = (req, res, next) => roditClient.authorize(req, res, next);\n\n    app.post('/api/login', (req, res) => {\n      req.logAction = 'login-attempt';\n      roditClient.login_client(req, res);\n    });\n    app.post('/api/logout', authenticateLogout, (req, res) => {\n      req.logAction = 'logout-attempt';\n      roditClient.logout_client(req, res);\n    });\n    app.get('/api/protected', authenticate, (req, res) => {\n      res.json({ message: 'Protected data', user: req.user });\n    });\n    app.use('/api/admin', authenticate, authorize, adminRoutes);\n\n    const port = 3000;\n    app.listen(port, () => {\n      logger.info(`RODiT Authentication Server running on port ${port}`);\n    });\n  } catch (error) {\n    console.error('Server initialization failed:', error);\n    process.exit(1);\n  }\n}\n\nstartServer();\n```\n\n## Core Concepts\n\n### The RoditClient Pattern\n\nThe SDK centers around the `RoditClient` class, which provides a unified interface for all RODiT operations:\n\n- **Single Initialization**: Create once with `RoditClient.create(role)` where role is `'server'`, `'client'`, or `'portal'`\n- **Shared Instance**: Store in `app.locals` for access across routes and middleware\n- **Self-Configuring**: Automatically loads configuration from Vault, files, or environment variables\n- **Encapsulated**: All SDK functionality accessed through the client instance\n- **Session Management**: Built-in session tracking with pluggable storage backends\n- **Performance Monitoring**: Integrated request tracking and metrics collection\n\n### App.locals Pattern\n\nStore the initialized client in `app.locals` for consistent access across your application:\n\n```javascript\n// In main app.js\nconst roditClient = await RoditClient.create('server')\napp.locals.roditClient = roditClient\n// In route modules\nconst router = express.Router()\nrouter.get('/data', (req, res) => {\n  const client = req.app.locals.roditClient\n  const logger = client.getLogger()\n  logger.info('Processing request', {\n    component: 'DataRoute',\n    userId: req.user?.id\n  })\nres.json({ data: 'example' })\n})\n```\n\n### Authentication Middleware Pattern\n\nCreate middleware functions that delegate to the RoditClient:\n\n```javascript\n// Create reusable middleware\nconst authenticate = (req, res, next) => {\n  const client = req.app.locals.roditClient\n  if (!client) {\n    res.status(503).json({ error: 'Authentication service unavailable' })\n  }\nclient.authenticate(req, res, next)\n}\nconst authorize = (req, res, next) => {\n  const client = req.app.locals.roditClient\n  if (!client) {\n    res.status(503).json({ error: 'Authorization service unavailable' })\n  }\nclient.authorize(req, res, next)\n}\n// Use in routes\napp.get('/api/protected', authenticate, handler)\napp.post('/api/admin', authenticate, authorize, adminHandler)\n```\n\n## Installation & Setup\n\n### Dependencies\n\n**Required:**\n```bash\nnpm install @rodit/rodit-auth-be express config winston\n```\n\n**Recommended for main:**\n```bash\nnpm install express-session connect-sqlite3\n```\n\n**Optional:**\n```bash\nnpm install node-vault  # For Vault-based credentials\nnpm install winston-loki  # For Grafana Loki logging\n```\n\n### Environment Variables\n\n**Vault Configuration (main):**\n```bash\nexport RODIT_NEAR_CREDENTIALS_SOURCE=vault\nexport VAULT_ENDPOINT=https://vault.example.com\nexport VAULT_ROLE_ID=your-role-id\nexport VAULT_SECRET_ID=your-secret-id\nexport VAULT_RODIT_KEYVALUE_PATH=secret/rodit\nexport SERVICE_NAME=your-service-name\nexport NEAR_CONTRACT_ID=discernible-io.near\n```\n\n**Application Configuration:**\n```bash\nexport NODE_ENV=main  # Environment: main, development, test\nexport LOG_LEVEL=info       # Logging: error, warn, info, debug, trace\nexport API_DEFAULT_OPTIONS_DB_PATH=/app/data/database.sqlite\n```\n\n**Session Configuration:**\n```bash\nexport SESSION_STORAGE_TYPE=express-session  # Storage: memory, express, express-session\nexport SESSION_CLEANUP_INTERVAL=3600000      # Cleanup interval in milliseconds (1 hour)\nexport SESSION_TOKEN_RETENTION_PERIOD=604800 # Token retention in seconds (7 days)\nexport SESSION_VALIDATION_CACHE_TTL=5000     # Cache TTL in milliseconds (5 seconds)\n```\n\n**Logging Configuration:**\n```bash\nexport LOKI_URL=https://loki.example.com:3100\nexport LOKI_BASIC_AUTH=username:password\n```\n\n### Configuration Files\n\nCreate `config/default.json`:\n\n```json\n{\n\"NEAR_CONTRACT_ID\": \"discernible-io.near\",\n\"SERVICE_NAME\": \"your-service\",\n\"SECURITY_OPTIONS\": {\n\"SILENT_LOGIN_FAILURES\": false,\n\"SESSION_TTL_SECONDS\": 5200\n\"FALLBACK_JWT_DURATION\": 3600\n}\n}\n```\n\n## Authentication\n\n### RODiT-Based Authentication\n\nRODiT provides cryptographic mutual authentication using blockchain-verified identities.\n\n#### Client Login Request\n\nFor API login documentation, use **`accountid`** with HTTP `POST /api/login`. The signed payload is **`accountid + timestamp_iso`** (no separator).\n\n| Field | Description |\n|-------|-------------|\n| `timestamp` | Recommended; Unix seconds from `GET /api/login/timestamp` |\n| `base64url_signature` | Ed25519 detached signature (base64url) over `accountid + timestamp_iso` |\n| `accountid` | 64-hex implicit NEAR account login identifier |\n\n```json\n{\n  \"accountid\": \"<64-char-hex>\",\n  \"timestamp\": 1640995200,\n  \"base64url_signature\": \"base64url-encoded-signature\"\n}\n```\n\nUse **`base64url_signature`** in login payloads for API login examples.\n\nRejected keys (HTTP 400, `LOGIN_PAYLOAD_DEPRECATED`): **`signature`** and **`account_id`**.\n\n#### Server Response\n\n```json\n{\n  \"jwt_token\": \"<jwt-token>\",\n  \"requestId\": \"01HQXYZ123ABC\"\n}\n```\n\n#### Authentication Flow\n\n1. **Client sends RODiT credentials** - RODiT ID or account ID, timestamp, and cryptographic signature\n2. **SDK verifies signature** - Validates against blockchain records (NEAR Protocol)\n3. **Session created** - New session stored in session manager\n4. **JWT token issued** - Token contains session ID, user claims, and always\n   `rodit_subjectuniqueidentifier_url` (`null` same-API; federated API URL when\n   peer home ≠ issuing server). See [Federated login](#federated-login-same-family-different-api-url).\n5. **Subsequent requests** - Client sends JWT in `Authorization: Bearer <token>` header\n6. **Token validation** - SDK validates JWT (issuer rules differ for federated vs\n   same-API) and checks session status\n\nSecurity hardening in current implementation:\n- JWT compact parts must be canonical base64url (non-canonical encodings are rejected).\n- Session registration is enforced during JWT validation (unknown/inactive/expired sessions are rejected).\n- Server session length defaults to `SECURITY_OPTIONS.SESSION_TTL_SECONDS` (5200 s); see [Session lifetime and TTL](#session-lifetime-and-ttl).\n- Token renewal uses `sessionManager` for session checks and updates (no `stateManager` session mutations).\n- Federated `login_server({ apiEndpoint })` rejects MITM when the signed\n  `rodit_subjectuniqueidentifier_url` does not match the intended endpoint.\n\n### Login Implementation\n\n```javascript\n// routes/login.js\nconst express = require('express')\nconst router = express.Router()\nrouter.post('/login', async (req, res) => {\n  req.logAction = 'login-attempt'\n  const client = req.app.locals.roditClient\n  if (!client) {\n    res.status(503).json({ error: 'Authentication service unavailable' })\n  }\n// Delegate to SDK's login_client method\nawait client.login_client(req, res)\n})\nmodule.exports = router\n```\n\n### Logout Implementation\n\n```javascript\n// Logout invalidates the JWT token and closes the session\n// Use logout-specific auth so signature-valid expired tokens can still logout.\nrouter.post('/logout', authenticateLogout, async (req, res) => {\n  req.logAction = 'logout-attempt'\n  const client = req.app.locals.roditClient\n  if (!client) {\n    res.status(503).json({ error: 'Authentication service unavailable' })\n  }\n// Delegate to SDK's logout_client method\nawait client.logout_client(req, res)\n})\n```\n\n### Protected Routes\n\n```javascript\n// Require authentication for access\napp.get('/api/data', authenticate, (req, res) => {\n  // req.user contains authenticated user information\n  const logger = req.app.locals.roditClient.getLogger()\n  logger.info('Protected route accessed', {\n    component: 'API',\n    userId: req.user.id,\n    roditId: req.user.roditId,\n    requestId: req.requestId\n  })\nres.json({\n  message: 'Authenticated data',\n  user: req.user,\n  requestId: req.requestId\n})\n})\n```\n\n### Authentication Middleware\n\nThe `authenticate` middleware validates JWT tokens and populates `req.user`:\n\n```javascript\nconst authenticate = (req, res, next) => {\n  const client = req.app.locals.roditClient\n  client.authenticate(req, res, next)\n}\n// After successful authentication, req.user contains:\n// {\n  // id: 'user-unique-id',\n  // roditId: '01K4G3D95QF6NR0RSJK9WEK6KA',\n  // aud: 'audience',\n  // iss: 'issuer',\n  // exp: 1640999999,\n  // iat: 1640995200,\n  // session_id: '01HQXYZ123ABC'\n  // }\n```\n\n### Login Mode Control\n\nThe SDK provides configurable access control for RODiT authentication, allowing you to restrict which types of logins are accepted by your server.\n\n#### Login Types\n\n**Partner Login (Client-Server)**\n- **Definition**: Authentication where the peer's service provider ID is **different** from the server's service provider ID\n- **Use Case**: Traditional client-server authentication where a client authenticates to a service provider\n- **Example**: A mobile app (client) authenticating to your API server\n\n**Peer Login (Peer-to-Peer)**\n- **Definition**: Authentication where the peer's service provider ID is **the same** as the server's service provider ID\n- **Use Case**: Peer-to-peer authentication between entities with the same service provider\n- **Example**: Two servers in the same organization authenticating to each other\n\n#### Configuration Options\n\n| Mode | Partner Logins | Peer Logins | Description |\n|------|---------------|-------------|-------------|\n| `partner` | ✅ Accepted | ❌ Rejected | **Default** - Only accept client-server authentication |\n| `promiscuous` | ✅ Accepted | ✅ Accepted | Accept all valid logins regardless of type |\n| `p2p` | ❌ Rejected | ✅ Accepted | Only accept peer-to-peer authentication |\n\n#### Usage Examples\n\n**Default (Partner Only):**\n```bash\n// No configuration needed - this is the default\n// Only client-server authentication is accepted\n```\n\n**Accept All Logins:**\n```bash\nexport SECURITY_OPTIONS_LOGIN_MODE=promiscuous\n// Both Partner and Peer logins are accepted\n```\n\n**Peer-to-Peer Only:**\n```bash\nexport SECURITY_OPTIONS_LOGIN_MODE=p2p\n// Only peer-to-peer authentication is accepted\n```\n\n**Docker/Podman:**\n```bash\npodman run -e SECURITY_OPTIONS_LOGIN_MODE=partner ...\n```\n\n**GitHub Actions:**\nAdd repository variable:\n- **Name**: `SECURITY_OPTIONS_LOGIN_MODE`\n- **Value**: `partner` | `promiscuous` | `p2p`\n\n#### Federated login (same family, different API URL)\n\nEnable a client RODiT issued for API A (`subjectuniqueidentifier_url`) to log\ninto API B in the **same SR/CR family**. Federation is **login → remint a local\nJWT** on B; foreign JWTs are not accepted without re-login. Mutual auth remains\noptional (no reverse `login_server` required).\n\n##### Client call\n\n```js\nconst { RoditClient } = require('@rodit/rodit-auth-be');\n\nconst client = await RoditClient.create('client');\nawait client.login_server({\n  apiEndpoint: 'https://api-b.example.com', // federated API; omit for home API\n});\n```\n\nWhen `apiEndpoint` is omitted, login targets the client's own\n`subjectuniqueidentifier_url` (same-API path).\n\n##### JWT claim contract\n\nUnset fields are **always present as `null`** (same rule as `config_*` in 9.12).\nDo not omit `rodit_subjectuniqueidentifier_url` on same-API tokens.\n\n| Claim | Always present? | Same-API login | Federated login |\n|-------|-----------------|----------------|-----------------|\n| `iss` | yes | peer home URL (= server URL in practice) | **client home** `subjectuniqueidentifier_url` |\n| `aud` | yes | server `owner_id` | server `owner_id` (federated API) |\n| `rodit_subjectuniqueidentifier_url` | **yes** | **`null`** | **federated API** `subjectuniqueidentifier_url` |\n| `config_iso639` / `config_iso3166` / `config_iso15924` / `config_timeoptions` | yes | `null` | `null` |\n\nA JWT is treated as federated when:\n\n```js\nrodit_subjectuniqueidentifier_url != null\n  && String(rodit_subjectuniqueidentifier_url).trim() !== \"\"\n```\n\n(Helper: `isNonEmptyUrlClaim`.)\n\n##### Server validation\n\nAfter signature verification:\n\n- **Federated JWT** — `rodit_subjectuniqueidentifier_url` must equal this\n  server's URL; `iss` must equal the login client's home URL (resolved from\n  `sub` via on-chain RODiT).\n- **Same-API / 9.12 tokens** — claim null/absent → `iss` must equal this\n  server's URL (unchanged 9.12 issuer rule).\n- **`aud`**, family match, and `LOGIN_MODE` are unchanged.\n\n##### Client MITM check\n\nAfter crypto validation of the received JWT, `login_server` checks that a\nfederated attempt (`apiEndpoint` ≠ client home) has:\n\n1. Non-empty `rodit_subjectuniqueidentifier_url`\n2. That claim equal to the intended `apiEndpoint`\n3. `iss` equal to the client home URL\n\n| Failure | `errorCode` |\n|---------|-------------|\n| Null/empty federated claim | `FEDERATED_ISSUER_MISSING` |\n| Claim or `iss` mismatch | `FEDERATED_ISSUER_MISMATCH` |\n\n##### Renewal\n\n- Federated JWT renewals preserve the non-null claim string.\n- Tokens minted by 9.12 (no claim) renew into `rodit_subjectuniqueidentifier_url: null`.\n\n##### Operational prerequisites (outside SDK)\n\n- Federated server RODiT shares the same `bc=;sc=;id=SR;id=CR` family.\n- Federated domain has DNS trust TXT (keyed off **own**\n  `subjectuniqueidentifier_url`).\n- Client calls `login_server({ apiEndpoint: '<federated URL>' })`.\n- Client→server federation needs `LOGIN_MODE=partner` (or `promiscuous`); a\n  server RODiT logging into a federated API under `partner` is still rejected\n  by existing role-separation policy.\n\n##### Helpers (package root)\n\n```js\nconst {\n  normalizeUrlWithoutPort,\n  isNonEmptyUrlClaim,\n  isFederatedRoditLogin,\n  validateFederatedLoginTarget,\n} = require('@rodit/rodit-auth-be');\n```\n\n#### Logging and Monitoring\n\n**Successful Login:**\n```json\n{\n  \"level\": \"info\",\n  \"message\": \"PARTNER login verified successfully\",\n  \"verificationType\": \"PARTNER\",\n  \"loginMode\": \"partner\",\n  \"duration\": 1234\n}\n```\n\n**Rejected Login:**\n```json\n{\n  \"level\": \"warn\",\n  \"message\": \"PEER login rejected by LOGIN_MODE policy\",\n  \"verificationType\": \"PEER\",\n  \"loginMode\": \"partner\",\n  \"policyReason\": \"LOGIN_MODE=partner does not accept PEER logins\"\n}\n```\n\n**Metrics:**\n- `rodit_match_verification` with `result: \"success\"` - Successful authentication\n- `rodit_match_verification` with `result: \"policy_rejected\"` - Rejected by policy\n\n#### Security Considerations\n\n1. **Default is Secure**: The default `partner` mode provides the most restrictive access control\n2. **Promiscuous Mode**: Use only when you need to accept both types of authentication\n3. **P2P Mode**: Use when building peer-to-peer systems where only same-provider authentication is needed\n4. **Policy Enforcement**: Rejections are logged with clear reasons for audit trails\n5. **Federated MITM**: Always pass the real peer URL as `apiEndpoint`; the client\n   verifies the signed federated issuer claim against that URL after login\n\n#### Troubleshooting\n\n**Login Rejected with \"policy_rejected\":**\n- If you see \"PEER login rejected\" and need to accept peer logins, set mode to `promiscuous` or `p2p`\n- If you see \"PARTNER login rejected\" and need to accept partner logins, set mode to `promiscuous` or `partner`\n\n**Federated login failures:**\n- `FEDERATED_ISSUER_MISSING` — JWT has null/empty `rodit_subjectuniqueidentifier_url` but `apiEndpoint` differs from client home (peer may be pre-9.13, or claim omitted incorrectly)\n- `FEDERATED_ISSUER_MISMATCH` — claim or `iss` does not match intended `apiEndpoint` / client home (wrong URL or MITM)\n- `Error 005: Invalid federated issuer` on the server — JWT federated claim is not this server's `subjectuniqueidentifier_url`\n\n**Check Current Mode:**\nLook for the log message during authentication:\n```\n\"Starting RODiT match verification\" with \"loginMode\": \"partner\"\n```\n\n## Authorization & Permissions\n\n### Route-Based Permissions\n\nPermissions are configured in your RODiT token metadata using the `permissioned_routes` field:\n\n```json\n{\n  \"permissioned_routes\": {\n    \"entities\": {\n      \"/\": {\n        \"methods\": \"+0\"\n      },\n      \"/api/echo\": {\n        \"methods\": \"+0\"\n      },\n      \"/api/cruda/create\": {\n        \"methods\": \"+0\"\n      },\n      \"/api/cruda/list\": {\n        \"methods\": \"+0\"\n      },\n      \"/api/admin\": {\n        \"methods\": \"+0\"\n      }\n    }\n  }\n}\n```\n\n**Permission Format:**\n\nValues are strings: optional scope prefix (`+` / `-`) plus a numeric rate:\n\n| Value | Meaning |\n|-------|---------|\n| `\"+0\"` | Allowed (entity + properties scope); **unlimited** rate |\n| `\"+60\"` | Allowed; **60 requests** per window (default window 60s) |\n| `\"-0\"` | Allowed (properties-only scope); unlimited |\n| (absent / no match) | Denied (`403`) |\n\n`validatepermissions` / `authorize` checks allow/deny and sets `req.rateLimit` from the numeric part. That metadata is inert until you mount `enforceRateLimitFromClaims()` (see [Dynamic Rate Limiting](#dynamic-rate-limiting)).\n\n### Permission Validation Middleware\n\nThe `authorize` middleware validates that the authenticated user has permission to access the requested route:\n\n```javascript\n// After RoditClient.create('server') and app.locals.roditClient = roditClient\nconst authenticate = (req, res, next) => roditClient.authenticate(req, res, next)\nconst authorize = (req, res, next) => roditClient.authorize(req, res, next)\nconst enforceClaimLimits = roditClient.getClaimRateLimitMiddleware()\n\n// Auth + permission allow/deny\napp.use('/api/admin', authenticate, authorize, adminRoutes)\n\n// Auth + permissions + per-route claim quotas from permissioned_routes\napp.use('/api/cruda', authenticate, authorize, enforceClaimLimits(), crudaRoutes)\n```\n\n### Permission Enforcement\n\n```javascript\n// Example: CRUDA routes with permission checking\nconst router = express.Router()\n// All routes require authentication + authorization\nrouter.post('/create', async (req, res) => {\n  // User must have permission for POST /api/cruda/create\n  const { comment, author } = req.body\n  // Create record in database\n  const result = await db.run(\n  'INSERT INTO comments (comment, author) VALUES (?, ?)',\n  [comment, author || req.user.roditId]\n  )\n  res.json({ id: result.lastID, requestId: req.requestId })\n})\nrouter.post('/list', async (req, res) => {\n  // User must have permission for POST /api/cruda/list\n  const records = await db.all('SELECT * FROM comments ORDER BY created_at DESC')\n  res.json({ records, requestId: req.requestId })\n})\nmodule.exports = router\n```\n\n### Dynamic Permission Checking\n\n```javascript\n// Check permissions programmatically\nconst client = req.app.locals.roditClient\nconst hasPermission = client.isOperationPermitted('POST', '/api/admin/users')\nif (!hasPermission) {\n  res.status(403).json({\n    error: 'Forbidden',\n    message: 'You do not have permission to access this resource',\n    requestId: req.requestId\n  })\n}\n// Proceed with operation\n```\n\n### Permission Validation in Client Token Minting\n\nWhen minting client tokens via `/api/signclient`, the server validates that requested permissions are a subset of the server's own permissions:\n\n```javascript\n// Client requests these permissions:\nconst requestedPermissions = {\n  \"/\": \"+0\",\n  \"/api/echo\": \"+0\",\n  \"/api/cruda/create\": \"+0\"\n}\n// Server validates against its own permissioned_routes\n// If any requested route is not in server's config, request is rejected with HTTP 400\n```\n\n## Session Management\n\n### Overview\n\nThe SDK includes a comprehensive session management system that:\n- Tracks active user sessions\n- Validates JWT tokens against session state\n- Supports pluggable storage backends\n- Automatically cleans up expired sessions\n- Integrates with performance metrics\n\n### Session lifetime and TTL\n\nServer sessions and JWT access credentials use **different** clocks:\n\n| Concept | Controlled by | Stored / carried as |\n|---------|----------------|---------------------|\n| **Server session** | `SECURITY_OPTIONS.SESSION_TTL_SECONDS` (host config) | `sessionManager` record `expiresAt`; JWT claim `session_exp` |\n| **Access credential (JWT `exp`)** | Passport `jwt_duration` on peer/own RODiT metadata (+ renewal) | JWT `exp`; renewed until `session_exp` |\n\nYou do **not** need to change on-chain `jwt_duration` on the server RODiT token to control how long a **session** lasts. Set session length in application config instead.\n\n#### `SECURITY_OPTIONS.SESSION_TTL_SECONDS`\n\n| Property | Value |\n|----------|--------|\n| **SDK default** | `5200` (~87 minutes) |\n| **Valid range** | `60` – `31536000` (365 days), or `0` to disable |\n| **Config path** | `SECURITY_OPTIONS.SESSION_TTL_SECONDS` |\n| **Env example** | `SECURITY_OPTIONS_SESSION_TTL_SECONDS=2592000` (30 days, with node-config style mapping) |\n\nAt login the SDK computes:\n\n```text\nsession_expiresAt = login_time + SESSION_TTL_SECONDS\n```\n\nThen applies **passport caps**: if either peer or own RODiT has a bounded `not_after`, the session cannot end later than the **earlier** of those dates.\n\nSet **`SESSION_TTL_SECONDS` to `0`** to fall back to passport-derived session end (bounded `not_after` when present, otherwise `max(peer, own) jwt_duration`).\n\n#### Examples\n\n**Default (5200 seconds):**\n\n```javascript\n// config/default.json — omit SESSION_TTL_SECONDS to use SDK default 5200\n{\n  \"SECURITY_OPTIONS\": {\n    \"FALLBACK_JWT_DURATION\": 3600\n  }\n}\n```\n\n**30-day sessions:**\n\n```javascript\n{\n  \"SECURITY_OPTIONS\": {\n    \"SESSION_TTL_SECONDS\": 2592000\n  }\n}\n```\n\n**Passport-derived session length (legacy):**\n\n```javascript\n{\n  \"SECURITY_OPTIONS\": {\n    \"SESSION_TTL_SECONDS\": 0\n  }\n}\n```\n\n#### Enforcement on each API request\n\nFor normal API authentication (`authenticate_apicall`), the SDK:\n\n1. Checks stored session: exists, `status === 'active'`, `expiresAt` not in the past.\n2. Validates JWT signature and `exp` (with renewal when eligible).\n3. Requires JWT `session_exp` to match stored `expiresAt` when session registration is enforced.\n\nPortal/outbound login token validation can skip session registration when `SECURITY_OPTIONS.RELAXED_SESSION_VALIDATION` is `true` (default).\n\n#### Related options\n\n| Option | Purpose |\n|--------|---------|\n| `FALLBACK_JWT_DURATION` | Access-token lifetime when passport `jwt_duration` is missing or invalid (default `3600`; max 7 days in validator) |\n| `JWT_MAX_DURATION_SECONDS_RODIT_UNBOUNDED` | Cap on JWT `exp` when peer `not_after` is unbounded |\n| `RELAXED_SESSION_VALIDATION` | Portal/outbound flows may skip server session lookup |\n| `SESSION_VALIDATION_CACHE_TTL` | Cache TTL for session invalidation checks after logout |\n\n### Session Storage Backends\n\n#### 1. In-Memory Storage (Default)\n\nNo configuration needed - works out of the box:\n\n```javascript\nconst client = await RoditClient.create('server')\n// Uses InMemorySessionStorage by default\n```\n\n**Pros:** Fast, zero configuration  \n**Cons:** Sessions lost on server restart, not suitable for multi-server deployments\n\n#### 2. SQLite Storage (Recommended for main)\n\nPersistent storage using SQLite database:\n\n```javascript\nconst express = require('express')\nconst session = require('express-session')\nconst SQLiteStore = require('connect-sqlite3')(session)\nconst { RoditClient } = require('@rodit/rodit-auth-be')\nconst { setExpressSessionStore } = require('@rodit/rodit-auth-be/lib/auth/sessionmanager')\n// Configure BEFORE initializing RoditClient\nconst sessionStore = new SQLiteStore({\n  db: 'sessions.db',\n  dir: './data',\n  table: 'sessions'\n})\nsetExpressSessionStore(sessionStore)\n// Now initialize client\nconst client = await RoditClient.create('server')\n```\n\n**Pros:** Persistent across restarts, simple setup, uses existing database infrastructure  \n**Cons:** Not suitable for multi-server deployments\n\n#### 3. Redis Storage (For Multi-Server)\n\n```bash\nnpm install express-session connect-redis redis\n```\n\n```javascript\nconst session = require('express-session')\nconst RedisStore = require('connect-redis').default\nconst { createClient } = require('redis')\nconst { setExpressSessionStore } = require('@rodit/rodit-auth-be/lib/auth/sessionmanager')\n// Create Redis client\nconst redisClient = createClient({\n  url: process.env.REDIS_URL || 'redis://127.0.0.1:6379'\n})\nawait redisClient.connect()\n// Create Redis store\nconst redisStore = new RedisStore({\n  client: redisClient,\n  prefix: 'rodit:sess:',\n  ttl: 86400 // 24 hours\n})\nsetExpressSessionStore(redisStore)\nconst client = await RoditClient.create('server')\n```\n\n**Pros:** Shared sessions across multiple servers, high performance  \n**Cons:** Requires Redis infrastructure\n\n### Session Storage Configuration\n\nThe SDK supports configurable session storage via the `SESSION_STORAGE_TYPE` environment variable.\n\n#### Storage Type Options\n\n**1. `\"memory\"` (Default)**\n- Uses SDK's standalone `InMemorySessionStorage`\n- No external dependencies required\n- Sessions stored in JavaScript `Map`\n- Sessions lost on server restart\n- Suitable for development or single-instance deployments\n\n```bash\nexport SESSION_STORAGE_TYPE=memory\n```\n\n**2. `\"express\"` or `\"express-session\"`**\n- Uses `express-session` compatible stores\n- Requires `express-session` to be installed\n- Defaults to `express-session` MemoryStore\n- Can be overridden with `setExpressSessionStore()` for Redis, SQLite, etc.\n- Suitable for main with persistent storage\n\n```bash\nexport SESSION_STORAGE_TYPE=express-session\n```\n\n#### Configuring Persistent Storage\n\n**SQLite Example:**\n```javascript\nconst session = require('express-session')\nconst SQLiteStore = require('connect-sqlite3')(session)\nconst { setExpressSessionStore } = require('@rodit/rodit-auth-be/lib/auth/sessionmanager')\n// Configure BEFORE initializing RoditClient\nconst sessionStore = new SQLiteStore({\n  db: 'sessions.db',\n  dir: './data',\n  table: 'sessions'\n})\nsetExpressSessionStore(sessionStore)\n// Now initialize client\nconst client = await RoditClient.create('server')\n```\n\n**Redis Example:**\n```javascript\nconst session = require('express-session')\nconst RedisStore = require('connect-redis').default\nconst { createClient } = require('redis')\nconst { setExpressSessionStore } = require('@rodit/rodit-auth-be/lib/auth/sessionmanager')\nconst redisClient = createClient({\n  url: process.env.REDIS_URL || 'redis://127.0.0.1:6379'\n})\nawait redisClient.connect()\nconst redisStore = new RedisStore({\n  client: redisClient,\n  prefix: 'rodit:sess:',\n  ttl: 86400\n})\nsetExpressSessionStore(redisStore)\n```\n\n#### Session Configuration Variables\n\n```bash\n// Storage backend type\nexport SESSION_STORAGE_TYPE=express-session\n// Cleanup interval (milliseconds) - how often to remove expired sessions\nexport SESSION_CLEANUP_INTERVAL=3600000  # 1 hour\n// Token retention period (seconds) - how long to keep closed sessions\nexport SESSION_TOKEN_RETENTION_PERIOD=604800  # 7 days\n// Validation cache TTL (milliseconds) - trades security for performance\n// Lower = more secure but more storage lookups\n// Higher = faster but longer window after logout where token may still work\n// Set to 0 to disable caching (always check session state)\nexport SESSION_VALIDATION_CACHE_TTL=5000  # 5 seconds\n```\n\n**Session Validation Cache:**\n\nThe SDK caches token validation results to reduce storage lookups:\n\n- **Enabled by default** with 5-second TTL\n- **Trade-off**: Performance vs. security\n- **After logout**: Cache is immediately invalidated for that session\n- **Recommendation**: Keep default (5s) for most use cases\n- **High security**: Set to `0` to disable caching\n\n```javascript\n// Get cache statistics\nconst sessionManager = roditClient.getSessionManager()\nconst cacheStats = sessionManager.getValidationCacheStats()\nconsole.log('Cache stats:', cacheStats)\n// Output: { totalEntries: 10, validEntries: 8, expiredEntries: 2, cacheTTL: 5000, cacheEnabled: true }\n```\n\n### Session Operations\n\n```javascript\n// Get session manager\nconst sessionManager = roditClient.getSessionManager()\n// Get active session count\nconst activeCount = await sessionManager.getActiveSessionCount()\n// Get storage information\nconst storageInfo = await sessionManager.getStorageInfo()\nconsole.log('Storage type:', storageInfo.type)\nconsole.log('Session count:', storageInfo.sessionCount)\n// Enumerate sessions via storage\nconst allSessions = await sessionManager.storage.getAll()\n// Or fallback using keys() + get()\nconst sessionIds = await sessionManager.storage.keys()\nconst sessions = []\nREPEAT: for (const id of sessionIds) {\n  const session = await sessionManager.storage.get(id)\n  if (session) sessions.push(session)\n}\n// Check if token is invalidated\nconst isInvalidated = await sessionManager.isTokenInvalidated(jwtToken)\n// Get detailed invalidation info\nconst invalidationInfo = await sessionManager.getTokenInvalidationInfo(jwtToken)\nif (invalidationInfo) {\n  console.log('Invalidation reason:', invalidationInfo.reason)\n  console.log('Invalidated at:', invalidationInfo.invalidatedAt)\n}\n// Manually close a session\nawait sessionManager.closeSession(sessionId, 'admin_action')\n// Run manual cleanup (removes expired sessions)\nconst cleanup = await sessionManager.runManualCleanup()\nconsole.log(`Removed ${cleanup.removedSessionsCount} expired sessions`)\n// Get validation cache statistics\nconst cacheStats = sessionManager.getValidationCacheStats()\nconsole.log('Cache entries:', cacheStats.totalEntries)\nconsole.log('Cache TTL:', cacheStats.cacheTTL)\n```\n\n### Session Lifecycle\n\n1. **Login** - Session created, JWT token issued with session ID\n2. **Active** - Token validated on each request, session last_accessed updated\n3. **Logout** - Session closed, token invalidated, termination token issued\n4. **Expiration** - Sessions expire when stored `expiresAt` is reached (`SESSION_TTL_SECONDS` from login, capped by passport `not_after`)\n5. **Cleanup** - Expired sessions removed by automatic cleanup process\n\n### Token Invalidation\n\nThe SDK validates tokens by checking session state:\n\n```javascript\n// Authentication middleware checks:\n// 1. JWT signature validity\n// 2. JWT expiration\n// 3. Session exists and is active\n// 4. Session not expired\n// After logout, tokens are invalidated because:\n// - Session status set to 'closed'\n// - Subsequent requests fail authentication\n```\n\n## Configuration\n\n### Configuration Priority\n\nThe SDK automatically configures itself from multiple sources with a clear priority hierarchy:\n\n1. **Environment Variables** (Highest priority) - Direct `process.env` access\n2. **Host Application Config** - Values from `config` package (with env mappings)\n3. **SDK Fallback Defaults** - Built-in defaults from `configsdk.js`\n4. **Provided Default Value** - Optional parameter to `config.get()`\n\n**Example:**\n```javascript\nconst config = roditClient.getConfig()\n// Priority 1: Checks process.env.SESSION_STORAGE_TYPE\n// Priority 2: Checks host config.get('SESSION_STORAGE_TYPE')\n// Priority 3: Uses SDK default 'memory'\n// Priority 4: Falls back to 'memory' if provided\nconst storageType = config.get('SESSION_STORAGE_TYPE', 'memory')\n```\n\nThis ensures that:\n- CI/CD environment variables always take precedence\n- Host applications can override SDK defaults\n- SDK provides sensible defaults for all settings\n- Configuration is predictable and debuggable\n\n### Automatic Configuration Loading\n\nThe SDK loads configuration from multiple sources:\n\n1. **Environment Variables** - Direct environment access\n2. **Configuration Files** - config/default.json, config/main.json, config/development.json\n3. **Vault Credentials** - Main credential storage\n4. **SDK Defaults** - Fallback values\n\n### Environment Configuration: NODE_ENV and LOG_LEVEL\n\nThe SDK uses **two separate environment variables** for configuration, following Node.js ecosystem standards:\n\n#### NODE_ENV - Environment Type & Security Behavior\n\nControls environment-specific behavior and security settings:\n\n**Values:**\n- `main` - Main branch deploy (strict security, no error details)\n- `development` - Development branch deploy (relaxed security, detailed errors)\n- `test` - Testing environment (allows bypasses for automated testing)\n\n**Default:** `development`\n\n**Controls:**\n- ✅ Error detail exposure in API responses\n- ✅ Peer public key requirement enforcement\n- ✅ Webhook verification bypass (test mode only)\n- ✅ Security-critical behavior\n\n#### LOG_LEVEL - Logging Verbosity\n\nControls Winston logger verbosity independently from environment:\n\n**Values:**\n- `error` - Only errors\n- `warn` - Warnings and errors\n- `info` - Informational messages, warnings, and errors (recommended for main)\n- `debug` - Detailed debugging information\n- `trace` - Maximum verbosity with full traces\n\n**Default:** `info`\n\n**Controls:**\n- ✅ Winston logger output level\n- ✅ Debug payload logging\n- ✅ Log verbosity only (not security)\n\n#### Separation of Concerns\n\n```javascript\n// Environment detection (security)\nconst isMain = process.env.NODE_ENV === 'main'\nconst isDevelopment = process.env.NODE_ENV === 'development'\nconst isTest = process.env.NODE_ENV === 'test'\n// Logging verbosity (independent)\nconst config = roditClient.getConfig()\nconst logLevel = config.get('LOG_LEVEL', 'info')\n```\n\n#### Configuration Examples\n\n**Main (normal):**\n```bash\nexport NODE_ENV=main\nexport LOG_LEVEL=info\n// Results in:\n// - Strict security enforcement\n// - No error details in responses\n// - Minimal logging output\n```\n\n**Main (troubleshooting):**\n```bash\nexport NODE_ENV=main\nexport LOG_LEVEL=debug\n// Results in:\n// - Strict security enforcement (still main)\n// - No error details in responses (still secure)\n// - Verbose logging for debugging\n```\n\n**Development:**\n```bash\nexport NODE_ENV=development\nexport LOG_LEVEL=debug\n// Results in:\n// - Relaxed security for development\n// - Detailed error messages in responses\n// - Verbose logging\n```\n\n**Testing:**\n```bash\nexport NODE_ENV=test\nexport LOG_LEVEL=error\n// Results in:\n// - Test mode (allows bypasses)\n// - Detailed error messages\n// - Only errors logged (cleaner test output)\n```\n\n#### Behavior Matrix\n\n| Scenario | NODE_ENV | LOG_LEVEL | Security | Error Details | Logging |\n|----------|----------|-----------|----------|---------------|---------|\n| Main | `main` | `info` | ✅ Strict | ❌ Hidden | Minimal |\n| Main Debug | `main` | `debug` | ✅ Strict | ❌ Hidden | Verbose |\n| Development | `development` | `debug` | ⚠️ Relaxed | ✅ Shown | Verbose |\n| Testing | `test` | `error` | ⚠️ Bypass OK | ✅ Shown | Errors only |\n\n### Vault-Based Configuration (main)\n\nFor main deployments, credentials are loaded from HashiCorp Vault:\n\n```bash\n// Environment variables for vault\nexport RODIT_NEAR_CREDENTIALS_SOURCE=vault\nexport VAULT_ENDPOINT=https://vault.example.com\nexport VAULT_ROLE_ID=your-role-id\nexport VAULT_SECRET_ID=your-secret-id\nexport VAULT_RODIT_KEYVALUE_PATH=secret/rodit\nexport SERVICE_NAME=your-service-name\nexport NEAR_CONTRACT_ID=discernible-io.near\n```\n\n### File-Based Configuration (Development)\n\nFor development, credentials can be loaded from files:\n\n```bash\nexport RODIT_NEAR_CREDENTIALS_SOURCE=file\nexport CREDENTIALS_FILE_PATH=./credentials/rodit-credentials.json\n```\n\n### Accessing Configuration\n\n```javascript\n// Get complete RODiT configuration\nconst configObject = await roditClient.getConfigOwnRodit()\nconst metadata = configObject.own_rodit.metadata\n// Access RODiT token metadata\nconst jwtDuration = metadata.jwt_duration;  // JWT expiration time\nconst maxRequests = metadata.max_requests;  // Rate limit\nconst maxRqWindow = metadata.maxrq_window;  // Rate limit window\nconst apiEndpoint = metadata.subjectuniqueidentifier_url;  // API URL\nconst webhookUrl = metadata.webhook_url;  // Webhook host[:port] (optional path)\n// Parse permissioned routes\nconst permissionedRoutes = JSON.parse(metadata.permissioned_routes || '{}')\n// Use SDK config for application settings\nconst config = roditClient.getConfig()\nconst logLevel = config.get('LOG_LEVEL', 'info')\nconst dbPath = config.get('API_DEFAULT_OPTIONS.DB_PATH')\n```\n\n### Dynamic Rate Limiting\n\nTwo complementary limiters:\n\n1. **Global** (`getRateLimitMiddleware` / `ratelimitmw`) — from own RODiT `max_requests` / `maxrq_window` (or fixed numbers). Applies across routes.\n2. **Per-route claim** (`getClaimRateLimitMiddleware` / `enforceRateLimitFromClaims`) — consumes `req.rateLimit` set by `validatepermissions` from each peer’s `permissioned_routes` value (e.g. `\"+60\"` → 60 req / 60s for that path). Returns `429 RATE_LIMIT_EXCEEDED` when exceeded. In-memory, per process.\n\n```javascript\n// 1) Global limiter from own RODiT metadata\nconst configObject = await roditClient.getConfigOwnRodit()\nconst metadata = configObject.own_rodit.metadata\nif (metadata.max_requests && metadata.maxrq_window) {\n  const maxRequests = parseInt(metadata.max_requests, 10)\n  const windowSeconds = parseInt(metadata.maxrq_window, 10)\n  const rateLimiter = roditClient.getRateLimitMiddleware()\n  // Note: factory args are (maxRequests, windowMinutes)\n  app.use(rateLimiter(maxRequests, Math.ceil(windowSeconds / 60) || 1))\n}\n\n// 2) Per-route claim enforcement (after authenticate + validatepermissions)\nconst { authenticate_apicall, validatepermissions, enforceRateLimitFromClaims } =\n  require('@rodit/rodit-auth-be')\n// Or: roditClient.getClaimRateLimitMiddleware()\napp.use(\n  '/api',\n  (req, res, next) => roditClient.authenticate(req, res, next),\n  validatepermissions,\n  enforceRateLimitFromClaims()\n)\n```\n\n### Environment Variables\n\nComplete list of SDK environment variables:\n\n#### Core Configuration\n```bash\n// Service identification\nexport SERVICE_NAME=your-service-name\nexport API_VERSION=1.0.0\n// Environment and logging\nexport NODE_ENV=main               # main, development, test\nexport LOG_LEVEL=info                # error, warn, info, debug, trace\n```\n\n#### Credentials and Authentication\n```bash\n// Credential source\nexport RODIT_NEAR_CREDENTIALS_SOURCE=vault  # vault, file, env\n// Vault configuration (main)\nexport VAULT_ENDPOINT=https://vault.example.com\nexport VAULT_ROLE_ID=your-role-id\nexport VAULT_SECRET_ID=your-secret-id\nexport VAULT_RODIT_KEYVALUE_PATH=secret/rodit\nexport VAULT_TOKEN_TTL=3600\n// File-based credentials (development)\nexport CREDENTIALS_FILEPATH=./credentials/rodit.json\n// NEAR blockchain\nexport NEAR_CONTRACT_ID=discernible-io.near\nexport NEAR_RPC_URL=https://rpc.mainnet.fastnear.com\nexport NEAR_RPC_CACHE_TTL=5000       # milliseconds\n```\n\n#### Session Management\n```bash\n// Session storage configuration\nexport SESSION_STORAGE_TYPE=express-session     # memory, express, express-session\nexport SESSION_CLEANUP_INTERVAL=3600000         # milliseconds (1 hour)\nexport SESSION_TOKEN_RETENTION_PERIOD=604800    # seconds (7 days)\nexport SESSION_VALIDATION_CACHE_TTL=5000        # milliseconds (5 seconds)\n```\n\n#### Logging and Monitoring\n```bash\n// Loki logging\nexport LOKI_URL=https://loki.example.com:3100\nexport LOKI_BASIC_AUTH=username:password\nexport LOKI_TLS_SKIP_VERIFY=false    # true to skip TLS verification\n```\n\n#### Security Options\n```bash\n// Webhook TLS (public destinations only; private/metadata URLs are always blocked)\nexport SECURITY_OPTIONS_WEBHOOK_TLS_SKIP_VERIFY=false  # true for self-signed public webhook hosts\n// Login mode control (see Login Mode section below)\nexport SECURITY_OPTIONS_LOGIN_MODE=partner  # partner, promiscuous, or p2p\n// Security thresholds\nexport SECURITY_OPTIONS_LAPSED_LIFETIME_PROPORTION_4RENEWAL_ELIGIBILITY=0.80\nexport SECURITY_OPTIONS_THRESHOLD_VALIDATION_TYPE=0.10\nexport SECURITY_OPTIONS_DURATIONRAMP=0.85\nexport SECURITY_OPTIONS_SERVERORCLIENT=SERVER-INITIATED\nexport SECURITY_OPTIONS_SILENT_LOGIN_FAILURES=false\n// Server session lifetime (seconds from login; SDK default 5200)\nexport SECURITY_OPTIONS_SESSION_TTL_SECONDS=5200\n// Access-token fallback when passport jwt_duration is invalid\nexport SECURITY_OPTIONS_FALLBACK_JWT_DURATION=3600\n```\n\n#### Database Configuration\n```bash\nexport API_DEFAULT_OPTIONS_DB_PATH=/app/data/database.sqlite\n```\n\n## Logging & Monitoring\n\n### Structured Logging\n\nThe SDK provides comprehensive structured logging:\n\n```javascript\nconst { logger } = require('@rodit/rodit-auth-be')\n// Basic logging\nlogger.info('Operation completed', {\n  component: 'UserService',\n  operation: 'createUser',\n  userId: '123',\n  duration: 150\n})\n// Context-aware logging\nlogger.infoWithContext('Request processed', {\n  component: 'API',\n  method: 'POST',\n  path: '/api/users',\n  requestId: req.requestId,\n  userId: req.user?.id,\n  duration: Date.now() - req.startTime\n})\n// Error logging with metrics\nlogger.errorWithContext('Operation failed', {\n  component: 'UserService',\n  operation: 'createUser',\n  requestId: req.requestId,\n  error: error.message,\n  stack: error.stack\n}, error)\n```\n\n### Loki with the SDK (canonical)\n\nUse this as the authoritative guide for configuring logging with the SDK.\n\n#### Environment variables\n\n```bash\nexport LOKI_URL=https://<your-loki-host>:3100\nexport LOKI_BASIC_AUTH=\"username:password\"   # store in secrets\nexport LOKI_TLS_SKIP_VERIFY=true              # only for self-signed/test\nexport LOG_LEVEL=info\nexport SERVICE_NAME=clienttest-idc\n```\n\nThese are already mapped in `config/custom-environment-variables.json`, so container/CI env vars will flow into the app.\n\n#### How the SDK selects/configures the logger\n\n- Default: JSON to stdout only (no Loki). Honors `LOG_LEVEL`, adds `service_name`.\n- Main: Create a Winston logger with a `winston-loki` transport and inject it once: `logger.setLogger(customLogger)`.\n- Access: `const { logger } = require('@rodit/rodit-auth-be')` or `roditClient.getLogger()` both delegate to the same facade.\n\n#### Direct-to-Loki via winston-loki (recommended)\n\n```javascript\nconst { logger } = require('@rodit/rodit-auth-be')\nconst winston = require('winston')\nconst LokiTransport = require('winston-loki')\nconst transports = [new winston.transports.Console({ format: winston.format.json() })]\nif (process.env.LOKI_URL) {\n  const lokiOptions = {\n    host: process.env.LOKI_URL,\n    basicAuth: process.env.LOKI_BASIC_AUTH, // Basic Auth for Loki\n    labels: { app: process.env.SERVICE_NAME || 'clienttest-idc', component: 'rodit-sdk' },\n    json: true,\n    batching: true\n  }\nif ((process.env.LOKI_TLS_SKIP_VERIFY || '').toLowerCase() === 'true') {\n  lokiOptions.ssl = { rejectUnauthorized: false }\n}\ntransports.push(new LokiTransport(lokiOptions))\n}\nconst customLogger = winston.createLogger({\n  level: process.env.LOG_LEVEL || 'info',\n  format: winston.format.json(),\n  transports\n})\nlogger.setLogger(customLogger)\n```\n\n#### CI/CD notes\n\n- `.github/workflows/deploy.yml` passes `LOKI_URL`, `LOKI_TLS_SKIP_VERIFY`, `LOKI_BASIC_AUTH` into the container; `src/app.js` config injects the transport at startup.\n- Store `LOKI_BASIC_AUTH` in CI/CD secrets; never commit credentials.\n\n#### Quick verification\n\n 1) Start the app with `LOKI_URL` and `LOKI_BASIC_AUTH` set.\n 2) Emit a test log: `logger.info('Loki test', { component: 'SmokeTest' })`.\n 3) In Grafana Explore, query with `{app=\"clienttest-idc\"}` and confirm logs.\n\n## Performance Tracking\n\nThe SDK includes comprehensive performance tracking and metrics collection.\n\n### Performance Service\n\n```javascript\nconst performanceService = roditClient.getPerformanceService()\n// Record incoming request\nperformanceService.recordRequest(req)\n// Record custom metrics with labels\nperformanceService.recordMetric('operation_duration', 150, {\n  operation: 'db_query',\n  table: 'users',\n  status: 'success'\n})\n// Record errors\nperformanceService.recordMetric('error_count', 1, {\n  method: req.method,\n  path: req.path,\n  status: res.statusCode\n})\n// Get aggregated metrics\nconst metrics = performanceService.getMetrics()\nconsole.log('Total requests:', metrics.totalRequests)\nconsole.log('Error count:', metrics.errorCount)\nconsole.log('Average response time:', metrics.avgResponseTime)\n```\n\n### Automatic Request Tracking\n\nIntegrate performance tracking into your middleware:\n\n```javascript\n// Performance monitoring middleware\napp.use((req, res, next) => {\n  req.startTime = Date.now()\n  const performanceService = roditClient.getPerformanceService()\n  if (performanceService) {\n    performanceService.recordRequest(req)\n  }\nres.on('finish', () => {\n  const duration = Date.now() - req.startTime\n  if (performanceService) {\n    // Record request duration\n    performanceService.recordMetric('request_duration_ms', duration, {\n      method: req.method,\n      path: req.path,\n      status: res.statusCode\n    })\n  // Record errors\n  if (res.statusCode >= 400) {\n    performanceService.recordMetric('error_count', 1, {\n      method: req.method,\n      path: req.path,\n      status: res.statusCode\n    })\n}\n}\n})\nnext()\n})\n```\n\n### Session Performance Metrics\n\nTrack session-related performance:\n\n```javascript\nconst sessionManager = roditClient.getSessionManager()\n// Get validation cache statistics\nconst cacheStats = sessionManager.getValidationCacheStats()\nlogger.info('Session cache performance', {\n  component: 'SessionManager',\n  totalEntries: cacheStats.totalEntries,\n  validEntries: cacheStats.validEntries,\n  expiredEntries: cacheStats.expiredEntries,\n  cacheTTL: cacheStats.cacheTTL,\n  cacheEnabled: cacheStats.cacheEnabled\n})\n// Get storage information\nconst storageInfo = await sessionManager.getStorageInfo()\nlogger.info('Session storage status', {\n  component: 'SessionManager',\n  storageType: storageInfo.type,\n  sessionCount: storageInfo.sessionCount,\n  timestamp: storageInfo.timestamp\n})\n```\n\n### Custom Metrics\n\nRecord application-specific metrics:\n\n```javascript\nconst performanceService = roditClient.getPerformanceService()\n// Database operation timing\nconst dbStart = Date.now()\nconst result = await db.query('SELECT * FROM users')\nconst dbDuration = Date.now() - dbStart\nperformanceService.recordMetric('db_query_duration', dbDuration, {\n  operation: 'select',\n  table: 'users',\n  rowCount: result.length\n})\n// External API call timing\nconst apiStart = Date.now()\nconst apiResponse = await fetch('https://api.example.com/data')\nconst apiDuration = Date.now() - apiStart\nperformanceService.recordMetric('external_api_duration', apiDuration, {\n  endpoint: 'api.example.com',\n  status: apiResponse.status,\n  success: apiResponse.ok\n})\n// Business metrics\nperformanceService.recordMetric('user_action', 1, {\n  action: 'comment_created',\n  userId: req.user.id,\n  timestamp: new Date().toISOString()\n})\n```\n\n## Webhooks\n\n### Overview\n\nThe SDK supports sending webhooks to multiple endpoints for important events. Webhook URLs come from the **peer JWT** claim `rodit_webhookurl` (from that peer’s RODiT `webhook_url` metadata).\n\n**Key Features:**\n- **Custom Endpoints** - Send webhooks to any endpoint path (e.g., `/hooks/wake`, `/hooks/agent`, `/webhook`)\n- **Ed25519 signed** - Payload + timestamp signed; receivers verify via `X-Signature` / signer identity headers\n- **Outbound SSRF controls (9.15+)** - Before `fetch`, reject userinfo and private / loopback / link-local / ULA / CGNAT / cloud-metadata hostnames and resolved A/AAAA; enforce peer `rodit_webhookcidr` when set; resolve-once and pin DNS for the request\n- **Self-signed TLS (public only)** - `SECURITY_OPTIONS.WEBHOOK_TLS_SKIP_VERIFY=true` still allowed for **public** destinations\n\nPeer RODiT metadata:\n\n```json\n{\n  \"webhook_url\": \"hooks.example.com:7443\",\n  \"webhook_cidr\": \"0.0.0.0/0\"\n}\n```\n\n- `webhook_url` — `host` or `host:port` (scheme optional), optionally with a path. If **no path** is present, the SDK appends `endpoint` (e.g. `https://hooks.example.com:7443/hooks/wake`). If a path is already present (e.g. `…:7443/hooks/agent`), that path is used as-is and nothing is appended.\n- `webhook_cidr` — allowlist for the **resolved** destination IP (`0.0.0.0/0` / empty = any public IP still subject to the SSRF blocklist)\n\nOutbound rejection codes (returned as `{ isValid: false, error: { code, message, requestId } }`):\n\n| Code | Meaning |\n|------|---------|\n| `WEBHOOK_URL_MISSING` | No `rodit_webhookurl` on `req.user` |\n| `WEBHOOK_URL_INVALID` | Bad URL / userinfo / scheme |\n| `WEBHOOK_URL_BLOCKED` | Private, loopback, or metadata target |\n| `WEBHOOK_URL_UNRESOLVABLE` | DNS lookup failed |\n| `WEBHOOK_CIDR_DENIED` | Resolved IP outside peer `webhook_cidr` |\n\n### Sending Webhooks to Default Endpoint\n\nSend webhooks to the default `/webhook` endpoint:\n\n```javascript\n// Get webhook handler from client\nconst roditClient = req.app.locals.roditClient\n// Send webhook for an event\nconst webhookPayload = {\n  event: 'comment_created',\n  data: {\n    id: comment.id,\n    author: comment.author,\n    timestamp: new Date().toISOString()\n  },\nisError: false\n}\ntry {\n  const result = await roditClient.sendWebhook(webhookPayload, req)\n  if (result.success) {\n    logger.info('Webhook sent successfully', {\n      component: 'CRUDA',\n      event: webhookPayload.event,\n      requestId: req.requestId\n    })\n}\n} catch (error) {\n// Webhook failures don't crash the application\nlogger.warn('Webhook delivery failed', {\n  component: 'CRUDA',\n  event: webhookPayload.event,\n  error: error.message,\n  requestId: req.requestId\n})\n}\n```\n\n### Sending Webhooks to Custom Endpoints\n\nSend webhooks to specific endpoints like `/hooks/wake` or `/hooks/agent`:\n\n```javascript\nconst roditClient = req.app.locals.roditClient\nconst webhookPayload = {\n  event: 'heartbeat_request',\n  data: {\n    timestamp: new Date().toISOString(),\n    source: '/api/testhola'\n  }\n}\n// Send to /hooks/wake endpoint (trigger immediate heartbeat)\nawait roditClient.sendWebhookToEndpoint(webhookPayload, '/hooks/wake', req)\n// Send to /hooks/agent endpoint (run isolated agent task)\nawait roditClient.sendWebhookToEndpoint(webhookPayload, '/hooks/agent', req)\n// Send to custom endpoint\nawait roditClient.sendWebhookToEndpoint(webhookPayload, '/hooks/custom', req)\n```\n\n### Convenience Methods for Common Endpoints\n\n```javascript\nconst roditClient = req.app.locals.roditClient\nconst payload = {\n  event: 'test_event',\n  data: { timestamp: new Date().toISOString() }\n}\n// Send to /hooks/wake (heartbeat confirmation)\nawait roditClient.sendWakeHook(payload, req)\n// Send to /hooks/agent (agent task confirmation)\nawait roditClient.sendAgentHook(payload, req)\n```\n\n### Webhook Endpoint Purposes\n\n| Endpoint | Purpose | Use Case |\n|----------|---------|----------|\n| `/webhook` | Default webhook endpoint | General event notifications |\n| `/hooks/wake` | Trigger immediate heartbeat | Enqueue system event for main session |\n| `/hooks/agent` | Run isolated agent task | Execute background tasks with optional reply to messaging channels |\n\n### Webhook Error Handling\n\n```javascript\n// Graceful webhook handling in CRUDA operations\nconst logAndSendWebhook = async (payload, req = null) => {\n  try {\n    const roditClient = req?.app?.locals?.roditClient\n    if (!roditClient) {\n      logger.warn('RoditClient not available, skipping webhook', {\n        component: 'CRUDA',\n        event: payload?.event\n      })\n    { success: false, error: 'RoditClient not available' }\n  }\nawait roditClient.sendWebhook(payload, req)\n} catch (error) {\n// Log but don't throw - webhook failures shouldn't crash the app\nlogger.error('Webhook delivery failed', {\n  component: 'CRUDA',\n  event: payload?.event,\n  error: error.message\n})\n{ success: false, error: error.message }\n}\n}\n```\n\n### Development/Testing Webhooks\n\nThe `/api/testhola` endpoint sends test webhooks in development mode (`NODE_ENV === 'development'`):\n\n```json\n{\n  \"event\": \"testhola_validation_success\",\n  \"data\": {\n    \"peerTokenId\": \"bcdfhjkmnpqr\",\n    \"serverTokenId\": \"bcdfhjkmnpqr\",\n    \"recipient\": \"MUNDO\",\n    \"timestamp\": \"2026-04-24T14:30:00.000Z\",\n    \"endpoint\": \"/api/testhola\"\n  }\n}\n```\n\n**Use Case:** Test webhook delivery and signature validation during development without needing a main deployment.\n\n## Advanced Usage\n\n### Route Module Pattern\n\nCreate reusable route modules that access the shared RoditClient:\n\n```javascript\n// routes/protected.js\nconst express = require('express')\nconst { logger } = require('@rodit/rodit-auth-be')\nconst router = express.Router()\n// Middleware that uses the shared client\nconst authenticate = (req, res, next) => {\n  const client = req.app.locals.roditClient\n  if (!client) {\n    res.status(503).json({ error: 'Authentication service unavailable' })\n  }\nclient.authenticate(req, res, next)\n}\nconst authorize = (req, res, next) => {\n  const client = req.app.locals.roditClient\n  if (!client) {\n    res.status(503).json({ error: 'Authentication service unavailable' })\n  }\nclient.authorize(req, res, next)\n}\n// Protected route with full authentication and authorization\nrouter.get('/data', authenticate, authorize, async (req, res) => {\n  const startTime = Date.now()\n  try {\n    // Your business logic here\n    const data = await processUserData(req.user.id)\n    logger.infoWithContext('Data retrieved successfully', {\n      component: 'ProtectedRoutes',\n      method: 'getData',\n      userId: req.user.id,\n      requestId: req.requestId,\n      duration: Date.now() - startTime\n    })\n  res.json({ data, requestId: req.requestId })\n} catch (error) {\nlogger.errorWithContext('Failed to retrieve data', {\n  component: 'ProtectedRoutes',\n  method: 'getData',\n  userId: req.user.id,\n  requestId: req.requestId,\n  duration: Date.now() - startTime,\n  error: error.message\n}, error)\nres.status(500).json({\n  error: 'Internal server error',\n  requestId: req.requestId\n})\n}\n})\nmodule.exports = router\n```\n\n### Portal Authentication (Server-to-Server)\n\nFor server-to-server authentication (e.g., minting client tokens):\n\n```javascript\n// routes/signclient.js\nconst router = express.Router()\nrouter.post('/signclient', authenticate, authorize, async (req, res) => {\n  const { tobesignedValues, mintingfee, mintingfeeaccount } = req.body\n  const client = req.app.locals.roditClient\n  const logger = client.getLogger()\n  try {\n    // Validate requested permissions against server's permissions\n    const configObject = await client.getConfigOwnRodit()\n    const serverPermissions = JSON.parse(\n    configObject.own_rodit.metadata.permissioned_routes || '{}'\n    )\n    const requestedPermissions = JSON.parse(\n    tobesignedValues.permissioned_routes || '{}'\n    )\n    // Validate that all requested routes exist in server config\n    // (Implementation details in actual code)\n    // Authenticate to portal and mint client token\n    const port = configObject.port || 8443\n    const result = await client.login_portal(configObject, port)\n    if (result.error) {\n      res.status(500).json({\n        error: 'Portal authentication failed',\n        details: result.message,\n        requestId: req.requestId\n      })\n  }\n// Sign the client token via portal\nconst signedToken = await signPortalRodit(\nport,\ntobesignedValues,\nmintingfee,\nmintingfeeaccount,\nclient\n)\nres.json({\n  signedToken,\n  requestId: req.requestId\n})\n} catch (error) {\nlogger.errorWithContext('Client token minting failed', {\n  component: 'SignClient',\n  requestId: req.requestId,\n  error: error.message\n}, error)\nres.status(500).json({\n  error: 'Token minting failed',\n  requestId: req.requestId\n})\n}\n})\nmodule.exports = router\n```\n\n### SignPortal URL Configuration\n\n#### Overview\n\nWhen performing server-to-server authentication with SignPortal (e.g., minting client tokens), the SDK automatically constructs the SignPortal URL from the `serviceprovider_id` field in your RODiT token metadata.\n\n#### Smart Contract Name Format\n\nThe SignPortal URL is derived from the smart contract component (`sc=`) in your `serviceprovider_id`. The SDK supports two formats:\n\n**Standard Format (3+ components):**\n```\nsc=<number>-<domain>-<tld>.near\n```\n\nExample:\n```\nserviceprovider_id: \"bc=near.org;sc=10975-discernible-org.near;id=...\"\n```\n\nParsing:\n- Split by `.`: `[\"10975-discernible-org\", \"near\"]`\n- Take first part: `10975-discernible-org`\n- Split by `-`: `[\"10975\", \"discernible\", \"org\"]`\n- Extract domain: `discernible` (index 1)\n- Extract TLD: `org` (index 2)\n- **Result**: `https://signportal.discernible.org:8443`\n\n**Alternative Format (2 components):**\n```\nsc=<domain>-<tld>.near\n```\n\nExample:\n```\nserviceprovider_id: \"bc=near.org;sc=roditcorp-com.near;id=...\"\n```\n\nParsing:\n- Split by `.`: `[\"roditcorp-com\", \"near\"]`\n- Take first part: `roditcorp-com`\n- Split by `-`: `[\"roditcorp\", \"com\"]`\n- Extract domain: `roditcorp` (index 0)\n- Extract TLD: `com` (index 1)\n- **Result**: `https://signportal.roditcorp.com:8443`\n\n#### serviceprovider_id Structure\n\nThe complete `serviceprovider_id` format:\n```\nbc=<blockchain>;sc=<smart-contract>;id=<identifier>[;id=<additional-id>]\n```\n\nComponents:\n- `bc=` - Blockchain identifier (e.g., `near.org`)\n- `sc=` - Smart contract name (used to construct SignPortal URL)\n- `id=` - One or more identifier components\n\nExample:\n```json\n{\n  \"serviceprovider_id\": \"bc=near.org;sc=roditcorp-com.near;id=01K8QECHMKFVNWQ54PJ2W2GMA7;id=01K8QECHMM1214VMDHSH7JM6H8\"\n}\n```\n\n#### URL Construction Method\n\nThe SDK uses `roditClient.getPortalUrl(serviceProviderId, port)` to construct the SignPortal URL:\n\n```javascript\nconst client = req.app.locals.roditClient\nconst configObject = await client.getConfigOwnRodit()\nconst serviceProviderId = configObject.own_rodit.metadata.serviceprovider_id\nconst portalPort = 8443\n// Automatically constructs: https://signportal.<domain>.<tld>:8443\nconst portalUrl = client.getPortalUrl(serviceProviderId, portalPort)\n```\n\n#### Troubleshooting\n\n**Error: \"Failed to parse URL from \" (empty string)**\n- **Cause**: `serviceprovider_id` is empty or undefined in your RODiT configuration\n- **Solution**: Verify your RODiT token has a valid `serviceprovider_id` field\n- **Check**: Run `./infra/roditwallet.sh <private-key> <token-id>` to view token metadata\n\n**Error: \"Invalid serviceprovider_id format: missing sc= component\"**\n- **Cause**: The `serviceprovider_id` doesn't contain an `sc=` component\n- **Solution**: Ensure your token includes the smart contract identifier\n- **Format**: `bc=near.org;sc=<contract-name>.near;id=...`\n\n**Error: \"Invalid domain format in smart contract\"**\n- **Cause**: Smart contract name has fewer than 2 components when split by `-`\n- **Solution**: Use format `<domain>-<tld>` or `<number>-<domain>-<tld>`\n- **Valid**: `roditcorp-com.near`, `10975-discernible-org.near`\n- **Invalid**: `roditcorp.near`, `mycontract.near`\n\n#### Configuration Verification\n\nTo verify your SignPortal URL configuration:\n\n```javascript\nconst client = req.app.locals.roditClient\nconst logger = client.getLogger()\ntry {\n  const configObject = await client.getConfigOwnRodit()\n  const serviceProviderId = configObject.own_rodit.metadata.serviceprovider_id\n  logger.info('RODiT Configuration', {\n    component: 'SignPortal',\n    serviceProviderId,\n    hasServiceProviderId: !!serviceProviderId\n  })\nif (serviceProviderId) {\n  const portalUrl = client.getPortalUrl(serviceProviderId, 8443)\n  logger.info('SignPortal URL constructed', {\n    component: 'SignPortal',\n    portalUrl\n  })\n}\n} catch (error) {\nlogger.error('SignPortal URL construction failed', {\n  component: 'SignPortal',\n  error: error.message\n})\n}\n```\n\n### CRUDA Operations Example\n\nComplete CRUD implementation with authentication, authorization, webhooks, and performance tracking:\n\n```javascript\n// protected/cruda.js\nconst express = require('express')\nconst router = express.Router()\nconst { RoditClient } = require('@rodit/rodit-auth-be')\nconst sqlite3 = require('sqlite3')\nconst { open } = require('sqlite')\nconst { ulid } = require('ulid')\nconst sdkClient = new RoditClient()\nconst logger = sdkClient.getLogger()\nlet db\n// Initialize database\nconst initializeDatabase = async () => {\n  const db = await open({\n    filename: '/app/data/database.sqlite',\n    driver: sqlite3.Database\n  })\nawait db.run(`CREATE TABLE IF NOT EXISTS comments (\nid INTEGER PRIMARY KEY AUTOINCREMENT,\ncomment TEXT NOT NULL,\nauthor TEXT,\ncreated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\nupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP\n)`)\n}\n// Webhook helper\nconst logAndSendWebhook = async (payload, req) => {\n  try {\n    const roditClient = req?.app?.locals?.roditClient\n    if (!roditClient) return { success: false }\n    await roditClient.send_webhook(payload, req)\n  } catch (error) {\n  logger.error('Webhook failed', { error: error.message })\n  { success: false, error: error.message }\n}\n}\n// CREATE\nrouter.post('/create', async (req, res) => {\n  const { comment, author } = req.body\n  const requestId = req.requestId || ulid()\n  try {\n    const result = await db.run(\n    'INSERT INTO comments (comment, author) VALUES (?, ?)',\n    [comment, author || req.user.roditId]\n    )\n    // Send webhook\n    await logAndSendWebhook({\n      event: 'comment_created',\n      data: { id: result.lastID, comment, author },\n      isError: false\n    }, req)\n  res.json({ id: result.lastID, requestId })\n} catch (error) {\nlogger.errorWithContext('Create failed', {\n  component: 'CRUDA',\n  error: error.message,\n  requestId\n}, error)\nres.status(500).json({ error: 'Create failed', requestId })\n}\n})\n// LIST\nrouter.post('/list', async (req, res) => {\n  try {\n    const records = await db.all(\n    'SELECT * FROM comments ORDER BY created_at DESC'\n    )\n    res.json(","readmeFilename":"README.md"}