From 6fc8b4458a222db20c438b1984d67ae9c2602bef Mon Sep 17 00:00:00 2001 From: robinmoisson Date: Mon, 8 Jun 2026 14:54:43 +0200 Subject: [PATCH 01/15] update --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c0dcf930..b0fbdc9c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "/lib" ], "bin": { - "staticrypt": "./cli/index.js" + "staticrypt": "cli/index.js" }, "dependencies": { "dotenv": "^16.0.3", From 74be960b8acec1d63d8b77bd191fde6acc4c620e Mon Sep 17 00:00:00 2001 From: robinmoisson Date: Mon, 8 Jun 2026 14:54:46 +0200 Subject: [PATCH 02/15] test: scaffold node:test runner and move legacy fixtures into test/fixtures/legacy-html - Remove `test/` from .gitignore so the directory can hold real test files - Move legacy manual fixtures into test/fixtures/legacy-html/ - Add `test` script using `node --test --test-reporter=spec` - Add jsdom ^24.0.0 as devDependency --- .gitignore | 1 - package-lock.json | 811 +++++++++++++++++- package.json | 4 +- test/fixtures/legacy-html/test.css | 3 + test/fixtures/legacy-html/test.html | 1 + .../legacy-html/test1/Selection_001.png | Bin 0 -> 47791 bytes test/fixtures/legacy-html/test1/index.html | 1 + test/fixtures/legacy-html/test2/index.html | 1 + 8 files changed, 818 insertions(+), 4 deletions(-) create mode 100755 test/fixtures/legacy-html/test.css create mode 100755 test/fixtures/legacy-html/test.html create mode 100755 test/fixtures/legacy-html/test1/Selection_001.png create mode 100755 test/fixtures/legacy-html/test1/index.html create mode 100755 test/fixtures/legacy-html/test2/index.html diff --git a/.gitignore b/.gitignore index e2bed4b5..4f5d5d25 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ node_modules encrypted/ !example/encrypted/ decrypted/ -test/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3db01a1e..0af2b442 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "staticrypt", - "version": "3.5.3", + "version": "3.5.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "staticrypt", - "version": "3.5.3", + "version": "3.5.4", "license": "MIT", "dependencies": { "dotenv": "^16.0.3", @@ -17,6 +17,7 @@ }, "devDependencies": { "husky": "^9.1.6", + "jsdom": "^24.0.0", "lint-staged": "^15.2.10", "prettier": "^2.8.8" }, @@ -28,6 +29,145 @@ "url": "https://github.com/robinmoisson/staticrypt?sponsor=1" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ansi-escapes": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", @@ -70,6 +210,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -83,6 +230,20 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", @@ -250,6 +411,19 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", @@ -275,6 +449,41 @@ "node": ">= 8" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -293,6 +502,23 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dotenv": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", @@ -305,6 +531,21 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", @@ -312,6 +553,19 @@ "dev": true, "license": "MIT" }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -325,6 +579,55 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -378,6 +681,33 @@ "node": ">=8" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -400,6 +730,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -413,6 +782,102 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -439,6 +904,19 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", @@ -462,6 +940,13 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -482,6 +967,47 @@ "dev": true, "license": "ISC" }, + "node_modules/jsdom": { + "version": "24.1.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz", + "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -594,6 +1120,23 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -615,6 +1158,29 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -677,6 +1243,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, "node_modules/onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -693,6 +1266,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -745,6 +1331,36 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -754,6 +1370,13 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -794,6 +1417,33 @@ "dev": true, "license": "MIT" }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -904,6 +1554,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -917,6 +1574,117 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -951,6 +1719,45 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index b0fbdc9c..048849a1 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "scripts": { "build": "bash ./scripts/build.sh", "format": "prettier --write \"**/*.{js,json,html}\"", - "prepare": "husky" + "prepare": "husky", + "test": "node --test --test-reporter=spec" }, "lint-staged": { "**/*.{js,json,html}": [ @@ -57,6 +58,7 @@ "homepage": "https://github.com/robinmoisson/staticrypt", "devDependencies": { "husky": "^9.1.6", + "jsdom": "^24.0.0", "lint-staged": "^15.2.10", "prettier": "^2.8.8" } diff --git a/test/fixtures/legacy-html/test.css b/test/fixtures/legacy-html/test.css new file mode 100755 index 00000000..816e5fe7 --- /dev/null +++ b/test/fixtures/legacy-html/test.css @@ -0,0 +1,3 @@ +html { + height: 100%; +} diff --git a/test/fixtures/legacy-html/test.html b/test/fixtures/legacy-html/test.html new file mode 100755 index 00000000..9b599001 --- /dev/null +++ b/test/fixtures/legacy-html/test.html @@ -0,0 +1 @@ +Yooo un test diff --git a/test/fixtures/legacy-html/test1/Selection_001.png b/test/fixtures/legacy-html/test1/Selection_001.png new file mode 100755 index 0000000000000000000000000000000000000000..189707c06e6f910a1267404b2498c2e0b5166cb7 GIT binary patch literal 47791 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 Date: Mon, 8 Jun 2026 14:54:47 +0200 Subject: [PATCH 03/15] remove accidentally-tracked Selection_001.png screenshot Was carried into the repo from the pre-existing untracked test1/ directory during Task 1's fixture move. Not a meaningful fixture. --- .../legacy-html/test1/Selection_001.png | Bin 47791 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100755 test/fixtures/legacy-html/test1/Selection_001.png diff --git a/test/fixtures/legacy-html/test1/Selection_001.png b/test/fixtures/legacy-html/test1/Selection_001.png deleted file mode 100755 index 189707c06e6f910a1267404b2498c2e0b5166cb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47791 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 Date: Mon, 8 Jun 2026 14:54:49 +0200 Subject: [PATCH 04/15] test: add fixture generator and golden encrypted-blob fixture --- test/fixtures/encrypted-blobs/basic.json | 9 +++++ test/fixtures/generate.js | 51 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 test/fixtures/encrypted-blobs/basic.json create mode 100644 test/fixtures/generate.js diff --git a/test/fixtures/encrypted-blobs/basic.json b/test/fixtures/encrypted-blobs/basic.json new file mode 100644 index 00000000..f1241d0c --- /dev/null +++ b/test/fixtures/encrypted-blobs/basic.json @@ -0,0 +1,9 @@ +{ + "plaintext": "Hello, world! héllo wörld", + "password": "testpassword123", + "salt": "abcd1234abcd1234abcd1234abcd1234", + "hashLegacyOnly": "2c604d17a2bd3f824ef29ee524a301a9f353f884814ca5b6b319fb59c59b04e5", + "hashLegacyAndSecond": "cb2b7fd427dfc23709b3e63647207228d1b97e202c2f650b4aa639292698ec0d", + "hashFull": "fae456c9423f706ed6784f2e5210c7f9e2013f6adc7e2a7d93ea168659613e81", + "encrypted": "21d5b2ba82eab7a5c495f706e9ea1061fe8d64ffcd484ddd8d617a9ba24181c8b013ab8ee437bb7c15a291f5e75bde3f5d8cb9907f6c05b0fed4c8813d4b27adf1120e2414612ca0434c678ebc57e737" +} diff --git a/test/fixtures/generate.js b/test/fixtures/generate.js new file mode 100644 index 00000000..a9e6a742 --- /dev/null +++ b/test/fixtures/generate.js @@ -0,0 +1,51 @@ +// Regenerates committed fixtures in test/fixtures/encrypted-blobs/. +// Run by hand: `node test/fixtures/generate.js`. +// CI does NOT run this; it consumes the committed JSON output. + +const fs = require("fs"); +const path = require("path"); +const cryptoEngine = require("../../lib/cryptoEngine.js"); +const codec = require("../../lib/codec.js").init(cryptoEngine); + +const OUT_DIR = path.join(__dirname, "encrypted-blobs"); + +async function main() { + fs.mkdirSync(OUT_DIR, { recursive: true }); + + // Fixed inputs — chosen for readability and to cover non-ASCII. + const password = "testpassword123"; + const salt = "abcd1234abcd1234abcd1234abcd1234"; + const plaintext = "Hello, world! héllo wörld"; + + // Compute every intermediate hash so the compat tests can exercise each retry branch. + const hashLegacyOnly = await cryptoEngine.hashLegacyRound(password, salt); + const hashLegacyAndSecond = await cryptoEngine.hashSecondRound(hashLegacyOnly, salt); + const hashFull = await cryptoEngine.hashThirdRound(hashLegacyAndSecond, salt); + + // Sanity: hashFull must equal the public hashPassword() output. + const hashFullViaPublic = await cryptoEngine.hashPassword(password, salt); + if (hashFull !== hashFullViaPublic) { + throw new Error("hashPassword() drifted from manual 3-round chain"); + } + + // Encode with the fully hashed password — this is what's stored in real pages. + const encrypted = await codec.encodeWithHashedPassword(plaintext, hashFull); + + const fixture = { + plaintext, + password, + salt, + hashLegacyOnly, + hashLegacyAndSecond, + hashFull, + encrypted, + }; + + fs.writeFileSync(path.join(OUT_DIR, "basic.json"), JSON.stringify(fixture, null, 2) + "\n"); + console.log("Wrote", path.join(OUT_DIR, "basic.json")); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); From 9dbdad8371afffbb30374040b9d5e9b975eb0ceb Mon Sep 17 00:00:00 2001 From: robinmoisson Date: Mon, 8 Jun 2026 14:54:50 +0200 Subject: [PATCH 05/15] test: pin cryptoEngine primitives and hash-chain iteration counts --- test/cryptoEngine.test.js | 87 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 test/cryptoEngine.test.js diff --git a/test/cryptoEngine.test.js b/test/cryptoEngine.test.js new file mode 100644 index 00000000..15e4fddc --- /dev/null +++ b/test/cryptoEngine.test.js @@ -0,0 +1,87 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const cryptoEngine = require("../lib/cryptoEngine.js"); + +const FIXTURE = JSON.parse(fs.readFileSync(path.join(__dirname, "fixtures", "encrypted-blobs", "basic.json"), "utf8")); + +test("HexEncoder round-trips arbitrary bytes (via encrypt/decrypt path)", async () => { + // HexEncoder isn't exported directly; we exercise it through encrypt/decrypt. + const cipher = await cryptoEngine.encrypt("abc", FIXTURE.hashFull); + const plain = await cryptoEngine.decrypt(cipher, FIXTURE.hashFull); + assert.equal(plain, "abc"); +}); + +test("encrypt produces fresh ciphertext each call (random IV) but always decrypts", async () => { + const a = await cryptoEngine.encrypt("same input", FIXTURE.hashFull); + const b = await cryptoEngine.encrypt("same input", FIXTURE.hashFull); + assert.notEqual(a, b, "two encryptions of the same input must differ (random IV)"); + assert.equal(await cryptoEngine.decrypt(a, FIXTURE.hashFull), "same input"); + assert.equal(await cryptoEngine.decrypt(b, FIXTURE.hashFull), "same input"); +}); + +test("encrypt output starts with 32 hex chars of IV", async () => { + const cipher = await cryptoEngine.encrypt("x", FIXTURE.hashFull); + assert.match(cipher.slice(0, 32), /^[0-9a-f]{32}$/); +}); + +test("hashLegacyRound matches pinned fixture (1000 iters SHA-1)", async () => { + const hash = await cryptoEngine.hashLegacyRound(FIXTURE.password, FIXTURE.salt); + assert.equal(hash, FIXTURE.hashLegacyOnly); +}); + +test("hashSecondRound matches pinned fixture (14000 iters SHA-256 on top of legacy)", async () => { + const hash = await cryptoEngine.hashSecondRound(FIXTURE.hashLegacyOnly, FIXTURE.salt); + assert.equal(hash, FIXTURE.hashLegacyAndSecond); +}); + +test("hashThirdRound matches pinned fixture (585000 iters SHA-256 on top of 2nd)", async () => { + const hash = await cryptoEngine.hashThirdRound(FIXTURE.hashLegacyAndSecond, FIXTURE.salt); + assert.equal(hash, FIXTURE.hashFull); +}); + +test("hashPassword full chain matches the manual composition", async () => { + const hash = await cryptoEngine.hashPassword(FIXTURE.password, FIXTURE.salt); + assert.equal(hash, FIXTURE.hashFull); +}); + +test("hashPassword output is 64 hex chars (256-bit key)", async () => { + const hash = await cryptoEngine.hashPassword("other", FIXTURE.salt); + assert.match(hash, /^[0-9a-f]{64}$/); +}); + +test("generateRandomSalt returns 32 hex chars", () => { + const salt = cryptoEngine.generateRandomSalt(); + assert.match(salt, /^[0-9a-f]{32}$/); +}); + +test("generateRandomSalt returns different values across calls", () => { + assert.notEqual(cryptoEngine.generateRandomSalt(), cryptoEngine.generateRandomSalt()); +}); + +test("generateRandomString returns the requested length of alphanums", () => { + const s = cryptoEngine.generateRandomString(21); + assert.equal(s.length, 21); + assert.match(s, /^[A-Za-z0-9]{21}$/); +}); + +test("signMessage is deterministic for the same inputs", async () => { + const a = await cryptoEngine.signMessage(FIXTURE.hashFull, "message"); + const b = await cryptoEngine.signMessage(FIXTURE.hashFull, "message"); + assert.equal(a, b); + assert.match(a, /^[0-9a-f]{64}$/); // HMAC-SHA-256 = 32 bytes = 64 hex +}); + +test("signMessage changes with a single-bit message change", async () => { + const a = await cryptoEngine.signMessage(FIXTURE.hashFull, "message"); + const b = await cryptoEngine.signMessage(FIXTURE.hashFull, "messagf"); + assert.notEqual(a, b); +}); + +test("decrypt throws on tampered ciphertext", async () => { + const cipher = await cryptoEngine.encrypt("abc", FIXTURE.hashFull); + // flip a hex digit in the ciphertext portion (after the 32-char IV) + const tampered = cipher.slice(0, 32) + (cipher[32] === "0" ? "1" : "0") + cipher.slice(33); + await assert.rejects(() => cryptoEngine.decrypt(tampered, FIXTURE.hashFull)); +}); From 2ff011ddd6c539d137f9e7d28515f49adcb89a69 Mon Sep 17 00:00:00 2001 From: robinmoisson Date: Mon, 8 Jun 2026 14:54:52 +0200 Subject: [PATCH 06/15] test: codec encode/decode round-trip + wire-format + tamper rejection --- test/codec.test.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 test/codec.test.js diff --git a/test/codec.test.js b/test/codec.test.js new file mode 100644 index 00000000..f4157f02 --- /dev/null +++ b/test/codec.test.js @@ -0,0 +1,54 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const cryptoEngine = require("../lib/cryptoEngine.js"); +const codec = require("../lib/codec.js").init(cryptoEngine); + +const FIXTURE = JSON.parse(fs.readFileSync(path.join(__dirname, "fixtures", "encrypted-blobs", "basic.json"), "utf8")); + +test("encode + decode round-trip with raw password", async () => { + const encoded = await codec.encode("secret content", FIXTURE.password, FIXTURE.salt); + const result = await codec.decode(encoded, FIXTURE.hashFull, FIXTURE.salt); + assert.equal(result.success, true); + assert.equal(result.decoded, "secret content"); +}); + +test("encodeWithHashedPassword + decode round-trip", async () => { + const encoded = await codec.encodeWithHashedPassword("hello", FIXTURE.hashFull); + const result = await codec.decode(encoded, FIXTURE.hashFull, FIXTURE.salt); + assert.equal(result.success, true); + assert.equal(result.decoded, "hello"); +}); + +test("decode of the committed fixture succeeds with the full hash", async () => { + const result = await codec.decode(FIXTURE.encrypted, FIXTURE.hashFull, FIXTURE.salt); + assert.equal(result.success, true); + assert.equal(result.decoded, FIXTURE.plaintext); +}); + +test("decode fails on tampered HMAC", async () => { + const encoded = await codec.encodeWithHashedPassword("hello", FIXTURE.hashFull); + // flip a bit in the HMAC (first 64 chars) + const tampered = (encoded[0] === "0" ? "1" : "0") + encoded.slice(1); + const result = await codec.decode(tampered, FIXTURE.hashFull, FIXTURE.salt); + assert.equal(result.success, false); + assert.equal(result.message, "Signature mismatch"); +}); + +test("decode fails on tampered ciphertext (HMAC catches it)", async () => { + const encoded = await codec.encodeWithHashedPassword("hello", FIXTURE.hashFull); + // flip a hex digit somewhere in the IV+ciphertext part (after the 64-char HMAC) + const i = 70; + const tampered = encoded.slice(0, i) + (encoded[i] === "0" ? "1" : "0") + encoded.slice(i + 1); + const result = await codec.decode(tampered, FIXTURE.hashFull, FIXTURE.salt); + assert.equal(result.success, false); + assert.equal(result.message, "Signature mismatch"); +}); + +test("wire format: encoded string = 64 hex HMAC + 32 hex IV + ciphertext", async () => { + const encoded = await codec.encodeWithHashedPassword("xyz", FIXTURE.hashFull); + assert.match(encoded.slice(0, 64), /^[0-9a-f]{64}$/, "HMAC prefix is 64 hex chars"); + assert.match(encoded.slice(64, 96), /^[0-9a-f]{32}$/, "IV is the next 32 hex chars"); + assert.match(encoded.slice(96), /^[0-9a-f]+$/, "ciphertext is hex"); +}); From 302632e0ebe5f02d17a5b4530bdf9399e4fd6179 Mon Sep 17 00:00:00 2001 From: robinmoisson Date: Mon, 8 Jun 2026 14:54:54 +0200 Subject: [PATCH 07/15] test: pin codec.decode backward-compat retry chain (3 cases + negative) --- test/codec-compat.test.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 test/codec-compat.test.js diff --git a/test/codec-compat.test.js b/test/codec-compat.test.js new file mode 100644 index 00000000..e6213bcf --- /dev/null +++ b/test/codec-compat.test.js @@ -0,0 +1,36 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const cryptoEngine = require("../lib/cryptoEngine.js"); +const codec = require("../lib/codec.js").init(cryptoEngine); + +const FIXTURE = JSON.parse(fs.readFileSync(path.join(__dirname, "fixtures", "encrypted-blobs", "basic.json"), "utf8")); + +test("decode succeeds with fully hashed password (no retry needed)", async () => { + const result = await codec.decode(FIXTURE.encrypted, FIXTURE.hashFull, FIXTURE.salt); + assert.equal(result.success, true); + assert.equal(result.decoded, FIXTURE.plaintext); +}); + +test("backward-compat: decode succeeds with hashLegacyAndSecond (attempt 0 applies hashThirdRound)", async () => { + // Simulates an old localStorage token that was hashed with 1000 + 14000 iters but not the final 585k. + const result = await codec.decode(FIXTURE.encrypted, FIXTURE.hashLegacyAndSecond, FIXTURE.salt); + assert.equal(result.success, true); + assert.equal(result.decoded, FIXTURE.plaintext); +}); + +test("backward-compat: decode succeeds with hashLegacyOnly (attempt 1 applies hashSecondRound + hashThirdRound)", async () => { + // Simulates a very old token that was hashed with only the original 1000 iters SHA-1. + const result = await codec.decode(FIXTURE.encrypted, FIXTURE.hashLegacyOnly, FIXTURE.salt); + assert.equal(result.success, true); + assert.equal(result.decoded, FIXTURE.plaintext); +}); + +test("backward-compat: decode fails after both retry attempts with wrong password", async () => { + // A completely wrong hash should fail even after the retry chain. + const wrong = "0".repeat(64); + const result = await codec.decode(FIXTURE.encrypted, wrong, FIXTURE.salt); + assert.equal(result.success, false); + assert.equal(result.message, "Signature mismatch"); +}); From 6cb10b8a7b84ef57572a54fd5a6143ba21161e5e Mon Sep 17 00:00:00 2001 From: robinmoisson Date: Mon, 8 Jun 2026 14:54:55 +0200 Subject: [PATCH 08/15] test: renderTemplate token substitution and edge cases --- test/formater.test.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 test/formater.test.js diff --git a/test/formater.test.js b/test/formater.test.js new file mode 100644 index 00000000..4887942b --- /dev/null +++ b/test/formater.test.js @@ -0,0 +1,38 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const { renderTemplate } = require("../lib/formater.js"); + +test("replaces a /*[|key|]*/0 token with the matching data value", () => { + const out = renderTemplate("hello /*[|name|]*/0!", { name: "world" }); + assert.equal(out, "hello world!"); +}); + +test("accepts optional whitespace inside the token brackets", () => { + const out = renderTemplate("a /*[| name |]*/0 b", { name: "X" }); + assert.equal(out, "a X b"); +}); + +test("accepts optional whitespace before the trailing 0 (prettier-formatted)", () => { + const out = renderTemplate("a /*[|name|]*/ 0 b", { name: "X" }); + assert.equal(out, "a X b"); +}); + +test("replaces multiple occurrences of the same key", () => { + const out = renderTemplate("/*[|x|]*/0 /*[|x|]*/0", { x: "Y" }); + assert.equal(out, "Y Y"); +}); + +test("object values are JSON-stringified", () => { + const out = renderTemplate("config = /*[|cfg|]*/0;", { cfg: { a: 1, b: "two" } }); + assert.equal(out, 'config = {"a":1,"b":"two"};'); +}); + +test("missing key falls back to the bare key name (current behavior)", () => { + const out = renderTemplate("hello /*[|missing|]*/0", { other: "x" }); + assert.equal(out, "hello missing"); +}); + +test("non-token text is preserved unchanged", () => { + const out = renderTemplate("plain text with /* not a token */", {}); + assert.equal(out, "plain text with /* not a token */"); +}); From 55e71889bb5c7751b50d1a14d04e9be9ff439078 Mon Sep 17 00:00:00 2001 From: robinmoisson Date: Mon, 8 Jun 2026 14:54:57 +0200 Subject: [PATCH 09/15] test: cli/helpers pure functions (bundler, template detection, salt validation) --- test/cli-helpers.test.js | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 test/cli-helpers.test.js diff --git a/test/cli-helpers.test.js b/test/cli-helpers.test.js new file mode 100644 index 00000000..7d44fb4b --- /dev/null +++ b/test/cli-helpers.test.js @@ -0,0 +1,55 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const { + convertCommonJSToBrowserJS, + buildStaticryptJS, + isCustomPasswordTemplateDefault, + getValidatedSalt, +} = require("../cli/helpers.js"); +const path = require("node:path"); + +test("convertCommonJSToBrowserJS produces an IIFE that returns exports", () => { + const result = convertCommonJSToBrowserJS("lib/cryptoEngine"); + assert.ok(result.startsWith("((function(){"), "starts with IIFE wrapper"); + assert.ok(result.endsWith("})())"), "ends with IIFE wrapper"); + assert.ok(result.includes("const exports = {};"), "declares exports object"); + assert.ok(result.includes("return exports;"), "returns exports"); +}); + +test("convertCommonJSToBrowserJS strips lines containing require(...)", () => { + const result = convertCommonJSToBrowserJS("lib/cryptoEngine"); + assert.ok(!result.includes("require("), "no require() calls remain in bundled output"); + assert.ok(!result.includes("node:crypto"), "node-only branch removed"); +}); + +test("buildStaticryptJS inlines codec and cryptoEngine into staticryptJs", () => { + const result = buildStaticryptJS(); + // The result should contain the IIFE-wrapped sub-modules in place of the tokens. + assert.ok(result.includes("hashPassword"), "cryptoEngine code is inlined"); + assert.ok(result.includes("function init(cryptoEngine)"), "codec init() is inlined"); + assert.ok(result.includes("function init(staticryptConfig, templateConfig)"), "staticryptJs init() is present"); + // After token replacement, no /*[|...|]*/0 placeholders remain. + assert.ok(!/\/\*\[\|\s*\w+\s*\|]\*\/\s*0/.test(result), "no template tokens left after inlining"); +}); + +test("isCustomPasswordTemplateDefault recognizes the default template path", () => { + const defaultPath = path.join(__dirname, "..", "lib", "password_template.html"); + assert.equal(isCustomPasswordTemplateDefault(defaultPath), true); + assert.equal(isCustomPasswordTemplateDefault("/some/other/template.html"), false); +}); + +test("getValidatedSalt prefers the --salt CLI flag over config and over generation", () => { + const namedArgs = { salt: "abcd1234abcd1234abcd1234abcd1234" }; + const config = { salt: "ffff0000ffff0000ffff0000ffff0000" }; + assert.equal(getValidatedSalt(namedArgs, config), "abcd1234abcd1234abcd1234abcd1234"); +}); + +test("getValidatedSalt falls back to config salt when no flag", () => { + const config = { salt: "ffff0000ffff0000ffff0000ffff0000" }; + assert.equal(getValidatedSalt({}, config), "ffff0000ffff0000ffff0000ffff0000"); +}); + +test("getValidatedSalt lowercases the --salt CLI flag", () => { + const namedArgs = { salt: "ABCD1234ABCD1234ABCD1234ABCD1234" }; + assert.equal(getValidatedSalt(namedArgs, {}), "abcd1234abcd1234abcd1234abcd1234"); +}); From ea4dd2695bdaa29b056677da93fb872dfff3345d Mon Sep 17 00:00:00 2001 From: robinmoisson Date: Mon, 8 Jun 2026 14:54:59 +0200 Subject: [PATCH 10/15] test: CLI end-to-end encrypt/decrypt round-trip and salt validation --- test/cli-e2e.test.js | 105 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 test/cli-e2e.test.js diff --git a/test/cli-e2e.test.js b/test/cli-e2e.test.js new file mode 100644 index 00000000..779549a0 --- /dev/null +++ b/test/cli-e2e.test.js @@ -0,0 +1,105 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const { spawnSync } = require("node:child_process"); +const fs = require("node:fs"); +const path = require("node:path"); +const os = require("node:os"); +const cryptoEngine = require("../lib/cryptoEngine.js"); +const codec = require("../lib/codec.js").init(cryptoEngine); + +const CLI = path.join(__dirname, "..", "cli", "index.js"); +const PASSWORD = "longenoughtestpassword123"; +const SALT = "abcd1234abcd1234abcd1234abcd1234"; + +function mkTmp() { + return fs.mkdtempSync(path.join(os.tmpdir(), "staticrypt-e2e-")); +} + +test("CLI encrypts an HTML file producing the expected wire-format payload", async () => { + const tmp = mkTmp(); + const input = path.join(tmp, "input.html"); + fs.writeFileSync(input, "

secret

"); + + const outDir = path.join(tmp, "encrypted"); + const result = spawnSync( + "node", + [CLI, input, "-p", PASSWORD, "--salt", SALT, "--short", "-d", outDir, "-c", "false"], + { encoding: "utf8" } + ); + assert.equal(result.status, 0, `CLI exited non-zero. stderr: ${result.stderr}\nstdout: ${result.stdout}`); + + const output = fs.readFileSync(path.join(outDir, "input.html"), "utf8"); + + // Extract the encrypted payload and salt from the output HTML. + const cipherMatch = output.match(/"staticryptEncryptedMsgUniqueVariableName":\s*"([^"]+)"/); + const saltMatch = output.match(/"staticryptSaltUniqueVariableName":\s*"([^"]+)"/); + assert.ok(cipherMatch, "output contains the encrypted message"); + assert.ok(saltMatch, "output contains the salt"); + assert.equal(saltMatch[1], SALT); + + // Verify the wire format & round-trip via the lib. + assert.match(cipherMatch[1].slice(0, 64), /^[0-9a-f]{64}$/, "first 64 chars are HMAC hex"); + assert.match(cipherMatch[1].slice(64, 96), /^[0-9a-f]{32}$/, "next 32 chars are IV hex"); + + const hashed = await cryptoEngine.hashPassword(PASSWORD, SALT); + const decoded = await codec.decode(cipherMatch[1], hashed, SALT); + assert.equal(decoded.success, true); + assert.equal(decoded.decoded, "

secret

"); + + fs.rmSync(tmp, { recursive: true, force: true }); +}); + +test("CLI --decrypt reverses an encrypted file back to the original plaintext", () => { + const tmp = mkTmp(); + const input = path.join(tmp, "input.html"); + fs.writeFileSync(input, "

original content

"); + + const encDir = path.join(tmp, "enc"); + const encResult = spawnSync( + "node", + [CLI, input, "-p", PASSWORD, "--salt", SALT, "--short", "-d", encDir, "-c", "false"], + { encoding: "utf8" } + ); + assert.equal(encResult.status, 0, `encrypt step failed: ${encResult.stderr}`); + + const decDir = path.join(tmp, "dec"); + const decResult = spawnSync( + "node", + [ + CLI, + path.join(encDir, "input.html"), + "-p", + PASSWORD, + "--salt", + SALT, + "--decrypt", + "-d", + decDir, + "-c", + "false", + ], + { encoding: "utf8" } + ); + assert.equal(decResult.status, 0, `decrypt step failed: ${decResult.stderr}`); + + const decrypted = fs.readFileSync(path.join(decDir, "input.html"), "utf8"); + assert.equal(decrypted, "

original content

"); + + fs.rmSync(tmp, { recursive: true, force: true }); +}); + +test("CLI rejects an invalid salt", () => { + const tmp = mkTmp(); + const input = path.join(tmp, "input.html"); + fs.writeFileSync(input, "

x

"); + + const result = spawnSync( + "node", + [CLI, input, "-p", PASSWORD, "--salt", "not-32-hex-chars", "--short", "-c", "false"], + { encoding: "utf8" } + ); + assert.notEqual(result.status, 0, "CLI must exit non-zero on invalid salt"); + assert.match(result.stdout + result.stderr, /salt/i); + + fs.rmSync(tmp, { recursive: true, force: true }); +}); From 5d893408fabd1d83adb8c3ea0d3e8d68dff38254 Mon Sep 17 00:00:00 2001 From: robinmoisson Date: Mon, 8 Jun 2026 14:55:00 +0200 Subject: [PATCH 11/15] test: jsdom helper that injects Node webcrypto as window.crypto --- test/setup/jsdomEnv.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 test/setup/jsdomEnv.js diff --git a/test/setup/jsdomEnv.js b/test/setup/jsdomEnv.js new file mode 100644 index 00000000..4dc7f68f --- /dev/null +++ b/test/setup/jsdomEnv.js @@ -0,0 +1,19 @@ +const { JSDOM } = require("jsdom"); +const { webcrypto } = require("node:crypto"); + +/** + * Build a jsdom window with WebCrypto wired up the way browsers expose it. + * The staticrypt browser runtime references `crypto.getRandomValues` and + * `crypto.subtle` as globals — jsdom does not provide them by default. + * + * @param {string} html - initial body HTML + * @param {string} url - location URL (controls window.location.search/hash) + * @returns {object} the jsdom Window + */ +function buildWindow(html = "", url = "https://example.com/") { + const dom = new JSDOM(html, { url }); + Object.defineProperty(dom.window, "crypto", { value: webcrypto, configurable: true }); + return dom.window; +} + +module.exports = { buildWindow }; From 6826d887016eb977d818762464ac46f99c7afe8b Mon Sep 17 00:00:00 2001 From: robinmoisson Date: Mon, 8 Jun 2026 14:55:02 +0200 Subject: [PATCH 12/15] test: browser runtime under jsdom (decrypt, remember-me, autodecrypt, logout) --- test/setup/jsdomEnv.js | 12 +++- test/staticryptJs.test.js | 126 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 test/staticryptJs.test.js diff --git a/test/setup/jsdomEnv.js b/test/setup/jsdomEnv.js index 4dc7f68f..39d43ec3 100644 --- a/test/setup/jsdomEnv.js +++ b/test/setup/jsdomEnv.js @@ -6,13 +6,23 @@ const { webcrypto } = require("node:crypto"); * The staticrypt browser runtime references `crypto.getRandomValues` and * `crypto.subtle` as globals — jsdom does not provide them by default. * + * Also injects TextEncoder/TextDecoder which are used by cryptoEngine but + * not always available in jsdom's script environment. + * * @param {string} html - initial body HTML * @param {string} url - location URL (controls window.location.search/hash) * @returns {object} the jsdom Window */ function buildWindow(html = "", url = "https://example.com/") { - const dom = new JSDOM(html, { url }); + const dom = new JSDOM(html, { url, runScripts: "dangerously" }); Object.defineProperty(dom.window, "crypto", { value: webcrypto, configurable: true }); + // jsdom's runScripts context may not expose TextEncoder/TextDecoder from Node. + if (!dom.window.TextEncoder) { + dom.window.TextEncoder = TextEncoder; + } + if (!dom.window.TextDecoder) { + dom.window.TextDecoder = TextDecoder; + } return dom.window; } diff --git a/test/staticryptJs.test.js b/test/staticryptJs.test.js new file mode 100644 index 00000000..e6913463 --- /dev/null +++ b/test/staticryptJs.test.js @@ -0,0 +1,126 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const cryptoEngine = require("../lib/cryptoEngine.js"); +const codec = require("../lib/codec.js").init(cryptoEngine); +const { buildStaticryptJS } = require("../cli/helpers.js"); +const { buildWindow } = require("./setup/jsdomEnv.js"); + +const FIXTURE = JSON.parse(fs.readFileSync(path.join(__dirname, "fixtures", "encrypted-blobs", "basic.json"), "utf8")); + +const STATICRYPT_JS_SOURCE = buildStaticryptJS(); + +// Standard templateConfig matching what's hardcoded in lib/password_template.html. +const TEMPLATE_CONFIG_LITERAL = `{ + rememberExpirationKey: "staticrypt_expiration", + rememberPassphraseKey: "staticrypt_passphrase", + replaceHtmlCallback: (html) => { window.__replacedWith = html; }, + clearLocalStorageCallback: undefined +}`; + +/** + * Build a fresh jsdom window with the staticrypt runtime evaluated in it, + * exposing `window.staticrypt` (the init() result) and `window.__replacedWith` + * (whatever the page would have been replaced with). + */ +function bootStaticrypt(staticryptConfig, url) { + const window = buildWindow("", url); + const initScript = ` + const staticryptModuleExports = ${STATICRYPT_JS_SOURCE}; + window.staticrypt = staticryptModuleExports.init( + ${JSON.stringify(staticryptConfig)}, + ${TEMPLATE_CONFIG_LITERAL} + ); + `; + window.eval(initScript); + return window; +} + +const STD_CONFIG = { + staticryptEncryptedMsgUniqueVariableName: FIXTURE.encrypted, + staticryptSaltUniqueVariableName: FIXTURE.salt, + isRememberEnabled: true, + rememberDurationInDays: 30, +}; + +test("init exposes handleDecryptionOfPage, handleDecryptionOfPageFromHash, handleDecryptOnLoad", () => { + const window = bootStaticrypt(STD_CONFIG, "https://example.com/"); + assert.equal(typeof window.staticrypt.handleDecryptionOfPage, "function"); + assert.equal(typeof window.staticrypt.handleDecryptionOfPageFromHash, "function"); + assert.equal(typeof window.staticrypt.handleDecryptOnLoad, "function"); +}); + +test("correct password decrypts and triggers replaceHtmlCallback with plaintext", async () => { + const window = bootStaticrypt(STD_CONFIG, "https://example.com/"); + const result = await window.staticrypt.handleDecryptionOfPage(FIXTURE.password, false); + assert.equal(result.isSuccessful, true); + assert.equal(window.__replacedWith, FIXTURE.plaintext); +}); + +test("wrong password returns isSuccessful=false and does NOT replace HTML", async () => { + const window = bootStaticrypt(STD_CONFIG, "https://example.com/"); + const result = await window.staticrypt.handleDecryptionOfPage("wrong-password", false); + assert.equal(result.isSuccessful, false); + assert.equal(window.__replacedWith, undefined); +}); + +test("remember-me writes hashedPassword + expiration to localStorage under documented keys", async () => { + const window = bootStaticrypt(STD_CONFIG, "https://example.com/"); + await window.staticrypt.handleDecryptionOfPage(FIXTURE.password, true); + assert.equal(window.localStorage.getItem("staticrypt_passphrase"), FIXTURE.hashFull); + const exp = parseInt(window.localStorage.getItem("staticrypt_expiration"), 10); + assert.ok(Number.isFinite(exp), "expiration is a finite integer"); + assert.ok(exp > Date.now(), "expiration is in the future"); +}); + +test("handleDecryptOnLoad: remember-me path decrypts on revisit when localStorage has the hash", async () => { + // First visit: prime localStorage. + const w1 = bootStaticrypt(STD_CONFIG, "https://example.com/"); + await w1.staticrypt.handleDecryptionOfPage(FIXTURE.password, true); + const storedHash = w1.localStorage.getItem("staticrypt_passphrase"); + const storedExp = w1.localStorage.getItem("staticrypt_expiration"); + + // Second visit: new window, prime localStorage with the values, then call handleDecryptOnLoad. + const w2 = bootStaticrypt(STD_CONFIG, "https://example.com/"); + w2.localStorage.setItem("staticrypt_passphrase", storedHash); + w2.localStorage.setItem("staticrypt_expiration", storedExp); + const { isSuccessful } = await w2.staticrypt.handleDecryptOnLoad(); + assert.equal(isSuccessful, true); + assert.equal(w2.__replacedWith, FIXTURE.plaintext); +}); + +test("handleDecryptOnLoad: expired remember-me clears localStorage and does NOT decrypt", async () => { + const window = bootStaticrypt(STD_CONFIG, "https://example.com/"); + window.localStorage.setItem("staticrypt_passphrase", FIXTURE.hashFull); + window.localStorage.setItem("staticrypt_expiration", "1"); // long expired + const { isSuccessful } = await window.staticrypt.handleDecryptOnLoad(); + assert.equal(isSuccessful, false); + assert.equal(window.localStorage.getItem("staticrypt_passphrase"), null); + assert.equal(window.localStorage.getItem("staticrypt_expiration"), null); +}); + +test("handleDecryptOnLoad: URL fragment #staticrypt_pwd= auto-decrypts", async () => { + const window = bootStaticrypt(STD_CONFIG, `https://example.com/#staticrypt_pwd=${FIXTURE.hashFull}`); + const { isSuccessful } = await window.staticrypt.handleDecryptOnLoad(); + // decryptOnLoadFromUrl returns the full handleDecryptionOfPageFromHash result object (truthy on success) + assert.ok(isSuccessful, "expected isSuccessful to be truthy"); + assert.equal(window.__replacedWith, FIXTURE.plaintext); +}); + +test("handleDecryptOnLoad: legacy query param ?staticrypt_pwd= auto-decrypts", async () => { + const window = bootStaticrypt(STD_CONFIG, `https://example.com/?staticrypt_pwd=${FIXTURE.hashFull}`); + const { isSuccessful } = await window.staticrypt.handleDecryptOnLoad(); + // decryptOnLoadFromUrl returns the full handleDecryptionOfPageFromHash result object (truthy on success) + assert.ok(isSuccessful, "expected isSuccessful to be truthy"); + assert.equal(window.__replacedWith, FIXTURE.plaintext); +}); + +test("logout via ?staticrypt_logout clears localStorage and skips decrypt", async () => { + const window = bootStaticrypt(STD_CONFIG, "https://example.com/?staticrypt_logout"); + window.localStorage.setItem("staticrypt_passphrase", FIXTURE.hashFull); + window.localStorage.setItem("staticrypt_expiration", String(Date.now() + 1_000_000)); + const { isSuccessful } = await window.staticrypt.handleDecryptOnLoad(); + assert.equal(isSuccessful, false); + assert.equal(window.localStorage.getItem("staticrypt_passphrase"), null); +}); From 65e703691ac2489883371234944ced32055023f6 Mon Sep 17 00:00:00 2001 From: robinmoisson Date: Mon, 8 Jun 2026 14:55:05 +0200 Subject: [PATCH 13/15] ci: run node:test on Node 20 and 22 for push and PRs --- .github/workflows/test.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..e68b683f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x, 22.x] + fail-fast: false + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + - run: npm ci + - run: npm test From c8fd28197c010b57e5890f4e655822c9970e3cfc Mon Sep 17 00:00:00 2001 From: robinmoisson Date: Mon, 8 Jun 2026 14:55:06 +0200 Subject: [PATCH 14/15] test: limit node:test discovery to *.test.js files Without an explicit glob, node --test recursively picks up everything under test/, including test/fixtures/generate.js and test/setup/jsdomEnv.js. The former actually re-ran on every `npm test`, overwriting the golden fixture with a new random IV. The latter was harmless noise but still "ran" as a 0-test file. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 048849a1..671d23d1 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "build": "bash ./scripts/build.sh", "format": "prettier --write \"**/*.{js,json,html}\"", "prepare": "husky", - "test": "node --test --test-reporter=spec" + "test": "node --test --test-reporter=spec test/*.test.js" }, "lint-staged": { "**/*.{js,json,html}": [ From 0ace4a2aebf6d07917abe2e341f3f74f28670ea7 Mon Sep 17 00:00:00 2001 From: robinmoisson Date: Mon, 8 Jun 2026 14:55:30 +0200 Subject: [PATCH 15/15] ignore AI tooling artifacts and untrack manual-test legacy fixtures --- .gitignore | 12 ++++++++++++ test/fixtures/legacy-html/test.css | 3 --- test/fixtures/legacy-html/test.html | 1 - test/fixtures/legacy-html/test1/index.html | 1 - test/fixtures/legacy-html/test2/index.html | 1 - 5 files changed, 12 insertions(+), 6 deletions(-) delete mode 100755 test/fixtures/legacy-html/test.css delete mode 100755 test/fixtures/legacy-html/test.html delete mode 100755 test/fixtures/legacy-html/test1/index.html delete mode 100755 test/fixtures/legacy-html/test2/index.html diff --git a/.gitignore b/.gitignore index 4f5d5d25..e4b0b173 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,15 @@ node_modules encrypted/ !example/encrypted/ decrypted/ + +# AI tooling artifacts (kept local) +AGENTS.md +CLAUDE.md +CLAUDE.local.md +.claude/ +docs/superpowers/ +.cursor/ +.aider* + +# Manual-test fixtures (used by scripts/build.sh, not by automated tests) +test/fixtures/legacy-html/ diff --git a/test/fixtures/legacy-html/test.css b/test/fixtures/legacy-html/test.css deleted file mode 100755 index 816e5fe7..00000000 --- a/test/fixtures/legacy-html/test.css +++ /dev/null @@ -1,3 +0,0 @@ -html { - height: 100%; -} diff --git a/test/fixtures/legacy-html/test.html b/test/fixtures/legacy-html/test.html deleted file mode 100755 index 9b599001..00000000 --- a/test/fixtures/legacy-html/test.html +++ /dev/null @@ -1 +0,0 @@ -Yooo un test diff --git a/test/fixtures/legacy-html/test1/index.html b/test/fixtures/legacy-html/test1/index.html deleted file mode 100755 index f59699a3..00000000 --- a/test/fixtures/legacy-html/test1/index.html +++ /dev/null @@ -1 +0,0 @@ -yo test1 diff --git a/test/fixtures/legacy-html/test2/index.html b/test/fixtures/legacy-html/test2/index.html deleted file mode 100755 index 07571cff..00000000 --- a/test/fixtures/legacy-html/test2/index.html +++ /dev/null @@ -1 +0,0 @@ -yoooo test 2