diff --git a/package-lock.json b/package-lock.json index 7cffc27f8..4777a8c4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -492,7 +492,6 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -533,7 +532,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1219,6 +1217,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "peer": true, "engines": { "node": ">=12" }, @@ -1230,6 +1229,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "peer": true, "engines": { "node": ">=12" }, @@ -1256,6 +1256,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "peer": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1528,7 +1529,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", - "peer": true, "engines": { "node": ">=12" } @@ -2017,7 +2017,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", - "peer": true, "dependencies": { "@jest/types": "^29.6.3" }, @@ -2632,7 +2631,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "peer": true, "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -2939,7 +2937,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "peer": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -2965,7 +2962,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -3000,7 +2996,6 @@ "version": "0.3.11", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -3203,7 +3198,6 @@ "version": "0.84.0", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.84.0.tgz", "integrity": "sha512-YiU9h1IN0pvvZsHbd03MaD7mE2q+ySaKMlE9tWK+3iiwtbEaMQOsMUuSJ1er2LU6ERMWfhfvCYgWpKRGOMeN8A==", - "peer": true, "engines": { "node": ">= 20.19.4" } @@ -3212,7 +3206,6 @@ "version": "0.84.0", "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.84.0.tgz", "integrity": "sha512-TcTAO58JigCw9onYTrbE2yK2js5YNgqbmnpYyq9oXz2mofbX7JcK53kIi7fhqyJhie8RkY+X85zSOTWNs6S3CA==", - "peer": true, "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", @@ -3233,7 +3226,6 @@ "version": "0.84.0", "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.84.0.tgz", "integrity": "sha512-uYoLBHnAzod4E5dA5rPPQeny2A5RD0PiIJQ4r+2F7cvA+5bZ8+znxw4TdaSiEk8uhN+clffI4d2bl9V4+xEK+Q==", - "peer": true, "dependencies": { "@react-native/dev-middleware": "0.84.0", "debug": "^4.4.0", @@ -3263,7 +3255,6 @@ "version": "0.84.0", "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.84.0.tgz", "integrity": "sha512-n7JKYVDCbA2aj8/5/OD1IK7nuiAYj5l/Z6yhGf7GG4EGaeQdthqdb0LZbseaRPyZK/7tLfdnLdqlqdTQC6/UTQ==", - "peer": true, "engines": { "node": ">= 20.19.4" } @@ -3272,7 +3263,6 @@ "version": "0.84.0", "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.84.0.tgz", "integrity": "sha512-5t/NvQLYk/d0kWlGOMNobkjfimqBc+/LYRmSOkgKm+pyOhxjygCLSnRjAUkeRALSZ8h6MKGTz1Wc4pbmJr7T0Q==", - "peer": true, "dependencies": { "cross-spawn": "^7.0.6", "debug": "^4.4.0", @@ -3286,7 +3276,6 @@ "version": "0.84.0", "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.84.0.tgz", "integrity": "sha512-c0o7YW39AUI1FSLV/TFSszr87kQGmaePAQK0ygIRnwZ2fAGDnQ5Iu/tk3u9O5lVH6nTjfAwTKJ3El9YeEWDeEQ==", - "peer": true, "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.84.0", @@ -3309,7 +3298,6 @@ "version": "0.84.0", "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.84.0.tgz", "integrity": "sha512-j8g/I4Z+SAdh2NXOVng4rmfYgPoeJBZwAKoGPpSe/wB/9XDLh9IRGUTg8dGS5BWUy2471xBUoGZPwHb6QMJmVw==", - "peer": true, "engines": { "node": ">= 20.19.4" } @@ -3318,7 +3306,6 @@ "version": "0.84.0", "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.84.0.tgz", "integrity": "sha512-xaxmzYWLgHH+2uAZQ0owEkDE58hOTWmuBKD/Gl+cDFD3mFfSK4lZpin/3hiXtE5LB4BwgqICsPN07zCAqx6Fpg==", - "peer": true, "engines": { "node": ">= 20.19.4" } @@ -3326,14 +3313,12 @@ "node_modules/@react-native/normalize-colors": { "version": "0.84.0", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.84.0.tgz", - "integrity": "sha512-7JgZyWtQ9Sz4qZvCTsURUtuv8/niEZ/iCorp7eExc3GgpBWNazPumieiUoWPdgRKofU0Bqpr2/dJevEn2hrlwA==", - "peer": true + "integrity": "sha512-7JgZyWtQ9Sz4qZvCTsURUtuv8/niEZ/iCorp7eExc3GgpBWNazPumieiUoWPdgRKofU0Bqpr2/dJevEn2hrlwA==" }, "node_modules/@react-native/virtualized-lists": { "version": "0.84.0", "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.84.0.tgz", "integrity": "sha512-ugwSj0Gb4MYrcm8uQrQw8qHPx5RKGDLuZRAP/AuwneFizHx8YCLBEFbOYRGWgxHBRtkJ70D1o+jpIx3CK3p5lw==", - "peer": true, "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" @@ -3362,8 +3347,7 @@ "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "peer": true + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" }, "node_modules/@sinonjs/commons": { "version": "3.0.1", @@ -3443,7 +3427,6 @@ "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "peer": true, "dependencies": { "@types/node": "*" } @@ -3578,6 +3561,7 @@ "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", @@ -4082,7 +4066,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "peer": true, "dependencies": { "event-target-shim": "^5.0.0" }, @@ -4094,7 +4077,6 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "peer": true, "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -4108,6 +4090,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4135,7 +4118,6 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "peer": true, "engines": { "node": ">= 14" } @@ -4160,8 +4142,7 @@ "node_modules/anser": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", - "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", - "peer": true + "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==" }, "node_modules/ansi-escapes": { "version": "4.3.2", @@ -4353,8 +4334,7 @@ "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "peer": true + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" }, "node_modules/asn1js": { "version": "3.0.6", @@ -4407,7 +4387,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "peer": true, "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", @@ -4428,7 +4407,6 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -4444,7 +4422,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "peer": true, "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", @@ -4460,7 +4437,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "peer": true, "bin": { "semver": "bin/semver.js" } @@ -4469,7 +4445,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "peer": true, "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", @@ -4484,7 +4459,6 @@ "version": "0.32.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz", "integrity": "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==", - "peer": true, "dependencies": { "hermes-parser": "0.32.0" } @@ -4519,7 +4493,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "peer": true, "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" @@ -4635,6 +4608,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4853,7 +4827,6 @@ "version": "0.15.2", "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", - "peer": true, "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", @@ -4871,7 +4844,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", - "peer": true, "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", @@ -4886,7 +4858,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -4907,7 +4878,6 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], - "peer": true, "engines": { "node": ">=8" } @@ -5073,7 +5043,6 @@ "version": "3.7.0", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", - "peer": true, "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", @@ -5088,7 +5057,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -5096,8 +5064,7 @@ "node_modules/connect/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "peer": true + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/convert-source-map": { "version": "2.0.0", @@ -5270,6 +5237,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -5353,7 +5321,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "peer": true, "engines": { "node": ">= 0.8" } @@ -5362,7 +5329,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "peer": true, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -5427,8 +5393,7 @@ "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "peer": true + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { "version": "1.5.264", @@ -5477,7 +5442,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "peer": true, "engines": { "node": ">= 0.8" } @@ -5517,7 +5481,6 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", - "peer": true, "dependencies": { "stackframe": "^1.3.4" } @@ -5679,8 +5642,7 @@ "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "peer": true + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -5699,6 +5661,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5759,6 +5722,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6102,7 +6066,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "peer": true, "engines": { "node": ">= 0.6" } @@ -6205,7 +6168,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "peer": true, "engines": { "node": ">=6" } @@ -6358,8 +6320,7 @@ "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "peer": true + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==" }, "node_modules/fast-base64-decode": { "version": "1.0.0", @@ -6442,7 +6403,6 @@ "version": "0.5.8", "resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz", "integrity": "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==", - "peer": true, "bin": { "dotslash": "bin/dotslash" }, @@ -6486,7 +6446,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -6504,7 +6463,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -6512,8 +6470,7 @@ "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "peer": true + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/find-up": { "version": "5.0.0", @@ -6555,8 +6512,7 @@ "node_modules/flow-enums-runtime": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", - "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", - "peer": true + "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==" }, "node_modules/follow-redirects": { "version": "1.15.9", @@ -6635,7 +6591,6 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "peer": true, "engines": { "node": ">= 0.6" } @@ -7053,20 +7008,17 @@ "node_modules/hermes-compiler": { "version": "250829098.0.7", "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-250829098.0.7.tgz", - "integrity": "sha512-8QOmg1VjAWv8poFVslJDY8qkvjTy/UiO3R/hyGoC0IAchLzBdS9/TmAvI9cN1F3yLTEjimAIQQtUslpBMPXVVg==", - "peer": true + "integrity": "sha512-8QOmg1VjAWv8poFVslJDY8qkvjTy/UiO3R/hyGoC0IAchLzBdS9/TmAvI9cN1F3yLTEjimAIQQtUslpBMPXVVg==" }, "node_modules/hermes-estree": { "version": "0.32.0", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", - "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", - "peer": true + "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==" }, "node_modules/hermes-parser": { "version": "0.32.0", "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", - "peer": true, "dependencies": { "hermes-estree": "0.32.0" } @@ -7092,7 +7044,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "peer": true, "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", @@ -7112,7 +7063,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "peer": true, "engines": { "node": ">= 0.8" } @@ -7121,7 +7071,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -7189,7 +7138,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "peer": true, "dependencies": { "queue": "6.0.2" }, @@ -7278,7 +7226,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "peer": true, "dependencies": { "loose-envify": "^1.0.0" } @@ -7442,7 +7389,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "peer": true, "bin": { "is-docker": "cli.js" }, @@ -7741,7 +7687,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "peer": true, "dependencies": { "is-docker": "^2.0.0" }, @@ -8830,7 +8775,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "peer": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -8839,7 +8783,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "peer": true, "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -9097,7 +9040,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "peer": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -10136,7 +10078,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "peer": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -10329,7 +10270,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "peer": true, "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -10344,7 +10284,6 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10398,6 +10337,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -10442,8 +10382,7 @@ "node_modules/jsc-safe-url": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", - "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", - "peer": true + "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==" }, "node_modules/jsesc": { "version": "3.1.0", @@ -10530,7 +10469,6 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", - "peer": true, "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" @@ -10540,7 +10478,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -10548,8 +10485,7 @@ "node_modules/lighthouse-logger/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "peer": true + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/lines-and-columns": { "version": "1.2.4", @@ -10722,8 +10658,7 @@ "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", - "peer": true + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" }, "node_modules/log-update": { "version": "6.1.0", @@ -10856,7 +10791,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -10905,8 +10839,7 @@ "node_modules/marky": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", - "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", - "peer": true + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==" }, "node_modules/math-intrinsics": { "version": "1.1.0", @@ -10920,8 +10853,7 @@ "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", - "peer": true + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" }, "node_modules/memorystream": { "version": "0.3.1", @@ -10949,7 +10881,6 @@ "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.3.tgz", "integrity": "sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q==", - "peer": true, "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", @@ -11003,7 +10934,6 @@ "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.3.tgz", "integrity": "sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g==", - "peer": true, "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", @@ -11018,7 +10948,6 @@ "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.3.tgz", "integrity": "sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q==", - "peer": true, "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", @@ -11033,7 +10962,6 @@ "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.3.tgz", "integrity": "sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw==", - "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6" }, @@ -11045,7 +10973,6 @@ "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.3.tgz", "integrity": "sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA==", - "peer": true, "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", @@ -11064,7 +10991,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "peer": true, "engines": { "node": ">=10" }, @@ -11076,7 +11002,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "peer": true, "engines": { "node": ">=10" }, @@ -11088,7 +11013,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "peer": true, "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", @@ -11105,7 +11029,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -11119,7 +11042,6 @@ "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.3.tgz", "integrity": "sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw==", - "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", @@ -11133,7 +11055,6 @@ "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.3.tgz", "integrity": "sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA==", - "peer": true, "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", @@ -11153,7 +11074,6 @@ "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.3.tgz", "integrity": "sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ==", - "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" @@ -11166,7 +11086,6 @@ "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.3.tgz", "integrity": "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ==", - "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6" }, @@ -11178,7 +11097,6 @@ "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.3.tgz", "integrity": "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" @@ -11191,7 +11109,6 @@ "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.3.tgz", "integrity": "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg==", - "peer": true, "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", @@ -11212,7 +11129,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11221,7 +11137,6 @@ "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.3.tgz", "integrity": "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw==", - "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", @@ -11241,7 +11156,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11250,7 +11164,6 @@ "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.3.tgz", "integrity": "sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A==", - "peer": true, "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", @@ -11267,7 +11180,6 @@ "version": "0.83.3", "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.3.tgz", "integrity": "sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA==", - "peer": true, "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", @@ -11290,14 +11202,12 @@ "node_modules/metro/node_modules/ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "peer": true + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" }, "node_modules/metro/node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11318,7 +11228,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "peer": true, "bin": { "mime": "cli.js" }, @@ -11330,7 +11239,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "peer": true, "engines": { "node": ">= 0.6" } @@ -11339,7 +11247,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -11478,7 +11385,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "peer": true, "engines": { "node": ">= 0.6" } @@ -11533,14 +11439,12 @@ "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", - "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", - "peer": true + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==" }, "node_modules/ob1": { "version": "0.83.3", "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.3.tgz", "integrity": "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA==", - "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6" }, @@ -11657,7 +11561,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "peer": true, "dependencies": { "ee-first": "1.1.1" }, @@ -11693,7 +11596,6 @@ "version": "7.4.2", "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "peer": true, "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" @@ -11830,7 +11732,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "peer": true, "engines": { "node": ">= 0.8" } @@ -12102,6 +12003,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12199,7 +12101,6 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", - "peer": true, "dependencies": { "asap": "~2.0.6" } @@ -12210,6 +12111,7 @@ "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", "hasInstallScript": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -12284,7 +12186,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "peer": true, "dependencies": { "inherits": "~2.0.3" } @@ -12329,7 +12230,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "peer": true, "engines": { "node": ">= 0.6" } @@ -12348,7 +12248,6 @@ "version": "6.1.5", "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz", "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", - "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -12433,7 +12332,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "peer": true, "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", @@ -12448,7 +12346,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "peer": true, "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", @@ -12465,7 +12362,6 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "peer": true, "dependencies": { "@sinonjs/commons": "^3.0.0" } @@ -12474,7 +12370,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "peer": true, "engines": { "node": ">=10" }, @@ -12486,7 +12381,6 @@ "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "peer": true, "engines": { "node": ">=18" } @@ -12495,7 +12389,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -12512,7 +12405,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "peer": true, "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", @@ -12532,7 +12424,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "peer": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -12546,7 +12437,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -12560,7 +12450,6 @@ "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -12625,8 +12514,7 @@ "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "peer": true + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", @@ -12970,8 +12858,7 @@ "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "peer": true + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" }, "node_modules/secure-json-parse": { "version": "2.7.0", @@ -12994,7 +12881,6 @@ "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "peer": true, "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -13018,7 +12904,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -13026,14 +12911,12 @@ "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "peer": true + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/send/node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "peer": true, "engines": { "node": ">= 0.8" } @@ -13042,7 +12925,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "peer": true, "dependencies": { "ee-first": "1.1.1" }, @@ -13054,7 +12936,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "peer": true, "engines": { "node": ">= 0.8" } @@ -13063,7 +12944,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13072,7 +12952,6 @@ "version": "1.16.3", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "peer": true, "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", @@ -13087,7 +12966,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "peer": true, "engines": { "node": ">= 0.8" } @@ -13144,8 +13022,7 @@ "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "peer": true + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "node_modules/shebang-command": { "version": "2.0.0", @@ -13170,7 +13047,6 @@ "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -13426,14 +13302,12 @@ "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", - "peer": true + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" }, "node_modules/stacktrace-parser": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", - "peer": true, "dependencies": { "type-fest": "^0.7.1" }, @@ -13445,7 +13319,6 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "peer": true, "engines": { "node": ">=8" } @@ -13454,7 +13327,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "peer": true, "engines": { "node": ">= 0.6" } @@ -13702,7 +13574,6 @@ "version": "5.46.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -13719,14 +13590,12 @@ "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "peer": true + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "node_modules/terser/node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -13757,8 +13626,7 @@ "node_modules/throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", - "peer": true + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==" }, "node_modules/through2": { "version": "2.0.5", @@ -13844,6 +13712,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13883,7 +13752,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "peer": true, "engines": { "node": ">=0.6" } @@ -14150,6 +14018,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14200,7 +14069,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "peer": true, "engines": { "node": ">= 0.8" } @@ -14304,7 +14172,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "peer": true, "engines": { "node": ">= 0.4.0" } @@ -14327,8 +14194,7 @@ "node_modules/vlq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", - "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", - "peer": true + "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==" }, "node_modules/walker": { "version": "1.0.8", @@ -14341,8 +14207,7 @@ "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "peer": true + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" }, "node_modules/which": { "version": "2.0.2", @@ -14503,7 +14368,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "peer": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -14516,7 +14380,6 @@ "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "peer": true, "engines": { "node": ">=8.3.0" }, diff --git a/src/core/shared/config/cli-options.ts b/src/core/shared/config/cli-options.ts index 37ec87fa3..d3fc3c781 100644 --- a/src/core/shared/config/cli-options.ts +++ b/src/core/shared/config/cli-options.ts @@ -11,6 +11,7 @@ import networkPluginManifest from '@/plugins/network/manifest'; import pluginManagementManifest from '@/plugins/plugin-management/manifest'; import tokenPluginManifest from '@/plugins/token/manifest'; import topicPluginManifest from '@/plugins/topic/manifest'; +import auctionlogPluginManifest from '@/plugins/auctionlog/manifest'; export const RESERVED_LONG_OPTIONS = new Set([ 'format', @@ -48,4 +49,5 @@ export const DEFAULT_PLUGIN_STATE: PluginManifest[] = [ configPluginManifest, contractErc20PluginManifest, contractErc721PluginManifest, + auctionlogPluginManifest, ]; diff --git a/src/plugins/auctionlog/README.md b/src/plugins/auctionlog/README.md new file mode 100644 index 000000000..5a4e5ccd6 --- /dev/null +++ b/src/plugins/auctionlog/README.md @@ -0,0 +1,362 @@ +# Auctionlog Plugin + +Privacy-preserving audit trail for B2B blind auctions using Hedera Consensus Service (HCS). + +Publishes SHA-256 commitment hashes to HCS topics that prove the fairness and timing of auction events — without revealing any business-sensitive data like bid values, delivery terms, or party identities. + +## Problem Solved + +In enterprise procurement, auctions must be **provably fair** — but the data is **confidential**. Public blockchains give transparency but destroy privacy. Private systems give privacy but demand trust. + +This plugin bridges the gap: it lets you publish a **public audit trail** that proves the **order and timing** of auction events, while the actual business data stays private. + +**Measurable impact:** +- Eliminates manual audit report generation (saves hours per procurement cycle) +- Provides tamper-evident proof of process integrity +- Reduces regulatory compliance burden with exportable, verifiable artifacts +- Two-layer verification: local hash integrity + on-chain publication proof + +## Architecture + +- **Stateless handlers** — no shared globals; all dependencies injected via `CommandHandlerArgs` +- **Manifest-driven** — commands, options, and output schemas declared in `manifest.ts` +- **Namespace isolation** — audit data persisted under `auctionlog-data` +- **Zod validation** — input and output schemas ensure type safety at runtime +- **Structured output** — every handler returns `CommandExecutionResult` with JSON + Handlebars templates +- **Typed Core API** — uses `api.topic` for HCS, `api.mirror` for on-chain verification, `api.state` for persistence +- **Cryptographically secure** — SHA-256 hashing with `crypto.randomBytes` nonces +- **Stage ordering** — enforces chronological auction lifecycle progression + +## Structure + +``` +src/plugins/auctionlog/ +├── manifest.ts # Plugin manifest with all 4 commands +├── types.ts # TypeScript types, stage definitions, ordering +├── index.ts # Plugin exports +├── commands/ +│ ├── publish/ +│ │ ├── handler.ts # Publish commitment to HCS topic +│ │ ├── input.ts # Zod input schema +│ │ ├── output.ts # Zod output schema + Handlebars template +│ │ └── index.ts # Command exports +│ ├── verify/ +│ │ ├── handler.ts # Two-layer verification (local + on-chain) +│ │ ├── input.ts +│ │ ├── output.ts +│ │ └── index.ts +│ ├── export/ +│ │ ├── handler.ts # Export audit trail as JSON or CSV +│ │ ├── input.ts +│ │ ├── output.ts +│ │ └── index.ts +│ └── list/ +│ ├── handler.ts # List all tracked auctions +│ ├── input.ts +│ ├── output.ts +│ └── index.ts +└── __tests__/ + └── unit/ + ├── publish.test.ts # 10 tests + ├── verify.test.ts # 7 tests + ├── export.test.ts # 5 tests + └── list.test.ts # 3 tests +``` + +## Commands + +### `auctionlog publish` + +Publish a SHA-256 commitment hash to an HCS topic for a specific auction stage. + +```bash +# Publish an "auction created" commitment +hcli auctionlog publish \ + --auction-id AUCTION-001 \ + --stage created \ + --metadata "canton-tx-abc123,adi-0xdeadbeef" + +# Publish "bidding closed" to an existing topic +hcli auctionlog publish \ + --auction-id AUCTION-001 \ + --stage bidding-closed \ + --topic 0.0.1234567 + +# Publish "awarded" with external references +hcli auctionlog publish \ + --auction-id AUCTION-001 \ + --stage awarded \ + --metadata "canton-award-xyz,scoring-ref-123" +``` + +**Expected output:** +``` +✅ Audit commitment published + + Auction ID: AUCTION-001 + Stage: awarded + Commitment: 0x7a3f8e2d... + Topic: 0.0.1234567 (→ HashScan link) + Sequence: 3 + Timestamp: 2026-02-21T12:34:56.789Z + Metadata: (included in hash, not published on-chain) +``` + +**Behavior:** +- If `--topic` is omitted and no topic exists for this auction, a new HCS topic is created automatically +- If a topic was previously created for this auction, it is reused from local state +- The commitment hash is deterministic — same inputs always produce the same hash +- A cryptographically secure random nonce ensures each publication is unique +- **Stage ordering is enforced** — you cannot publish `awarded` before `bidding-closed` +- **Duplicate stages are rejected** — each stage can only be published once per auction +- **`disputed` is special** — it can be published at any time after the auction exists + +**Options:** + +| Flag | Short | Required | Description | +|------|-------|----------|-------------| +| `--auction-id` | `-a` | ✅ | Unique auction identifier | +| `--stage` | `-s` | ✅ | Auction stage (see [Valid Stages](#valid-stages)) | +| `--topic` | `-t` | ❌ | Existing HCS topic ID (auto-creates if omitted) | +| `--metadata` | `-m` | ❌ | Private metadata included in hash but NOT published on-chain | + +### `auctionlog verify` + +Verify commitment integrity with two layers: +1. **Local verification** (default): Re-computes the SHA-256 hash from stored fields +2. **On-chain verification** (`--on-chain`): Fetches HCS messages from the mirror node and compares + +```bash +# Local-only verification (default) +hcli auctionlog verify --auction-id AUCTION-001 + +# Full verification: local + on-chain +hcli auctionlog verify --auction-id AUCTION-001 --on-chain + +# Verify a specific stage +hcli auctionlog verify --auction-id AUCTION-001 --stage awarded --on-chain +``` + +**Expected output (all valid, on-chain):** +``` +✅ All local commitments verified for auction AUCTION-001 +✅ All on-chain commitments match + + Topic: 0.0.1234567 (→ HashScan link) + Local verified: 3 / 3 + On-chain verified: 3 / 3 + + ✅ 🔗 created — seq #1 — 2026-02-21T10:00:00.000Z + Hash: 0x7a3f8e2d... + ✅ 🔗 bidding-closed — seq #2 — 2026-02-21T11:00:00.000Z + Hash: 0x91bc4e8a... + ✅ 🔗 awarded — seq #3 — 2026-02-21T12:00:00.000Z + Hash: 0xc5d2f710... +``` + +| Flag | Short | Required | Description | +|------|-------|----------|-------------| +| `--auction-id` | `-a` | ✅ | Auction ID to verify | +| `--stage` | `-s` | ❌ | Specific stage to verify (verifies all if omitted) | +| `--on-chain` | `-o` | ❌ | Also verify against on-chain HCS messages via mirror node | + +### `auctionlog export` + +Export the full audit timeline as a JSON or CSV artifact. Use `--redact` to strip sensitive fields before sharing. + +```bash +# Export as JSON (default) — CONTAINS SENSITIVE DATA +hcli auctionlog export --auction-id AUCTION-001 + +# Export with sensitive fields redacted +hcli auctionlog export --auction-id AUCTION-001 --redact + +# Export as CSV to file +hcli auctionlog export \ + --auction-id AUCTION-001 \ + --type csv \ + --file ./audit-AUCTION-001.csv \ + --redact +``` + +**Expected output:** +``` +📦 Exported 4 audit entries for auction AUCTION-001 + + Topic: 0.0.1234567 (→ HashScan link) + Format: json + Mode: REDACTED (nonces and metadata omitted) + +Timeline: + 1. [created] 2026-02-21T10:00:00.000Z — 0x7a3f8e2d... + 2. [bidding-closed] 2026-02-21T11:00:00.000Z — 0x91bc4e8a... + 3. [awarded] 2026-02-21T12:00:00.000Z — 0xc5d2f710... + 4. [settled] 2026-02-21T13:00:00.000Z — 0xe8f1a234... +``` + +| Flag | Short | Required | Description | +|------|-------|----------|-------------| +| `--auction-id` | `-a` | ✅ | Auction ID to export | +| `--type` | `-T` | ❌ | Export format: `json` (default) or `csv` | +| `--file` | `-f` | ❌ | Output file path (prints to stdout if omitted) | +| `--redact` | `-r` | ❌ | Redact nonces and metadata from the export | + +### `auctionlog list` + +List all auctions that have at least one published audit commitment. + +```bash +hcli auctionlog list +``` + +**Expected output:** +``` +📋 Tracked Auctions (2) + + AUCTION-001 + Topic: 0.0.1234567 + Last Stage: settled + Updated: 2026-02-21T13:00:00.000Z + Stages Published: 4 + + AUCTION-002 + Topic: 0.0.1234568 + Last Stage: bidding-closed + Updated: 2026-02-21T11:30:00.000Z + Stages Published: 2 +``` + +## Valid Stages + +| Stage | Order | Description | +|-------|-------|-------------| +| `created` | 0 | Auction has been created with terms and scoring weights | +| `bidding-open` | 1 | Bidding window is open for submissions | +| `bidding-closed` | 2 | Bidding window has closed, no more bids accepted | +| `awarded` | 3 | Winner has been selected and award proof generated | +| `settled` | 4 | Payment/escrow settled on-chain | +| `disputed` | * | Dispute raised — can occur at any time after creation | + +Stages must be published in chronological order (except `disputed`). Duplicate stage publications are rejected. + +## Privacy Guarantee + +The commitment hash is computed as: + +``` +SHA-256(JSON.stringify({ + auctionId, // e.g. "AUCTION-001" + stage, // e.g. "awarded" + metadata, // e.g. "canton-ref,adi-tx,scoring-proof" (private) + timestamp, // ISO 8601 + nonce // cryptographically secure 16-byte hex +})) +``` + +**What is published to HCS (public):** +- The commitment hash (SHA-256) +- The stage name +- The auction ID +- The timestamp +- A protocol version number + +**What is NOT published (private):** +- Private metadata (external transaction references, scoring data, etc.) +- The nonce (prevents rainbow table attacks on the hash) +- All bid values, terms, identities, and business data + +This means anyone can verify the *sequence and timing* of auction events, but nobody can reverse-engineer the hash into actual business data. + +**Export sensitivity:** The `export` command can include private fields (nonces, metadata). Use `--redact` to strip them when sharing exports externally. + +## Two-Layer Verification + +The `verify` command provides two complementary integrity checks: + +| Layer | What it proves | How | +|-------|---------------|-----| +| **Local** (default) | Hash matches preimage | Re-computes SHA-256 from stored fields | +| **On-chain** (`--on-chain`) | Hash was published to HCS | Fetches mirror node messages and compares | + +Together, these prove: +1. The local data hasn't been modified since publication (local check) +2. The hash actually exists on the Hedera ledger (on-chain check) + +If the mirror node is unavailable, on-chain verification gracefully degrades to local-only. + +## Core API Integration + +| API | Usage | +|-----|-------| +| `api.topic.createTopic()` | Create new HCS topic for auction | +| `api.topic.submitMessage()` | Publish commitment as HCS message | +| `api.txExecution.signAndExecute()` | Sign and submit HCS transactions | +| `api.mirror.getTopicMessages()` | Fetch on-chain messages for verification | +| `api.network.getCurrentNetwork()` | Resolve current network (testnet/mainnet) | +| `api.state.get/set/getKeys()` | Persist audit entries and auction metadata | + +## State Management + +Audit entries are stored under the `auctionlog-data` namespace: + +| Key Pattern | Data | +|-------------|------| +| `{auctionId}` | Auction metadata: `{ topicId, lastStage, lastUpdated }` | +| `{auctionId}:{stage}` | Full audit entry: commitment hash, nonce, metadata, sequence number | + +## Testing + +25 unit tests covering all commands: + +``` +auctionlog publish (10 tests) + ✓ should publish a commitment and return success + ✓ should create a new topic if none exists + ✓ should reuse existing topic from state + ✓ should reject invalid stage + ✓ should reject missing auctionId + ✓ should return failure when topic creation fails + ✓ should reject duplicate stage publication + ✓ should enforce stage ordering + ✓ should allow disputed at any time after auction exists + ✓ should produce deterministic hashes for same inputs + ✓ should produce different hashes for different inputs + +auctionlog verify (7 tests) + ✓ should verify a valid commitment (local) + ✓ should detect tampered commitment + ✓ should fail if no audit log found + ✓ should perform on-chain verification when --on-chain is set + ✓ should detect on-chain hash mismatch + ✓ should gracefully handle mirror node failure + +auctionlog export (5 tests) + ✓ should export as JSON by default + ✓ should export as CSV when requested + ✓ should fail if no entries found + ✓ should redact sensitive fields when --redact is set + ✓ should fail if no audit log found at all + +auctionlog list (3 tests) + ✓ should list tracked auctions + ✓ should return empty when no auctions exist + ✓ should list multiple auctions +``` + +Run tests: +```bash +npx jest --testPathPatterns="auctionlog" --verbose +``` + +## Use Cases Beyond This Hackathon + +This plugin is designed to be **generally useful** for any workflow that needs a tamper-evident public audit trail on Hedera: + +- **Procurement & RFP processes** — prove bid timing and scoring fairness +- **Regulatory compliance** — exportable JSON/CSV audit artifacts for legal review +- **Supply chain events** — publish commitment hashes for shipment, customs, delivery milestones +- **Corporate governance** — voting results, board resolutions, compliance checkpoints +- **Multi-party agreements** — prove the sequence of approvals without exposing terms +- **Financial audits** — timestamp-ordered commitment trail for transaction verification + +The `publish → verify → export` workflow is a reusable pattern for any process where you need to prove "this happened at this time" without revealing "what exactly happened." diff --git a/src/plugins/auctionlog/__tests__/unit/export.test.ts b/src/plugins/auctionlog/__tests__/unit/export.test.ts new file mode 100644 index 000000000..a4ba54e68 --- /dev/null +++ b/src/plugins/auctionlog/__tests__/unit/export.test.ts @@ -0,0 +1,177 @@ +import { Status } from '@/core/shared/constants'; +import { exportAuditLog } from '../../commands/export/handler'; + +import { + makeArgs, + makeLogger, +} from '@/__tests__/mocks/mocks'; + +// ─── Helpers ──────────────────────────────────────────────────────────────────── + +function makeStateStore(entries: Record = {}) { + const store = new Map(Object.entries(entries)); + return { + get: jest.fn((ns: string, key: string) => store.get(`${ns}:${key}`)), + set: jest.fn((ns: string, key: string, val: unknown) => + store.set(`${ns}:${key}`, val), + ), + getKeys: jest.fn(() => [...store.keys()]), + list: jest.fn(), + delete: jest.fn(), + clear: jest.fn(), + has: jest.fn(), + getNamespaces: jest.fn(), + subscribe: jest.fn(), + getActions: jest.fn(), + getState: jest.fn(), + getStorageDirectory: jest.fn(), + isInitialized: jest.fn().mockReturnValue(true), + }; +} + +function makeEntry(stage: string, seq: number) { + return { + auctionId: 'AUCTION-E-001', + stage, + metadata: `meta-${stage}`, + timestamp: `2026-02-21T0${seq}:00:00.000Z`, + nonce: `0x${stage}nonce`, + commitmentHash: `0x${stage}hash`, + topicId: '0.0.7777', + sequenceNumber: seq, + network: 'testnet', + }; +} + +// ─── Tests ────────────────────────────────────────────────────────────────────── + +describe('auctionlog export', () => { + it('should export as JSON by default', async () => { + const logger = makeLogger(); + + const stateStore = makeStateStore({ + 'auctionlog-data:AUCTION-E-001': { topicId: '0.0.7777' }, + 'auctionlog-data:AUCTION-E-001:created': makeEntry('created', 1), + 'auctionlog-data:AUCTION-E-001:awarded': makeEntry('awarded', 2), + }); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + }; + + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-E-001', + }); + (args as any).state = stateStore; + + const result = await exportAuditLog(args); + + expect(result.status).toBe(Status.Success); + const output = JSON.parse(result.outputJson!); + expect(output.exportFormat).toBe('json'); + expect(output.entryCount).toBe(2); + expect(output.entries[0].stage).toBe('created'); + expect(output.entries[1].stage).toBe('awarded'); + expect(output.redacted).toBe(false); + }); + + it('should export as CSV when requested', async () => { + const logger = makeLogger(); + + const stateStore = makeStateStore({ + 'auctionlog-data:AUCTION-E-002': { topicId: '0.0.7777' }, + 'auctionlog-data:AUCTION-E-002:created': makeEntry('created', 1), + }); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + }; + + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-E-002', + type: 'csv', + }); + (args as any).state = stateStore; + + const result = await exportAuditLog(args); + + expect(result.status).toBe(Status.Success); + const output = JSON.parse(result.outputJson!); + expect(output.exportFormat).toBe('csv'); + expect(output.entryCount).toBe(1); + }); + + it('should fail if no entries found', async () => { + const logger = makeLogger(); + + const stateStore = makeStateStore({ + 'auctionlog-data:AUCTION-E-003': { topicId: '0.0.7777' }, + }); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + }; + + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-E-003', + }); + (args as any).state = stateStore; + + const result = await exportAuditLog(args); + + expect(result.status).toBe(Status.Failure); + expect(result.errorMessage).toContain('No published stages found'); + }); + + it('should redact sensitive fields when --redact is set', async () => { + const logger = makeLogger(); + + const stateStore = makeStateStore({ + 'auctionlog-data:AUCTION-E-004': { topicId: '0.0.7777' }, + 'auctionlog-data:AUCTION-E-004:created': { + ...makeEntry('created', 1), + auctionId: 'AUCTION-E-004', + }, + }); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + }; + + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-E-004', + redact: true, + }); + (args as any).state = stateStore; + + const result = await exportAuditLog(args); + + expect(result.status).toBe(Status.Success); + const output = JSON.parse(result.outputJson!); + expect(output.redacted).toBe(true); + expect(output.entries[0].nonce).toBe('[REDACTED]'); + expect(output.entries[0].metadata).toBe('[REDACTED]'); + // Commitment hash should still be visible + expect(output.entries[0].commitmentHash).not.toBe('[REDACTED]'); + }); + + it('should fail if no audit log found at all', async () => { + const logger = makeLogger(); + + const stateStore = makeStateStore({}); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + }; + + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-NOPE', + }); + (args as any).state = stateStore; + + const result = await exportAuditLog(args); + + expect(result.status).toBe(Status.Failure); + expect(result.errorMessage).toContain('No audit log found'); + }); +}); diff --git a/src/plugins/auctionlog/__tests__/unit/list.test.ts b/src/plugins/auctionlog/__tests__/unit/list.test.ts new file mode 100644 index 000000000..cda0fac7d --- /dev/null +++ b/src/plugins/auctionlog/__tests__/unit/list.test.ts @@ -0,0 +1,140 @@ +import { Status } from '@/core/shared/constants'; +import { listAuctions } from '../../commands/list/handler'; + +import { + makeArgs, + makeLogger, +} from '@/__tests__/mocks/mocks'; + +// ─── Helpers ──────────────────────────────────────────────────────────────────── + +function makeStateStore(entries: Record = {}) { + const store = new Map(Object.entries(entries)); + return { + get: jest.fn((ns: string, key: string) => store.get(`${ns}:${key}`)), + set: jest.fn((ns: string, key: string, val: unknown) => + store.set(`${ns}:${key}`, val), + ), + getKeys: jest.fn((ns: string) => { + const prefix = `${ns}:`; + return [...store.keys()] + .filter((k) => k.startsWith(prefix)) + .map((k) => k.slice(prefix.length)); + }), + list: jest.fn(), + delete: jest.fn(), + clear: jest.fn(), + has: jest.fn(), + getNamespaces: jest.fn(), + subscribe: jest.fn(), + getActions: jest.fn(), + getState: jest.fn(), + getStorageDirectory: jest.fn(), + isInitialized: jest.fn().mockReturnValue(true), + }; +} + +// ─── Tests ────────────────────────────────────────────────────────────────────── + +describe('auctionlog list', () => { + it('should list tracked auctions', async () => { + const logger = makeLogger(); + + const stateStore = makeStateStore({ + 'auctionlog-data:AUCTION-L-001': { + topicId: '0.0.8888', + lastStage: 'awarded', + lastUpdated: '2026-02-21T00:00:00.000Z', + }, + 'auctionlog-data:AUCTION-L-001:created': { + auctionId: 'AUCTION-L-001', + stage: 'created', + topicId: '0.0.8888', + }, + 'auctionlog-data:AUCTION-L-001:awarded': { + auctionId: 'AUCTION-L-001', + stage: 'awarded', + topicId: '0.0.8888', + }, + }); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + }; + + const args = makeArgs(api as any, logger, {}); + (args as any).state = stateStore; + + const result = await listAuctions(args); + + expect(result.status).toBe(Status.Success); + const output = JSON.parse(result.outputJson!); + expect(output.totalAuctions).toBe(1); + expect(output.auctions[0].auctionId).toBe('AUCTION-L-001'); + expect(output.auctions[0].topicId).toBe('0.0.8888'); + expect(output.auctions[0].stageCount).toBe(2); + }); + + it('should return empty when no auctions exist', async () => { + const logger = makeLogger(); + + const stateStore = makeStateStore({}); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + }; + + const args = makeArgs(api as any, logger, {}); + (args as any).state = stateStore; + + const result = await listAuctions(args); + + expect(result.status).toBe(Status.Success); + const output = JSON.parse(result.outputJson!); + expect(output.totalAuctions).toBe(0); + expect(output.auctions).toEqual([]); + }); + + it('should list multiple auctions', async () => { + const logger = makeLogger(); + + const stateStore = makeStateStore({ + 'auctionlog-data:AUCTION-L-001': { + topicId: '0.0.8888', + lastStage: 'awarded', + lastUpdated: '2026-02-21T00:00:00.000Z', + }, + 'auctionlog-data:AUCTION-L-001:created': { + auctionId: 'AUCTION-L-001', + stage: 'created', + topicId: '0.0.8888', + }, + 'auctionlog-data:AUCTION-L-002': { + topicId: '0.0.9999', + lastStage: 'created', + lastUpdated: '2026-02-21T01:00:00.000Z', + }, + 'auctionlog-data:AUCTION-L-002:created': { + auctionId: 'AUCTION-L-002', + stage: 'created', + topicId: '0.0.9999', + }, + }); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + }; + + const args = makeArgs(api as any, logger, {}); + (args as any).state = stateStore; + + const result = await listAuctions(args); + + expect(result.status).toBe(Status.Success); + const output = JSON.parse(result.outputJson!); + expect(output.totalAuctions).toBe(2); + const ids = output.auctions.map((a: any) => a.auctionId); + expect(ids).toContain('AUCTION-L-001'); + expect(ids).toContain('AUCTION-L-002'); + }); +}); diff --git a/src/plugins/auctionlog/__tests__/unit/publish.test.ts b/src/plugins/auctionlog/__tests__/unit/publish.test.ts new file mode 100644 index 000000000..9a52f00f6 --- /dev/null +++ b/src/plugins/auctionlog/__tests__/unit/publish.test.ts @@ -0,0 +1,365 @@ +import { Status } from '@/core/shared/constants'; +import { publishCommitment } from '../../commands/publish/handler'; + +import { + makeArgs, + makeLogger, +} from '@/__tests__/mocks/mocks'; + +// ─── Helpers ──────────────────────────────────────────────────────────────────── + +function makeStateStore(entries: Record = {}) { + const store = new Map(Object.entries(entries)); + return { + get: jest.fn((ns: string, key: string) => store.get(`${ns}:${key}`)), + set: jest.fn((ns: string, key: string, val: unknown) => + store.set(`${ns}:${key}`, val), + ), + getKeys: jest.fn((ns: string) => { + const prefix = `${ns}:`; + return [...store.keys()] + .filter((k) => k.startsWith(prefix)) + .map((k) => k.slice(prefix.length)); + }), + list: jest.fn(), + delete: jest.fn(), + clear: jest.fn(), + has: jest.fn(), + getNamespaces: jest.fn(), + subscribe: jest.fn(), + getActions: jest.fn(), + getState: jest.fn(), + getStorageDirectory: jest.fn(), + isInitialized: jest.fn().mockReturnValue(true), + }; +} + +// ─── Tests ────────────────────────────────────────────────────────────────────── + +describe('auctionlog publish', () => { + it('should publish a commitment and return success', async () => { + const logger = makeLogger(); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + topic: { + createTopic: jest.fn().mockReturnValue({ + transaction: {}, + }), + submitMessage: jest.fn().mockReturnValue({ + transaction: {}, + }), + }, + txExecution: { + signAndExecute: jest.fn().mockResolvedValue({ + success: true, + topicId: '0.0.9999999', + topicSequenceNumber: 1, + }), + }, + }; + + const stateStore = makeStateStore({}); + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-TEST-001', + stage: 'created', + metadata: 'canton-tx-abc123', + }); + (args as any).state = stateStore; + (args as any).api = api; + + const result = await publishCommitment(args); + + expect(result.status).toBe(Status.Success); + expect(result.outputJson).toBeDefined(); + + const output = JSON.parse(result.outputJson!); + expect(output.auctionId).toBe('AUCTION-TEST-001'); + expect(output.stage).toBe('created'); + expect(output.commitmentHash).toMatch(/^0x[a-f0-9]{64}$/); + expect(output.topicId).toBe('0.0.9999999'); + expect(output.sequenceNumber).toBe(1); + expect(output.network).toBe('testnet'); + // Nonce should be a proper crypto nonce (32 hex chars + 0x prefix) + expect(output.nonce).toMatch(/^0x[a-f0-9]{32}$/); + }); + + it('should create a new topic if none exists', async () => { + const logger = makeLogger(); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + topic: { + createTopic: jest.fn().mockReturnValue({ + transaction: {}, + }), + submitMessage: jest.fn().mockReturnValue({ + transaction: {}, + }), + }, + txExecution: { + signAndExecute: jest.fn().mockResolvedValue({ + success: true, + topicId: '0.0.9999999', + topicSequenceNumber: 1, + }), + }, + }; + + const stateStore = makeStateStore({}); + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-NEW-001', + stage: 'created', + }); + (args as any).state = stateStore; + (args as any).api = api; + + const result = await publishCommitment(args); + + expect(result.status).toBe(Status.Success); + expect(api.topic.createTopic).toHaveBeenCalledWith({ + memo: 'auctionlog: AUCTION-NEW-001', + }); + }); + + it('should reuse existing topic from state', async () => { + const logger = makeLogger(); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + topic: { + createTopic: jest.fn().mockReturnValue({ + transaction: {}, + }), + submitMessage: jest.fn().mockReturnValue({ + transaction: {}, + }), + }, + txExecution: { + signAndExecute: jest.fn().mockResolvedValue({ + success: true, + topicId: '0.0.1111111', + topicSequenceNumber: 2, + }), + }, + }; + + // Pre-populate state with existing auction (created stage done) + const stateStore = makeStateStore({ + 'auctionlog-data:AUCTION-REUSE-001': { + topicId: '0.0.1111111', + lastStage: 'created', + lastUpdated: '2026-02-21T00:00:00.000Z', + }, + 'auctionlog-data:AUCTION-REUSE-001:created': { + auctionId: 'AUCTION-REUSE-001', + stage: 'created', + topicId: '0.0.1111111', + }, + }); + + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-REUSE-001', + stage: 'bidding-open', + }); + (args as any).state = stateStore; + (args as any).api = api; + + const result = await publishCommitment(args); + + expect(result.status).toBe(Status.Success); + // Should NOT create a new topic + expect(api.topic.createTopic).not.toHaveBeenCalled(); + }); + + it('should reject invalid stage', async () => { + const logger = makeLogger(); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + }; + + const stateStore = makeStateStore({}); + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-BAD-001', + stage: 'invalid-stage', + }); + (args as any).state = stateStore; + (args as any).api = api; + + await expect(publishCommitment(args)).rejects.toThrow(); + }); + + it('should reject missing auctionId', async () => { + const logger = makeLogger(); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + }; + + const stateStore = makeStateStore({}); + const args = makeArgs(api as any, logger, { + stage: 'created', + }); + (args as any).state = stateStore; + (args as any).api = api; + + await expect(publishCommitment(args)).rejects.toThrow(); + }); + + it('should return failure when topic creation fails', async () => { + const logger = makeLogger(); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + topic: { + createTopic: jest.fn().mockReturnValue({ + transaction: {}, + }), + submitMessage: jest.fn().mockReturnValue({ + transaction: {}, + }), + }, + txExecution: { + signAndExecute: jest.fn().mockResolvedValue({ + success: false, + }), + }, + }; + + const stateStore = makeStateStore({}); + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-FAIL-001', + stage: 'created', + }); + (args as any).state = stateStore; + (args as any).api = api; + + const result = await publishCommitment(args); + expect(result.status).toBe(Status.Failure); + expect(result.errorMessage).toContain('Failed to create HCS topic'); + }); + + it('should reject duplicate stage publication', async () => { + const logger = makeLogger(); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + }; + + const stateStore = makeStateStore({ + 'auctionlog-data:AUCTION-DUP-001': { + topicId: '0.0.5555', + lastStage: 'created', + lastUpdated: '2026-02-21T00:00:00.000Z', + }, + 'auctionlog-data:AUCTION-DUP-001:created': { + auctionId: 'AUCTION-DUP-001', + stage: 'created', + topicId: '0.0.5555', + }, + }); + + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-DUP-001', + stage: 'created', + }); + (args as any).state = stateStore; + (args as any).api = api; + + const result = await publishCommitment(args); + expect(result.status).toBe(Status.Failure); + expect(result.errorMessage).toContain('already been published'); + }); + + it('should enforce stage ordering — reject out-of-order stages', async () => { + const logger = makeLogger(); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + }; + + // Only 'created' exists — skip directly to 'awarded' + const stateStore = makeStateStore({ + 'auctionlog-data:AUCTION-ORDER-001': { + topicId: '0.0.5555', + lastStage: 'created', + lastUpdated: '2026-02-21T00:00:00.000Z', + }, + 'auctionlog-data:AUCTION-ORDER-001:created': { + auctionId: 'AUCTION-ORDER-001', + stage: 'created', + topicId: '0.0.5555', + }, + }); + + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-ORDER-001', + stage: 'awarded', + }); + (args as any).state = stateStore; + (args as any).api = api; + + const result = await publishCommitment(args); + expect(result.status).toBe(Status.Failure); + expect(result.errorMessage).toContain('bidding-closed'); + expect(result.errorMessage).toContain('has not been published yet'); + }); + + it('should allow disputed at any time after auction exists', async () => { + const logger = makeLogger(); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + topic: { + createTopic: jest.fn().mockReturnValue({ transaction: {} }), + submitMessage: jest.fn().mockReturnValue({ transaction: {} }), + }, + txExecution: { + signAndExecute: jest.fn().mockResolvedValue({ + success: true, + topicId: '0.0.5555', + topicSequenceNumber: 2, + }), + }, + }; + + const stateStore = makeStateStore({ + 'auctionlog-data:AUCTION-DISPUTE-001': { + topicId: '0.0.5555', + lastStage: 'created', + lastUpdated: '2026-02-21T00:00:00.000Z', + }, + 'auctionlog-data:AUCTION-DISPUTE-001:created': { + auctionId: 'AUCTION-DISPUTE-001', + stage: 'created', + topicId: '0.0.5555', + }, + }); + + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-DISPUTE-001', + stage: 'disputed', + }); + (args as any).state = stateStore; + (args as any).api = api; + + const result = await publishCommitment(args); + expect(result.status).toBe(Status.Success); + }); + + it('should produce deterministic hashes for same inputs', async () => { + const { buildCommitmentHash } = require('../../commands/publish/handler'); + const hash1 = buildCommitmentHash('A-001', 'created', 'meta', '2026-01-01T00:00:00Z', '0xabc'); + const hash2 = buildCommitmentHash('A-001', 'created', 'meta', '2026-01-01T00:00:00Z', '0xabc'); + expect(hash1).toBe(hash2); + expect(hash1).toMatch(/^0x[a-f0-9]{64}$/); + }); + + it('should produce different hashes for different inputs', async () => { + const { buildCommitmentHash } = require('../../commands/publish/handler'); + const hash1 = buildCommitmentHash('A-001', 'created', 'meta', '2026-01-01T00:00:00Z', '0xabc'); + const hash2 = buildCommitmentHash('A-001', 'created', 'meta', '2026-01-01T00:00:00Z', '0xdef'); + expect(hash1).not.toBe(hash2); + }); +}); diff --git a/src/plugins/auctionlog/__tests__/unit/verify.test.ts b/src/plugins/auctionlog/__tests__/unit/verify.test.ts new file mode 100644 index 000000000..e46764bb1 --- /dev/null +++ b/src/plugins/auctionlog/__tests__/unit/verify.test.ts @@ -0,0 +1,334 @@ +import { Status } from '@/core/shared/constants'; +import { verifyCommitments } from '../../commands/verify/handler'; + +import { + makeArgs, + makeLogger, +} from '@/__tests__/mocks/mocks'; + +// ─── Helpers ──────────────────────────────────────────────────────────────────── + +function makeStateStore(entries: Record = {}) { + const store = new Map(Object.entries(entries)); + return { + get: jest.fn((ns: string, key: string) => store.get(`${ns}:${key}`)), + set: jest.fn((ns: string, key: string, val: unknown) => + store.set(`${ns}:${key}`, val), + ), + getKeys: jest.fn(() => [...store.keys()]), + list: jest.fn(), + delete: jest.fn(), + clear: jest.fn(), + has: jest.fn(), + getNamespaces: jest.fn(), + subscribe: jest.fn(), + getActions: jest.fn(), + getState: jest.fn(), + getStorageDirectory: jest.fn(), + isInitialized: jest.fn().mockReturnValue(true), + }; +} + +function computeHash(fields: Record): string { + return ( + '0x' + + require('crypto') + .createHash('sha256') + .update(JSON.stringify(fields)) + .digest('hex') + ); +} + +// ─── Tests ────────────────────────────────────────────────────────────────────── + +describe('auctionlog verify', () => { + it('should verify a valid commitment (local)', async () => { + const logger = makeLogger(); + + const fields = { + auctionId: 'AUCTION-V-001', + stage: 'created', + metadata: 'test-meta', + timestamp: '2026-02-21T00:00:00.000Z', + nonce: '0xdeadbeef', + }; + + const stateStore = makeStateStore({ + 'auctionlog-data:AUCTION-V-001': { topicId: '0.0.5555' }, + 'auctionlog-data:AUCTION-V-001:created': { + ...fields, + commitmentHash: computeHash(fields), + topicId: '0.0.5555', + sequenceNumber: 1, + network: 'testnet', + }, + }); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + mirror: { getTopicMessages: jest.fn() }, + }; + + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-V-001', + }); + (args as any).state = stateStore; + + const result = await verifyCommitments(args); + + expect(result.status).toBe(Status.Success); + expect(result.outputJson).toBeDefined(); + const output = JSON.parse(result.outputJson!); + expect(output.allLocalValid).toBe(true); + expect(output.localVerifiedCount).toBe(1); + expect(output.entries[0].localVerified).toBe(true); + expect(output.onChainEnabled).toBe(false); + expect(output.allOnChainValid).toBeNull(); + }); + + it('should detect tampered commitment', async () => { + const logger = makeLogger(); + + const stateStore = makeStateStore({ + 'auctionlog-data:AUCTION-V-002': { topicId: '0.0.5555' }, + 'auctionlog-data:AUCTION-V-002:created': { + auctionId: 'AUCTION-V-002', + stage: 'created', + metadata: 'test-meta', + timestamp: '2026-02-21T00:00:00.000Z', + nonce: '0xdeadbeef', + commitmentHash: '0xTAMPERED_HASH_THAT_SHOULD_NOT_MATCH', + topicId: '0.0.5555', + sequenceNumber: 1, + network: 'testnet', + }, + }); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + mirror: { getTopicMessages: jest.fn() }, + }; + + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-V-002', + }); + (args as any).state = stateStore; + + const result = await verifyCommitments(args); + + expect(result.status).toBe(Status.Success); + const output = JSON.parse(result.outputJson!); + expect(output.allLocalValid).toBe(false); + expect(output.entries[0].localVerified).toBe(false); + expect(output.entries[0].reason).toContain('Local hash mismatch'); + }); + + it('should fail if no audit log found', async () => { + const logger = makeLogger(); + + const stateStore = makeStateStore({}); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + mirror: { getTopicMessages: jest.fn() }, + }; + + const args = makeArgs(api as any, logger, { + auctionId: 'NONEXISTENT', + }); + (args as any).state = stateStore; + + const result = await verifyCommitments(args); + + expect(result.status).toBe(Status.Failure); + expect(result.errorMessage).toContain('No audit log found'); + }); + + it('should perform on-chain verification when --on-chain is set', async () => { + const logger = makeLogger(); + + const fields = { + auctionId: 'AUCTION-V-OC-001', + stage: 'created', + metadata: '', + timestamp: '2026-02-21T00:00:00.000Z', + nonce: '0xaabbccdd', + }; + const hash = computeHash(fields); + + const stateStore = makeStateStore({ + 'auctionlog-data:AUCTION-V-OC-001': { topicId: '0.0.7777' }, + 'auctionlog-data:AUCTION-V-OC-001:created': { + ...fields, + commitmentHash: hash, + topicId: '0.0.7777', + sequenceNumber: 1, + network: 'testnet', + }, + }); + + // Mock mirror node response with matching on-chain message + const onChainMessage = JSON.stringify({ + version: 1, + auctionId: 'AUCTION-V-OC-001', + stage: 'created', + commitmentHash: hash, + timestamp: '2026-02-21T00:00:00.000Z', + }); + const base64Message = Buffer.from(onChainMessage).toString('base64'); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + mirror: { + getTopicMessages: jest.fn().mockResolvedValue({ + messages: [ + { + consensus_timestamp: '2026-02-21T00:00:00.000Z', + topic_id: '0.0.7777', + message: base64Message, + running_hash: '', + sequence_number: 1, + }, + ], + }), + }, + }; + + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-V-OC-001', + onChain: true, + }); + (args as any).state = stateStore; + + const result = await verifyCommitments(args); + + expect(result.status).toBe(Status.Success); + const output = JSON.parse(result.outputJson!); + expect(output.onChainEnabled).toBe(true); + expect(output.allLocalValid).toBe(true); + expect(output.allOnChainValid).toBe(true); + expect(output.entries[0].onChainVerified).toBe(true); + expect(api.mirror.getTopicMessages).toHaveBeenCalledWith({ + topicId: '0.0.7777', + }); + }); + + it('should detect on-chain hash mismatch', async () => { + const logger = makeLogger(); + + const fields = { + auctionId: 'AUCTION-V-OCM-001', + stage: 'created', + metadata: '', + timestamp: '2026-02-21T00:00:00.000Z', + nonce: '0xaabbccdd', + }; + const localHash = computeHash(fields); + + const stateStore = makeStateStore({ + 'auctionlog-data:AUCTION-V-OCM-001': { topicId: '0.0.7777' }, + 'auctionlog-data:AUCTION-V-OCM-001:created': { + ...fields, + commitmentHash: localHash, + topicId: '0.0.7777', + sequenceNumber: 1, + network: 'testnet', + }, + }); + + // On-chain has a different hash + const onChainMessage = JSON.stringify({ + version: 1, + auctionId: 'AUCTION-V-OCM-001', + stage: 'created', + commitmentHash: '0xdifferenthashfromon-chain', + timestamp: '2026-02-21T00:00:00.000Z', + }); + const base64Message = Buffer.from(onChainMessage).toString('base64'); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + mirror: { + getTopicMessages: jest.fn().mockResolvedValue({ + messages: [ + { + consensus_timestamp: '2026-02-21T00:00:00.000Z', + topic_id: '0.0.7777', + message: base64Message, + running_hash: '', + sequence_number: 1, + }, + ], + }), + }, + }; + + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-V-OCM-001', + onChain: true, + }); + (args as any).state = stateStore; + + const result = await verifyCommitments(args); + + expect(result.status).toBe(Status.Success); + const output = JSON.parse(result.outputJson!); + expect(output.allOnChainValid).toBe(false); + expect(output.entries[0].onChainVerified).toBe(false); + expect(output.entries[0].reason).toContain('On-chain hash mismatch'); + }); + + it('should gracefully handle mirror node failure', async () => { + const logger = makeLogger(); + + const fields = { + auctionId: 'AUCTION-V-MF-001', + stage: 'created', + metadata: '', + timestamp: '2026-02-21T00:00:00.000Z', + nonce: '0xaabbccdd', + }; + const hash = computeHash(fields); + + const stateStore = makeStateStore({ + 'auctionlog-data:AUCTION-V-MF-001': { topicId: '0.0.7777' }, + 'auctionlog-data:AUCTION-V-MF-001:created': { + ...fields, + commitmentHash: hash, + topicId: '0.0.7777', + sequenceNumber: 1, + network: 'testnet', + }, + }); + + const api = { + network: { getCurrentNetwork: jest.fn().mockReturnValue('testnet') }, + mirror: { + getTopicMessages: jest.fn().mockRejectedValue( + new Error('Mirror node unavailable'), + ), + }, + }; + + const args = makeArgs(api as any, logger, { + auctionId: 'AUCTION-V-MF-001', + onChain: true, + }); + (args as any).state = stateStore; + + const result = await verifyCommitments(args); + + // Should still succeed with local verification + expect(result.status).toBe(Status.Success); + const output = JSON.parse(result.outputJson!); + expect(output.allLocalValid).toBe(true); + // On-chain is false (no messages fetched) since mirror failed, + // but local verification still passes + expect(output.entries[0].localVerified).toBe(true); + // Mirror failure means on-chain messages map was empty, + // so no matching message was found → false + expect(output.entries[0].onChainVerified).toBe(false); + // Reason should note the missing on-chain message + expect(output.entries[0].reason).toContain('no matching message found'); + }); +}); diff --git a/src/plugins/auctionlog/commands/export/handler.ts b/src/plugins/auctionlog/commands/export/handler.ts new file mode 100644 index 000000000..594295c43 --- /dev/null +++ b/src/plugins/auctionlog/commands/export/handler.ts @@ -0,0 +1,191 @@ +/** + * Auctionlog Export Command Handler + * + * Exports the full audit timeline for an auction as JSON or CSV. + * This produces an artifact that procurement teams, auditors, or + * regulators can review — it proves fairness and timing without + * revealing any confidential bid data. + * + * ⚠️ SENSITIVE: The export includes nonces and private metadata that + * are NOT published on-chain. Treat the exported file as confidential. + */ +import type { CommandExecutionResult, CommandHandlerArgs } from '@/core'; +import { Status } from '@/core/shared/constants'; +import { formatError } from '@/core/utils/errors'; +import * as fs from 'fs'; +import * as path from 'path'; + +import { AUCTIONLOG_NAMESPACE } from '../../manifest'; +import type { AuditLogEntry, AuctionMeta } from '../../types'; +import { VALID_STAGES } from '../../types'; +import { ExportInputSchema } from './input'; +import { ExportOutputSchema, type ExportOutput } from './output'; + +/** + * Escape a CSV field value to handle commas, quotes, and newlines. + */ +function escapeCsvField(value: string | number): string { + const str = String(value); + if (str.includes(',') || str.includes('"') || str.includes('\n')) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; +} + +export async function exportAuditLog( + args: CommandHandlerArgs, +): Promise { + const { logger, state } = args; + + const validArgs = ExportInputSchema.parse(args.args); + const currentNetwork = args.api.network.getCurrentNetwork(); + const exportFormat = validArgs.type ?? 'json'; + const redact = validArgs.redact === true; + + logger.info( + `Exporting audit log for auction ${validArgs.auctionId} as ${exportFormat}`, + ); + + try { + // Load auction metadata + const auctionMeta = state.get( + AUCTIONLOG_NAMESPACE, + validArgs.auctionId, + ); + + if (!auctionMeta || !auctionMeta.topicId) { + return { + status: Status.Failure, + errorMessage: `No audit log found for auction ${validArgs.auctionId}`, + }; + } + + // Collect all published stages + const entries: Array<{ + stage: string; + commitmentHash: string; + timestamp: string; + sequenceNumber: number; + metadata: string; + nonce: string; + }> = []; + + for (const stage of VALID_STAGES) { + const stateKey = `${validArgs.auctionId}:${stage}`; + const entry = state.get(AUCTIONLOG_NAMESPACE, stateKey); + if (entry) { + entries.push({ + stage: entry.stage, + commitmentHash: entry.commitmentHash, + timestamp: entry.timestamp, + sequenceNumber: entry.sequenceNumber, + metadata: redact ? '[REDACTED]' : (entry.metadata ?? ''), + nonce: redact ? '[REDACTED]' : entry.nonce, + }); + } + } + + // Sort by sequence number + entries.sort((a, b) => a.sequenceNumber - b.sequenceNumber); + + if (entries.length === 0) { + return { + status: Status.Failure, + errorMessage: `No published stages found for auction ${validArgs.auctionId}`, + }; + } + + // Generate export content + let fileContent: string; + + if (exportFormat === 'csv') { + const headers = [ + 'sequence', + 'stage', + 'timestamp', + 'commitmentHash', + 'metadata', + 'nonce', + ]; + const rows = entries.map((e) => + [ + escapeCsvField(e.sequenceNumber), + escapeCsvField(e.stage), + escapeCsvField(e.timestamp), + escapeCsvField(e.commitmentHash), + escapeCsvField(e.metadata), + escapeCsvField(e.nonce), + ].join(','), + ); + fileContent = [headers.join(','), ...rows].join('\n'); + } else { + fileContent = JSON.stringify( + { + auctionId: validArgs.auctionId, + topicId: auctionMeta.topicId, + network: currentNetwork, + exportedAt: new Date().toISOString(), + sensitive: !redact, + entries, + }, + null, + 2, + ); + } + + // Write to file if path provided + let filePath: string | undefined; + if (validArgs.file) { + try { + // Resolve to absolute path and ensure parent directory exists + const resolvedPath = path.resolve(validArgs.file); + const parentDir = path.dirname(resolvedPath); + + if (!fs.existsSync(parentDir)) { + return { + status: Status.Failure, + errorMessage: `Output directory does not exist: ${parentDir}`, + }; + } + + fs.writeFileSync(resolvedPath, fileContent, 'utf8'); + filePath = resolvedPath; + logger.info(`Audit log written to ${filePath}`); + if (!redact) { + logger.warn( + 'This export contains sensitive data (nonces, metadata). Treat the file as confidential.', + ); + } + } catch (writeError: unknown) { + return { + status: Status.Failure, + errorMessage: formatError( + `Failed to write export file to ${validArgs.file}`, + writeError, + ), + }; + } + } + + const output: ExportOutput = ExportOutputSchema.parse({ + auctionId: validArgs.auctionId, + topicId: auctionMeta.topicId, + network: currentNetwork, + exportFormat, + entryCount: entries.length, + entries, + filePath, + redacted: redact, + }); + + return { + status: Status.Success, + outputJson: JSON.stringify(output), + }; + } catch (error: unknown) { + return { + status: Status.Failure, + errorMessage: formatError('Failed to export audit log', error), + }; + } +} diff --git a/src/plugins/auctionlog/commands/export/index.ts b/src/plugins/auctionlog/commands/export/index.ts new file mode 100644 index 000000000..1f4dfa6fe --- /dev/null +++ b/src/plugins/auctionlog/commands/export/index.ts @@ -0,0 +1,3 @@ +export { exportAuditLog } from './handler'; +export type { ExportOutput } from './output'; +export { EXPORT_TEMPLATE, ExportOutputSchema } from './output'; diff --git a/src/plugins/auctionlog/commands/export/input.ts b/src/plugins/auctionlog/commands/export/input.ts new file mode 100644 index 000000000..1258c2f0d --- /dev/null +++ b/src/plugins/auctionlog/commands/export/input.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; + +/** + * Input schema for auctionlog export command. + * Exports the audit trail as JSON or CSV. + */ +export const ExportInputSchema = z.object({ + auctionId: z + .string() + .min(1, 'Auction ID is required') + .describe('Auction ID to export audit trail for'), + type: z + .enum(['json', 'csv']) + .optional() + .default('json') + .describe('Export format: json (default) or csv'), + file: z + .string() + .optional() + .describe( + 'Output file path. If omitted, prints to stdout.', + ), + redact: z + .boolean() + .optional() + .default(false) + .describe( + 'Redact sensitive fields (nonces, metadata) from the export. Use for sharing without revealing commitment preimages.', + ), +}); + +export type ExportInput = z.infer; diff --git a/src/plugins/auctionlog/commands/export/output.ts b/src/plugins/auctionlog/commands/export/output.ts new file mode 100644 index 000000000..91f5d9521 --- /dev/null +++ b/src/plugins/auctionlog/commands/export/output.ts @@ -0,0 +1,42 @@ +import { z } from 'zod'; + +import { NetworkSchema } from '@/core/schemas'; + +const ExportEntrySchema = z.object({ + stage: z.string(), + commitmentHash: z.string(), + timestamp: z.string(), + sequenceNumber: z.number().int().nonnegative(), + metadata: z.string(), + nonce: z.string(), +}); + +/** + * Export command output schema. + */ +export const ExportOutputSchema = z.object({ + auctionId: z.string(), + topicId: z.string(), + network: NetworkSchema, + exportFormat: z.enum(['json', 'csv']), + entryCount: z.number().int().nonnegative(), + entries: z.array(ExportEntrySchema), + filePath: z.string().optional(), + redacted: z.boolean(), +}); + +export type ExportOutput = z.infer; + +export const EXPORT_TEMPLATE = ` +📦 Exported {{entryCount}} audit entries for auction {{auctionId}} + + Topic: {{hashscanLink topicId "topic" network}} + Format: {{exportFormat}} +{{#if redacted}} Mode: REDACTED (nonces and metadata omitted){{else}} ⚠️ Contains sensitive data — treat as confidential{{/if}} +{{#if filePath}} File: {{filePath}}{{/if}} + +Timeline: +{{#each entries}} + {{sequenceNumber}}. [{{stage}}] {{timestamp}} — {{commitmentHash}} +{{/each}} +`.trim(); diff --git a/src/plugins/auctionlog/commands/list/handler.ts b/src/plugins/auctionlog/commands/list/handler.ts new file mode 100644 index 000000000..343829b16 --- /dev/null +++ b/src/plugins/auctionlog/commands/list/handler.ts @@ -0,0 +1,76 @@ +/** + * Auctionlog List Command Handler + * + * Lists all tracked auctions and their audit stages. + */ +import type { CommandExecutionResult, CommandHandlerArgs } from '@/core'; +import { Status } from '@/core/shared/constants'; +import { formatError } from '@/core/utils/errors'; + +import { AUCTIONLOG_NAMESPACE } from '../../manifest'; +import type { AuditLogEntry, AuctionMeta } from '../../types'; +import { VALID_STAGES } from '../../types'; +import { ListOutputSchema, type ListOutput } from './output'; + +export async function listAuctions( + args: CommandHandlerArgs, +): Promise { + const { state } = args; + const currentNetwork = args.api.network.getCurrentNetwork(); + + try { + // Get all keys from the auctionlog namespace + const allKeys = state.getKeys(AUCTIONLOG_NAMESPACE) || []; + + // Filter to auction-level keys (no colon = auction metadata, not stage entries) + const auctionKeys = allKeys.filter((k: string) => !k.includes(':')); + + const auctions: Array<{ + auctionId: string; + topicId: string; + lastStage: string; + lastUpdated: string; + stageCount: number; + }> = []; + + for (const key of auctionKeys) { + const meta = state.get(AUCTIONLOG_NAMESPACE, key); + + if (meta && meta.topicId) { + // Count stages published for this auction + let stageCount = 0; + for (const stage of VALID_STAGES) { + const entry = state.get( + AUCTIONLOG_NAMESPACE, + `${key}:${stage}`, + ); + if (entry) stageCount++; + } + + auctions.push({ + auctionId: key, + topicId: meta.topicId, + lastStage: meta.lastStage || 'unknown', + lastUpdated: meta.lastUpdated || 'unknown', + stageCount, + }); + } + } + + const output: ListOutput = ListOutputSchema.parse({ + network: currentNetwork, + auctions, + totalAuctions: auctions.length, + }); + + return { + status: Status.Success, + outputJson: JSON.stringify(output), + }; + } catch (error: unknown) { + return { + status: Status.Failure, + errorMessage: formatError('Failed to list auctions', error), + }; + } +} diff --git a/src/plugins/auctionlog/commands/list/index.ts b/src/plugins/auctionlog/commands/list/index.ts new file mode 100644 index 000000000..8933d379b --- /dev/null +++ b/src/plugins/auctionlog/commands/list/index.ts @@ -0,0 +1,3 @@ +export { listAuctions } from './handler'; +export type { ListOutput } from './output'; +export { LIST_TEMPLATE, ListOutputSchema } from './output'; diff --git a/src/plugins/auctionlog/commands/list/input.ts b/src/plugins/auctionlog/commands/list/input.ts new file mode 100644 index 000000000..c267857e9 --- /dev/null +++ b/src/plugins/auctionlog/commands/list/input.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +/** + * Input schema for auctionlog list command. + * Lists all tracked auctions and their stages. + */ +export const ListInputSchema = z.object({ + // No required inputs — lists all known auctions +}); + +export type ListInput = z.infer; diff --git a/src/plugins/auctionlog/commands/list/output.ts b/src/plugins/auctionlog/commands/list/output.ts new file mode 100644 index 000000000..8a928b083 --- /dev/null +++ b/src/plugins/auctionlog/commands/list/output.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; + +import { NetworkSchema } from '@/core/schemas'; + +const AuctionSummarySchema = z.object({ + auctionId: z.string(), + topicId: z.string(), + lastStage: z.string(), + lastUpdated: z.string(), + stageCount: z.number().int().nonnegative(), +}); + +/** + * List command output schema. + */ +export const ListOutputSchema = z.object({ + network: NetworkSchema, + auctions: z.array(AuctionSummarySchema), + totalAuctions: z.number().int().nonnegative(), +}); + +export type ListOutput = z.infer; + +export const LIST_TEMPLATE = ` +📋 Tracked Auctions ({{totalAuctions}}) + +{{#each auctions}} + {{auctionId}} + Topic: {{topicId}} + Last Stage: {{lastStage}} + Updated: {{lastUpdated}} + Stages Published: {{stageCount}} +{{/each}} +{{#unless auctions.length}} + No auctions tracked yet. Use: auctionlog publish --auction-id --stage created +{{/unless}} +`.trim(); diff --git a/src/plugins/auctionlog/commands/publish/handler.ts b/src/plugins/auctionlog/commands/publish/handler.ts new file mode 100644 index 000000000..f090cb444 --- /dev/null +++ b/src/plugins/auctionlog/commands/publish/handler.ts @@ -0,0 +1,254 @@ +/** + * Auctionlog Publish Command Handler + * + * Publishes a commitment hash to a Hedera Consensus Service topic. + * The commitment is: SHA-256(JSON.stringify({ auctionId, stage, metadata, timestamp, nonce })) + * + * This proves the order and timing of auction events on a public ledger, + * without revealing any business-sensitive data (prices, terms, identities). + */ +import type { CommandExecutionResult, CommandHandlerArgs } from '@/core'; +import { Status } from '@/core/shared/constants'; +import { formatError } from '@/core/utils/errors'; +import { createHash, randomBytes } from 'crypto'; + +import { AUCTIONLOG_NAMESPACE } from '../../manifest'; +import type { AuditLogEntry, AuctionMeta, AuctionStage } from '../../types'; +import { STAGE_ORDER, VALID_STAGES } from '../../types'; +import { PublishInputSchema } from './input'; +import { PublishOutputSchema, type PublishOutput } from './output'; + +/** + * Build a deterministic commitment hash from auction event fields. + * Uses SHA-256 for maximum portability across runtimes and platforms. + * + * The payload is a canonical JSON string of all commitment fields. + * The nonce ensures each publication is unique even for repeated stages. + */ +export function buildCommitmentHash( + auctionId: string, + stage: string, + metadata: string, + timestamp: string, + nonce: string, +): string { + const payload = JSON.stringify({ + auctionId, + stage, + metadata, + timestamp, + nonce, + }); + const hash = createHash('sha256').update(payload).digest('hex'); + return `0x${hash}`; +} + +/** + * Generate a cryptographically secure random hex nonce (16 bytes / 128 bits). + * Uses Node.js crypto.randomBytes — NOT Math.random(). + */ +function secureNonce(): string { + return `0x${randomBytes(16).toString('hex')}`; +} + +/** + * Validate that the requested stage is allowed given the auction's current state. + * - 'disputed' can be published at any time after 'created' + * - All other stages must follow chronological order + * - Duplicate stage publications are rejected + */ +function validateStageProgression( + requestedStage: AuctionStage, + auctionId: string, + state: CommandHandlerArgs['state'], +): string | null { + // Check for duplicate stage + const stateKey = `${auctionId}:${requestedStage}`; + const existing = state.get(AUCTIONLOG_NAMESPACE, stateKey); + if (existing) { + return `Stage '${requestedStage}' has already been published for auction ${auctionId}. Each stage can only be published once.`; + } + + // 'created' is always valid as the first stage + if (requestedStage === 'created') { + return null; + } + + // 'disputed' can occur at any time (after auction exists) + if (requestedStage === 'disputed') { + const meta = state.get(AUCTIONLOG_NAMESPACE, auctionId); + if (!meta) { + return `Cannot publish 'disputed' for auction ${auctionId}: auction has no published stages. Publish 'created' first.`; + } + return null; + } + + // For all other stages, check that the previous stage in the sequence exists + const requestedOrder = STAGE_ORDER[requestedStage]; + const previousStages = VALID_STAGES.filter( + (s) => STAGE_ORDER[s] < requestedOrder && s !== 'disputed', + ); + + if (previousStages.length > 0) { + const immediatePrevious = previousStages[previousStages.length - 1]; + const prevKey = `${auctionId}:${immediatePrevious}`; + const prevEntry = state.get(AUCTIONLOG_NAMESPACE, prevKey); + if (!prevEntry) { + return `Cannot publish '${requestedStage}' for auction ${auctionId}: stage '${immediatePrevious}' has not been published yet. Stages must follow chronological order.`; + } + } + + return null; +} + +export async function publishCommitment( + args: CommandHandlerArgs, +): Promise { + const { api, logger, state } = args; + + // Validate input + const validArgs = PublishInputSchema.parse(args.args); + + // Validate stage progression + const progressionError = validateStageProgression( + validArgs.stage, + validArgs.auctionId, + state, + ); + if (progressionError) { + return { + status: Status.Failure, + errorMessage: progressionError, + }; + } + + const currentNetwork = api.network.getCurrentNetwork(); + const timestamp = new Date().toISOString(); + const nonce = secureNonce(); + const metadata = validArgs.metadata ?? ''; + + // Build commitment hash + const commitmentHash = buildCommitmentHash( + validArgs.auctionId, + validArgs.stage, + metadata, + timestamp, + nonce, + ); + + logger.info( + `Publishing commitment for auction ${validArgs.auctionId}, stage: ${validArgs.stage}`, + ); + + try { + // Step 1: Resolve or create topic + let topicId = validArgs.topic; + + if (!topicId) { + // Check if we already have a topic for this auction in state + const existingMeta = state.get( + AUCTIONLOG_NAMESPACE, + validArgs.auctionId, + ); + if (existingMeta && existingMeta.topicId) { + topicId = existingMeta.topicId; + logger.info(`Using existing topic: ${topicId}`); + } else { + // Create a new topic for this auction + logger.info('Creating new HCS topic for auction audit log...'); + const createTx = api.topic.createTopic({ + memo: `auctionlog: ${validArgs.auctionId}`, + }); + const txResult = await api.txExecution.signAndExecute( + createTx.transaction, + ); + + if (!txResult.success || !txResult.topicId) { + return { + status: Status.Failure, + errorMessage: 'Failed to create HCS topic for auction log', + }; + } + topicId = txResult.topicId; + logger.info(`Created topic: ${topicId}`); + } + } + + // Step 2: Publish commitment as HCS message + // Only the hash and non-sensitive identifiers are published. + // metadata and nonce are NOT included — this is the privacy guarantee. + const message = JSON.stringify({ + version: 1, + auctionId: validArgs.auctionId, + stage: validArgs.stage, + commitmentHash, + timestamp, + }); + + const submitTx = api.topic.submitMessage({ + topicId, + message, + }); + + const submitResult = await api.txExecution.signAndExecute( + submitTx.transaction, + ); + + if (!submitResult.success) { + return { + status: Status.Failure, + errorMessage: `Failed to publish commitment to topic ${topicId}`, + }; + } + + const sequenceNumber = submitResult.topicSequenceNumber ?? 0; + + // Step 3: Save to local state for later verification & export + const entry: AuditLogEntry = { + auctionId: validArgs.auctionId, + stage: validArgs.stage, + metadata, + timestamp, + nonce, + commitmentHash, + topicId, + sequenceNumber, + network: currentNetwork, + }; + + // Save with key = `{auctionId}:{stage}` for unique lookup + const stateKey = `${validArgs.auctionId}:${validArgs.stage}`; + state.set(AUCTIONLOG_NAMESPACE, stateKey, entry); + + // Also maintain an auction-level pointer to the topic + const auctionMeta: AuctionMeta = { + topicId, + lastStage: validArgs.stage, + lastUpdated: timestamp, + }; + state.set(AUCTIONLOG_NAMESPACE, validArgs.auctionId, auctionMeta); + + // Step 4: Build output + const output: PublishOutput = PublishOutputSchema.parse({ + auctionId: validArgs.auctionId, + stage: validArgs.stage, + commitmentHash, + topicId, + sequenceNumber, + metadata, + timestamp, + nonce, + network: currentNetwork, + }); + + return { + status: Status.Success, + outputJson: JSON.stringify(output), + }; + } catch (error: unknown) { + return { + status: Status.Failure, + errorMessage: formatError('Failed to publish audit commitment', error), + }; + } +} diff --git a/src/plugins/auctionlog/commands/publish/index.ts b/src/plugins/auctionlog/commands/publish/index.ts new file mode 100644 index 000000000..92e8e6560 --- /dev/null +++ b/src/plugins/auctionlog/commands/publish/index.ts @@ -0,0 +1,3 @@ +export { publishCommitment } from './handler'; +export type { PublishOutput } from './output'; +export { PUBLISH_TEMPLATE, PublishOutputSchema } from './output'; diff --git a/src/plugins/auctionlog/commands/publish/input.ts b/src/plugins/auctionlog/commands/publish/input.ts new file mode 100644 index 000000000..477904b71 --- /dev/null +++ b/src/plugins/auctionlog/commands/publish/input.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; + +import { EntityReferenceSchema } from '@/core/schemas'; + +/** + * Input schema for auctionlog publish command. + * All fields are validated before business logic runs. + */ +export const PublishInputSchema = z.object({ + auctionId: z + .string() + .min(1, 'Auction ID is required') + .describe('Unique auction identifier (e.g. AUCTION-001)'), + stage: z + .enum([ + 'created', + 'bidding-open', + 'bidding-closed', + 'awarded', + 'settled', + 'disputed', + ]) + .describe( + 'Auction stage: created | bidding-open | bidding-closed | awarded | settled | disputed', + ), + topic: EntityReferenceSchema.optional().describe( + 'Existing HCS topic ID (e.g. 0.0.123456). If omitted, a new topic is created.', + ), + metadata: z + .string() + .optional() + .describe( + 'Private metadata to include in the commitment hash (e.g. external transaction references). Not published on-chain.', + ), +}); + +export type PublishInput = z.infer; diff --git a/src/plugins/auctionlog/commands/publish/output.ts b/src/plugins/auctionlog/commands/publish/output.ts new file mode 100644 index 000000000..6b123e44c --- /dev/null +++ b/src/plugins/auctionlog/commands/publish/output.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; + +import { EntityIdSchema, IsoTimestampSchema, NetworkSchema } from '@/core/schemas'; + +/** + * Publish command output schema. + * Structured output for commitment publication. + */ +export const PublishOutputSchema = z.object({ + auctionId: z.string(), + stage: z.string(), + commitmentHash: z.string().describe('SHA-256 hash of the commitment fields'), + topicId: EntityIdSchema.describe('HCS topic where commitment was published'), + sequenceNumber: z.number().int().nonnegative(), + metadata: z.string(), + timestamp: IsoTimestampSchema, + nonce: z.string(), + network: NetworkSchema, +}); + +export type PublishOutput = z.infer; + +export const PUBLISH_TEMPLATE = ` +✅ Audit commitment published + + Auction ID: {{auctionId}} + Stage: {{stage}} + Commitment: {{commitmentHash}} + Topic: {{hashscanLink topicId "topic" network}} + Sequence: {{sequenceNumber}} + Timestamp: {{timestamp}} +{{#if metadata}} Metadata: (included in hash, not published on-chain){{/if}} +`.trim(); diff --git a/src/plugins/auctionlog/commands/verify/handler.ts b/src/plugins/auctionlog/commands/verify/handler.ts new file mode 100644 index 000000000..f08761cc6 --- /dev/null +++ b/src/plugins/auctionlog/commands/verify/handler.ts @@ -0,0 +1,240 @@ +/** + * Auctionlog Verify Command Handler + * + * Performs two-layer verification of auction audit commitments: + * + * Layer 1 — Local integrity check: + * Re-computes the commitment hash from stored fields and compares + * against the locally stored hash. + * + * Layer 2 — On-chain verification (when --on-chain flag is set): + * Fetches the actual HCS topic messages from the Hedera mirror node + * and verifies that the commitment hash published on-chain matches + * the locally stored hash. This proves the local data hasn't been + * altered since publication. + * + * Together, these two layers provide a full tamper-evidence guarantee: + * - Layer 1 proves internal consistency (hash matches preimage) + * - Layer 2 proves the hash was actually published to HCS + */ +import type { CommandExecutionResult, CommandHandlerArgs } from '@/core'; +import { Status } from '@/core/shared/constants'; +import { formatError } from '@/core/utils/errors'; +import { createHash } from 'crypto'; + +import { AUCTIONLOG_NAMESPACE } from '../../manifest'; +import type { AuditLogEntry, AuctionMeta, AuctionStage } from '../../types'; +import { VALID_STAGES } from '../../types'; +import { VerifyInputSchema } from './input'; +import { VerifyOutputSchema, type VerifyOutput } from './output'; + +/** + * Recompute the SHA-256 commitment hash from an audit log entry's fields. + * This must use the exact same canonical JSON format as buildCommitmentHash + * in the publish handler. + */ +function recomputeHash(entry: AuditLogEntry): string { + const payload = JSON.stringify({ + auctionId: entry.auctionId, + stage: entry.stage, + metadata: entry.metadata, + timestamp: entry.timestamp, + nonce: entry.nonce, + }); + return `0x${createHash('sha256').update(payload).digest('hex')}`; +} + +/** + * Parse an on-chain HCS message and extract the commitment hash. + * Returns null if the message is not a valid auctionlog commitment. + */ +function parseOnChainMessage(base64Message: string): { + commitmentHash: string; + auctionId: string; + stage: string; +} | null { + try { + const decoded = Buffer.from(base64Message, 'base64').toString('utf8'); + const parsed = JSON.parse(decoded) as Record; + if ( + typeof parsed.commitmentHash === 'string' && + typeof parsed.auctionId === 'string' && + typeof parsed.stage === 'string' + ) { + return { + commitmentHash: parsed.commitmentHash as string, + auctionId: parsed.auctionId as string, + stage: parsed.stage as string, + }; + } + return null; + } catch { + return null; + } +} + +export async function verifyCommitments( + args: CommandHandlerArgs, +): Promise { + const { api, logger, state } = args; + + const validArgs = VerifyInputSchema.parse(args.args); + const currentNetwork = api.network.getCurrentNetwork(); + const onChain = validArgs.onChain === true; + + logger.info(`Verifying audit log for auction: ${validArgs.auctionId}`); + if (onChain) { + logger.info('On-chain verification enabled — will fetch HCS messages'); + } + + try { + // Determine which stages to verify + const stagesToCheck: AuctionStage[] = validArgs.stage + ? [validArgs.stage] + : VALID_STAGES; + + // Load auction metadata to get topicId + const auctionMeta = state.get( + AUCTIONLOG_NAMESPACE, + validArgs.auctionId, + ); + + if (!auctionMeta || !auctionMeta.topicId) { + return { + status: Status.Failure, + errorMessage: `No audit log found for auction ${validArgs.auctionId}. Publish commitments first with: auctionlog publish --auction-id ${validArgs.auctionId} --stage created`, + }; + } + + // If on-chain verification is requested, fetch all topic messages up front + const onChainMessages = new Map(); // stage → commitmentHash + if (onChain) { + try { + logger.info( + `Fetching messages from topic ${auctionMeta.topicId}...`, + ); + const response = await api.mirror.getTopicMessages({ + topicId: auctionMeta.topicId, + }); + + for (const msg of response.messages) { + const parsed = parseOnChainMessage(msg.message); + if (parsed && parsed.auctionId === validArgs.auctionId) { + onChainMessages.set(parsed.stage, parsed.commitmentHash); + } + } + logger.info( + `Found ${onChainMessages.size} on-chain commitment(s) for this auction`, + ); + } catch (mirrorError: unknown) { + logger.warn( + `Mirror node query failed: ${mirrorError instanceof Error ? mirrorError.message : String(mirrorError)}. Falling back to local-only verification.`, + ); + } + } + + const entries: Array<{ + stage: string; + commitmentHash: string; + localVerified: boolean; + onChainVerified: boolean | null; + timestamp: string; + sequenceNumber: number; + reason?: string; + }> = []; + + for (const stage of stagesToCheck) { + const stateKey = `${validArgs.auctionId}:${stage}`; + const entry = state.get(AUCTIONLOG_NAMESPACE, stateKey); + + if (!entry) { + // Stage not published yet — skip + continue; + } + + // Layer 1: Local integrity check + const recomputed = recomputeHash(entry); + const localVerified = recomputed === entry.commitmentHash; + + // Layer 2: On-chain verification (if available) + let onChainVerified: boolean | null = null; + const reasons: string[] = []; + + if (!localVerified) { + reasons.push( + `Local hash mismatch: expected ${entry.commitmentHash}, recomputed ${recomputed}. Local data may have been tampered with.`, + ); + } + + if (onChain) { + const onChainHash = onChainMessages.get(stage); + if (onChainHash === undefined) { + onChainVerified = false; + reasons.push( + `On-chain: no matching message found for stage '${stage}' in topic ${auctionMeta.topicId}.`, + ); + } else if (onChainHash === entry.commitmentHash) { + onChainVerified = true; + } else { + onChainVerified = false; + reasons.push( + `On-chain hash mismatch: local has ${entry.commitmentHash}, chain has ${onChainHash}. Local state may have been altered after publication.`, + ); + } + } + + entries.push({ + stage, + commitmentHash: entry.commitmentHash, + localVerified, + onChainVerified, + timestamp: entry.timestamp, + sequenceNumber: entry.sequenceNumber, + reason: reasons.length > 0 ? reasons.join(' ') : undefined, + }); + } + + if (entries.length === 0) { + return { + status: Status.Failure, + errorMessage: `No published stages found for auction ${validArgs.auctionId}${validArgs.stage ? ` at stage '${validArgs.stage}'` : ''}`, + }; + } + + const localVerifiedCount = entries.filter((e) => e.localVerified).length; + const onChainVerifiedCount = onChain + ? entries.filter((e) => e.onChainVerified === true).length + : null; + + const allLocalValid = localVerifiedCount === entries.length; + const allOnChainValid = onChain + ? onChainVerifiedCount === entries.length + : null; + + const output: VerifyOutput = VerifyOutputSchema.parse({ + auctionId: validArgs.auctionId, + topicId: auctionMeta.topicId, + network: currentNetwork, + totalStages: entries.length, + localVerifiedCount, + onChainVerifiedCount, + allLocalValid, + allOnChainValid, + onChainEnabled: onChain, + entries, + }); + + return { + status: Status.Success, + outputJson: JSON.stringify(output), + }; + } catch (error: unknown) { + return { + status: Status.Failure, + errorMessage: formatError( + 'Failed to verify audit commitments', + error, + ), + }; + } +} diff --git a/src/plugins/auctionlog/commands/verify/index.ts b/src/plugins/auctionlog/commands/verify/index.ts new file mode 100644 index 000000000..9c48ce37a --- /dev/null +++ b/src/plugins/auctionlog/commands/verify/index.ts @@ -0,0 +1,3 @@ +export { verifyCommitments } from './handler'; +export type { VerifyOutput } from './output'; +export { VERIFY_TEMPLATE, VerifyOutputSchema } from './output'; diff --git a/src/plugins/auctionlog/commands/verify/input.ts b/src/plugins/auctionlog/commands/verify/input.ts new file mode 100644 index 000000000..b1cf5e51a --- /dev/null +++ b/src/plugins/auctionlog/commands/verify/input.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; + +/** + * Input schema for auctionlog verify command. + * Verifies the integrity of audit commitments. + */ +export const VerifyInputSchema = z.object({ + auctionId: z + .string() + .min(1, 'Auction ID is required') + .describe('Auction ID to verify'), + stage: z + .enum([ + 'created', + 'bidding-open', + 'bidding-closed', + 'awarded', + 'settled', + 'disputed', + ]) + .optional() + .describe( + 'Specific stage to verify. If omitted, verifies the entire timeline.', + ), + onChain: z + .boolean() + .optional() + .default(false) + .describe( + 'Enable on-chain verification: fetch HCS messages from the mirror node and compare against local state.', + ), +}); + +export type VerifyInput = z.infer; diff --git a/src/plugins/auctionlog/commands/verify/output.ts b/src/plugins/auctionlog/commands/verify/output.ts new file mode 100644 index 000000000..9a7cada0d --- /dev/null +++ b/src/plugins/auctionlog/commands/verify/output.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; + +import { NetworkSchema } from '@/core/schemas'; + +const VerifyEntrySchema = z.object({ + stage: z.string(), + commitmentHash: z.string(), + localVerified: z.boolean(), + onChainVerified: z.boolean().nullable(), + timestamp: z.string(), + sequenceNumber: z.number().int().nonnegative(), + reason: z.string().optional(), +}); + +/** + * Verify command output schema. + * Shows verification result for each stage in the auction timeline. + */ +export const VerifyOutputSchema = z.object({ + auctionId: z.string(), + topicId: z.string(), + network: NetworkSchema, + totalStages: z.number().int().nonnegative(), + localVerifiedCount: z.number().int().nonnegative(), + onChainVerifiedCount: z.number().int().nonnegative().nullable(), + allLocalValid: z.boolean(), + allOnChainValid: z.boolean().nullable(), + onChainEnabled: z.boolean(), + entries: z.array(VerifyEntrySchema), +}); + +export type VerifyOutput = z.infer; + +export const VERIFY_TEMPLATE = ` +{{#if allLocalValid}}✅ All local commitments verified for auction {{auctionId}}{{else}}⚠️ Some commitments FAILED local verification for auction {{auctionId}}{{/if}} +{{#if onChainEnabled}}{{#if allOnChainValid}}✅ All on-chain commitments match{{else}}⚠️ Some on-chain commitments do NOT match{{/if}}{{/if}} + + Topic: {{hashscanLink topicId "topic" network}} + Local verified: {{localVerifiedCount}} / {{totalStages}} +{{#if onChainEnabled}} On-chain verified: {{onChainVerifiedCount}} / {{totalStages}}{{/if}} + +{{#each entries}} + {{#if localVerified}}✅{{else}}❌{{/if}}{{#if ../onChainEnabled}} {{#if onChainVerified}}🔗{{else}}{{#unless onChainVerified}}⛓️‍💥{{/unless}}{{/if}}{{/if}} {{stage}} — seq #{{sequenceNumber}} — {{timestamp}} + Hash: {{commitmentHash}} +{{#if reason}} Reason: {{reason}}{{/if}} +{{/each}} +`.trim(); diff --git a/src/plugins/auctionlog/index.ts b/src/plugins/auctionlog/index.ts new file mode 100644 index 000000000..b7f5e59a6 --- /dev/null +++ b/src/plugins/auctionlog/index.ts @@ -0,0 +1,9 @@ +/** + * Auctionlog Plugin Index + * Exports the auctionlog plugin manifest and command handlers + */ +export { publishCommitment } from './commands/publish/handler'; +export { verifyCommitments } from './commands/verify/handler'; +export { exportAuditLog } from './commands/export/handler'; +export { listAuctions } from './commands/list/handler'; +export { auctionlogPluginManifest } from './manifest'; diff --git a/src/plugins/auctionlog/manifest.ts b/src/plugins/auctionlog/manifest.ts new file mode 100644 index 000000000..854232d9d --- /dev/null +++ b/src/plugins/auctionlog/manifest.ts @@ -0,0 +1,190 @@ +/** + * Auctionlog Plugin Manifest + * + * A Hiero CLI plugin that publishes, verifies, and exports privacy-preserving + * audit commitments for B2B blind auction workflows. + * + * Each critical auction stage (created, bidding-open, bidding-closed, awarded, + * settled, disputed) is recorded as a SHA-256 commitment hash on a Hedera + * Consensus Service (HCS) topic. The hash proves timing and sequence without + * revealing any business-sensitive data like bid values, delivery terms, or + * identities. + * + * Commands: + * publish — Publish a commitment for an auction stage + * verify — Verify the integrity of published commitments (local + on-chain) + * export — Export the audit trail as JSON or CSV + * list — List all tracked auctions + */ +import type { PluginManifest } from '@/core'; +import { OptionType } from '@/core/types/shared.types'; + +import { + PUBLISH_TEMPLATE, + publishCommitment, + PublishOutputSchema, +} from './commands/publish'; +import { + VERIFY_TEMPLATE, + verifyCommitments, + VerifyOutputSchema, +} from './commands/verify'; +import { + EXPORT_TEMPLATE, + exportAuditLog, + ExportOutputSchema, +} from './commands/export'; +import { + LIST_TEMPLATE, + listAuctions, + ListOutputSchema, +} from './commands/list'; + +export const AUCTIONLOG_NAMESPACE = 'auctionlog-data'; + +export const auctionlogPluginManifest: PluginManifest = { + name: 'auctionlog', + version: '1.0.0', + displayName: 'Auction Audit Log', + description: + 'Privacy-preserving audit trail for B2B blind auctions. Publishes SHA-256 commitment hashes to HCS that prove fairness and timing without revealing business data.', + commands: [ + { + name: 'publish', + summary: 'Publish an audit commitment for an auction stage', + description: + 'Publish a SHA-256 commitment hash to an HCS topic for a specific auction stage. The commitment is SHA-256(auctionId, stage, metadata, timestamp, nonce). No business data is revealed on-chain.', + options: [ + { + name: 'auction-id', + short: 'a', + type: OptionType.STRING, + required: true, + description: 'Unique auction identifier (e.g. AUCTION-001)', + }, + { + name: 'stage', + short: 's', + type: OptionType.STRING, + required: true, + description: + 'Auction stage: created | bidding-open | bidding-closed | awarded | settled | disputed', + }, + { + name: 'topic', + short: 't', + type: OptionType.STRING, + required: false, + description: + 'Existing HCS topic ID (e.g. 0.0.123456). If omitted, creates a new topic.', + }, + { + name: 'metadata', + short: 'm', + type: OptionType.STRING, + required: false, + description: + 'Private metadata included in the commitment hash but NOT published on-chain (e.g. external tx references)', + }, + ], + handler: publishCommitment, + output: { + schema: PublishOutputSchema, + humanTemplate: PUBLISH_TEMPLATE, + }, + }, + { + name: 'verify', + summary: 'Verify audit commitments for an auction', + description: + 'Verify commitment integrity. By default performs local hash re-computation. With --on-chain, also fetches HCS messages from the mirror node to verify on-chain publication.', + options: [ + { + name: 'auction-id', + short: 'a', + type: OptionType.STRING, + required: true, + description: 'Auction ID to verify', + }, + { + name: 'stage', + short: 's', + type: OptionType.STRING, + required: false, + description: + 'Specific stage to verify. If omitted, verifies all stages.', + }, + { + name: 'on-chain', + short: 'o', + type: OptionType.BOOLEAN, + required: false, + description: + 'Also verify against on-chain HCS messages via the mirror node', + }, + ], + handler: verifyCommitments, + output: { + schema: VerifyOutputSchema, + humanTemplate: VERIFY_TEMPLATE, + }, + }, + { + name: 'export', + summary: 'Export audit trail as JSON or CSV', + description: + 'Export the full audit timeline for an auction. Produces a JSON or CSV artifact suitable for compliance review, legal discovery, or regulatory audit. Use --redact to strip sensitive fields.', + options: [ + { + name: 'auction-id', + short: 'a', + type: OptionType.STRING, + required: true, + description: 'Auction ID to export', + }, + { + name: 'type', + short: 'T', + type: OptionType.STRING, + required: false, + description: 'Export format: json (default) or csv', + }, + { + name: 'file', + short: 'f', + type: OptionType.STRING, + required: false, + description: + 'Output file path. If omitted, prints to stdout.', + }, + { + name: 'redact', + short: 'r', + type: OptionType.BOOLEAN, + required: false, + description: + 'Redact sensitive fields (nonces, metadata) from the export', + }, + ], + handler: exportAuditLog, + output: { + schema: ExportOutputSchema, + humanTemplate: EXPORT_TEMPLATE, + }, + }, + { + name: 'list', + summary: 'List all tracked auctions', + description: + 'Show all auctions that have at least one published audit commitment.', + options: [], + handler: listAuctions, + output: { + schema: ListOutputSchema, + humanTemplate: LIST_TEMPLATE, + }, + }, + ], +}; + +export default auctionlogPluginManifest; diff --git a/src/plugins/auctionlog/types.ts b/src/plugins/auctionlog/types.ts new file mode 100644 index 000000000..dc0e5e6c2 --- /dev/null +++ b/src/plugins/auctionlog/types.ts @@ -0,0 +1,73 @@ +/** + * Auction Log Plugin Types + * + * Types for the auctionlog audit trail plugin. + * Commitments are published to HCS topics — they prove timing and + * ordering of auction events without revealing any business data. + */ + +/** + * Valid auction lifecycle stages in chronological order. + * The STAGE_ORDER map encodes the expected sequence. + */ +export type AuctionStage = + | 'created' + | 'bidding-open' + | 'bidding-closed' + | 'awarded' + | 'settled' + | 'disputed'; + +export const VALID_STAGES: AuctionStage[] = [ + 'created', + 'bidding-open', + 'bidding-closed', + 'awarded', + 'settled', + 'disputed', +]; + +/** + * Ordinal position of each stage. Used for enforcing chronological + * ordering (except 'disputed', which may occur at any point). + */ +export const STAGE_ORDER: Record = { + 'created': 0, + 'bidding-open': 1, + 'bidding-closed': 2, + 'awarded': 3, + 'settled': 4, + 'disputed': 5, +}; + +/** + * Core commitment fields that are hashed together. + * The hash is published on-chain; these fields stay private. + */ +export interface AuditCommitment { + auctionId: string; + stage: AuctionStage; + metadata: string; + timestamp: string; + nonce: string; + commitmentHash: string; +} + +/** + * Full audit log entry stored in local state. + * Extends AuditCommitment with HCS-specific fields. + */ +export interface AuditLogEntry extends AuditCommitment { + topicId: string; + sequenceNumber: number; + network: string; +} + +/** + * Auction-level metadata pointer stored in state. + */ +export interface AuctionMeta { + topicId: string; + lastStage: AuctionStage; + lastUpdated: string; +}