diff --git a/package-lock.json b/package-lock.json index c9f1633e..e0d955a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@jellyfin/sdk": "^0.11.0", "@kenyip/backoff-strategies": "^1.0.4", "@lukehagar/plexjs": "^0.32.1", + "@mswjs/interceptors": "^0.38.0", "@react-nano/use-event-source": "^0.13.0", "@reduxjs/toolkit": "^1.9.5", "@supercharge/promise-pool": "^3.0.0", @@ -42,6 +43,7 @@ "compare-versions": "^4.1.2", "concat-stream": "^2.0.0", "cors": "^2.8.5", + "curl-generator": "^0.4.1", "dayjs": "^1.10.4", "dbus-ts": "^0.0.7", "dotenv": "^10.0.0", @@ -167,6 +169,23 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.1.tgz", + "integrity": "sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==", + "dependencies": { + "@csstools/css-calc": "^2.1.2", + "@csstools/css-color-parser": "^3.0.8", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, "node_modules/@astronautlabs/mdns": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@astronautlabs/mdns/-/mdns-1.0.10.tgz", @@ -490,6 +509,111 @@ "tough-cookie": "^4.1.4" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.2.tgz", + "integrity": "sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.8.tgz", + "integrity": "sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", + "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", + "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@curvenote/ansi-to-react": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@curvenote/ansi-to-react/-/ansi-to-react-7.0.0.tgz", @@ -1522,15 +1646,15 @@ } }, "node_modules/@mswjs/interceptors": { - "version": "0.36.7", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.36.7.tgz", - "integrity": "sha512-sdx02Wlus5hv6Bx7uUDb25gb0WGjCuSgnJB2LVERemoSGuqkZMe3QI6nEXhieFGtYwPrZbYrT2vPbsFN2XfbUw==", - "dev": true, + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.0.tgz", + "integrity": "sha512-nPHVM+LUl4V1kXPXuTcNN5OMD//ltCQ0lccuEagvidJdpbig3hP3W6/ctWHx6mee7vZIWE0L+Mqj3vx0ASlm/w==", "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", + "jsdom": "^26.0.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" }, @@ -1573,14 +1697,12 @@ "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", - "dev": true + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==" }, "node_modules/@open-draft/logger": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", - "dev": true, "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" @@ -1589,8 +1711,7 @@ "node_modules/@open-draft/until": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", - "dev": true + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==" }, "node_modules/@parcel/watcher": { "version": "2.4.1", @@ -3254,12 +3375,9 @@ } }, "node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "engines": { "node": ">= 14" } @@ -4290,11 +4408,74 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.0.tgz", + "integrity": "sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==", + "dependencies": { + "@asamuzakjp/css-color": "^3.1.1", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/curl-generator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/curl-generator/-/curl-generator-0.4.1.tgz", + "integrity": "sha512-7eFnageM2mNeQhUuckwXRaoIbTP0RMszrpLw7Z6cyueLgNTGlvUImWugfM1ryhbdxCtJHN7JjKALLmx6fKPBYA==", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.0.tgz", + "integrity": "sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", @@ -4366,6 +4547,11 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==" + }, "node_modules/decompress-response": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-5.0.0.tgz", @@ -4679,6 +4865,17 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -6003,6 +6200,17 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -6030,12 +6238,24 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { @@ -6367,8 +6587,7 @@ "node_modules/is-node-process": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", - "dev": true + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==" }, "node_modules/is-number": { "version": "7.0.0", @@ -6411,6 +6630,11 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -6896,6 +7120,87 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.0.0.tgz", + "integrity": "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.1", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.0.tgz", + "integrity": "sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -7544,6 +7849,23 @@ } } }, + "node_modules/msw/node_modules/@mswjs/interceptors": { + "version": "0.36.10", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.36.10.tgz", + "integrity": "sha512-GXrJgakgJW3DWKueebkvtYgGKkxA7s0u5B0P5syJM5rvQUnrpLPigvci8Hukl7yEM+sU06l+er2Fgvx/gmiRgg==", + "dev": true, + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/msw/node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -7850,6 +8172,11 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/nwsapi": { + "version": "2.2.19", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.19.tgz", + "integrity": "sha512-94bcyI3RsqiZufXjkr3ltkI86iEl+I7uiHVDtcq9wJUTwYQJ5odHDeSzkkrRzi80jJ8MaeZgqKjH1bAWAFw9bA==" + }, "node_modules/nyc": { "version": "17.1.0", "resolved": "https://registry.npmjs.org/nyc/-/nyc-17.1.0.tgz", @@ -8239,8 +8566,7 @@ "node_modules/outvariant": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", - "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", - "dev": true + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==" }, "node_modules/p-cancelable": { "version": "2.1.1", @@ -8369,6 +8695,17 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -9119,7 +9456,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -9665,6 +10001,11 @@ } } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9767,6 +10108,17 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -10093,8 +10445,7 @@ "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", - "dev": true + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==" }, "node_modules/string_decoder": { "version": "1.3.0", @@ -10427,6 +10778,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, "node_modules/tailwindcss": { "version": "3.4.14", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz", @@ -10538,6 +10894,22 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" }, + "node_modules/tldts": { + "version": "6.1.85", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.85.tgz", + "integrity": "sha512-gBdZ1RjCSevRPFix/hpaUWeak2/RNUZB4/8frF1r5uYMHjFptkiT0JXIebWvgI/0ZHXvxaUDDJshiA0j6GdL3w==", + "dependencies": { + "tldts-core": "^6.1.85" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.85", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.85.tgz", + "integrity": "sha512-DTjUVvxckL1fIoPSb3KE7ISNtkWSawZdpfxGxwiIrZoO6EbHVDXXUIlIuWympPaeS+BLGyggozX/HTMsRAdsoA==" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -11563,11 +11935,52 @@ "phin": "^3.6.1" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -11814,6 +12227,14 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "engines": { + "node": ">=18" + } + }, "node_modules/xml2js": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.1.tgz", @@ -11834,6 +12255,11 @@ "node": ">=4.0" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index afdfaede..a92fe9b7 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@jellyfin/sdk": "^0.11.0", "@kenyip/backoff-strategies": "^1.0.4", "@lukehagar/plexjs": "^0.32.1", + "@mswjs/interceptors": "^0.38.0", "@react-nano/use-event-source": "^0.13.0", "@reduxjs/toolkit": "^1.9.5", "@supercharge/promise-pool": "^3.0.0", @@ -74,6 +75,7 @@ "compare-versions": "^4.1.2", "concat-stream": "^2.0.0", "cors": "^2.8.5", + "curl-generator": "^0.4.1", "dayjs": "^1.10.4", "dbus-ts": "^0.0.7", "dotenv": "^10.0.0", diff --git a/src/backend/scrobblers/LastfmScrobbler.ts b/src/backend/scrobblers/LastfmScrobbler.ts index f7f0f5a3..056b80b8 100644 --- a/src/backend/scrobblers/LastfmScrobbler.ts +++ b/src/backend/scrobblers/LastfmScrobbler.ts @@ -11,6 +11,8 @@ import { LastfmClientConfig } from "../common/infrastructure/config/client/lastf import LastfmApiClient from "../common/vendor/LastfmApiClient.js"; import { Notifiers } from "../notifier/Notifiers.js"; import AbstractScrobbleClient from "./AbstractScrobbleClient.js"; +import { deleteIntercept, getIntercept, interceptRequest } from "../utils/InterceptUtils.js"; +import { logCurlSafe, logInterceptCurlSafe } from "../utils/RequestUtils.js"; export default class LastfmScrobbler extends AbstractScrobbleClient { @@ -118,9 +120,12 @@ export default class LastfmScrobbler extends AbstractScrobbleClient { const scrobblePayload = this.api.playToClientPayload(playObj); + const iid = interceptRequest(undefined, {url: 'ws.audioscrobbler.com', method: 'POST'}); try { + const response = await this.api.callApi((client: any) => client.trackScrobble( - scrobblePayload)); + scrobblePayload, undefined, true)); + const intercept = getIntercept(iid, 'request'); const { scrobbles: { '@attr': { @@ -152,6 +157,7 @@ export default class LastfmScrobbler extends AbstractScrobbleClient { if(ignored > 0) { await this.notifier.notify({title: `Client - ${capitalize(this.type)} - ${this.name} - Scrobble Error`, message: `Failed to scrobble => ${buildTrackString(playObj)} | Error: Service ignored this scrobble 😬 => (Code ${ignoreCode}) ${(ignoreMsg === '' ? '(No error message returned)' : ignoreMsg)}`, priority: 'warn'}); this.logger.warn(`Service ignored this scrobble 😬 => (Code ${ignoreCode}) ${(ignoreMsg === '' ? '(No error message returned)' : ignoreMsg)} -- See https://www.last.fm/api/errorcodes for more information`, {payload: scrobblePayload}); + await logInterceptCurlSafe(intercept, this.logger); throw new UpstreamError('LastFM ignored scrobble', {showStopper: false}); } @@ -159,15 +165,18 @@ export default class LastfmScrobbler extends AbstractScrobbleClient { // last fm has rate limits but i can't find a specific example of what that limit is. going to default to 1 scrobble/sec to be safe //await sleep(1000); } catch (e) { + const intercept = getIntercept(iid, 'request'); await this.notifier.notify({title: `Client - ${capitalize(this.type)} - ${this.name} - Scrobble Error`, message: `Failed to scrobble => ${buildTrackString(playObj)} | Error: ${e.message}`, priority: 'error'}); this.logger.error(`Scrobble Error (${sType})`, {playInfo: buildTrackString(playObj), payload: scrobblePayload}); + await logInterceptCurlSafe(intercept, this.logger); if(!(e instanceof UpstreamError)) { throw new UpstreamError('Error received from LastFM API', {cause: e, showStopper: true}); } else { throw e; } } finally { - this.logger.debug('Raw Payload: ', scrobblePayload); + deleteIntercept(iid); + this.logger.debug({scrobblePayload}, `MS Payload`); } } } diff --git a/src/backend/scrobblers/ListenbrainzScrobbler.ts b/src/backend/scrobblers/ListenbrainzScrobbler.ts index e63117e5..27f901dc 100644 --- a/src/backend/scrobblers/ListenbrainzScrobbler.ts +++ b/src/backend/scrobblers/ListenbrainzScrobbler.ts @@ -11,6 +11,8 @@ import { ListenbrainzApiClient, ListenPayload } from "../common/vendor/Listenbra import { Notifiers } from "../notifier/Notifiers.js"; import AbstractScrobbleClient from "./AbstractScrobbleClient.js"; +import { getIntercept, interceptRequest } from "../utils/InterceptUtils.js"; +import { logInterceptCurlSafe } from "../utils/RequestUtils.js"; export default class ListenbrainzScrobbler extends AbstractScrobbleClient { @@ -76,7 +78,7 @@ export default class ListenbrainzScrobbler extends AbstractScrobbleClient { newFromSource = false, } = {} } = playObj; - + const iid = interceptRequest(undefined, {url: `${this.api.url}1/submit-listens`, method: 'POST'}); try { await this.api.submitListen(playObj, true); @@ -87,7 +89,9 @@ export default class ListenbrainzScrobbler extends AbstractScrobbleClient { } return playObj; } catch (e) { + const intercept = getIntercept(iid, 'request'); await this.notifier.notify({title: `Client - ${capitalize(this.type)} - ${this.name} - Scrobble Error`, message: `Failed to scrobble => ${buildTrackString(playObj)} | Error: ${e.message}`, priority: 'error'}); + await logInterceptCurlSafe(intercept, this.logger); throw new UpstreamError(`Error occurred while making Listenbrainz API scrobble request: ${e.message}`, {cause: e, showStopper: !(e instanceof UpstreamError)}); } } diff --git a/src/backend/scrobblers/MalojaScrobbler.ts b/src/backend/scrobblers/MalojaScrobbler.ts index a9f07361..0924a637 100644 --- a/src/backend/scrobblers/MalojaScrobbler.ts +++ b/src/backend/scrobblers/MalojaScrobbler.ts @@ -27,6 +27,8 @@ import { Notifiers } from "../notifier/Notifiers.js"; import { parseRetryAfterSecsFromObj, sleep } from "../utils.js"; import { getScrobbleTsSOCDate, getScrobbleTsSOCDateWithContext } from "../utils/TimeUtils.js"; import AbstractScrobbleClient from "./AbstractScrobbleClient.js"; +import { deleteIntercept, getIntercept, Intercept, interceptRequest } from "../utils/InterceptUtils.js"; +import { logInterceptCurlSafe } from "../utils/RequestUtils.js"; const feat = ["ft.", "ft", "feat.", "feat", "featuring", "Ft.", "Ft", "Feat.", "Feat", "Featuring"]; @@ -402,11 +404,15 @@ export default class MalojaScrobbler extends AbstractScrobbleClient { let responseBody: MalojaScrobbleV3ResponseData; + const iid = interceptRequest(undefined, {url: `${url}/apis/mlj_1/newscrobble`, method: 'POST'}); + let intercept: Intercept; try { const response = await this.callApi(request.post(`${url}/apis/mlj_1/newscrobble`) .type('json') .send(scrobbleData)); + intercept = getIntercept(iid, 'request'); + let scrobbleResponse: any | undefined = undefined, scrobbledPlay: PlayObject; @@ -437,6 +443,7 @@ export default class MalojaScrobbler extends AbstractScrobbleClient { } } if(warnings.length > 0) { + await logInterceptCurlSafe(intercept, this.logger); for(const w of warnings) { const warnStr = buildWarningString(w); if(warnStr.includes('The submitted scrobble was not added')) { @@ -472,12 +479,18 @@ export default class MalojaScrobbler extends AbstractScrobbleClient { if(warning !== '') { this.logger.warn(`${scrobbleInfo} | ${warning}`); this.logger.debug(`Response: ${this.logger.debug(JSON.stringify(response.body))}`); + await logInterceptCurlSafe(intercept, this.logger); } else { + await logInterceptCurlSafe(intercept, this.logger); this.logger.info(scrobbleInfo); } return scrobbledPlay; } catch (e) { await this.notifier.notify({title: `Client - ${capitalize(this.type)} - ${this.name} - Scrobble Error`, message: `Failed to scrobble => ${buildTrackString(playObj)} | Error: ${e.message}`, priority: 'error'}); + if(intercept === undefined) { + intercept = getIntercept(iid, 'request'); + } + await logInterceptCurlSafe(intercept, this.logger); this.logger.error(`Scrobble Error (${sType})`, {playInfo: buildTrackString(playObj), payload: scrobbleData}); const responseError = getMalojaResponseError(e); if(responseError !== undefined) { @@ -491,6 +504,7 @@ export default class MalojaScrobbler extends AbstractScrobbleClient { throw e; } finally { this.logger.debug('Raw Payload:', scrobbleData); + deleteIntercept(iid); } } } diff --git a/src/backend/utils/InterceptUtils.ts b/src/backend/utils/InterceptUtils.ts new file mode 100644 index 00000000..34246fa6 --- /dev/null +++ b/src/backend/utils/InterceptUtils.ts @@ -0,0 +1,149 @@ +import { BatchInterceptor, HttpRequestEventMap, InterceptorReadyState } from '@mswjs/interceptors' +import { ClientRequestInterceptor } from '@mswjs/interceptors/ClientRequest' +import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest' +import { nanoid } from 'nanoid'; +import { isDebugMode, parseBool, parseRegexSingleOrFail } from '../utils.js'; + +interface InterceptFilterOptions { + url?: string | RegExp + method?: string + //body?: string | RegExp +} + +type ReqListener = (args: HttpRequestEventMap["request"][0]) => void; +type ResListener = (args: HttpRequestEventMap["response"][0]) => void; + +export interface Intercept { + req?: Request, + id: string, + res?: Response, +} + +interface InterceptData extends Intercept { + reqListener?: ReqListener + resListener?: ResListener + reqId?: string +} + +const enabled = isDebugMode() || parseBool(process.env.INTERCEPT_REQUESTS); + +const interceptor = new BatchInterceptor({ + name: 'global-interceptor', + interceptors: [ + new ClientRequestInterceptor(), + new XMLHttpRequestInterceptor(), + ], +}); + +const enableInterceptor = () => { + if (interceptor.readyState !== InterceptorReadyState.APPLIED && interceptor.readyState !== InterceptorReadyState.APPLYING) { + interceptor.apply(); + } +} + +const disableInterceptor = () => { + if (interceptor.readyState == InterceptorReadyState.APPLIED || interceptor.readyState == InterceptorReadyState.APPLYING) { + interceptor.dispose(); + } +} + +const scopedIntercepts = new Map(); + +const getScopedIntercept = (id: string): InterceptData => scopedIntercepts.get(id) ?? { id }; + +type RequestFilterFunc = (req: Request) => boolean; + +const generateRequestFilter = (opts?: InterceptFilterOptions): RequestFilterFunc => { + if(opts === undefined) { + return (req: Request) => true; + } + const filters: RequestFilterFunc[] = []; + if(opts.method !== undefined) { + filters.push((req: Request) => req.method.toLocaleLowerCase() === opts.method.toLocaleLowerCase()); + } + if(opts.url !== undefined) { + if(typeof opts.url === 'string') { + filters.push((req: Request) => req.url.toString().includes(opts.url as string)); + } else { + filters.push((req: Request) => parseRegexSingleOrFail(opts.url as RegExp, req.url.toString()) !== undefined); + } + } + // if(opts.body !== undefined) { + // const bodyFunc = typeof opts.body === 'string' ? (str: string) => str.includes(opts.body as string) : (str: string) => parseRegexSingleOrFail(opts.body as RegExp, str) !== undefined; + // filters.push((req: Request) => { + // const body = req.clone().text(); // async :/ + // }) + // } + + return (req: Request) => { + for(const f of filters) { + if(f(req) === false) { + return false; + } + } + return true; + } +} + +export const interceptRequest = (listenerId?: string, opts?: InterceptFilterOptions): string => { + const lid = listenerId || nanoid(); + + if(!enabled) { + return lid; + } + + enableInterceptor(); + + const data = getScopedIntercept(lid); + + scopedIntercepts.set(lid, { ...data, id: lid }); + + const filterFunc = generateRequestFilter(opts); + + const reqLis: ReqListener = (args) => { + if(filterFunc(args.request)) { + interceptor.off('request', reqLis); + scopedIntercepts.set(lid, { ...getScopedIntercept(lid), req: args.request, reqListener: undefined, reqId: args.requestId }); + } + } + scopedIntercepts.set(lid, { ...getScopedIntercept(lid), reqListener: reqLis }); + interceptor.on('request', reqLis); + + const resLis: ResListener = (args) => { + if (args.requestId === getScopedIntercept(lid).reqId) { + interceptor.off('response', resLis); + scopedIntercepts.set(lid, { ...getScopedIntercept(lid), res: args.response, resListener: undefined }); + } + } + scopedIntercepts.set(lid, { ...getScopedIntercept(lid), resListener: resLis }); + interceptor.on('response', resLis); + return lid; +} + +export const getIntercept = (id: string, deleteOn: 'any' | 'request' | 'response' = 'response'): Intercept | undefined => { + const d = scopedIntercepts.get(id); + if (d !== undefined) { + if(deleteOn === 'any') { + deleteIntercept(id); + } else if(deleteOn === 'request' && d.req !== undefined) { + deleteIntercept(id); + } else if(deleteOn === 'response' && d.res !== undefined) { + deleteIntercept(id); + } + } + return d; +} + +export const deleteIntercept = (id: string): void => { + const d = scopedIntercepts.get(id); + if(d !== undefined) { + if(d.reqListener !== undefined) { + interceptor.off('request', d.reqListener); + } + if(d.resListener !== undefined) { + interceptor.off('response', d.resListener); + } + scopedIntercepts.delete(id); + } +} + diff --git a/src/backend/utils/RequestUtils.ts b/src/backend/utils/RequestUtils.ts index 6a93eb61..b3dd9600 100644 --- a/src/backend/utils/RequestUtils.ts +++ b/src/backend/utils/RequestUtils.ts @@ -1,5 +1,9 @@ import { Files, File } from "formidable"; import VolatileFile from "formidable/VolatileFile.js"; +import {CurlGenerator} from "curl-generator"; +import { CurlBody } from "curl-generator/dist/bodies/body.js"; +import { Logger, LogLevel } from "@foxxmd/logging"; +import { Intercept } from "./InterceptUtils.js"; // typings from Formidable are all nuts. // VolatileFile is missing buffer and also does not extend File even though it should @@ -69,3 +73,69 @@ const isVolatileFile = (val: unknown): val is File => { export const getFileIdentifier = (f: File): string => { return f.originalFilename === null ? f.newFilename : f.originalFilename; } + +export const generateCurl = async (val: Request): Promise => { + try { + const req = val.clone(); + req.headers.delete('host'); + const headers = Object.fromEntries(req.headers); + let body: CurlBody | undefined; + const b = await req.text(); + if (b.length !== 0) { + body = { + type: 'raw', + content: b.toString() + } + } + return CurlGenerator({ + url: req.url.toString(), + method: req.method as "GET" | "get" | "POST" | "post" | "PUT" | "put" | "PATCH" | "patch" | "DELETE" | "delete", + headers, + body + }); + } catch (e) { + throw new Error('Could not generate CURL request', { cause: e }); + } +} + +export const generateCurlSafe = async (val: Request, logger?: Logger): Promise => { + try { + return await generateCurl(val); + } catch (e) { + if (logger !== undefined) { + logger.warn(new Error('Could not generate CURL command', { cause: e })); + } + } + return; +} + +export interface LogCurlOptions { + msg?: string, + prefix?: string, + level?: LogLevel +} + +export const logCurlSafe = async (val: Request, logger: Logger, opts: LogCurlOptions = {}): Promise => { + const curlCmd = await generateCurlSafe(val, logger); + if (curlCmd !== undefined) { + const { msg = 'CURL', level = 'debug', prefix = '' } = opts; + logger[level](`${prefix !== '' ? `${prefix} ` : ''}${msg} +${curlCmd}`); + } +} + +export const logInterceptCurlSafe = async (intercept: Intercept | undefined, logger: Logger, opts: {logOnMiss?: boolean} & LogCurlOptions = {}): Promise => { + const {logOnMiss = false} = opts; + if(intercept === undefined) { + if(logOnMiss) { + logger.debug(`No intercept found!`); + } + return; + } + + if(intercept.req !== undefined) { + await logCurlSafe(intercept.req, logger, {prefix: '(REQ)', ...opts}); + } else if(logOnMiss) { + logger.debug(`No request to log for Intercept ${intercept.id}`); + } +} \ No newline at end of file