diff --git a/ChangeLog.md b/ChangeLog.md index d06a5d2d5..291b2c7a6 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -4,6 +4,20 @@ ## Upcoming Release +General: + +- Applied npm audit fix to updates across multiple dependencies to address security vulnerabilities and maintenance updates. +- Bumped the default Blob, Queue, and Table service API version to `2026-06-06`. +- Added support for service API versions `2026-04-06` and `2026-02-06` for Blob, Queue, and Table endpoints. + +Blob: + +- Fixed issue #2672 startup failures with legacy persisted data by adding backward-compatible restore for persisted `contentMD5` formats. + +Queue: + +- Fixed issue #2672 startup race condition by avoiding GC-triggered `close()` calls while server status is `Starting`. + ## 2026.06 Version 3.36.0 General: @@ -22,7 +36,6 @@ General: - Fix Windows SEA executable build producing a corrupted/bad `.exe` by stripping the Authenticode signature from the copied Node.js binary before injecting the SEA blob via `postject`. - Replace the hardcoded user delegation signing key literal with a deterministic internal signing seed to avoid VS Code extension publish secret-scan blocks while preserving stable user delegation SAS behavior. - Blob: - Remove the default value for the `BlobSequenceNumber` parameter in the Swagger definition. diff --git a/package-lock.json b/package-lock.json index 27b9ce2ef..7f3f0cbad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,14 +12,14 @@ "@azure/ms-rest-js": "^2.7.0", "applicationinsights": "^2.9.6", "args": "^5.0.1", - "axios": "^1.17.0", + "axios": "^1.18.1", "etag": "^1.8.1", "express": "^4.16.4", "fs-extra": "^11.1.1", "glob-to-regexp": "^0.4.1", "jsonwebtoken": "^9.0.0", "lokijs": "^1.5.6", - "morgan": "^1.9.1", + "morgan": "^1.11.0", "multistream": "^2.1.1", "mysql2": "^3.10.1", "rimraf": "^3.0.2", @@ -69,7 +69,7 @@ "autorest": "^3.6.0", "cross-env": "^7.0.3", "cross-var": "^1.1.0", - "esbuild": "^0.25.5", + "esbuild": "^0.28.1", "eslint": "^8.35.0", "find-process": "^1.4.4", "husky": "^8.0.1", @@ -334,16 +334,6 @@ "version": "1.14.1", "license": "0BSD" }, - "node_modules/@azure/ms-rest-js/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@azure/ms-rest-js/node_modules/xml2js": { "version": "0.5.0", "license": "MIT", @@ -397,23 +387,27 @@ } }, "node_modules/@azure/opentelemetry-instrumentation-azure-sdk": { - "version": "1.0.0-beta.9", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0.tgz", + "integrity": "sha512-Y8rZOIMXQY/GwNRL+uLVuwIn9aEa/KnnggyYUmFxC1MigmRJCNH5NxMmxKSpddXF9SW6Z1ijRd6Pptd2A5OhGw==", "license": "MIT", "dependencies": { "@azure/core-tracing": "^1.2.0", "@azure/logger": "^1.0.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "tslib": "^2.7.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/opentelemetry-instrumentation-azure-sdk/node_modules/@opentelemetry/core": { - "version": "2.0.1", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -527,9 +521,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -544,9 +538,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -561,9 +555,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -578,9 +572,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -595,9 +589,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -612,9 +606,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -629,9 +623,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -646,9 +640,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -663,9 +657,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -680,9 +674,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -697,9 +691,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -714,9 +708,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -731,9 +725,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -748,9 +742,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -765,9 +759,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -782,9 +776,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -799,7 +793,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -814,9 +810,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -831,9 +827,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -848,9 +844,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -865,9 +861,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -882,9 +878,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -899,9 +895,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -916,9 +912,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -933,9 +929,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -950,9 +946,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -1160,7 +1156,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.200.0", + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.211.0.tgz", + "integrity": "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -1190,14 +1188,14 @@ } }, "node_modules/@opentelemetry/instrumentation": { - "version": "0.200.0", + "version": "0.211.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.211.0.tgz", + "integrity": "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.200.0", - "@types/shimmer": "^1.2.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "shimmer": "^1.2.1" + "@opentelemetry/api-logs": "0.211.0", + "import-in-the-middle": "^2.0.0", + "require-in-the-middle": "^8.0.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1227,6 +1225,23 @@ "node": ">=14" } }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz", + "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-trace-base": { "version": "1.30.1", "license": "Apache-2.0", @@ -1250,11 +1265,13 @@ } }, "node_modules/@opentelemetry/sdk-trace-web": { - "version": "2.0.1", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.9.0.tgz", + "integrity": "sha512-LS4XlzOK3e6YYdt84m15AmRR04121rKipmifi4XFZToH1h75f7F2bzHLbB1NMgIEYJ0jSKYm4VsK5mws/5kfTQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.1", - "@opentelemetry/sdk-trace-base": "2.0.1" + "@opentelemetry/core": "2.9.0", + "@opentelemetry/sdk-trace-base": "2.9.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1264,7 +1281,9 @@ } }, "node_modules/@opentelemetry/sdk-trace-web/node_modules/@opentelemetry/core": { - "version": "2.0.1", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -1277,10 +1296,12 @@ } }, "node_modules/@opentelemetry/sdk-trace-web/node_modules/@opentelemetry/resources": { - "version": "2.0.1", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.1", + "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1291,11 +1312,45 @@ } }, "node_modules/@opentelemetry/sdk-trace-web/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.0.1", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.9.0.tgz", + "integrity": "sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.1", - "@opentelemetry/resources": "2.0.1", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1540,10 +1595,6 @@ "@types/send": "*" } }, - "node_modules/@types/shimmer": { - "version": "1.2.0", - "license": "MIT" - }, "node_modules/@types/stoppable": { "version": "1.1.3", "dev": true, @@ -1808,6 +1859,8 @@ }, "node_modules/acorn-import-attributes": { "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "license": "MIT", "peerDependencies": { "acorn": "^8" @@ -2057,9 +2110,9 @@ } }, "node_modules/axios": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", - "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", @@ -2081,16 +2134,16 @@ } }, "node_modules/axios/node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -3297,7 +3350,9 @@ "license": "ISC" }, "node_modules/cjs-module-lexer": { - "version": "1.4.3", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, "node_modules/cli-cursor": { @@ -4110,9 +4165,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4123,32 +4178,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -4844,13 +4899,15 @@ } }, "node_modules/form-data": { - "version": "2.5.5", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz", + "integrity": "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", + "hasown": "^2.0.4", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" }, @@ -5166,7 +5223,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5363,13 +5422,15 @@ } }, "node_modules/import-in-the-middle": { - "version": "1.14.2", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", + "integrity": "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^1.2.2", - "module-details-from-path": "^1.0.3" + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" } }, "node_modules/imurmurhash": { @@ -5598,8 +5659,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6504,6 +6577,8 @@ }, "node_modules/module-details-from-path": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", "license": "MIT" }, "node_modules/moment": { @@ -6524,17 +6599,23 @@ } }, "node_modules/morgan": { - "version": "1.10.1", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", + "integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==", "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", - "on-finished": "~2.3.0", + "on-finished": "~2.4.1", "on-headers": "~1.1.0" }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/morgan/node_modules/debug": { @@ -6548,16 +6629,6 @@ "version": "2.0.0", "license": "MIT" }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/mri": { "version": "1.1.4", "license": "MIT", @@ -6973,10 +7044,6 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "license": "MIT" - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -7420,33 +7487,16 @@ } }, "node_modules/require-in-the-middle": { - "version": "7.5.2", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", "license": "MIT", "dependencies": { "debug": "^4.3.5", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.8" + "module-details-from-path": "^1.0.3" }, "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, "node_modules/resolve-from": { @@ -7457,19 +7507,6 @@ "node": ">=4" } }, - "node_modules/resolve/node_modules/is-core-module": { - "version": "2.16.1", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/restore-cursor": { "version": "5.1.0", "dev": true, @@ -7735,13 +7772,6 @@ "node": ">= 10.0.0" } }, - "node_modules/sequelize/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/serialize-javascript": { "version": "6.0.2", "dev": true, @@ -8077,16 +8107,6 @@ "node": ">=4" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -8518,6 +8538,16 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "dev": true, diff --git a/package.json b/package.json index 29497be40..5b765830e 100644 --- a/package.json +++ b/package.json @@ -23,14 +23,14 @@ "@azure/ms-rest-js": "^2.7.0", "applicationinsights": "^2.9.6", "args": "^5.0.1", - "axios": "^1.17.0", + "axios": "^1.18.1", "etag": "^1.8.1", "express": "^4.16.4", "fs-extra": "^11.1.1", "glob-to-regexp": "^0.4.1", "jsonwebtoken": "^9.0.0", "lokijs": "^1.5.6", - "morgan": "^1.9.1", + "morgan": "^1.11.0", "multistream": "^2.1.1", "mysql2": "^3.10.1", "rimraf": "^3.0.2", @@ -74,7 +74,7 @@ "autorest": "^3.6.0", "cross-env": "^7.0.3", "cross-var": "^1.1.0", - "esbuild": "^0.25.5", + "esbuild": "^0.28.1", "eslint": "^8.35.0", "find-process": "^1.4.4", "husky": "^8.0.1", @@ -354,4 +354,4 @@ "url": "https://github.com/azure/azurite/issues" }, "homepage": "https://github.com/azure/azurite#readme" -} \ No newline at end of file +} diff --git a/scripts/azurite-cli.sh b/scripts/azurite-cli.sh old mode 100644 new mode 100755 diff --git a/src/azurite.ts b/src/azurite.ts index e856c3e25..297b86083 100644 --- a/src/azurite.ts +++ b/src/azurite.ts @@ -25,6 +25,7 @@ import TableServer from "./table/TableServer"; import { DEFAULT_TABLE_LOKI_DB_PATH } from "./table/utils/constants"; import { setExtentMemoryLimit } from "./common/ConfigurationBase"; import { AzuriteTelemetryClient } from "./common/Telemetry"; +import { ServerStatus } from "./common/ServerBase"; // tslint:disable:no-console @@ -42,30 +43,50 @@ function shutdown( AzuriteTelemetryClient.TraceStopEvent(); - console.log(blobBeforeCloseMessage); - blobServer.close().then(() => { - console.log(blobAfterCloseMessage); - }); - - console.log(queueBeforeCloseMessage); - queueServer.close().then(() => { - console.log(queueAfterCloseMessage); - }); - - console.log(tableBeforeCloseMessage); - tableServer.close().then(() => { - console.log(tableAfterCloseMessage); - }); + const closeWithDiagnostics = ( + beforeMessage: string, + afterMessage: string, + server: { getStatus(): ServerStatus; close(): Promise } + ) => { + const statusBeforeClose = server.getStatus(); + console.log(beforeMessage); + server + .close() + .then(() => { + console.log(afterMessage); + }) + .catch((err) => { + const errorMsg = err instanceof Error ? err.message : String(err); + console.error( + `Shutdown close failed in status ${statusBeforeClose}: ${errorMsg}` + ); + }); + }; + + closeWithDiagnostics( + blobBeforeCloseMessage, + blobAfterCloseMessage, + blobServer + ); + closeWithDiagnostics( + queueBeforeCloseMessage, + queueAfterCloseMessage, + queueServer + ); + closeWithDiagnostics( + tableBeforeCloseMessage, + tableAfterCloseMessage, + tableServer + ); } /** * Entry for Azurite services. */ async function main() { - // Initialize and validate environment values from command line parameters const env = new Environment(); - + const location = await env.location(); await ensureDir(location); await access(location); @@ -105,7 +126,7 @@ async function main() { env.pwd(), env.oauth(), env.disableProductStyleUrl(), - env.inMemoryPersistence(), + env.inMemoryPersistence() ); const tableConfig = new TableConfiguration( @@ -124,7 +145,7 @@ async function main() { env.pwd(), env.oauth(), env.disableProductStyleUrl(), - env.inMemoryPersistence(), + env.inMemoryPersistence() ); // We use logger singleton as global debugger logger to track detailed outputs cross layers @@ -167,7 +188,7 @@ async function main() { console.log( `Azurite Table service is successfully listening at ${tableServer.getHttpServerAddress()}` ); - + AzuriteTelemetryClient.init(location, !env.disableTelemetry(), env); await AzuriteTelemetryClient.TraceStartEvent(); diff --git a/src/blob/BlobServer.ts b/src/blob/BlobServer.ts index 2f0e10dc3..fcf5952eb 100644 --- a/src/blob/BlobServer.ts +++ b/src/blob/BlobServer.ts @@ -9,10 +9,13 @@ import IGCManager from "../common/IGCManager"; import IRequestListenerFactory from "../common/IRequestListenerFactory"; import logger from "../common/Logger"; import FSExtentStore from "../common/persistence/FSExtentStore"; -import MemoryExtentStore, { SharedChunkStore } from "../common/persistence/MemoryExtentStore"; +import MemoryExtentStore, { + SharedChunkStore +} from "../common/persistence/MemoryExtentStore"; import IExtentMetadataStore from "../common/persistence/IExtentMetadataStore"; import IExtentStore from "../common/persistence/IExtentStore"; import LokiExtentMetadataStore from "../common/persistence/LokiExtentMetadataStore"; +import { handleGCCriticalErrorClose } from "../common/GCCriticalErrorCloseHelper"; import ServerBase, { ServerStatus } from "../common/ServerBase"; import BlobConfiguration from "./BlobConfiguration"; import BlobRequestListenerFactory from "./BlobRequestListenerFactory"; @@ -79,39 +82,43 @@ export default class BlobServer extends ServerBase implements ICleaner { configuration.isMemoryPersistence ); - const extentMetadataStore: IExtentMetadataStore = new LokiExtentMetadataStore( - configuration.extentDBPath, - configuration.isMemoryPersistence - ); - - const extentStore: IExtentStore = configuration.isMemoryPersistence ? new MemoryExtentStore( - "blob", - configuration.memoryStore ?? SharedChunkStore, - extentMetadataStore, - logger, - (sc, er, em, ri) => new StorageError(sc, er, em, ri) - ) : new FSExtentStore( - extentMetadataStore, - configuration.persistencePathArray, - logger - ); + const extentMetadataStore: IExtentMetadataStore = + new LokiExtentMetadataStore( + configuration.extentDBPath, + configuration.isMemoryPersistence + ); + + const extentStore: IExtentStore = configuration.isMemoryPersistence + ? new MemoryExtentStore( + "blob", + configuration.memoryStore ?? SharedChunkStore, + extentMetadataStore, + logger, + (sc, er, em, ri) => new StorageError(sc, er, em, ri) + ) + : new FSExtentStore( + extentMetadataStore, + configuration.persistencePathArray, + logger + ); const accountDataStore: IAccountDataStore = new AccountDataStore(logger); // We can also change the HTTP framework here by // creating a new XXXListenerFactory implementing IRequestListenerFactory interface // and replace the default Express based request listener - const requestListenerFactory: IRequestListenerFactory = new BlobRequestListenerFactory( - metadataStore, - extentStore, - accountDataStore, - configuration.enableAccessLog, // Access log includes every handled HTTP request - configuration.accessLogWriteStream, - configuration.loose, - configuration.skipApiVersionCheck, - configuration.getOAuthLevel(), - configuration.disableProductStyleUrl - ); + const requestListenerFactory: IRequestListenerFactory = + new BlobRequestListenerFactory( + metadataStore, + extentStore, + accountDataStore, + configuration.enableAccessLog, // Access log includes every handled HTTP request + configuration.accessLogWriteStream, + configuration.loose, + configuration.skipApiVersionCheck, + configuration.getOAuthLevel(), + configuration.disableProductStyleUrl + ); super(host, port, httpServer, requestListenerFactory, configuration); @@ -121,14 +128,26 @@ export default class BlobServer extends ServerBase implements ICleaner { metadataStore, extentMetadataStore, extentStore, - () => { + (err: Error) => { // tslint:disable-next-line:no-console console.log(BEFORE_CLOSE_MESSAGE_GC_ERROR); logger.info(BEFORE_CLOSE_MESSAGE_GC_ERROR); - this.close().then(() => { - // tslint:disable-next-line:no-console - console.log(AFTER_CLOSE_MESSAGE); - logger.info(AFTER_CLOSE_MESSAGE); + logger.error( + `Blob GC Manager critical error: ${err.message}\nStack: ${err.stack}` + ); + logger.warn( + "This may be due to: " + + "1) Persisted database format changes, " + + "2) File system permission issues, " + + "3) Corrupted metadata files. " + + "See error details above." + ); + + handleGCCriticalErrorClose({ + serviceName: "Blob", + getStatus: () => this.status, + close: () => this.close(), + logger }); }, logger diff --git a/src/blob/SqlBlobServer.ts b/src/blob/SqlBlobServer.ts index c0e07e6d3..020a9b6a5 100644 --- a/src/blob/SqlBlobServer.ts +++ b/src/blob/SqlBlobServer.ts @@ -6,6 +6,7 @@ import { CertOptions } from "../common/ConfigurationBase"; import IAccountDataStore from "../common/IAccountDataStore"; import IGCManager from "../common/IGCManager"; import IRequestListenerFactory from "../common/IRequestListenerFactory"; +import { handleGCCriticalErrorClose } from "../common/GCCriticalErrorCloseHelper"; import logger from "../common/Logger"; import FSExtentStore from "../common/persistence/FSExtentStore"; import IExtentMetadataStore from "../common/persistence/IExtentMetadataStore"; @@ -69,12 +70,13 @@ export default class SqlBlobServer extends ServerBase { configuration.sequelizeOptions ); - const extentMetadataStore: IExtentMetadataStore = new SqlExtentMetadataStore( - // Currently, extent metadata and blob metadata share same database - // But they can use separate databases per future requirements - configuration.sqlURL, - configuration.sequelizeOptions - ); + const extentMetadataStore: IExtentMetadataStore = + new SqlExtentMetadataStore( + // Currently, extent metadata and blob metadata share same database + // But they can use separate databases per future requirements + configuration.sqlURL, + configuration.sequelizeOptions + ); const extentStore: IExtentStore = new FSExtentStore( extentMetadataStore, @@ -87,17 +89,18 @@ export default class SqlBlobServer extends ServerBase { // We can also change the HTTP framework here by // creating a new XXXListenerFactory implementing IRequestListenerFactory interface // and replace the default Express based request listener - const requestListenerFactory: IRequestListenerFactory = new BlobRequestListenerFactory( - metadataStore, - extentStore, - accountDataStore, - configuration.enableAccessLog, // Access log includes every handled HTTP request - configuration.accessLogWriteStream, - configuration.loose, - configuration.skipApiVersionCheck, - configuration.getOAuthLevel(), - configuration.disableProductStyleUrl - ); + const requestListenerFactory: IRequestListenerFactory = + new BlobRequestListenerFactory( + metadataStore, + extentStore, + accountDataStore, + configuration.enableAccessLog, // Access log includes every handled HTTP request + configuration.accessLogWriteStream, + configuration.loose, + configuration.skipApiVersionCheck, + configuration.getOAuthLevel(), + configuration.disableProductStyleUrl + ); super(host, port, httpServer, requestListenerFactory, configuration); @@ -107,16 +110,16 @@ export default class SqlBlobServer extends ServerBase { metadataStore, extentMetadataStore, extentStore, - error => { + (error) => { // tslint:disable-next-line:no-console console.log(BEFORE_CLOSE_MESSAGE_GC_ERROR, error); logger.info(BEFORE_CLOSE_MESSAGE_GC_ERROR + JSON.stringify(error)); - // TODO: Bring this back when GC based on SQL implemented - this.close().then(() => { - // tslint:disable-next-line:no-console - console.log(AFTER_CLOSE_MESSAGE); - logger.info(AFTER_CLOSE_MESSAGE); + handleGCCriticalErrorClose({ + serviceName: "Blob", + getStatus: () => this.status, + close: () => this.close(), + logger }); }, logger diff --git a/src/blob/persistence/LokiBlobMetadataStore.ts b/src/blob/persistence/LokiBlobMetadataStore.ts index 7efc4d7c6..d0e3f62d6 100644 --- a/src/blob/persistence/LokiBlobMetadataStore.ts +++ b/src/blob/persistence/LokiBlobMetadataStore.ts @@ -65,7 +65,11 @@ import IBlobMetadataStore, { import PageWithDelimiter from "./PageWithDelimiter"; import FilterBlobPage from "./FilterBlobPage"; import { generateQueryBlobWithTagsWhereFunction } from "./QueryInterpreter/QueryInterpreter"; -import { getBlobTagsCount, getTagsFromString, toBlobTags } from "../utils/utils"; +import { + getBlobTagsCount, + getTagsFromString, + toBlobTags +} from "../utils/utils"; /** * This is a metadata source implementation for blob based on loki DB. @@ -95,7 +99,8 @@ import { getBlobTagsCount, getTagsFromString, toBlobTags } from "../utils/utils" * @class LokiBlobMetadataStore */ export default class LokiBlobMetadataStore - implements IBlobMetadataStore, IGCExtentProvider { + implements IBlobMetadataStore, IGCExtentProvider +{ private readonly db: Loki; private initialized: boolean = false; @@ -108,14 +113,22 @@ export default class LokiBlobMetadataStore private readonly pageBlobRangesManager = new PageBlobRangesManager(); - public constructor(public readonly lokiDBPath: string, inMemory: boolean) { - this.db = new Loki(lokiDBPath, inMemory ? { - persistenceMethod: "memory" - } : { - persistenceMethod: "fs", - autosave: true, - autosaveInterval: 5000 - }); + public constructor( + public readonly lokiDBPath: string, + inMemory: boolean + ) { + this.db = new Loki( + lokiDBPath, + inMemory + ? { + persistenceMethod: "memory" + } + : { + persistenceMethod: "fs", + autosave: true, + autosaveInterval: 5000 + } + ); } public isInitialized(): boolean { @@ -329,9 +342,9 @@ export default class LokiBlobMetadataStore prefix === "" ? { name: { $gt: marker }, accountName: account } : { - name: { $regex: `^${this.escapeRegex(prefix)}`, $gt: marker }, - accountName: account - }; + name: { $regex: `^${this.escapeRegex(prefix)}`, $gt: marker }, + accountName: account + }; // Workaround for loki which will ignore $gt when providing $regex const query2 = { name: { $gt: marker } }; @@ -753,10 +766,10 @@ export default class LokiBlobMetadataStore const leaseTimeSeconds: number = doc.properties.leaseState === Models.LeaseStateType.Breaking && - doc.leaseBreakTime + doc.leaseBreakTime ? Math.round( - (doc.leaseBreakTime.getTime() - context.startTime!.getTime()) / 1000 - ) + (doc.leaseBreakTime.getTime() - context.startTime!.getTime()) / 1000 + ) : 0; coll.update(doc); @@ -830,7 +843,7 @@ export default class LokiBlobMetadataStore container?: string, where?: string, maxResults: number = DEFAULT_LIST_BLOBS_MAX_RESULTS, - marker: string = "", + marker: string = "" ): Promise<[FilterBlobModel[], string | undefined]> { const query: any = {}; if (account !== undefined) { @@ -838,14 +851,13 @@ export default class LokiBlobMetadataStore } if (container !== undefined) { query.containerName = container; - await this.checkContainerExist( - context, - account, - container - ); + await this.checkContainerExist(context, account, container); } - const filterFunction = generateQueryBlobWithTagsWhereFunction(context, where!); + const filterFunction = generateQueryBlobWithTagsWhereFunction( + context, + where! + ); const coll = this.db.getCollection(this.BLOBS_COLLECTION); const page = new FilterBlobPage(maxResults); @@ -857,7 +869,7 @@ export default class LokiBlobMetadataStore return obj.name > marker!; }) .where((obj) => { - return obj.snapshot === undefined || obj.snapshot === ''; + return obj.snapshot === undefined || obj.snapshot === ""; }) .sort((obj1, obj2) => { if (obj1.name === obj2.name) return 0; @@ -868,22 +880,24 @@ export default class LokiBlobMetadataStore .limit(maxResults) .data(); - return doc.map((item) => { - let blobItem: FilterBlobModel; - blobItem = { - name: item.name, - containerName: item.containerName, - tags: item.blobTags - }; - return blobItem; - }).filter((blobItem) => { - const tagsMeetConditions = filterFunction(blobItem); - if (tagsMeetConditions.length !== 0) { - blobItem.tags = { blobTagSet: toBlobTags(tagsMeetConditions) }; - return true; - } - return false; - }); + return doc + .map((item) => { + let blobItem: FilterBlobModel; + blobItem = { + name: item.name, + containerName: item.containerName, + tags: item.blobTags + }; + return blobItem; + }) + .filter((blobItem) => { + const tagsMeetConditions = filterFunction(blobItem); + if (tagsMeetConditions.length !== 0) { + blobItem.tags = { blobTagSet: toBlobTags(tagsMeetConditions) }; + return true; + } + return false; + }); }; const nameItem = (item: FilterBlobModel) => { @@ -892,10 +906,7 @@ export default class LokiBlobMetadataStore const [blobItems, nextMarker] = await page.fill(readPage, nameItem); - return [ - blobItems, - nextMarker - ]; + return [blobItems, nextMarker]; } public async listBlobs( @@ -925,7 +936,11 @@ export default class LokiBlobMetadataStore } const coll = this.db.getCollection(this.BLOBS_COLLECTION); - const page = new PageWithDelimiter(maxResults, delimiter, prefix); + const page = new PageWithDelimiter( + maxResults, + delimiter, + prefix + ); const readPage = async (offset: number): Promise => { return await coll .chain() @@ -953,7 +968,10 @@ export default class LokiBlobMetadataStore return item.name; }; - const [blobItems, blobPrefixes, nextMarker] = await page.fill(readPage, nameItem); + const [blobItems, blobPrefixes, nextMarker] = await page.fill( + readPage, + nameItem + ); return [ blobItems.map((doc) => { @@ -1766,10 +1784,10 @@ export default class LokiBlobMetadataStore const leaseTimeSeconds: number = doc.properties.leaseState === Models.LeaseStateType.Breaking && - doc.leaseBreakTime + doc.leaseBreakTime ? Math.round( - (doc.leaseBreakTime.getTime() - context.startTime!.getTime()) / 1000 - ) + (doc.leaseBreakTime.getTime() - context.startTime!.getTime()) / 1000 + ) : 0; coll.update(doc); @@ -1934,8 +1952,10 @@ export default class LokiBlobMetadataStore throw StorageErrorFactory.getBlobNotFound(context.contextId!); } - if (sourceBlob.properties.accessTier === Models.AccessTier.Archive - && (tier === undefined || source.account !== destination.account)) { + if ( + sourceBlob.properties.accessTier === Models.AccessTier.Archive && + (tier === undefined || source.account !== destination.account) + ) { throw StorageErrorFactory.getBlobArchived(context.contextId!); } @@ -1981,7 +2001,9 @@ export default class LokiBlobMetadataStore remainingRetentionDays: undefined, archiveStatus: undefined, accessTierChangeTime: undefined, - ...(sourceBlob.properties.blobType === Models.BlobType.AppendBlob && { isSealed: options.sealBlob }), + ...(sourceBlob.properties.blobType === Models.BlobType.AppendBlob && { + isSealed: options.sealBlob + }) }, metadata: metadata === undefined || Object.keys(metadata).length === 0 @@ -2000,7 +2022,10 @@ export default class LokiBlobMetadataStore destBlob !== undefined ? destBlob.leaseBreakTime : undefined, committedBlocksInOrder: sourceBlob.committedBlocksInOrder, persistency: sourceBlob.persistency, - blobTags: options.blobTagsString === undefined ? undefined : getTagsFromString(options.blobTagsString, context.contextId!) + blobTags: + options.blobTagsString === undefined + ? undefined + : getTagsFromString(options.blobTagsString, context.contextId!) }; if ( @@ -2076,7 +2101,7 @@ export default class LokiBlobMetadataStore ifUnmodifiedSince: options.sourceModifiedAccessConditions.sourceIfUnmodifiedSince, ifMatch: options.sourceModifiedAccessConditions.sourceIfMatch, - ifNoneMatch: options.sourceModifiedAccessConditions.sourceIfNoneMatch, + ifNoneMatch: options.sourceModifiedAccessConditions.sourceIfNoneMatch // Storage service will ignore x-ms-source-if-tags header for copyFromUrl }, sourceBlob @@ -2188,9 +2213,12 @@ export default class LokiBlobMetadataStore destBlob !== undefined ? destBlob.leaseBreakTime : undefined, committedBlocksInOrder: sourceBlob.committedBlocksInOrder, persistency: sourceBlob.persistency, - blobTags: options.copySourceTags === Models.BlobCopySourceTags.COPY ? - sourceBlob.blobTags - : options.blobTagsString === undefined ? undefined : getTagsFromString(options.blobTagsString, context.contextId!) + blobTags: + options.copySourceTags === Models.BlobCopySourceTags.COPY + ? sourceBlob.blobTags + : options.blobTagsString === undefined + ? undefined + : getTagsFromString(options.blobTagsString, context.contextId!) }; if ( @@ -2281,7 +2309,9 @@ export default class LokiBlobMetadataStore // Archive -> Coo/Hot will return 202 if ( doc.properties.accessTier === Models.AccessTier.Archive && - (tier === Models.AccessTier.Cool || tier === Models.AccessTier.Hot || tier === Models.AccessTier.Cold) + (tier === Models.AccessTier.Cool || + tier === Models.AccessTier.Hot || + tier === Models.AccessTier.Cold) ) { responseCode = 202; } @@ -2366,7 +2396,7 @@ export default class LokiBlobMetadataStore const existBlockDoc = coll.findOne({ blobName: block.blobName, accountName: block.accountName, - containerName: block.containerName, + containerName: block.containerName }); if (existBlockDoc) { if ( @@ -2418,10 +2448,7 @@ export default class LokiBlobMetadataStore } const lease = new BlobLeaseAdapter(doc); - new BlobWriteLeaseValidator(leaseAccessConditions).validate( - lease, - context - ); + new BlobWriteLeaseValidator(leaseAccessConditions).validate(lease, context); if (doc.properties.isSealed) { throw StorageErrorFactory.getBlobSealed(context.contextId); @@ -3109,7 +3136,7 @@ export default class LokiBlobMetadataStore * @memberof LokiBlobMetadataStore */ private restoreUint8Array(obj: any): Uint8Array | undefined { - if (typeof obj !== "object") { + if (obj === null || typeof obj !== "object") { return undefined; } @@ -3117,6 +3144,17 @@ export default class LokiBlobMetadataStore return new Uint8Array(obj); } + // Backward compatibility: persisted Buffer JSON shape from previous versions + // e.g. { type: "Buffer", data: [1,2,3] } + if (obj.type === "Buffer" && Array.isArray(obj.data)) { + return new Uint8Array(obj.data); + } + + // Backward compatibility: plain array-like persisted by serializers + if (Array.isArray(obj)) { + return new Uint8Array(obj); + } + const length = Object.keys(obj).length; const arr = Buffer.allocUnsafe(length); @@ -3517,22 +3555,22 @@ export default class LokiBlobMetadataStore } /** - * Seal blob. - * - * @param {Context} context - * @param {string} account - * @param {string} container - * @param {string} blob - * @returns {Promise} - * @memberof IBlobMetadataStore - */ + * Seal blob. + * + * @param {Context} context + * @param {string} account + * @param {string} container + * @param {string} blob + * @returns {Promise} + * @memberof IBlobMetadataStore + */ public async sealBlob( context: Context, account: string, container: string, blob: string, snapshot: string | undefined, - options: Models.AppendBlobSealOptionalParams, + options: Models.AppendBlobSealOptionalParams ): Promise { const coll = this.db.getCollection(this.BLOBS_COLLECTION); const doc = await this.getBlob(context, account, container, blob); @@ -3548,7 +3586,10 @@ export default class LokiBlobMetadataStore } const lease = new BlobLeaseAdapter(doc); - new BlobWriteLeaseValidator(options.leaseAccessConditions).validate(lease, context); + new BlobWriteLeaseValidator(options.leaseAccessConditions).validate( + lease, + context + ); new BlobWriteLeaseSyncer(doc).sync(lease); doc.properties.isSealed = true; diff --git a/src/blob/utils/constants.ts b/src/blob/utils/constants.ts index 40319eea4..5a93a7dcd 100644 --- a/src/blob/utils/constants.ts +++ b/src/blob/utils/constants.ts @@ -2,7 +2,7 @@ import { StoreDestinationArray } from "../../common/persistence/IExtentStore"; import * as Models from "../generated/artifacts/models"; export const VERSION = "3.36.0"; -export const BLOB_API_VERSION = "2025-11-05"; +export const BLOB_API_VERSION = "2026-06-06"; export const DEFAULT_BLOB_SERVER_HOST_NAME = "127.0.0.1"; // Change to 0.0.0.0 when needs external access export const DEFAULT_LIST_BLOBS_MAX_RESULTS = 5000; export const DEFAULT_LIST_CONTAINERS_MAX_RESULTS = 5000; @@ -98,6 +98,9 @@ export const DEFAULT_BLOB_PERSISTENCE_ARRAY: StoreDestinationArray = [ ]; export const ValidAPIVersions = [ + "2026-06-06", + "2026-04-06", + "2026-02-06", "2025-11-05", "2025-07-05", "2025-05-05", diff --git a/src/common/GCCriticalErrorCloseHelper.ts b/src/common/GCCriticalErrorCloseHelper.ts new file mode 100644 index 000000000..6b7112d32 --- /dev/null +++ b/src/common/GCCriticalErrorCloseHelper.ts @@ -0,0 +1,70 @@ +import ILogger from "./ILogger"; +import { ServerStatus } from "./ServerBase"; + +export interface GCCriticalErrorCloseHelperOptions { + readonly serviceName: string; + readonly getStatus: () => ServerStatus; + readonly close: () => Promise; + readonly logger: ILogger; + readonly waitIntervalMs?: number; + readonly maxAttempts?: number; +} + +function delay(ms: number): Promise { + return new Promise((resolve) => { + const timer = setTimeout(resolve, ms) as unknown as { + unref?: () => void; + }; + if (typeof timer.unref === "function") { + timer.unref(); + } + }); +} + +/** + * Handles the startup/close race when a critical GC error occurs while a server is still Starting. + */ +export async function handleGCCriticalErrorClose( + options: GCCriticalErrorCloseHelperOptions +): Promise { + const waitIntervalMs = options.waitIntervalMs ?? 100; + const maxAttempts = options.maxAttempts ?? 50; + + try { + let attempts = 0; + let startupTimeoutLogged = false; + while (options.getStatus() === ServerStatus.Starting) { + if (!startupTimeoutLogged && attempts >= maxAttempts) { + startupTimeoutLogged = true; + options.logger.error( + `GC error occurred during server startup. Server did not transition to Running state ` + + `within ~${waitIntervalMs * maxAttempts}ms. Current status: ${ServerStatus.Starting}. ` + + `Will keep waiting and close if server reaches Running later.` + ); + } + + await delay(waitIntervalMs); + attempts++; + } + + const status = options.getStatus(); + if (status === ServerStatus.Running) { + options.logger.info( + `Shutting down ${options.serviceName} server due to GC critical error` + ); + await options.close(); + return; + } + + options.logger.warn( + `${options.serviceName} server status is ${status} (expected Running). ` + + `Server state may be inconsistent. Check logs for root cause.` + ); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + options.logger.error( + `${options.serviceName} server error during GC error handling: ${errorMsg}. ` + + `Server status: ${options.getStatus()}` + ); + } +} diff --git a/src/queue/QueueServer.ts b/src/queue/QueueServer.ts index cc61bd200..802007082 100644 --- a/src/queue/QueueServer.ts +++ b/src/queue/QueueServer.ts @@ -8,10 +8,13 @@ import IGCManager from "../common/IGCManager"; import IRequestListenerFactory from "../common/IRequestListenerFactory"; import logger from "../common/Logger"; import FSExtentStore from "../common/persistence/FSExtentStore"; -import MemoryExtentStore, { SharedChunkStore } from "../common/persistence/MemoryExtentStore"; +import MemoryExtentStore, { + SharedChunkStore +} from "../common/persistence/MemoryExtentStore"; import IExtentMetadataStore from "../common/persistence/IExtentMetadataStore"; import IExtentStore from "../common/persistence/IExtentStore"; import LokiExtentMetadataStore from "../common/persistence/LokiExtentMetadataStore"; +import { handleGCCriticalErrorClose } from "../common/GCCriticalErrorCloseHelper"; import ServerBase, { ServerStatus } from "../common/ServerBase"; import QueueGCManager from "./gc/QueueGCManager"; import IQueueMetadataStore from "./persistence/IQueueMetadataStore"; @@ -71,7 +74,7 @@ export default class QueueServer extends ServerBase { } if (configuration.keepAliveTimeout > 0) { - httpServer.keepAliveTimeout = configuration.keepAliveTimeout * 1000 + httpServer.keepAliveTimeout = configuration.keepAliveTimeout * 1000; } // We can change the persistency layer implementation by @@ -87,33 +90,36 @@ export default class QueueServer extends ServerBase { configuration.isMemoryPersistence ); - const extentStore: IExtentStore = configuration.isMemoryPersistence ? new MemoryExtentStore( - "queue", - configuration.memoryStore ?? SharedChunkStore, - extentMetadataStore, - logger, - (sc, er, em, ri) => new StorageError(sc, er, em, ri) - ) : new FSExtentStore( - extentMetadataStore, - configuration.persistencePathArray, - logger - ); + const extentStore: IExtentStore = configuration.isMemoryPersistence + ? new MemoryExtentStore( + "queue", + configuration.memoryStore ?? SharedChunkStore, + extentMetadataStore, + logger, + (sc, er, em, ri) => new StorageError(sc, er, em, ri) + ) + : new FSExtentStore( + extentMetadataStore, + configuration.persistencePathArray, + logger + ); const accountDataStore: IAccountDataStore = new AccountDataStore(logger); // We can also change the HTTP framework here by // creating a new XXXListenerFactory implementing IRequestListenerFactory interface // and replace the default Express based request listener - const requestListenerFactory: IRequestListenerFactory = new QueueRequestListenerFactory( - metadataStore, - extentStore, - accountDataStore, - configuration.enableAccessLog, // Access log includes every handled HTTP request - configuration.accessLogWriteStream, - configuration.skipApiVersionCheck, - configuration.getOAuthLevel(), - configuration.disableProductStyleUrl - ); + const requestListenerFactory: IRequestListenerFactory = + new QueueRequestListenerFactory( + metadataStore, + extentStore, + accountDataStore, + configuration.enableAccessLog, // Access log includes every handled HTTP request + configuration.accessLogWriteStream, + configuration.skipApiVersionCheck, + configuration.getOAuthLevel(), + configuration.disableProductStyleUrl + ); super(host, port, httpServer, requestListenerFactory, configuration); @@ -121,14 +127,26 @@ export default class QueueServer extends ServerBase { metadataStore, extentMetadataStore, extentStore, - () => { + (err: Error) => { // tslint:disable-next-line:no-console console.log(BEFORE_CLOSE_MESSAGE_GC_ERROR); logger.info(BEFORE_CLOSE_MESSAGE_GC_ERROR); - this.close().then(() => { - // tslint:disable-next-line:no-console - console.log(AFTER_CLOSE_MESSAGE); - logger.info(AFTER_CLOSE_MESSAGE); + logger.error( + `Queue GC Manager critical error: ${err.message}\nStack: ${err.stack}` + ); + logger.warn( + "This may be due to: " + + "1) Persisted database format changes, " + + "2) File system permission issues, " + + "3) Corrupted metadata files. " + + "See error details above." + ); + + handleGCCriticalErrorClose({ + serviceName: "Queue", + getStatus: () => this.status, + close: () => this.close(), + logger }); }, logger diff --git a/src/queue/utils/constants.ts b/src/queue/utils/constants.ts index 950bd2a70..53033a172 100644 --- a/src/queue/utils/constants.ts +++ b/src/queue/utils/constants.ts @@ -1,7 +1,7 @@ import { StoreDestinationArray } from "../../common/persistence/IExtentStore"; export const VERSION = "3.36.0"; -export const QUEUE_API_VERSION = "2025-11-05"; +export const QUEUE_API_VERSION = "2026-06-06"; export const DEFAULT_QUEUE_SERVER_HOST_NAME = "127.0.0.1"; // Change to 0.0.0.0 when needs external access export const DEFAULT_QUEUE_LISTENING_PORT = 10001; export const IS_PRODUCTION = process.env.NODE_ENV === "production"; @@ -91,6 +91,9 @@ export const DEFAULT_QUEUE_PERSISTENCE_ARRAY: StoreDestinationArray = [ ]; export const ValidAPIVersions = [ + "2026-06-06", + "2026-04-06", + "2026-02-06", "2025-11-05", "2025-07-05", "2025-05-05", diff --git a/src/table/utils/constants.ts b/src/table/utils/constants.ts index a6e6174ae..4ecdfb303 100644 --- a/src/table/utils/constants.ts +++ b/src/table/utils/constants.ts @@ -18,7 +18,7 @@ export enum TABLE_STATUSCODE { } export const DEFAULT_TABLE_CONTEXT_PATH = "azurite_table_context"; -export const TABLE_API_VERSION = "2025-11-05"; +export const TABLE_API_VERSION = "2026-06-06"; export const VERSION = "3.36.0"; // Max Body size is 4 MB export const BODY_SIZE_MAX = 1024 * 1024 * 4; @@ -74,6 +74,9 @@ export const DEFAULT_TABLE_PERSISTENCE_ARRAY: StoreDestinationArray = [ export const QUERY_RESULT_MAX_NUM = 1000; export const ValidAPIVersions = [ + "2026-06-06", + "2026-04-06", + "2026-02-06", "2025-11-05", "2025-07-05", "2025-05-05", diff --git a/tests/blob/sas.test.ts b/tests/blob/sas.test.ts index 3385324d3..d3294a12c 100644 --- a/tests/blob/sas.test.ts +++ b/tests/blob/sas.test.ts @@ -40,9 +40,8 @@ configLogger(false); describe("Shared Access Signature (SAS) authentication", () => { // Setup two accounts for validating cross-account copy operations - process.env[ - "AZURITE_ACCOUNTS" - ] = `${EMULATOR_ACCOUNT_NAME}:${EMULATOR_ACCOUNT_KEY_STR}:${EMULATOR_ACCOUNT_KEY2_STR};${EMULATOR_ACCOUNT2_NAME}:${EMULATOR_ACCOUNT2_KEY_STR}`; + process.env["AZURITE_ACCOUNTS"] = + `${EMULATOR_ACCOUNT_NAME}:${EMULATOR_ACCOUNT_KEY_STR}:${EMULATOR_ACCOUNT_KEY2_STR};${EMULATOR_ACCOUNT2_NAME}:${EMULATOR_ACCOUNT2_KEY_STR}`; const factory = new BlobTestServerFactory(); const server = factory.createServer(); @@ -81,10 +80,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const serviceClientInvalid = new BlobServiceClient( baseURL, newPipeline( - new StorageSharedKeyCredential( - EMULATOR_ACCOUNT_NAME, - "invalidKey" - ), + new StorageSharedKeyCredential(EMULATOR_ACCOUNT_NAME, "invalidKey"), { retryOptions: { maxTries: 1 }, // Make sure socket is closed once the operation is done. @@ -119,11 +115,11 @@ describe("Shared Access Signature (SAS) authentication", () => { }); it("generateAccountSASQueryParameters should generate correct hashes", async () => { - const startDate = new Date(Date.UTC(2022, 3, 16, 6, 31, 48, 0)); const endDate = new Date(Date.UTC(2022, 3, 17, 6, 31, 48, 0)); - const storageSharedKeyCredential = (serviceClient as any).credential as StorageSharedKeyCredential; + const storageSharedKeyCredential = (serviceClient as any) + .credential as StorageSharedKeyCredential; const sas = generateAccountSASQueryParameters( { @@ -139,7 +135,10 @@ describe("Shared Access Signature (SAS) authentication", () => { storageSharedKeyCredential as StorageSharedKeyCredential ).toString(); - assert.equal(sas, "sv=2016-05-31&ss=btqf&srt=sco&spr=https%2Chttp&st=2022-04-16T06%3A31%3A48Z&se=2022-04-17T06%3A31%3A48Z&sip=0.0.0.0-255.255.255.255&sp=rwdlacup&sig=bTjnB%2ByT4DkYQcmMfpedew72%2FKdvTMuz29uatYuWYME%3D"); + assert.equal( + sas, + "sv=2016-05-31&ss=btqf&srt=sco&spr=https%2Chttp&st=2022-04-16T06%3A31%3A48Z&se=2022-04-17T06%3A31%3A48Z&sip=0.0.0.0-255.255.255.255&sp=rwdlacup&sig=bTjnB%2ByT4DkYQcmMfpedew72%2FKdvTMuz29uatYuWYME%3D" + ); const sas2 = generateAccountSASQueryParameters( { @@ -155,7 +154,10 @@ describe("Shared Access Signature (SAS) authentication", () => { storageSharedKeyCredential as StorageSharedKeyCredential ).toString(); - assert.equal(sas2, "sv=2018-11-09&ss=btqf&srt=sco&spr=https%2Chttp&st=2022-04-16T06%3A31%3A48Z&se=2022-04-17T06%3A31%3A48Z&sip=0.0.0.0-255.255.255.255&sp=rwdlacup&sig=zty6%2BwJgp86CuAlzq9w%2F%2BavVwjoEHgBOTXXmN7xAyio%3D"); + assert.equal( + sas2, + "sv=2018-11-09&ss=btqf&srt=sco&spr=https%2Chttp&st=2022-04-16T06%3A31%3A48Z&se=2022-04-17T06%3A31%3A48Z&sip=0.0.0.0-255.255.255.255&sp=rwdlacup&sig=zty6%2BwJgp86CuAlzq9w%2F%2BavVwjoEHgBOTXXmN7xAyio%3D" + ); const sas3 = generateAccountSASQueryParameters( { @@ -171,7 +173,10 @@ describe("Shared Access Signature (SAS) authentication", () => { storageSharedKeyCredential as StorageSharedKeyCredential ).toString(); - assert.equal(sas3, "sv=2020-12-06&ss=btqf&srt=sco&spr=https%2Chttp&st=2022-04-16T06%3A31%3A48Z&se=2022-04-17T06%3A31%3A48Z&sip=0.0.0.0-255.255.255.255&sp=rwdlacup&sig=QB5jKCJTqn7jcPidkkXAazBQBL6VDMY7HumUIM78ROE%3D"); + assert.equal( + sas3, + "sv=2020-12-06&ss=btqf&srt=sco&spr=https%2Chttp&st=2022-04-16T06%3A31%3A48Z&se=2022-04-17T06%3A31%3A48Z&sip=0.0.0.0-255.255.255.255&sp=rwdlacup&sig=QB5jKCJTqn7jcPidkkXAazBQBL6VDMY7HumUIM78ROE%3D" + ); }); it("generateAccountSASQueryParameters should work @loki @sql", async () => { @@ -362,6 +367,45 @@ describe("Shared Access Signature (SAS) authentication", () => { assert.ok(error); }); + it("generateAccountSASQueryParameters should reject duplicate SAS signature query @loki @sql", async () => { + const tmr = new Date(); + tmr.setDate(tmr.getDate() + 1); + + const storageSharedKeyCredential = (serviceClient as any).credential; + + const sas = generateAccountSASQueryParameters( + { + expiresOn: tmr, + permissions: AccountSASPermissions.parse("rwdlacup"), + resourceTypes: AccountSASResourceTypes.parse("sco").toString(), + services: AccountSASServices.parse("btqf").toString(), + protocol: SASProtocol.HttpsAndHttp, + version: "2016-05-31" + }, + storageSharedKeyCredential as StorageSharedKeyCredential + ).toString(); + + // Duplicate a critical auth field to emulate ambiguous query parsing cases. + const sasWithDuplicateSig = `${sas}&sig=duplicated-signature`; + const sasURL = `${serviceClient.url}?${sasWithDuplicateSig}`; + const serviceClientWithSAS = new BlobServiceClient( + sasURL, + newPipeline(new AnonymousCredential(), { + keepAliveOptions: { enable: false } + }) + ); + + let error; + try { + await serviceClientWithSAS.getProperties(); + } catch (err) { + error = err; + } + + assert.ok(error); + assert.deepStrictEqual(error.statusCode, 403); + }); + it("Synchronized copy blob should work with write permission in account SAS to override an existing blob @loki", async () => { const tmr = new Date(); tmr.setDate(tmr.getDate() + 1); @@ -391,9 +435,8 @@ describe("Shared Access Signature (SAS) authentication", () => { ); const containerName = getUniqueName("con"); - const containerClient = serviceClientWithSAS.getContainerClient( - containerName - ); + const containerClient = + serviceClientWithSAS.getContainerClient(containerName); await containerClient.create(); const blobName1 = getUniqueName("blob"); @@ -437,9 +480,8 @@ describe("Shared Access Signature (SAS) authentication", () => { ); const containerName = getUniqueName("con"); - const containerClient = serviceClientWithSAS.getContainerClient( - containerName - ); + const containerClient = + serviceClientWithSAS.getContainerClient(containerName); await containerClient.create(); const blobName1 = getUniqueName("blob"); @@ -490,9 +532,8 @@ describe("Shared Access Signature (SAS) authentication", () => { ); const containerName = getUniqueName("con"); - const containerClient = serviceClientWithSAS.getContainerClient( - containerName - ); + const containerClient = + serviceClientWithSAS.getContainerClient(containerName); await containerClient.create(); const blobName1 = getUniqueName("blob"); @@ -535,9 +576,8 @@ describe("Shared Access Signature (SAS) authentication", () => { ); const containerName = getUniqueName("con"); - const containerClient = serviceClientWithSAS.getContainerClient( - containerName - ); + const containerClient = + serviceClientWithSAS.getContainerClient(containerName); await containerClient.create(); const blobName1 = getUniqueName("blob"); @@ -581,9 +621,8 @@ describe("Shared Access Signature (SAS) authentication", () => { ); const containerName = getUniqueName("con"); - const containerClient = serviceClientWithSAS.getContainerClient( - containerName - ); + const containerClient = + serviceClientWithSAS.getContainerClient(containerName); await containerClient.create(); const blobName1 = getUniqueName("blob"); @@ -635,9 +674,8 @@ describe("Shared Access Signature (SAS) authentication", () => { const containerName = getUniqueName("con"); const containerClient = serviceClient.getContainerClient(containerName); - const containerClientWithSAS = serviceClientWithSAS.getContainerClient( - containerName - ); + const containerClientWithSAS = + serviceClientWithSAS.getContainerClient(containerName); await containerClient.create(); const blobName1 = getUniqueName("blob"); @@ -731,7 +769,9 @@ describe("Shared Access Signature (SAS) authentication", () => { assert.deepStrictEqual(err.statusCode, 403); } - const serviceClientWithSAS = new BlobServiceClient(`${serviceClient.url}?${containerSAS}`); + const serviceClientWithSAS = new BlobServiceClient( + `${serviceClient.url}?${containerSAS}` + ); try { await serviceClientWithSAS.getAccountInfo(); @@ -792,7 +832,8 @@ describe("Shared Access Signature (SAS) authentication", () => { assert.deepStrictEqual(err.statusCode, 403); } - const serviceClientWithSAS = new BlobServiceClient(`${serviceClient.url}?${blobSAS}`, + const serviceClientWithSAS = new BlobServiceClient( + `${serviceClient.url}?${blobSAS}`, newPipeline(new AnonymousCredential()) ); @@ -835,8 +876,10 @@ describe("Shared Access Signature (SAS) authentication", () => { storageSharedKeyCredential as StorageSharedKeyCredential ); - const serviceClientWithSAS = new BlobServiceClient(`${serviceClient.url}?${blobSAS}`, - new AnonymousCredential()); + const serviceClientWithSAS = new BlobServiceClient( + `${serviceClient.url}?${blobSAS}`, + new AnonymousCredential() + ); const blobBatchClient = serviceClientWithSAS.getBlobBatchClient(); // Assemble batch delete request. @@ -881,8 +924,10 @@ describe("Shared Access Signature (SAS) authentication", () => { storageSharedKeyCredential as StorageSharedKeyCredential ); - const containerClientWithSAS = new ContainerClient(`${serviceClient.url}?${blobSAS}`, - new AnonymousCredential()); + const containerClientWithSAS = new ContainerClient( + `${serviceClient.url}?${blobSAS}`, + new AnonymousCredential() + ); const blobBatchClient = containerClientWithSAS.getBlobBatchClient(); // Assemble batch delete request. @@ -932,7 +977,7 @@ describe("Shared Access Signature (SAS) authentication", () => { let error; try { - await containerClientWithSAS.getBlobClient('blob').deleteIfExists(); + await containerClientWithSAS.getBlobClient("blob").deleteIfExists(); } catch (err) { error = err; } finally { @@ -953,7 +998,8 @@ describe("Shared Access Signature (SAS) authentication", () => { const tmr = new Date(); tmr.setDate(tmr.getDate() + 1); - const storageSharedKeyCredential = (serviceClientSecondKey as any).credential; + const storageSharedKeyCredential = (serviceClientSecondKey as any) + .credential; const containerName = getUniqueName("container"); const containerClient = serviceClient.getContainerClient(containerName); @@ -978,7 +1024,7 @@ describe("Shared Access Signature (SAS) authentication", () => { newPipeline(new AnonymousCredential()) ); - await containerClientWithSAS.getBlobClient('blob').deleteIfExists(); + await containerClientWithSAS.getBlobClient("blob").deleteIfExists(); await containerClient.delete(); }); @@ -1038,7 +1084,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const downloadResponse = await blobClientWithSAS.download(); assert.equal(downloadResponse.cacheControl, "cache-control-original"); - assert.equal(downloadResponse.contentDisposition, "content-disposition-original"); + assert.equal( + downloadResponse.contentDisposition, + "content-disposition-original" + ); assert.equal(downloadResponse.contentEncoding, "content-encoding-original"); assert.equal(downloadResponse.contentLanguage, "content-language-original"); assert.equal(downloadResponse.contentType, "content-type-original"); @@ -1059,7 +1108,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const containerClient = serviceClient.getContainerClient(containerName); await containerClient.create(); - const blobName = "this is a test file Ж 大 仮.jpg"; //filename contains spaces and special characters + const blobName = "this is a test file Ж 大 仮.jpg"; //filename contains spaces and special characters const blobClient = containerClient.getPageBlobClient(blobName); await blobClient.create(1024, { blobHTTPHeaders: { @@ -1098,14 +1147,20 @@ describe("Shared Access Signature (SAS) authentication", () => { const properties = await blobClientWithSAS.getProperties(); assert.equal(properties.cacheControl, "cache-control-original"); - assert.equal(properties.contentDisposition, `attachment; filename=\"${escapedblobName}\"; filename*=UTF-8''${escapedblobName}`); + assert.equal( + properties.contentDisposition, + `attachment; filename=\"${escapedblobName}\"; filename*=UTF-8''${escapedblobName}` + ); assert.equal(properties.contentEncoding, "content-encoding-original"); assert.equal(properties.contentLanguage, "content-language-original"); assert.equal(properties.contentType, "content-type-original"); const downloadResponse = await blobClientWithSAS.download(); assert.equal(downloadResponse.cacheControl, "cache-control-original"); - assert.equal(downloadResponse.contentDisposition, `attachment; filename=\"${escapedblobName}\"; filename*=UTF-8''${escapedblobName}`); + assert.equal( + downloadResponse.contentDisposition, + `attachment; filename=\"${escapedblobName}\"; filename*=UTF-8''${escapedblobName}` + ); assert.equal(downloadResponse.contentEncoding, "content-encoding-original"); assert.equal(downloadResponse.contentLanguage, "content-language-original"); assert.equal(downloadResponse.contentType, "content-type-original"); @@ -1170,7 +1225,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const downloadResponse = await blobClientWithSAS.download(); assert.equal(downloadResponse.cacheControl, "cache-control-override"); - assert.equal(downloadResponse.contentDisposition, "content-disposition-override"); + assert.equal( + downloadResponse.contentDisposition, + "content-disposition-override" + ); assert.equal(downloadResponse.contentEncoding, "content-encoding-override"); assert.equal(downloadResponse.contentLanguage, "content-language-override"); assert.equal(downloadResponse.contentType, "content-type-override"); @@ -1234,7 +1292,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const downloadResponse = await blobClientWithSAS.download(); assert.equal(downloadResponse.cacheControl, "cache-control-original"); - assert.equal(downloadResponse.contentDisposition, "content-disposition-original"); + assert.equal( + downloadResponse.contentDisposition, + "content-disposition-original" + ); assert.equal(downloadResponse.contentEncoding, "content-encoding-original"); assert.equal(downloadResponse.contentLanguage, "content-language-original"); assert.equal(downloadResponse.contentType, "content-type-original"); @@ -1299,7 +1360,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const downloadResponse = await blobClientWithSAS.download(); assert.equal(downloadResponse.cacheControl, "cache-control-override"); - assert.equal(downloadResponse.contentDisposition, "content-disposition-override"); + assert.equal( + downloadResponse.contentDisposition, + "content-disposition-override" + ); assert.equal(downloadResponse.contentEncoding, "content-encoding-override"); assert.equal(downloadResponse.contentLanguage, "content-language-override"); assert.equal(downloadResponse.contentType, "content-type-override"); @@ -1368,7 +1432,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const downloadResponse = await blobClientWithSAS.download(); assert.equal(downloadResponse.cacheControl, "cache-control-override"); - assert.equal(downloadResponse.contentDisposition, "content-disposition-override"); + assert.equal( + downloadResponse.contentDisposition, + "content-disposition-override" + ); assert.equal(downloadResponse.contentEncoding, "content-encoding-override"); assert.equal(downloadResponse.contentLanguage, "content-language-override"); assert.equal(downloadResponse.contentType, "content-type-override"); @@ -1748,8 +1815,9 @@ describe("Shared Access Signature (SAS) authentication", () => { storageSharedKeyCredential as StorageSharedKeyCredential ); - const sasURL = `${blobClient.withSnapshot(response.snapshot!).url - }&${blobSAS}`; + const sasURL = `${ + blobClient.withSnapshot(response.snapshot!).url + }&${blobSAS}`; const blobClientWithSAS = new PageBlobClient( sasURL, newPipeline(new AnonymousCredential()) @@ -1765,7 +1833,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const downloadResponse = await blobClientWithSAS.download(); assert.equal(downloadResponse.cacheControl, "cache-control-override"); - assert.equal(downloadResponse.contentDisposition, "content-disposition-override"); + assert.equal( + downloadResponse.contentDisposition, + "content-disposition-override" + ); assert.equal(downloadResponse.contentEncoding, "content-encoding-override"); assert.equal(downloadResponse.contentLanguage, "content-language-override"); assert.equal(downloadResponse.contentType, "content-type-override"); @@ -1783,12 +1854,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const sourceStorageSharedKeyCredential = (serviceClient as any).credential; const containerName = getUniqueName("con"); - const sourceContainerClient = serviceClient.getContainerClient( - containerName - ); - const targetContainerClient = serviceClient2.getContainerClient( - containerName - ); + const sourceContainerClient = + serviceClient.getContainerClient(containerName); + const targetContainerClient = + serviceClient2.getContainerClient(containerName); await sourceContainerClient.create(); await targetContainerClient.create(); @@ -1841,12 +1910,10 @@ describe("Shared Access Signature (SAS) authentication", () => { tmr.setDate(tmr.getDate() + 1); const containerName = getUniqueName("con"); - const sourceContainerClient = serviceClient.getContainerClient( - containerName - ); - const targetContainerClient = serviceClient2.getContainerClient( - containerName - ); + const sourceContainerClient = + serviceClient.getContainerClient(containerName); + const targetContainerClient = + serviceClient2.getContainerClient(containerName); await sourceContainerClient.create(); await targetContainerClient.create(); @@ -1876,12 +1943,10 @@ describe("Shared Access Signature (SAS) authentication", () => { tmr.setDate(tmr.getDate() + 1); const containerName = getUniqueName("con"); - const sourceContainerClient = serviceClient.getContainerClient( - containerName - ); - const targetContainerClient = serviceClient2.getContainerClient( - containerName - ); + const sourceContainerClient = + serviceClient.getContainerClient(containerName); + const targetContainerClient = + serviceClient2.getContainerClient(containerName); await sourceContainerClient.create({ access: "blob" }); @@ -1907,12 +1972,10 @@ describe("Shared Access Signature (SAS) authentication", () => { tmr.setDate(tmr.getDate() + 1); const containerName = getUniqueName("con"); - const sourceContainerClient = serviceClient.getContainerClient( - containerName - ); - const targetContainerClient = serviceClient2.getContainerClient( - containerName - ); + const sourceContainerClient = + serviceClient.getContainerClient(containerName); + const targetContainerClient = + serviceClient2.getContainerClient(containerName); await sourceContainerClient.create({ access: "container" }); @@ -1940,12 +2003,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const sourceStorageSharedKeyCredential = (serviceClient as any).credential; const containerName = getUniqueName("con"); - const sourceContainerClient = serviceClient.getContainerClient( - containerName - ); - const targetContainerClient = serviceClient2.getContainerClient( - containerName - ); + const sourceContainerClient = + serviceClient.getContainerClient(containerName); + const targetContainerClient = + serviceClient2.getContainerClient(containerName); await sourceContainerClient.create(); await targetContainerClient.create(); @@ -1983,9 +2044,8 @@ describe("Shared Access Signature (SAS) authentication", () => { assert.equal(properties.metadata!["foo"], "1"); assert.equal(properties.metadata!["bar"], "2"); - const targetBlobWithProps = targetContainerClient.getBlockBlobClient( - blobName2 - ); + const targetBlobWithProps = + targetContainerClient.getBlockBlobClient(blobName2); const operation2 = await targetBlobWithProps.beginCopyFromURL( `${sourceBlob.url}?${sas}`, { @@ -2013,12 +2073,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const sourceStorageSharedKeyCredential = (serviceClient as any).credential; const containerName = getUniqueName("con"); - const sourceContainerClient = serviceClient.getContainerClient( - containerName - ); - const targetContainerClient = serviceClient2.getContainerClient( - containerName - ); + const sourceContainerClient = + serviceClient.getContainerClient(containerName); + const targetContainerClient = + serviceClient2.getContainerClient(containerName); await sourceContainerClient.create(); await targetContainerClient.create(); @@ -2064,12 +2122,10 @@ describe("Shared Access Signature (SAS) authentication", () => { const sourceStorageSharedKeyCredential = (serviceClient as any).credential; const containerName = getUniqueName("con"); - const sourceContainerClient = serviceClient.getContainerClient( - containerName - ); - const targetContainerClient = serviceClient2.getContainerClient( - containerName - ); + const sourceContainerClient = + serviceClient.getContainerClient(containerName); + const targetContainerClient = + serviceClient2.getContainerClient(containerName); await sourceContainerClient.create(); await targetContainerClient.create(); @@ -2099,9 +2155,8 @@ describe("Shared Access Signature (SAS) authentication", () => { // Copy From URI const targetBlob = targetContainerClient.getBlockBlobClient(blobName); - const targetBlobWithProps = targetContainerClient.getBlockBlobClient( - blobName2 - ); + const targetBlobWithProps = + targetContainerClient.getBlockBlobClient(blobName2); const copyResponse3 = await targetBlob.syncCopyFromURL( `${sourceBlob.url}?${sas}` ); @@ -2136,7 +2191,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const tags = { tag1: "val1", - tag2: "val2", + tag2: "val2" }; const blockBlockName = getUniqueName("blockblob"); @@ -2146,16 +2201,18 @@ describe("Shared Access Signature (SAS) authentication", () => { const sasURL = await containerClient.generateSasUrl({ expiresOn: tmr, permissions: ContainerSASPermissions.parse("f"), - protocol: SASProtocol.HttpsAndHttp, + protocol: SASProtocol.HttpsAndHttp }); const containerClientWithSAS = new ContainerClient(sasURL); const expectedTags1: Tags = { - tag1: "val1", + tag1: "val1" }; - for await (const blob of containerClientWithSAS.findBlobsByTags(`tag1='val1'`)) { + for await (const blob of containerClientWithSAS.findBlobsByTags( + `tag1='val1'` + )) { assert.deepStrictEqual(blob.name, blockBlockName); assert.deepStrictEqual(blob.tags, expectedTags1); assert.deepStrictEqual(blob.tagValue, "val1"); @@ -2174,7 +2231,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const tags = { tag1: "val1", - tag2: "val2", + tag2: "val2" }; const blockBlobName = getUniqueName("blockblob"); @@ -2183,22 +2240,28 @@ describe("Shared Access Signature (SAS) authentication", () => { const sourceStorageSharedKeyCredential = (serviceClient as any).credential; - const sasURL = generateAccountSASQueryParameters({ - expiresOn: tmr, - services: 'b', - resourceTypes: 'so', - permissions: AccountSASPermissions.parse("f"), - protocol: SASProtocol.HttpsAndHttp, - }, - sourceStorageSharedKeyCredential).toString(); + const sasURL = generateAccountSASQueryParameters( + { + expiresOn: tmr, + services: "b", + resourceTypes: "so", + permissions: AccountSASPermissions.parse("f"), + protocol: SASProtocol.HttpsAndHttp + }, + sourceStorageSharedKeyCredential + ).toString(); - const serviceClientWithSAS = new BlobServiceClient(`${serviceClient.url}?${sasURL}`); + const serviceClientWithSAS = new BlobServiceClient( + `${serviceClient.url}?${sasURL}` + ); const expectedTags1: Tags = { - tag1: "val1", + tag1: "val1" }; - for await (const blob of serviceClientWithSAS.findBlobsByTags(`tag1='val1'`)) { + for await (const blob of serviceClientWithSAS.findBlobsByTags( + `tag1='val1'` + )) { assert.deepStrictEqual(blob.tags, expectedTags1); assert.deepStrictEqual(blob.tagValue, "val1"); } @@ -2216,7 +2279,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const tags = { tag1: "val1", - tag2: "val2", + tag2: "val2" }; const blockBlobName = getUniqueName("blockblob"); @@ -2225,22 +2288,28 @@ describe("Shared Access Signature (SAS) authentication", () => { const sourceStorageSharedKeyCredential = (serviceClient as any).credential; - const sasURL = generateAccountSASQueryParameters({ - expiresOn: tmr, - services: 'b', - resourceTypes: 'c', - permissions: AccountSASPermissions.parse("f"), - protocol: SASProtocol.HttpsAndHttp, - }, - sourceStorageSharedKeyCredential).toString(); + const sasURL = generateAccountSASQueryParameters( + { + expiresOn: tmr, + services: "b", + resourceTypes: "c", + permissions: AccountSASPermissions.parse("f"), + protocol: SASProtocol.HttpsAndHttp + }, + sourceStorageSharedKeyCredential + ).toString(); - const containerClientWithSas = new ContainerClient(`${containerClient.url}?${sasURL}`); + const containerClientWithSas = new ContainerClient( + `${containerClient.url}?${sasURL}` + ); const expectedTags1: Tags = { - tag1: "val1", + tag1: "val1" }; - for await (const blob of containerClientWithSas.findBlobsByTags(`tag1='val1'`)) { + for await (const blob of containerClientWithSas.findBlobsByTags( + `tag1='val1'` + )) { assert.deepStrictEqual(blob.tags, expectedTags1); assert.deepStrictEqual(blob.tagValue, "val1"); } @@ -2258,7 +2327,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const tags = { tag1: "val1", - tag2: "val2", + tag2: "val2" }; const blockBlobName = getUniqueName("blockblob"); @@ -2268,7 +2337,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const sasURL = await blockBlobClient.generateSasUrl({ expiresOn: tmr, permissions: BlobSASPermissions.parse("t"), - protocol: SASProtocol.HttpsAndHttp, + protocol: SASProtocol.HttpsAndHttp }); const blobClientWithSAS = new BlobClient(sasURL); @@ -2290,7 +2359,7 @@ describe("Shared Access Signature (SAS) authentication", () => { const tags = { tag1: "val1", - tag2: "val2", + tag2: "val2" }; const blockBlobName = getUniqueName("blockblob"); @@ -2299,16 +2368,20 @@ describe("Shared Access Signature (SAS) authentication", () => { const sourceStorageSharedKeyCredential = (serviceClient as any).credential; - const sasURL = generateAccountSASQueryParameters({ - expiresOn: tmr, - services: 'b', - resourceTypes: 'o', - permissions: AccountSASPermissions.parse("t"), - protocol: SASProtocol.HttpsAndHttp, - }, - sourceStorageSharedKeyCredential).toString(); - - const blobClientWithSAS = new BlobClient(`${blockBlobClient.url}?${sasURL}`); + const sasURL = generateAccountSASQueryParameters( + { + expiresOn: tmr, + services: "b", + resourceTypes: "o", + permissions: AccountSASPermissions.parse("t"), + protocol: SASProtocol.HttpsAndHttp + }, + sourceStorageSharedKeyCredential + ).toString(); + + const blobClientWithSAS = new BlobClient( + `${blockBlobClient.url}?${sasURL}` + ); await blobClientWithSAS.setTags(tags); const getTagsResult = await blobClientWithSAS.getTags(); diff --git a/tests/blob/startupErrorRecovery.test.ts b/tests/blob/startupErrorRecovery.test.ts new file mode 100644 index 000000000..d153ace82 --- /dev/null +++ b/tests/blob/startupErrorRecovery.test.ts @@ -0,0 +1,226 @@ +import * as assert from "assert"; +import * as fs from "fs-extra"; + +import BlobConfiguration from "../../src/blob/BlobConfiguration"; +import BlobServer from "../../src/blob/BlobServer"; +import { configLogger } from "../../src/common/Logger"; +import { DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT } from "../../src/blob/utils/constants"; +import { ServerStatus } from "../../src/common/ServerBase"; + +// Set true to enable debug log +configLogger(false); + +describe("Blob Server Startup Error Recovery - Issue #2672 @loki", () => { + const testDbPath = "__test_startup_error_db_blob__.json"; + const testDbExtentPath = "__test_startup_error_db_blob_extent__.json"; + const blobStoragePath = "__test_startup_error_blobstorage__"; + + async function startWithTimeout(server: BlobServer): Promise { + let timeout: ReturnType | undefined; + + try { + await Promise.race([ + server.start(), + new Promise((_, reject) => { + timeout = setTimeout(() => { + reject(new Error("Server startup timeout after 10 seconds")); + }, 10000); + + const nodeTimer = timeout as unknown as { unref?: () => void }; + if (typeof nodeTimer.unref === "function") { + nodeTimer.unref(); + } + }) + ]); + } finally { + if (timeout !== undefined) { + clearTimeout(timeout); + } + } + } + + after(async () => { + // Clean up test artifacts + if (fs.existsSync(testDbPath)) { + fs.removeSync(testDbPath); + } + if (fs.existsSync(testDbExtentPath)) { + fs.removeSync(testDbExtentPath); + } + if (fs.existsSync(blobStoragePath)) { + fs.removeSync(blobStoragePath); + } + }); + + it("should start successfully when no persisted data exists (fresh start)", async () => { + // Clean paths before test + [testDbPath, testDbExtentPath, blobStoragePath].forEach((p) => { + if (fs.existsSync(p)) { + fs.removeSync(p); + } + }); + + const config = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + testDbPath, + testDbExtentPath, + [ + { + locationId: "test", + locationPath: blobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server = new BlobServer(config); + + try { + // Should start without errors + await server.start(); + assert.strictEqual(server.getStatus(), ServerStatus.Running); + + await server.close(); + assert.strictEqual(server.getStatus(), ServerStatus.Closed); + } finally { + await server.clean(); + } + }); + + it("should handle GC startup errors gracefully without throwing 'Cannot close server in status Starting'", async () => { + // Clean paths before test + [testDbPath, testDbExtentPath, blobStoragePath].forEach((p) => { + if (fs.existsSync(p)) { + fs.removeSync(p); + } + }); + + // Create a corrupted metadata file to simulate legacy data that causes GC errors + // This simulates loading data from 3.35.0 that causes compatibility issues + const corruptedMetadata = { + schemaVersion: "3.35.0", + data: { + collections: [ + { + name: "containers", + binaryIndices: [{ name: "name", keyType: "string" }], + data: [ + { + // Minimal container entry - may cause issues during GC init + name: "test-container", + meta: { revision: 0, created: 1600000000000, version: 0 } + } + ] + } + ] + } + }; + + // Write corrupted metadata to simulate legacy data + fs.writeFileSync(testDbPath, JSON.stringify(corruptedMetadata, null, 2)); + + const config = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + testDbPath, + testDbExtentPath, + [ + { + locationId: "test", + locationPath: blobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server = new BlobServer(config); + try { + // Attempt to start server + await startWithTimeout(server); + + // If we get here, startup completed and should no longer be stuck in Starting. + assert.notStrictEqual( + server.getStatus(), + ServerStatus.Starting, + "Server should not remain in Starting state after startup completes" + ); + + // Try to close gracefully + if (server.getStatus() === ServerStatus.Running) { + await server.close(); + } + } catch (err) { + if (!(err instanceof Error)) { + throw err; + } + + if (err.message.includes("Cannot close server in status Starting")) { + assert.fail( + `Bug not fixed: Server attempted to close while in Starting state. Error: ${err.message}` + ); + } + + // Do not swallow startup failures such as timeout/hang. + throw err; + } finally { + try { + await server.clean(); + } catch { + // Cleanup may fail if startup failed, that's ok + } + } + }); + + it("should successfully recover from concurrent startup and GC initialization", async () => { + // Clean paths before test + [testDbPath, testDbExtentPath, blobStoragePath].forEach((p) => { + if (fs.existsSync(p)) { + fs.removeSync(p); + } + }); + + const config = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + testDbPath, + testDbExtentPath, + [ + { + locationId: "test", + locationPath: blobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server = new BlobServer(config); + + try { + // Start the server - GC manager will initialize concurrently during startup + await server.start(); + + // Verify server reached Running state + assert.strictEqual( + server.getStatus(), + ServerStatus.Running, + "Server should be in Running status after successful start" + ); + + // Give GC some time to run + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Close should work without issues + await server.close(); + assert.strictEqual(server.getStatus(), ServerStatus.Closed); + } finally { + await server.clean(); + } + }); +}); diff --git a/tests/blob/unit/gcErrorHandler.unit.test.ts b/tests/blob/unit/gcErrorHandler.unit.test.ts new file mode 100644 index 000000000..c61cb6941 --- /dev/null +++ b/tests/blob/unit/gcErrorHandler.unit.test.ts @@ -0,0 +1,118 @@ +import * as assert from "assert"; + +import { handleGCCriticalErrorClose } from "../../../src/common/GCCriticalErrorCloseHelper"; +import ILogger from "../../../src/common/ILogger"; +import { ServerStatus } from "../../../src/common/ServerBase"; + +describe("GC Error Handler Helper Unit Tests - Issue #2672 @loki", () => { + function createTestLogger(events: string[]): ILogger { + return { + error: (message: string) => events.push(`error:${message}`), + warn: (message: string) => events.push(`warn:${message}`), + info: (message: string) => events.push(`info:${message}`), + verbose: () => { + // no-op for these tests + }, + debug: () => { + // no-op for these tests + } + }; + } + + it("waits for Running status, then closes", async () => { + let serverStatus: ServerStatus = ServerStatus.Starting; + let closeCalled = false; + const events: string[] = []; + + const transitionToRunning = new Promise((resolve) => { + setTimeout(() => { + serverStatus = ServerStatus.Running; + events.push("transitioned_to_running"); + resolve(); + }, 20); + }); + + await Promise.all([ + transitionToRunning, + handleGCCriticalErrorClose({ + serviceName: "Blob", + getStatus: () => serverStatus, + close: async () => { + closeCalled = true; + events.push("close_called"); + }, + logger: createTestLogger(events), + waitIntervalMs: 5, + maxAttempts: 20 + }) + ]); + + assert.strictEqual(closeCalled, true, "close should be called"); + assert.ok( + events.some((e) => + e.includes("Shutting down Blob server due to GC critical error") + ), + "should log shutdown intent when status becomes Running" + ); + }); + + it("does not close if server transitions from Starting to Closed", async () => { + let serverStatus: ServerStatus = ServerStatus.Starting; + let closeCalled = false; + const events: string[] = []; + + // Ensure this happens well after maxAttempts * waitIntervalMs so timeout logging is deterministic. + setTimeout(() => { + serverStatus = ServerStatus.Closed; + events.push("transitioned_to_closed"); + }, 60); + + await handleGCCriticalErrorClose({ + serviceName: "Queue", + getStatus: () => serverStatus, + close: async () => { + closeCalled = true; + }, + logger: createTestLogger(events), + waitIntervalMs: 5, + maxAttempts: 3 + }); + + assert.strictEqual(closeCalled, false, "close should not be called"); + assert.ok( + events.some((e) => e.includes("did not transition to Running state")), + "should log startup transition timeout" + ); + assert.ok( + events.some((e) => + e.includes("Queue server status is Closed (expected Running)") + ), + "should log status mismatch warning" + ); + }); + + it("swallows close errors and logs diagnostics", async () => { + let serverStatus: ServerStatus = ServerStatus.Running; + const events: string[] = []; + + await handleGCCriticalErrorClose({ + serviceName: "Blob", + getStatus: () => serverStatus, + close: async () => { + throw new Error("synthetic close failure"); + }, + logger: createTestLogger(events), + waitIntervalMs: 5, + maxAttempts: 1 + }); + + assert.ok( + events.some((e) => + e.includes( + "Blob server error during GC error handling: synthetic close failure" + ) + ), + "should log close diagnostics instead of throwing" + ); + }); +}); diff --git a/tests/blob/upgradeRegression.test.ts b/tests/blob/upgradeRegression.test.ts new file mode 100644 index 000000000..ef44b979d --- /dev/null +++ b/tests/blob/upgradeRegression.test.ts @@ -0,0 +1,660 @@ +import { + newPipeline, + BlobServiceClient, + StorageSharedKeyCredential, + ContainerClient +} from "@azure/storage-blob"; +import * as assert from "assert"; +import * as nodefs from "fs"; +import * as fs from "fs-extra"; + +import BlobConfiguration from "../../src/blob/BlobConfiguration"; +import BlobServer from "../../src/blob/BlobServer"; +import { configLogger } from "../../src/common/Logger"; +import { DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT } from "../../src/blob/utils/constants"; +import { ServerStatus } from "../../src/common/ServerBase"; +import { EMULATOR_ACCOUNT_KEY, EMULATOR_ACCOUNT_NAME } from "../testutils"; + +// Set true to enable debug log +configLogger(false); + +describe("Azurite Upgrade Regression Tests @loki", () => { + let upgradeTestDbPath = ""; + let upgradeTestDbExtentPath = ""; + let upgradeBlobStoragePath = ""; + const cleanupTargets: string[] = []; + const containerName = "upgrade-test-container"; + const blobName = "upgrade-test-blob.txt"; + const blobContent = "This data was created in Azurite 3.35.0"; + + function allocatePaths(scope: string): void { + const token = `${scope}_${Date.now()}_${Math.random() + .toString(16) + .slice(2, 10)}`; + + upgradeTestDbPath = `__test_upgrade_db_blob__${token}.json`; + upgradeTestDbExtentPath = `__test_upgrade_db_blob_extent__${token}.json`; + upgradeBlobStoragePath = `__test_upgrade_blobstorage__${token}`; + + cleanupTargets.push( + upgradeTestDbPath, + upgradeTestDbExtentPath, + upgradeBlobStoragePath + ); + } + + function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + async function removePathWithRetry(path: string): Promise { + try { + await nodefs.promises.rm(path, { + recursive: true, + force: true, + maxRetries: 20, + retryDelay: 100 + }); + return; + } catch (err) { + const code = (err as { code?: string })?.code; + + if (code === "ENOENT") { + return; + } + + // Last fallback for sporadic Windows lock races. + await delay(200); + await fs.remove(path); + } + } + + before(async () => { + allocatePaths("suite"); + }); + + beforeEach(() => { + allocatePaths("case"); + }); + + after(async () => { + // Clean up test artifacts + await cleanUpgradeArtifacts(); + }); + + async function cleanUpgradeArtifacts(): Promise { + const paths = Array.from(new Set(cleanupTargets)); + + for (const p of paths) { + try { + await removePathWithRetry(p); + } catch (err) { + const code = (err as { code?: string })?.code; + if (code !== "EPERM") { + throw err; + } + } + } + } + + function extractMd5Bytes(contentMD5: any): number[] { + if (contentMD5 === undefined || contentMD5 === null) { + return []; + } + + if (contentMD5.type === "Buffer" && Array.isArray(contentMD5.data)) { + return contentMD5.data; + } + + if (Array.isArray(contentMD5)) { + return contentMD5; + } + + return Object.keys(contentMD5) + .sort((a, b) => Number(a) - Number(b)) + .map((k) => contentMD5[k]); + } + + function rewritePersistedMd5Shape( + shape: "buffer-json" | "numeric-object" | "plain-array" + ): void { + const db = fs.readJSONSync(upgradeTestDbPath); + const blobCollection = db.collections.find( + (c: any) => c.name === "$BLOBS_COLLECTION$" + ); + + assert.ok(blobCollection !== undefined, "Blob collection should exist"); + + for (const doc of blobCollection.data) { + const existing = doc.properties?.contentMD5; + const bytes = extractMd5Bytes(existing); + assert.ok(bytes.length > 0, "Persisted contentMD5 bytes should exist"); + + if (shape === "buffer-json") { + doc.properties.contentMD5 = { type: "Buffer", data: bytes }; + } else if (shape === "numeric-object") { + const numericObj: { [k: string]: number } = {}; + bytes.forEach((v, i) => { + numericObj[String(i)] = v; + }); + doc.properties.contentMD5 = numericObj; + } else { + doc.properties.contentMD5 = bytes; + } + } + + fs.writeJSONSync(upgradeTestDbPath, db); + } + + function rewritePersistedMd5AsNull(): void { + const db = fs.readJSONSync(upgradeTestDbPath); + const blobCollection = db.collections.find( + (c: any) => c.name === "$BLOBS_COLLECTION$" + ); + + assert.ok(blobCollection !== undefined, "Blob collection should exist"); + + for (const doc of blobCollection.data) { + doc.properties.contentMD5 = null; + } + + fs.writeJSONSync(upgradeTestDbPath, db); + } + + /** + * Simulate the upgrade path + * This test verifies that persisted data can be loaded after version changes + */ + it("should upgrade without data loss", async () => { + // PHASE 1: Simulate old version behavior - create initial data + + const config1 = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server1 = new BlobServer(config1); + await server1.start(); + + try { + const baseURL1 = `${server1.getHttpServerAddress()}/devstoreaccount1`; + const blobServiceClient1 = new BlobServiceClient( + baseURL1, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + // Create container and blob + const containerClient: ContainerClient = + blobServiceClient1.getContainerClient(containerName); + await containerClient.create(); + + const blockBlobClient = containerClient.getBlockBlobClient(blobName); + await blockBlobClient.upload(blobContent, blobContent.length); + // Verify data was written + const downloadResponse = await blockBlobClient.download(0); + const downloadedStream = downloadResponse.readableStreamBody; + assert.ok( + downloadedStream !== undefined, + "Downloaded stream should exist" + ); + } finally { + await server1.close(); + // Don't clean - we want to preserve the persisted data for the upgrade test + // await server1.clean(); + } + + // Verify persistence files exist + assert.ok( + fs.existsSync(upgradeTestDbPath), + "Blob metadata database should be persisted" + ); + + // PHASE 2: Simulate upgrade - load existing data + + const config2 = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server2 = new BlobServer(config2); + + try { + // This is where the bug would occur - loading legacy data with new GC manager + await server2.start(); + assert.ok( + server2.getStatus() === ServerStatus.Running, + "Server should be in Running state" + ); + + const baseURL2 = `${server2.getHttpServerAddress()}/devstoreaccount1`; + const blobServiceClient2 = new BlobServiceClient( + baseURL2, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + // Verify existing data is accessible + const containerClient2: ContainerClient = + blobServiceClient2.getContainerClient(containerName); + const blockBlobClient2 = containerClient2.getBlockBlobClient(blobName); + + // Download the blob that was created in phase 1 + const downloadResponse2 = await blockBlobClient2.download(0); + assert.ok( + downloadResponse2.readableStreamBody !== undefined, + "Should be able to download persisted blob after upgrade" + ); + + // PHASE 3: Create new data with new version + const newBlobName = "new-blob.txt"; + const newBlobContent = "This data was created in the new version"; + + const newBlockBlobClient = + containerClient2.getBlockBlobClient(newBlobName); + await newBlockBlobClient.upload(newBlobContent, newBlobContent.length); + + // Verify new data + const newDownloadResponse = await newBlockBlobClient.download(0); + assert.ok( + newDownloadResponse.readableStreamBody !== undefined, + "New blob should be downloadable" + ); + + // Verify old data is still there + const oldDownloadResponse = await blockBlobClient2.download(0); + assert.ok( + oldDownloadResponse.readableStreamBody !== undefined, + "Old blob should still be accessible" + ); + + await server2.close(); + } finally { + try { + await server2.clean(); + } catch (err) { + console.log("Note: Cleanup had an issue, but that's ok for this test"); + } + } + }); + + /** + * Test that verifies the specific scenario from issue #2672: + * Multiple accounts with existing persisted data + */ + it("should handle startup with multiple existing accounts and containers", async () => { + const config3 = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server3 = new BlobServer(config3); + await server3.start(); + + try { + const baseURL3 = `${server3.getHttpServerAddress()}/devstoreaccount1`; + const blobServiceClient3 = new BlobServiceClient( + baseURL3, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + // Create multiple containers with blobs + const containers = ["container-1", "container-2", "container-3"]; + for (const containerName of containers) { + const containerClient = + blobServiceClient3.getContainerClient(containerName); + await containerClient.create(); + + // Add a blob to each container + for (let i = 0; i < 3; i++) { + const payload = `Content for ${containerName} blob ${i}`; + const blobClient = containerClient.getBlockBlobClient( + `blob-${i}.txt` + ); + await blobClient.upload(payload, Buffer.byteLength(payload)); + } + } + } finally { + await server3.close(); + } + + // Now restart and verify all data is still accessible + const config4 = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server4 = new BlobServer(config4); + + try { + await server4.start(); + + const baseURL4 = `${server4.getHttpServerAddress()}/devstoreaccount1`; + const blobServiceClient4 = new BlobServiceClient( + baseURL4, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + // Verify all containers and blobs are accessible + const containers = ["container-1", "container-2", "container-3"]; + for (const containerName of containers) { + const containerClient = + blobServiceClient4.getContainerClient(containerName); + + // List blobs to verify they're accessible + let blobCount = 0; + for await (const blob of containerClient.listBlobsFlat()) { + blobCount++; + assert.ok(blob.name !== undefined, "Blob should have a name"); + } + + assert.strictEqual( + blobCount, + 3, + `Container ${containerName} should have 3 blobs` + ); + } + + await server4.close(); + } finally { + try { + await server4.clean(); + } catch (err) { + console.log("Note: Cleanup had an issue, but that's ok for this test"); + } + } + }); + + it("should load persisted metadata across legacy contentMD5 serialization formats", async () => { + const shapes: Array<"buffer-json" | "numeric-object" | "plain-array"> = [ + "buffer-json", + "numeric-object", + "plain-array" + ]; + + for (const shape of shapes) { + allocatePaths(`shape_${shape}`); + const compatibilityContainer = `compat-${shape}`; + const compatibilityBlob = `blob-${shape}.txt`; + const compatibilityContent = `compatibility data for ${shape}`; + + const createConfig = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const createServer = new BlobServer(createConfig); + await createServer.start(); + + try { + const baseURL = `${createServer.getHttpServerAddress()}/devstoreaccount1`; + const blobServiceClient = new BlobServiceClient( + baseURL, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + const containerClient = blobServiceClient.getContainerClient( + compatibilityContainer + ); + await containerClient.create(); + + const blobClient = + containerClient.getBlockBlobClient(compatibilityBlob); + await blobClient.upload( + compatibilityContent, + compatibilityContent.length + ); + } finally { + await createServer.close(); + } + + rewritePersistedMd5Shape(shape); + + const loadConfig = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const loadServer = new BlobServer(loadConfig); + await loadServer.start(); + + try { + await new Promise((resolve) => setTimeout(resolve, 300)); + assert.strictEqual( + loadServer.getStatus(), + ServerStatus.Running, + `Server should remain Running for md5 shape ${shape}` + ); + + const loadBaseURL = `${loadServer.getHttpServerAddress()}/devstoreaccount1`; + const loadClient = new BlobServiceClient( + loadBaseURL, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + const containerClient = loadClient.getContainerClient( + compatibilityContainer + ); + const blobClient = + containerClient.getBlockBlobClient(compatibilityBlob); + const properties = await blobClient.getProperties(); + assert.ok( + properties.contentMD5 !== undefined, + `contentMD5 should be restored for md5 shape ${shape}` + ); + } finally { + await loadServer.close(); + await loadServer.clean(); + } + } + }); + + it("should handle persisted null contentMD5 without startup failure", async () => { + const compatibilityContainer = "compat-null-md5"; + const compatibilityBlob = "blob-null-md5.txt"; + const compatibilityContent = "compatibility data for null md5"; + + const createConfig = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const createServer = new BlobServer(createConfig); + await createServer.start(); + + try { + const baseURL = `${createServer.getHttpServerAddress()}/devstoreaccount1`; + const blobServiceClient = new BlobServiceClient( + baseURL, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + const containerClient = blobServiceClient.getContainerClient( + compatibilityContainer + ); + await containerClient.create(); + + const blobClient = containerClient.getBlockBlobClient(compatibilityBlob); + await blobClient.upload( + compatibilityContent, + compatibilityContent.length + ); + } finally { + await createServer.close(); + } + + rewritePersistedMd5AsNull(); + + const loadConfig = new BlobConfiguration( + "127.0.0.1", + 0, + DEFAULT_BLOB_KEEP_ALIVE_TIMEOUT, + upgradeTestDbPath, + upgradeTestDbExtentPath, + [ + { + locationId: "test", + locationPath: upgradeBlobStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const loadServer = new BlobServer(loadConfig); + await loadServer.start(); + + try { + await new Promise((resolve) => setTimeout(resolve, 300)); + assert.strictEqual( + loadServer.getStatus(), + ServerStatus.Running, + "Server should remain Running for null contentMD5" + ); + + const loadBaseURL = `${loadServer.getHttpServerAddress()}/devstoreaccount1`; + const loadClient = new BlobServiceClient( + loadBaseURL, + newPipeline( + new StorageSharedKeyCredential( + EMULATOR_ACCOUNT_NAME, + EMULATOR_ACCOUNT_KEY + ), + { retryOptions: { maxTries: 1 } } + ) + ); + + const containerClient = loadClient.getContainerClient( + compatibilityContainer + ); + const blobClient = containerClient.getBlockBlobClient(compatibilityBlob); + const properties = await blobClient.getProperties(); + + assert.strictEqual( + properties.contentMD5, + undefined, + "Null persisted contentMD5 should be restored as undefined" + ); + } finally { + await loadServer.close(); + await loadServer.clean(); + } + }); +}); diff --git a/tests/queue/queueSas.test.ts b/tests/queue/queueSas.test.ts index 4052a99c2..e6caa0917 100644 --- a/tests/queue/queueSas.test.ts +++ b/tests/queue/queueSas.test.ts @@ -242,7 +242,44 @@ describe("Queue SAS test", () => { assert.ok(error); assert.deepEqual(error.statusCode, 403); - assert.deepEqual(error.code, 'AuthenticationFailed'); + assert.deepEqual(error.code, "AuthenticationFailed"); + }); + + it("generateAccountSASQueryParameters should reject duplicate SAS signature query @loki", async () => { + const tmr = new Date(); + tmr.setDate(tmr.getDate() + 1); + + const storageSharedKeyCredential = (serviceClient as any).credential; + + const sas = generateAccountSASQueryParameters( + { + expiresOn: tmr, + permissions: AccountSASPermissions.parse("rwdlacup"), + resourceTypes: AccountSASResourceTypes.parse("sco").toString(), + services: AccountSASServices.parse("btqf").toString(), + protocol: SASProtocol.HttpsAndHttp, + version: "2019-02-02" + }, + storageSharedKeyCredential as StorageSharedKeyCredential + ).toString(); + + const sasWithDuplicateSig = `${sas}&sig=duplicated-signature`; + const sasURL = `${serviceClient.url}?${sasWithDuplicateSig}`; + const serviceClientWithSAS = new QueueServiceClient( + sasURL, + newPipeline(new AnonymousCredential()) + ); + + let error; + try { + await serviceClientWithSAS.getProperties(); + } catch (err) { + error = err; + } + + assert.ok(error); + assert.deepStrictEqual(error.statusCode, 403); + assert.deepStrictEqual(error.code, "AuthenticationFailed"); }); it("Create queue should work with write (w) or create (c) permission in account SAS @loki", async () => { diff --git a/tests/queue/startupErrorRecovery.test.ts b/tests/queue/startupErrorRecovery.test.ts new file mode 100644 index 000000000..f6abd7215 --- /dev/null +++ b/tests/queue/startupErrorRecovery.test.ts @@ -0,0 +1,166 @@ +import * as assert from "assert"; +import * as fs from "fs-extra"; + +import QueueConfiguration from "../../src/queue/QueueConfiguration"; +import QueueServer from "../../src/queue/QueueServer"; +import { configLogger } from "../../src/common/Logger"; +import { DEFAULT_QUEUE_KEEP_ALIVE_TIMEOUT } from "../../src/queue/utils/constants"; +import { ServerStatus } from "../../src/common/ServerBase"; + +// Set true to enable debug log +configLogger(false); + +describe("Queue Server Startup Error Recovery - Issue #2672 @loki", () => { + const testDbPath = "__test_queue_startup_error_db__.json"; + const testDbExtentPath = "__test_queue_startup_error_db_extent__.json"; + const queueStoragePath = "__test_queue_startup_error_storage__"; + + async function startWithTimeout(server: QueueServer): Promise { + let timeout: ReturnType | undefined; + + try { + await Promise.race([ + server.start(), + new Promise((_, reject) => { + timeout = setTimeout(() => { + reject(new Error("Server startup timeout after 10 seconds")); + }, 10000); + + const nodeTimer = timeout as unknown as { unref?: () => void }; + if (typeof nodeTimer.unref === "function") { + nodeTimer.unref(); + } + }) + ]); + } finally { + if (timeout !== undefined) { + clearTimeout(timeout); + } + } + } + + after(async () => { + // Clean up test artifacts + [testDbPath, testDbExtentPath, queueStoragePath].forEach((p) => { + if (fs.existsSync(p)) { + fs.removeSync(p); + } + }); + }); + + it("Queue: should start successfully when no persisted data exists (fresh start)", async () => { + // Clean paths before test + [testDbPath, testDbExtentPath, queueStoragePath].forEach((p) => { + if (fs.existsSync(p)) { + fs.removeSync(p); + } + }); + + const config = new QueueConfiguration( + "127.0.0.1", + 0, + DEFAULT_QUEUE_KEEP_ALIVE_TIMEOUT, + testDbPath, + testDbExtentPath, + [ + { + locationId: "test", + locationPath: queueStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server = new QueueServer(config); + + try { + // Should start without errors + await server.start(); + assert.strictEqual(server.getStatus(), ServerStatus.Running); + + await server.close(); + assert.strictEqual(server.getStatus(), ServerStatus.Closed); + } finally { + await server.clean(); + } + }); + + it("Queue: should handle GC startup errors gracefully without throwing 'Cannot close server in status Starting'", async () => { + // Clean paths before test + [testDbPath, testDbExtentPath, queueStoragePath].forEach((p) => { + if (fs.existsSync(p)) { + fs.removeSync(p); + } + }); + + // Create a corrupted metadata file to simulate legacy data + const corruptedMetadata = { + schemaVersion: "3.35.0", + data: { + collections: [ + { + name: "queues", + data: [ + { + name: "test-queue", + meta: { revision: 0, created: 1600000000000, version: 0 } + } + ] + } + ] + } + }; + + fs.writeFileSync(testDbPath, JSON.stringify(corruptedMetadata, null, 2)); + + const config = new QueueConfiguration( + "127.0.0.1", + 0, + DEFAULT_QUEUE_KEEP_ALIVE_TIMEOUT, + testDbPath, + testDbExtentPath, + [ + { + locationId: "test", + locationPath: queueStoragePath, + maxConcurrency: 10 + } + ], + false + ); + + const server = new QueueServer(config); + + try { + await startWithTimeout(server); + + assert.notStrictEqual( + server.getStatus(), + ServerStatus.Starting, + "Queue server should not remain in Starting state after startup completes" + ); + + if (server.getStatus() === ServerStatus.Running) { + await server.close(); + } + } catch (err) { + if (!(err instanceof Error)) { + throw err; + } + + if (err.message.includes("Cannot close server in status Starting")) { + assert.fail(`Bug not fixed in Queue: ${err.message}`); + } + + // Do not swallow startup failures such as timeout/hang. + throw err; + } finally { + try { + await server.clean(); + } catch { + // Cleanup may fail if startup failed, that's ok + } + } + }); +}); diff --git a/tests/table/auth/sas.test.ts b/tests/table/auth/sas.test.ts index 952e1ae5d..13808c053 100644 --- a/tests/table/auth/sas.test.ts +++ b/tests/table/auth/sas.test.ts @@ -707,4 +707,57 @@ describe("Shared Access Signature (SAS) authentication", () => { stored = await adminClient.getEntity("part1", "row5"); assert.strictEqual(stored.myValue, "value2"); }); + + it("12. duplicate SAS signature query should fail authentication @loki", async () => { + const tableName: string = getUniqueName("sas12"); + + const conn = createConnectionStringForTest(testLocalAzuriteInstance); + const accountName = /AccountName=([^;]*)/.exec(conn)![1]; + const accountKey = /AccountKey=([^;]*)/.exec(conn)![1]; + const baseUrl = getBaseUrlForTest(); + + const adminClient = TableClient.fromConnectionString(conn, tableName, { + allowInsecureConnection: testLocalAzuriteInstance + }); + + await adminClient.createTable(); + + const now = Date.now(); + const expiry = new Date(now + 5 * 60 * 1000).toISOString(); + + const sas = generateTableSasToken({ + accountName, + accountKey, + tableName, + permissions: "a", + expiry + }); + + const duplicateSigToken = `${sas}&sig=duplicated-signature`; + const sasClient = new TableClient( + baseUrl, + tableName, + new AzureSASCredential(duplicateSigToken), + { + allowInsecureConnection: testLocalAzuriteInstance + } + ); + + let error; + try { + await sasClient.createEntity({ + partitionKey: "part1", + rowKey: "row-duplicate-sig", + myValue: "value1" + }); + } catch (err: any) { + error = err; + } + + assert.ok(error); + assert.strictEqual(error.statusCode, 403); + if (error.code) { + assert.strictEqual(error.code, "AuthenticationFailed"); + } + }); });