From ea3947867461dde2eb0e407f8c79e53d3564fb12 Mon Sep 17 00:00:00 2001 From: Francois Ferrand Date: Wed, 30 Sep 2026 09:17:33 +0200 Subject: [PATCH 1/6] Bump smithy-aws-typescript-codegen to 0.54.0 The 0.34.0 codegen pinned exact, now outdated @aws-sdk/* versions in the generated clients (@aws-sdk/core 3.856.0 brings a vulnerable fast-xml-parser). 0.54.0 emits a handful of caret ranges on current @aws-sdk/core and @smithy/core instead of ~40 packages. It is built against Smithy 1.73.0, so smithy-aws-traits (and the CLI used in CI) follows. The new schema-based serde reads error bodies itself and only surfaces a JSON SyntaxError for cloudserver's XML errors, so the custom error handler now sits inside the deserializer and maps XML/HTML error responses from the raw response. The generated *FilterSensitiveLog helpers are gone; nothing uses them. Issue: CLDSRVCLT-19 --- smithy-build.json | 4 +- src/clients/backbeatRoutes.ts | 4 +- src/utils.ts | 131 ++++++++++++++++++---------------- 3 files changed, 74 insertions(+), 65 deletions(-) diff --git a/smithy-build.json b/smithy-build.json index 78886c2f..26d36af9 100644 --- a/smithy-build.json +++ b/smithy-build.json @@ -3,8 +3,8 @@ "sources": ["models", "service"], "maven": { "dependencies": [ - "software.amazon.smithy:smithy-aws-traits:1.61.0", - "software.amazon.smithy.typescript:smithy-aws-typescript-codegen:0.34.0" + "software.amazon.smithy:smithy-aws-traits:1.73.0", + "software.amazon.smithy.typescript:smithy-aws-typescript-codegen:0.54.0" ] }, "projections": { diff --git a/src/clients/backbeatRoutes.ts b/src/clients/backbeatRoutes.ts index fa19b571..bc5ba57c 100644 --- a/src/clients/backbeatRoutes.ts +++ b/src/clients/backbeatRoutes.ts @@ -12,10 +12,10 @@ export class BackbeatRoutesClient extends CloudserverBackbeatRoutesClient { signingEscapePath: false, }); - this.middlewareStack.add(createCustomErrorMiddleware(), { + this.middlewareStack.add(createCustomErrorMiddleware(this.config), { step: 'deserialize', name: 'cloudserverErrorHandler', - priority: 'normal', + priority: 'low', }); } } diff --git a/src/utils.ts b/src/utils.ts index 8812565a..208b846d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import { addExpectContinueMiddleware } from '@aws-sdk/middleware-expect-continue'; -import { MiddlewareStack, RequestHandler } from '@smithy/types'; +import { MiddlewareStack, RequestHandler, StreamCollector } from '@smithy/types'; import { XMLParser } from 'fast-xml-parser'; import { CloudserverBackbeatRoutesServiceException @@ -82,71 +82,80 @@ export function addContentLengthMiddleware( return; } -export function createCustomErrorMiddleware() { - return (next: any) => async (args: any) => { - try { - return await next(args); - } catch (error: any) { - const parseXmlError = (xml: string) => { - try { - const result = new XMLParser({}).parse(xml); - return { - code: result.Error?.Code, - message: result.Error?.Message, - requestId: result.Error?.RequestId, - }; - } catch (_parseError) { - return { - code: null, - message: 'Malformed XML error response', - requestId: null, - }; - } - }; - - const response = error.$response; - const statusCode = error.$metadata?.httpStatusCode; - const headers = response?.headers || {}; - const contentType = (headers['content-type'] || '').toLowerCase(); - if (contentType.includes('application/xml') || contentType.includes('text/xml')) { - const body = response?.body; - const xml = body?.toString() || ''; - const errorInfo = parseXmlError(xml); - - const xmlError: any = new CloudserverBackbeatRoutesServiceException({ - name: errorInfo.code || error.name, - message: errorInfo.message || 'XML error response', - $fault: statusCode >= 500 ? 'server' : 'client', - $metadata: error.$metadata || {}, - $response: error.$response, - }); - xmlError.parsedXml = errorInfo; - xmlError.code = errorInfo.code; +function parseXmlError(xml: string) { + try { + const result = new XMLParser({}).parse(xml); + return { + code: result.Error?.Code, + message: result.Error?.Message, + requestId: result.Error?.RequestId, + }; + } catch (_parseError) { + return { + code: null, + message: 'Malformed XML error response', + requestId: null, + }; + } +} - throw xmlError; - } +/** + * Turns XML and HTML error responses into service exceptions, as cloudserver + * and the S3C nginx proxy may return them regardless of the service protocol. + * + * Must run after (inside) the deserializer, on the raw HTTP response: errors + * thrown by the deserializer no longer carry the response body. + */ +export function createCustomErrorMiddleware(config: { streamCollector: StreamCollector }) { + return (next: any) => async (args: any) => { + const result = await next(args); + const response = result?.response; + const statusCode = response?.statusCode; + if (!statusCode || statusCode < 300) { + return result; + } - const s3cNginxProxyResponse = contentType.includes('text/html'); - if (s3cNginxProxyResponse) { - const body = response?.body; - const html = body?.toString() || ''; - const title = html.match(/]*>([^<]+)<\/title>/i); - const message = title && title[1] || 'HTML error response'; + const headers = response.headers || {}; + const contentType = (headers['content-type'] || '').toLowerCase(); + const isXml = contentType.includes('application/xml') || contentType.includes('text/xml'); + const s3cNginxProxyResponse = contentType.includes('text/html'); + if (!isXml && !s3cNginxProxyResponse) { + return result; + } - const htmlError: any = new CloudserverBackbeatRoutesServiceException({ - name: `HTML ${response?.reason || 'Error'}`, - message, - $fault: statusCode >= 500 ? 'server' : 'client', - $metadata: error.$metadata || {}, - $response: error.$response, - }); - htmlError.rawBody = html; - - throw htmlError; - } + const body = new TextDecoder().decode(await config.streamCollector(response.body)); + response.body = body; + const $fault = statusCode >= 500 ? 'server' : 'client'; + const $metadata = { + httpStatusCode: statusCode, + requestId: headers['x-amz-request-id'], + extendedRequestId: headers['x-amz-id-2'], + }; - throw error; + if (isXml) { + const errorInfo = parseXmlError(body); + const xmlError: any = new CloudserverBackbeatRoutesServiceException({ + name: errorInfo.code || 'UnknownError', + message: errorInfo.message || 'XML error response', + $fault, + $metadata, + }); + Object.defineProperty(xmlError, '$response', { value: response, enumerable: false }); + xmlError.parsedXml = errorInfo; + xmlError.code = errorInfo.code; + throw xmlError; } + + const title = body.match(/]*>([^<]+)<\/title>/i); + const htmlError: any = new CloudserverBackbeatRoutesServiceException({ + name: `HTML ${response.reason || 'Error'}`, + message: title && title[1] || 'HTML error response', + $fault, + $metadata, + }); + Object.defineProperty(htmlError, '$response', { value: response, enumerable: false }); + htmlError.rawBody = body; + throw htmlError; }; } From b16535e721a21871c9c3932b6cb0b34cee57781f Mon Sep 17 00:00:00 2001 From: Francois Ferrand Date: Wed, 30 Sep 2026 09:21:42 +0200 Subject: [PATCH 2/6] Stop publishing the codegen node_modules The package listed each typescript-codegen directory wholesale in `files`, so their node_modules got published too: ~400 MB installed, including build-only tools and an old @aws-sdk/core pulling a vulnerable fast-xml-parser. Consumers could not de-duplicate any of it, and yarn audit / osv did not see it either, only image scanners did. The generated clients now resolve their runtime dependencies from the root package.json, which declares them, and the package only ships each client's package.json and dist-* output. The build drops the nested node_modules once the clients are compiled so nothing resolves from there by accident. Issue: CLDSRVCLT-19 --- README.md | 3 + package.json | 18 ++- yarn.lock | 400 +++++++++++++++++++++++++-------------------------- 3 files changed, 216 insertions(+), 205 deletions(-) diff --git a/README.md b/README.md index 4416d2d2..f998ca42 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ The client generation is performed this way : yarn build ``` +The generated clients are shipped inside this package and load their dependencies from the root +`package.json`, so any runtime dependency of a generated client must be declared there as well. + ### Local testing 1. Install dependencies & build the smithy client: `yarn build` diff --git a/package.json b/package.json index 8fe837bb..00ecaf7d 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,8 @@ }, "files": [ "dist", - "build/smithy/cloudserverBackbeatRoutes/typescript-codegen", - "build/smithy/cloudserverBucketQuota/typescript-codegen", - "build/smithy/cloudserverProxyBackbeatApis/typescript-codegen" + "build/smithy/*/typescript-codegen/package.json", + "build/smithy/*/typescript-codegen/dist-*/**" ], "publishConfig": { "access": "public" @@ -30,11 +29,12 @@ "clean:build": "rm -rf build dist", "build:smithy": "smithy build && yarn clean:codegen-gitignore", "clean:codegen-gitignore": "rm -f build/smithy/*/typescript-codegen/.gitignore", + "clean:codegen-node-modules": "rm -rf build/smithy/*/typescript-codegen/node_modules", "build:generated:backbeatRoutes": "cd build/smithy/cloudserverBackbeatRoutes/typescript-codegen && yarn install && yarn build", "build:generated:bucketQuota": "cd build/smithy/cloudserverBucketQuota/typescript-codegen && yarn install && yarn build", "build:generated:proxyBackbeatApis": "cd build/smithy/cloudserverProxyBackbeatApis/typescript-codegen && yarn install && yarn build", "build:wrapper": "tsc", - "build": "yarn install && yarn clean:build && yarn build:smithy && yarn build:generated:backbeatRoutes && yarn build:generated:bucketQuota && yarn build:generated:proxyBackbeatApis && yarn build:wrapper", + "build": "yarn install && yarn clean:build && yarn build:smithy && yarn build:generated:backbeatRoutes && yarn build:generated:bucketQuota && yarn build:generated:proxyBackbeatApis && yarn clean:codegen-node-modules && yarn build:wrapper", "test": "NODE_OPTIONS='--experimental-vm-modules' jest", "test:mongo-backend": "BACKEND_TYPE=mongo yarn test", "test:metadata-backend": "BACKEND_TYPE=metadata yarn test", @@ -56,9 +56,17 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.1009.0", + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/credential-provider-node": "^3.972.84", "@aws-sdk/middleware-expect-continue": "^3.972.8", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", "JSONStream": "^1.3.5", - "fast-xml-parser": "^5.5.7" + "fast-xml-parser": "^5.5.7", + "tslib": "^2.6.2" }, "resolutions": { "flatted": "^3.4.2" diff --git a/yarn.lock b/yarn.lock index 3de035a9..ccec6247 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,226 +2,226 @@ # yarn lockfile v1 -"@aws-sdk/checksums@^3.1000.28": - version "3.1000.28" - resolved "https://registry.yarnpkg.com/@aws-sdk/checksums/-/checksums-3.1000.28.tgz#2a057727dc4b575dd84fe756d437caf4331bc974" - integrity sha512-VCpnmyHQ1IH49ni3LXnQj7DPr7rmcJmzYeiCkYdCcfgNtkvOj38cdcL9lapBWoItZWFACJPFJlymqC7/gem3Gw== - dependencies: - "@aws-sdk/core" "^3.977.8" - "@aws-sdk/types" "^3.974.4" - "@smithy/core" "^3.31.1" - "@smithy/types" "^4.16.1" +"@aws-sdk/checksums@^3.1001.1": + version "3.1001.1" + resolved "https://registry.yarnpkg.com/@aws-sdk/checksums/-/checksums-3.1001.1.tgz#b6c42737c673e3904ead8c83ad5d79c377d746f4" + integrity sha512-x12Q17KYlJAd3nKf8LV5LV0vt8sh8/6YfQLGPtrGnQf/tW4jqxPGq5GPpuVitpQYM3eUR4XB7CbxZf751NMbLw== + dependencies: + "@aws-sdk/core" "^3.978.1" + "@aws-sdk/types" "^3.974.6" + "@smithy/core" "^3.35.0" + "@smithy/types" "^4.19.0" tslib "^2.6.2" "@aws-sdk/client-s3@^3.1009.0": - version "3.1114.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.1114.0.tgz#887da16565a3db735aa03fe71ecbcc016daae5e6" - integrity sha512-ZeAgOtB+CXFaWXph98U7a/XrBlVx1lQ2rCJRWLxLmAaQ9k5lht6DWfwnEcVjdzduK/ySao1tCjq0fBAScGqjAg== - dependencies: - "@aws-sdk/checksums" "^3.1000.28" - "@aws-sdk/core" "^3.977.8" - "@aws-sdk/credential-provider-node" "^3.972.80" - "@aws-sdk/middleware-sdk-s3" "^3.972.74" - "@aws-sdk/signature-v4-multi-region" "^3.996.45" - "@aws-sdk/types" "^3.974.4" - "@smithy/core" "^3.31.1" - "@smithy/fetch-http-handler" "^5.6.13" - "@smithy/node-http-handler" "^4.9.13" - "@smithy/types" "^4.16.1" + version "3.1143.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.1143.0.tgz#0ae1fef4c592cf7d2869af270347a93a066bba87" + integrity sha512-YVcwL17cUDru7OJFMoGB6aJCg7TmMiuAUjKfswtSm+t8NhO5BLLFTUhjnGWrhdw7ZgYyU2Y8u19t8a7yu0EhEg== + dependencies: + "@aws-sdk/checksums" "^3.1001.1" + "@aws-sdk/core" "^3.978.1" + "@aws-sdk/credential-provider-node" "^3.972.84" + "@aws-sdk/middleware-sdk-s3" "^3.972.77" + "@aws-sdk/signature-v4-multi-region" "^3.996.47" + "@aws-sdk/types" "^3.974.6" + "@smithy/core" "^3.35.0" + "@smithy/fetch-http-handler" "^5.8.0" + "@smithy/node-http-handler" "^4.12.1" + "@smithy/types" "^4.19.0" tslib "^2.6.2" -"@aws-sdk/core@^3.977.8": - version "3.977.8" - resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.977.8.tgz#b2860d6d6bd4b147dbf906c5e3c8155337742657" - integrity sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg== +"@aws-sdk/core@^3.978.1": + version "3.978.1" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.978.1.tgz#1e4db18f9c450e0aba550edfee4c6897e09af879" + integrity sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg== dependencies: - "@aws-sdk/types" "^3.974.4" - "@aws-sdk/xml-builder" "^3.972.39" + "@aws-sdk/types" "^3.974.6" + "@aws-sdk/xml-builder" "^3.972.41" "@aws/lambda-invoke-store" "^0.3.0" - "@smithy/core" "^3.31.1" - "@smithy/signature-v4" "^5.6.12" - "@smithy/types" "^4.16.1" + "@smithy/core" "^3.35.0" + "@smithy/signature-v4" "^5.7.3" + "@smithy/types" "^4.19.0" bowser "^2.11.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-env@^3.972.69": - version "3.972.69" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.69.tgz#9ef8e8e6abd048ae4c9bd8ea71fe8e79bcb48204" - integrity sha512-AreCFzcB4kH2HF9031Ot0jSJr3KXvRg6e8uDeub20JEVdZU3Bv0sTq1plc7VsT3KiqutlzH7l0j50UcCWHUioA== +"@aws-sdk/credential-provider-env@^3.972.72": + version "3.972.72" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.72.tgz#5bf3c87784901426716dad12f2036b87ce2d9793" + integrity sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew== dependencies: - "@aws-sdk/core" "^3.977.8" - "@aws-sdk/types" "^3.974.4" - "@smithy/core" "^3.31.1" - "@smithy/types" "^4.16.1" + "@aws-sdk/core" "^3.978.1" + "@aws-sdk/types" "^3.974.6" + "@smithy/core" "^3.35.0" + "@smithy/types" "^4.19.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-http@^3.972.71": - version "3.972.71" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.71.tgz#94f74f2e145df28b0b15849330b99f9c83c3f978" - integrity sha512-A8ObcqVmDMnk4F9NozZ7JwmUu9Q4xyBJkmyq1C5U+wNM9ht9J7+EuuyabsLWXZnOoTqFaJuYBYTKf5CTipkEjA== - dependencies: - "@aws-sdk/core" "^3.977.8" - "@aws-sdk/types" "^3.974.4" - "@smithy/core" "^3.31.1" - "@smithy/fetch-http-handler" "^5.6.13" - "@smithy/node-http-handler" "^4.9.13" - "@smithy/types" "^4.16.1" +"@aws-sdk/credential-provider-http@^3.972.74": + version "3.972.74" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.74.tgz#15b366f17177ac07a7b463662e02f2ead1fcfa67" + integrity sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw== + dependencies: + "@aws-sdk/core" "^3.978.1" + "@aws-sdk/types" "^3.974.6" + "@smithy/core" "^3.35.0" + "@smithy/fetch-http-handler" "^5.8.0" + "@smithy/node-http-handler" "^4.12.1" + "@smithy/types" "^4.19.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-ini@^3.973.14": - version "3.973.14" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.14.tgz#650d856227a36fbfb954f8b93b89f747e8430dd4" - integrity sha512-7c+Wti2LsERNWMfm7ySz3/6RPopFW3Nmn7s63Xpcq6R/tRuY5hpvkHA2xVgi5ukJbvok9l0IDtVEvqTtg+X7dw== - dependencies: - "@aws-sdk/core" "^3.977.8" - "@aws-sdk/credential-provider-env" "^3.972.69" - "@aws-sdk/credential-provider-http" "^3.972.71" - "@aws-sdk/credential-provider-login" "^3.972.76" - "@aws-sdk/credential-provider-process" "^3.972.69" - "@aws-sdk/credential-provider-sso" "^3.973.13" - "@aws-sdk/credential-provider-web-identity" "^3.972.75" - "@aws-sdk/nested-clients" "^3.997.43" - "@aws-sdk/types" "^3.974.4" - "@smithy/core" "^3.31.1" - "@smithy/credential-provider-imds" "^4.4.16" - "@smithy/types" "^4.16.1" +"@aws-sdk/credential-provider-ini@^3.973.17": + version "3.973.17" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.17.tgz#4f4b329cb3252322dc2261fe2985c45eeefbcd22" + integrity sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA== + dependencies: + "@aws-sdk/core" "^3.978.1" + "@aws-sdk/credential-provider-env" "^3.972.72" + "@aws-sdk/credential-provider-http" "^3.972.74" + "@aws-sdk/credential-provider-login" "^3.972.79" + "@aws-sdk/credential-provider-process" "^3.972.72" + "@aws-sdk/credential-provider-sso" "^3.973.16" + "@aws-sdk/credential-provider-web-identity" "^3.972.78" + "@aws-sdk/nested-clients" "^3.997.46" + "@aws-sdk/types" "^3.974.6" + "@smithy/core" "^3.35.0" + "@smithy/credential-provider-imds" "^4.5.2" + "@smithy/types" "^4.19.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-login@^3.972.76": - version "3.972.76" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.76.tgz#83dc4012f8d218c6867ecea389103c4ab78f4f7f" - integrity sha512-LVixwOnEJfrrfKHeZjBA8pIMTZjNDq8ak8VpcoWUuCJDrSnBNU8POJksULMgvN089P0MXtQYH2Zs627/MK1K0g== +"@aws-sdk/credential-provider-login@^3.972.79": + version "3.972.79" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.79.tgz#7b7b75f051e0cb79958a0d2e894f26660e65ad0f" + integrity sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw== dependencies: - "@aws-sdk/core" "^3.977.8" - "@aws-sdk/nested-clients" "^3.997.43" - "@aws-sdk/types" "^3.974.4" - "@smithy/core" "^3.31.1" - "@smithy/types" "^4.16.1" + "@aws-sdk/core" "^3.978.1" + "@aws-sdk/nested-clients" "^3.997.46" + "@aws-sdk/types" "^3.974.6" + "@smithy/core" "^3.35.0" + "@smithy/types" "^4.19.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-node@^3.972.80": - version "3.972.80" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.80.tgz#1a3db0a35091468c5cd32d869f031fc6a2420d8e" - integrity sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg== - dependencies: - "@aws-sdk/credential-provider-env" "^3.972.69" - "@aws-sdk/credential-provider-http" "^3.972.71" - "@aws-sdk/credential-provider-ini" "^3.973.14" - "@aws-sdk/credential-provider-process" "^3.972.69" - "@aws-sdk/credential-provider-sso" "^3.973.13" - "@aws-sdk/credential-provider-web-identity" "^3.972.75" - "@aws-sdk/types" "^3.974.4" - "@smithy/core" "^3.31.1" - "@smithy/credential-provider-imds" "^4.4.16" - "@smithy/types" "^4.16.1" +"@aws-sdk/credential-provider-node@^3.972.84": + version "3.972.84" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.84.tgz#226bc8d9110d0562dc895e609ec54b4bc8f68155" + integrity sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA== + dependencies: + "@aws-sdk/credential-provider-env" "^3.972.72" + "@aws-sdk/credential-provider-http" "^3.972.74" + "@aws-sdk/credential-provider-ini" "^3.973.17" + "@aws-sdk/credential-provider-process" "^3.972.72" + "@aws-sdk/credential-provider-sso" "^3.973.16" + "@aws-sdk/credential-provider-web-identity" "^3.972.78" + "@aws-sdk/types" "^3.974.6" + "@smithy/core" "^3.35.0" + "@smithy/credential-provider-imds" "^4.5.2" + "@smithy/types" "^4.19.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-process@^3.972.69": - version "3.972.69" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.69.tgz#d97e64a37d25662913f6314562781a9c9e95516b" - integrity sha512-9kpTNdZTrcqXTfhxM7fgl9Z68ek3Fu5oe3Yf+A/pJGibEqpgZxz2tSY7SinmyCIU2PJ+ygY4FPoBBnLpocMtrQ== +"@aws-sdk/credential-provider-process@^3.972.72": + version "3.972.72" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.72.tgz#c4efac7cf0582351c98ab16675d8d6944b6df000" + integrity sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A== dependencies: - "@aws-sdk/core" "^3.977.8" - "@aws-sdk/types" "^3.974.4" - "@smithy/core" "^3.31.1" - "@smithy/types" "^4.16.1" + "@aws-sdk/core" "^3.978.1" + "@aws-sdk/types" "^3.974.6" + "@smithy/core" "^3.35.0" + "@smithy/types" "^4.19.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-sso@^3.973.13": - version "3.973.13" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.13.tgz#005f76bf069e78089c79ec2c1b9d50a07eddabb3" - integrity sha512-Oc81qauMPzUoTnAS2YKpNwY6sY/LUyQTEeaf6yP197WMxkEBQfcKLR1MFpD7+pNTubXnfkH6gwpji+Gc7iyD2Q== - dependencies: - "@aws-sdk/core" "^3.977.8" - "@aws-sdk/nested-clients" "^3.997.43" - "@aws-sdk/token-providers" "3.1111.0" - "@aws-sdk/types" "^3.974.4" - "@smithy/core" "^3.31.1" - "@smithy/types" "^4.16.1" +"@aws-sdk/credential-provider-sso@^3.973.16": + version "3.973.16" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.16.tgz#bdaf55e64c96df4308fd1fdabbdc13136b58c144" + integrity sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g== + dependencies: + "@aws-sdk/core" "^3.978.1" + "@aws-sdk/nested-clients" "^3.997.46" + "@aws-sdk/token-providers" "3.1138.0" + "@aws-sdk/types" "^3.974.6" + "@smithy/core" "^3.35.0" + "@smithy/types" "^4.19.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-web-identity@^3.972.75": - version "3.972.75" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.75.tgz#c0a5980c55301aabfbf5f9938f2ba78444ee026e" - integrity sha512-YPN6uoGDgjjjeVFZrcOeCJqmB6zpXoeeNgIjqe+DexJaWqdjVfCCe+VAZwli9Z2h8KhFW8oxkO39emQ1tyz/Mw== +"@aws-sdk/credential-provider-web-identity@^3.972.78": + version "3.972.78" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.78.tgz#2ff347600d79a2b838296da3278998c7a1cdcbac" + integrity sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA== dependencies: - "@aws-sdk/core" "^3.977.8" - "@aws-sdk/nested-clients" "^3.997.43" - "@aws-sdk/types" "^3.974.4" - "@smithy/core" "^3.31.1" - "@smithy/types" "^4.16.1" + "@aws-sdk/core" "^3.978.1" + "@aws-sdk/nested-clients" "^3.997.46" + "@aws-sdk/types" "^3.974.6" + "@smithy/core" "^3.35.0" + "@smithy/types" "^4.19.0" tslib "^2.6.2" "@aws-sdk/middleware-expect-continue@^3.972.8": - version "3.972.43" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.43.tgz#b2c9902f4d24614b9638dfe3cff6872bfd93ad5c" - integrity sha512-AghIJeHCYdofFmEzg0gCbMtfB+g9VFNfy7EWV9f+lI59g4p9FpAd1fAjPj39K2LXzHn+J0U/9Qa4RwrauCDe1w== + version "3.972.46" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.46.tgz#3b31060962836799c3bb1876240b0e827042b9e6" + integrity sha512-+dyYf1VYmZu1hD5yl9V8W9/OHymUANnKgLRu4ABizzaPv4n0JoDEnBBZKWZQRDParc8h7IzRicBTw3kEycaAAQ== dependencies: - "@aws-sdk/middleware-sdk-s3" "^3.972.74" + "@aws-sdk/middleware-sdk-s3" "^3.972.77" tslib "^2.6.2" -"@aws-sdk/middleware-sdk-s3@^3.972.74": - version "3.972.74" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.74.tgz#203bb14775dcdc8f2b0c4984125da09708d8496d" - integrity sha512-2lzoV2z2QO5KJZYGOCnIZ1WVQgzMECvwuzr1xb034a++8QW4U4eGrmC2u4yg1xvNv4TLL/Uv5DLyuAiw0b9z7Q== - dependencies: - "@aws-sdk/core" "^3.977.8" - "@aws-sdk/signature-v4-multi-region" "^3.996.45" - "@aws-sdk/types" "^3.974.4" - "@smithy/core" "^3.31.1" - "@smithy/types" "^4.16.1" +"@aws-sdk/middleware-sdk-s3@^3.972.77": + version "3.972.77" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.77.tgz#f1006f029292db2658dbec0d175b66f6c2427523" + integrity sha512-E7W2UOeUoc+lg3uIfR/dM7ZwusHwhBQrKMnlkRv4EXRR+C0YtV1pg25xC7GdZIhXH+NAMgZPCbE7o5to2cjFiw== + dependencies: + "@aws-sdk/core" "^3.978.1" + "@aws-sdk/signature-v4-multi-region" "^3.996.47" + "@aws-sdk/types" "^3.974.6" + "@smithy/core" "^3.35.0" + "@smithy/types" "^4.19.0" tslib "^2.6.2" -"@aws-sdk/nested-clients@^3.997.43": - version "3.997.43" - resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.997.43.tgz#6fa60e5fad1e97267b2773f0750d37511f9d698a" - integrity sha512-bit+VpqWNyi3wHxFoTsTliNXimCSL2r2OeDTm7ZrG+YsTZ2D7ofDJ6r/t9PVBn80i6/v0X2h9Tgw6QP2MAKfPw== - dependencies: - "@aws-sdk/core" "^3.977.8" - "@aws-sdk/signature-v4-multi-region" "^3.996.45" - "@aws-sdk/types" "^3.974.4" - "@smithy/core" "^3.31.1" - "@smithy/fetch-http-handler" "^5.6.13" - "@smithy/node-http-handler" "^4.9.13" - "@smithy/types" "^4.16.1" +"@aws-sdk/nested-clients@^3.997.46": + version "3.997.46" + resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.997.46.tgz#71fce1a139f9f81c81603d704c9300f45be74477" + integrity sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ== + dependencies: + "@aws-sdk/core" "^3.978.1" + "@aws-sdk/signature-v4-multi-region" "^3.996.47" + "@aws-sdk/types" "^3.974.6" + "@smithy/core" "^3.35.0" + "@smithy/fetch-http-handler" "^5.8.0" + "@smithy/node-http-handler" "^4.12.1" + "@smithy/types" "^4.19.0" tslib "^2.6.2" -"@aws-sdk/signature-v4-multi-region@^3.996.45": - version "3.996.45" - resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.45.tgz#7d8d1d2c769327b9b5c624ee577ea1248826af7a" - integrity sha512-bBuyztukzXq6plzFGHAWiQt0QXo+HL8b8lX5cFTzkez/74PtS1c0qPFCIVuHkyoT+miH2qOjAcm1/yoro2ESPA== +"@aws-sdk/signature-v4-multi-region@^3.996.47": + version "3.996.47" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.47.tgz#4a707c1d5b43ea242897c393b03f8583fcfbb8ab" + integrity sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g== dependencies: - "@aws-sdk/types" "^3.974.4" - "@smithy/signature-v4" "^5.6.12" - "@smithy/types" "^4.16.1" + "@aws-sdk/types" "^3.974.6" + "@smithy/signature-v4" "^5.7.3" + "@smithy/types" "^4.19.0" tslib "^2.6.2" -"@aws-sdk/token-providers@3.1111.0": - version "3.1111.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.1111.0.tgz#0a91fe03ab928b32032d0d30c8a191eccb91b3a5" - integrity sha512-JfljgoVtl+s3Qy21n9a7Z48uCQaOXcN74KJ3TEQfPoB293GrXFSt6HSQJF1sTZ8c/5QedEvd3NjJQMO4u9qa5A== +"@aws-sdk/token-providers@3.1138.0": + version "3.1138.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.1138.0.tgz#bf7042681ed21c64a0c4f1bc0e8f18f9be59af2c" + integrity sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw== dependencies: - "@aws-sdk/core" "^3.977.8" - "@aws-sdk/nested-clients" "^3.997.43" - "@aws-sdk/types" "^3.974.4" - "@smithy/core" "^3.31.1" - "@smithy/types" "^4.16.1" + "@aws-sdk/core" "^3.978.1" + "@aws-sdk/nested-clients" "^3.997.46" + "@aws-sdk/types" "^3.974.6" + "@smithy/core" "^3.35.0" + "@smithy/types" "^4.19.0" tslib "^2.6.2" -"@aws-sdk/types@^3.974.4": - version "3.974.4" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.974.4.tgz#c68582caa8568de90d106e4717f1559164b6949a" - integrity sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A== +"@aws-sdk/types@^3.974.6": + version "3.974.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.974.6.tgz#e388d89a012ce70e60fef26174562f99285b1e95" + integrity sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg== dependencies: - "@smithy/types" "^4.16.1" + "@smithy/types" "^4.19.0" tslib "^2.6.2" -"@aws-sdk/xml-builder@^3.972.39": - version "3.972.39" - resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.39.tgz#d55108a214b60f1bf88429dc952cb4e9ba9e0632" - integrity sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA== +"@aws-sdk/xml-builder@^3.972.41": + version "3.972.41" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz#d3154d8893c3a85f58ce629b70a17ce70e0a1137" + integrity sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ== dependencies: - "@smithy/types" "^4.16.1" + "@smithy/types" "^4.19.0" tslib "^2.6.2" "@aws/lambda-invoke-store@^0.3.0": @@ -982,15 +982,15 @@ dependencies: "@sinonjs/commons" "^3.0.1" -"@smithy/core@^3.31.1", "@smithy/core@^3.33.2": - version "3.33.2" - resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.33.2.tgz#64cad79d96e293e1b2bddd8491d869afe5d94696" - integrity sha512-CUGXpnPkVdjUCbix+83sWLW9VFgQOm44MDOx/ihITJMAnOZKvL8YYIc7DR9pP/tZ8CIRvMiON/TucvygqbHO3w== +"@smithy/core@^3.33.2", "@smithy/core@^3.33.3", "@smithy/core@^3.35.0": + version "3.35.0" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.35.0.tgz#52f180a314d735af8ba8970967c4c9499fc21d3e" + integrity sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A== dependencies: - "@smithy/types" "^4.17.2" + "@smithy/types" "^4.19.0" tslib "^2.6.2" -"@smithy/credential-provider-imds@^4.4.16": +"@smithy/credential-provider-imds@^4.5.2": version "4.5.2" resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz#da3ea9636b5566b36b0761382d841a6d8fdde14f" integrity sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg== @@ -999,37 +999,37 @@ "@smithy/types" "^4.17.2" tslib "^2.6.2" -"@smithy/fetch-http-handler@^5.6.13": - version "5.7.2" - resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.7.2.tgz#29e95cd74fe91495225e26a60569617fecae48cc" - integrity sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw== +"@smithy/fetch-http-handler@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz#349464e31682d739a74efa42362756be49eb2883" + integrity sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA== dependencies: - "@smithy/core" "^3.33.2" - "@smithy/types" "^4.17.2" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.18.0" tslib "^2.6.2" -"@smithy/node-http-handler@^4.9.13": - version "4.11.2" - resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.11.2.tgz#68088896f09db7c7cc91bda7bba80376190a11b0" - integrity sha512-avwAh9HM3h2lcfjvP3zYIZGf+XVgLQ91wOJ2qoFbNpW1UZeZb33aGlhTZvtkANHfcGhJroRY64525OjfgOg30g== +"@smithy/node-http-handler@^4.12.1": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz#e4da9379d228517ba85845081e4e6758ca315c5d" + integrity sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw== dependencies: - "@smithy/core" "^3.33.2" - "@smithy/types" "^4.17.2" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.18.0" tslib "^2.6.2" -"@smithy/signature-v4@^5.6.12": - version "5.7.2" - resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.7.2.tgz#26b0c17492d912ce352c3479d1153dd6fefa7f75" - integrity sha512-P7Ki6px6OOrxVtx8K7nLmyx4SlXUW/uTKDdMG44UHefmPGSRMBKe2v+TM59WdLcpUIrBrnuCsIqiM2MbsZjmhw== +"@smithy/signature-v4@^5.7.3": + version "5.7.4" + resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.7.4.tgz#f188be5238ddb5c5b3117f6fb75d36a38f8b033f" + integrity sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ== dependencies: - "@smithy/core" "^3.33.2" - "@smithy/types" "^4.17.2" + "@smithy/core" "^3.35.0" + "@smithy/types" "^4.19.0" tslib "^2.6.2" -"@smithy/types@^4.16.1", "@smithy/types@^4.17.2": - version "4.17.2" - resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.17.2.tgz#ad71f6f62460a6409f0e8efc173e2e0f883ea4e6" - integrity sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA== +"@smithy/types@^4.17.2", "@smithy/types@^4.18.0", "@smithy/types@^4.19.0": + version "4.19.0" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.19.0.tgz#a0dd9c9d7b2a070fc7c3d69efedea8404b3646be" + integrity sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q== dependencies: tslib "^2.6.2" From 9e857c329e213bde1f2778f752d6085bf0b609be Mon Sep 17 00:00:00 2001 From: Francois Ferrand Date: Wed, 30 Sep 2026 09:22:38 +0200 Subject: [PATCH 3/6] Check the npm package installs and loads in CI Packaging regressions have slipped through twice now: 1.0.10 shipped without the generated clients, 1.0.12 with their node_modules. Neither showed up in our tests, which run from the repository. CI now packs the build, rejects it if it contains node_modules, installs it in a scratch consumer with production dependencies only, and checks the package and each generated client load, can send a request and type-check. The release workflow runs the same check on the tarball it publishes. Issue: CLDSRVCLT-19 --- .github/workflows/release.yml | 3 +++ .github/workflows/test.yml | 3 +++ README.md | 1 + eslint.config.mjs | 10 +++++++- package.json | 1 + tests/packaging/check-package.sh | 32 +++++++++++++++++++++++ tests/packaging/consumer.package.json | 5 ++++ tests/packaging/smoke.js | 37 +++++++++++++++++++++++++++ tests/packaging/tsconfig.json | 12 +++++++++ tests/packaging/types.ts | 23 +++++++++++++++++ 10 files changed, 126 insertions(+), 1 deletion(-) create mode 100755 tests/packaging/check-package.sh create mode 100644 tests/packaging/consumer.package.json create mode 100644 tests/packaging/smoke.js create mode 100644 tests/packaging/tsconfig.json create mode 100644 tests/packaging/types.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73e2bc58..0f90868f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,6 +66,9 @@ jobs: - name: Pack tarball run: npm pack --ignore-scripts + - name: Check tarball + run: bash tests/packaging/check-package.sh *.tgz + - name: Attest build provenance uses: actions/attest-build-provenance@v4 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09538e30..0e639826 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,9 @@ jobs: - name: Lint run: yarn lint + - name: Check npm package + run: yarn check:package + test-mongodb-backend: name: Test with MongoDB backend runs-on: ubuntu-24.04 diff --git a/README.md b/README.md index f998ca42..09d4e870 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ yarn build The generated clients are shipped inside this package and load their dependencies from the root `package.json`, so any runtime dependency of a generated client must be declared there as well. +`yarn check:package` packs the build and verifies it installs and loads in a clean consumer. ### Local testing diff --git a/eslint.config.mjs b/eslint.config.mjs index be152356..864aa952 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -34,5 +34,13 @@ export default tseslint.config( }], '@typescript-eslint/ban-ts-comment': 'off', } - } + }, + { + // Runs as a plain CommonJS consumer of the published package + files: ['tests/packaging/**/*.js'], + rules: { + '@typescript-eslint/no-require-imports': 'off', + 'no-console': 'off', + }, + }, ); diff --git a/package.json b/package.json index 00ecaf7d..c17eb9e2 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "test:metadata-backend": "BACKEND_TYPE=metadata yarn test", "test:backbeat-apis": "BACKBEAT_SETUP=true yarn test -- tests/testBackbeatProxyApis.test.ts", "lint": "eslint src tests", + "check:package": "bash tests/packaging/check-package.sh", "typecheck": "tsc --noEmit" }, "devDependencies": { diff --git a/tests/packaging/check-package.sh b/tests/packaging/check-package.sh new file mode 100755 index 00000000..80c81699 --- /dev/null +++ b/tests/packaging/check-package.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Installs the npm tarball into a scratch consumer and checks it loads, sends +# requests and type-checks there. +# +# Usage: check-package.sh [package.tgz] (packs the current build if omitted) +set -euo pipefail + +HERE=$(cd "$(dirname "$0")" && pwd) +WORK_DIR=$(mktemp -d) +trap 'rm -rf "$WORK_DIR"' EXIT + +if [ $# -ge 1 ]; then + TARBALL=$(cd "$(dirname "$1")" && pwd)/$(basename "$1") +else + (cd "$HERE/../.." && npm pack --ignore-scripts --silent --pack-destination "$WORK_DIR" >/dev/null) + TARBALL=$(ls "$WORK_DIR"/*.tgz) +fi + +if tar -tzf "$TARBALL" | grep -q '/node_modules/'; then + echo "ERROR: $(basename "$TARBALL") contains node_modules" >&2 + exit 1 +fi + +cd "$WORK_DIR" +cp "$HERE/consumer.package.json" package.json +cp "$HERE/smoke.js" "$HERE/types.ts" "$HERE/tsconfig.json" . +npm install --omit=dev --ignore-scripts --no-audit --no-fund --silent "$TARBALL" +node smoke.js + +npm install --no-save --ignore-scripts --no-audit --no-fund --silent typescript @types/node@20 +npx --no-install tsc -p tsconfig.json +echo "types OK" diff --git a/tests/packaging/consumer.package.json b/tests/packaging/consumer.package.json new file mode 100644 index 00000000..2d07cac3 --- /dev/null +++ b/tests/packaging/consumer.package.json @@ -0,0 +1,5 @@ +{ + "name": "cloudserverclient-packaging-consumer", + "version": "0.0.0", + "private": true +} diff --git a/tests/packaging/smoke.js b/tests/packaging/smoke.js new file mode 100644 index 00000000..86442cb4 --- /dev/null +++ b/tests/packaging/smoke.js @@ -0,0 +1,37 @@ +// Sends one request with each generated client, from a consumer that only +// has the published tarball installed. +const assert = require('assert'); +const http = require('http'); +const pkg = require('@scality/cloudserverclient'); +require('@scality/cloudserverclient/clients/backbeatRoutes'); +require('@scality/cloudserverclient/clients/bucketQuota'); +require('@scality/cloudserverclient/clients/proxyBackbeatApis'); + +const requests = []; +const server = http.createServer((req, res) => { + requests.push(`${req.method} ${req.url}`); + assert.match(req.headers.authorization || '', /^AWS4-HMAC-SHA256 /); + req.resume(); + req.on('end', () => res.writeHead(200, { 'content-length': 0 }).end()); +}); + +server.listen(0, '127.0.0.1', async () => { + const client = new pkg.CloudserverClient({ + endpoint: `http://127.0.0.1:${server.address().port}`, + region: 'us-east-1', + credentials: { accessKeyId: 'accessKey', secretAccessKey: 'secretKey' }, + maxAttempts: 1, + }); + try { + await client.backbeatRoutes.send(new pkg.GetRaftIdCommand({ Bucket: 'bucket' })); + await client.bucketQuota.send(new pkg.GetBucketQuotaCommand({ Bucket: 'bucket' })); + await client.proxyBackbeatApis.send(new pkg.CheckConnectionCommand({})); + assert.strictEqual(requests.length, 3); + console.log(`runtime OK: ${requests.join(', ')}`); + } catch (err) { + console.error(err); + process.exitCode = 1; + } finally { + server.close(); + } +}); diff --git a/tests/packaging/tsconfig.json b/tests/packaging/tsconfig.json new file mode 100644 index 00000000..7f25955f --- /dev/null +++ b/tests/packaging/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "node16", + "moduleResolution": "node16", + "target": "es2022", + "strict": true, + "noEmit": true, + "skipLibCheck": false, + "types": ["node"] + }, + "files": ["types.ts"] +} diff --git a/tests/packaging/types.ts b/tests/packaging/types.ts new file mode 100644 index 00000000..3a8c8fa0 --- /dev/null +++ b/tests/packaging/types.ts @@ -0,0 +1,23 @@ +import { + CloudserverClient, + GetRaftIdCommand, + GetBucketQuotaCommand, + CheckConnectionCommand, +} from '@scality/cloudserverclient'; +import { BackbeatRoutesClient } from '@scality/cloudserverclient/clients/backbeatRoutes'; +import { BucketQuotaClient } from '@scality/cloudserverclient/clients/bucketQuota'; +import { ProxyBackbeatApisClient } from '@scality/cloudserverclient/clients/proxyBackbeatApis'; + +const client = new CloudserverClient({ endpoint: 'http://localhost:8000', region: 'us-east-1' }); +const backbeatRoutes: BackbeatRoutesClient = client.backbeatRoutes; +const bucketQuota: BucketQuotaClient = client.bucketQuota; +const proxyBackbeatApis: ProxyBackbeatApisClient = client.proxyBackbeatApis; + +export async function statusCodes(): Promise<(number | undefined)[]> { + const outputs = await Promise.all([ + backbeatRoutes.send(new GetRaftIdCommand({ Bucket: 'bucket' })), + bucketQuota.send(new GetBucketQuotaCommand({ Bucket: 'bucket' })), + proxyBackbeatApis.send(new CheckConnectionCommand({})), + ]); + return outputs.map(o => o.$metadata.httpStatusCode); +} From 70394fbc113c4677714ec81c90ca799f75fb3d82 Mon Sep 17 00:00:00 2001 From: Francois Ferrand Date: Wed, 30 Sep 2026 09:24:11 +0200 Subject: [PATCH 4/6] Fail the build when a generated client dependency is not declared The generated clients load their dependencies from our package.json, and the set they need changes with the Smithy codegen version. A codegen bump that adds a dependency would otherwise only break consumers at runtime. The build now checks every dependency of the generated package.json files is declared at the root, and prints the `yarn add` line to fix it. Issue: CLDSRVCLT-19 --- README.md | 3 ++- package.json | 3 ++- tests/packaging/check-deps.sh | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100755 tests/packaging/check-deps.sh diff --git a/README.md b/README.md index 09d4e870..2b915387 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ yarn build ``` The generated clients are shipped inside this package and load their dependencies from the root -`package.json`, so any runtime dependency of a generated client must be declared there as well. +`package.json`, so any runtime dependency of a generated client must be declared there as well +(`yarn build` fails otherwise). `yarn check:package` packs the build and verifies it installs and loads in a clean consumer. ### Local testing diff --git a/package.json b/package.json index c17eb9e2..23bf44b0 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ }, "scripts": { "clean:build": "rm -rf build dist", - "build:smithy": "smithy build && yarn clean:codegen-gitignore", + "build:smithy": "smithy build && yarn clean:codegen-gitignore && yarn check:deps", + "check:deps": "bash tests/packaging/check-deps.sh", "clean:codegen-gitignore": "rm -f build/smithy/*/typescript-codegen/.gitignore", "clean:codegen-node-modules": "rm -rf build/smithy/*/typescript-codegen/node_modules", "build:generated:backbeatRoutes": "cd build/smithy/cloudserverBackbeatRoutes/typescript-codegen && yarn install && yarn build", diff --git a/tests/packaging/check-deps.sh b/tests/packaging/check-deps.sh new file mode 100755 index 00000000..f9b71b40 --- /dev/null +++ b/tests/packaging/check-deps.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# The generated clients are shipped inside this package, so their runtime +# dependencies must be declared in the root package.json too. +set -euo pipefail + +cd "$(dirname "$0")/../.." + +generated=(build/smithy/*/typescript-codegen/package.json) + +missing=$(comm -23 \ + <(jq -r '.dependencies // {} | keys[]' "${generated[@]}" | sort -u) \ + <(jq -r '.dependencies // {} | keys[]' package.json | sort -u)) + +if [ -n "$missing" ]; then + echo "ERROR: dependencies of the generated clients missing from package.json, add them with:" >&2 + echo " yarn add $(for name in $missing; do + jq -r --arg n "$name" '.dependencies[$n] // empty | "\($n)@\(.)"' "${generated[@]}" | head -1 + done | tr '\n' ' ')" >&2 + exit 1 +fi From 0a09a996933024a5dac05c68317059dad50ea153 Mon Sep 17 00:00:00 2001 From: Francois Ferrand Date: Thu, 1 Oct 2026 22:12:01 +0200 Subject: [PATCH 5/6] Handle cloudserver XML and HTML errors in the protocol The error middleware had to wrap the deserializer and rebuild exceptions by hand from the raw response. Parsing these bodies in the protocol instead hands them to the SDK's own error handling, so modeled errors come back as their generated classes with their header members, the same as for JSON responses. createCustomErrorMiddleware is removed: nothing outside this package uses it. Issue: CLDSRVCLT-19 --- src/clients/backbeatRoutes.ts | 13 +-- src/utils.ts | 117 ++++++++++------------- tests/testErrorHandling.test.ts | 20 ---- tests/testErrorProtocol.test.ts | 164 ++++++++++++++++++++++++++++++++ 4 files changed, 219 insertions(+), 95 deletions(-) create mode 100644 tests/testErrorProtocol.test.ts diff --git a/src/clients/backbeatRoutes.ts b/src/clients/backbeatRoutes.ts index bc5ba57c..9e06401b 100644 --- a/src/clients/backbeatRoutes.ts +++ b/src/clients/backbeatRoutes.ts @@ -1,21 +1,16 @@ -import { - CloudserverBackbeatRoutesClient, +import { + CloudserverBackbeatRoutesClient, CloudserverBackbeatRoutesClientConfig, } from '../../build/smithy/cloudserverBackbeatRoutes/typescript-codegen'; -import { createCustomErrorMiddleware } from '../utils'; +import { CloudserverRestJsonProtocol } from '../utils'; export * from '../../build/smithy/cloudserverBackbeatRoutes/typescript-codegen'; export class BackbeatRoutesClient extends CloudserverBackbeatRoutesClient { constructor(config: CloudserverBackbeatRoutesClientConfig) { super({ + protocol: CloudserverRestJsonProtocol, ...config, signingEscapePath: false, }); - - this.middlewareStack.add(createCustomErrorMiddleware(this.config), { - step: 'deserialize', - name: 'cloudserverErrorHandler', - priority: 'low', - }); } } diff --git a/src/utils.ts b/src/utils.ts index 208b846d..aa08c3dd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,16 @@ +import { AwsRestJsonProtocol } from '@aws-sdk/core/protocols'; import { addExpectContinueMiddleware } from '@aws-sdk/middleware-expect-continue'; -import { MiddlewareStack, RequestHandler, StreamCollector } from '@smithy/types'; -import { XMLParser } from 'fast-xml-parser'; +import { collectBody } from '@smithy/core/protocols'; import { - CloudserverBackbeatRoutesServiceException -} from '../build/smithy/cloudserverBackbeatRoutes/typescript-codegen'; + HandlerExecutionContext, + HttpResponse, + MetadataBearer, + MiddlewareStack, + OperationSchema, + RequestHandler, + SerdeFunctions, +} from '@smithy/types'; +import { XMLParser } from 'fast-xml-parser'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type WithMiddlewareStack = { middlewareStack?: MiddlewareStack }; @@ -82,81 +89,59 @@ export function addContentLengthMiddleware( return; } -function parseXmlError(xml: string) { +function parseXmlError(body: string) { try { - const result = new XMLParser({}).parse(xml); + const { Error: error } = new XMLParser({}).parse(body); return { - code: result.Error?.Code, - message: result.Error?.Message, - requestId: result.Error?.RequestId, + code: error?.Code || 'UnknownError', + message: error?.Message || 'XML error response', }; } catch (_parseError) { return { - code: null, + code: 'UnknownError', message: 'Malformed XML error response', - requestId: null, }; } } +function parseHtmlError(body: string, response: HttpResponse) { + const title = body.match(/]*>([^<]+)<\/title>/i); + return { + code: `HTML ${response.reason || 'Error'}`, + message: title?.[1] || 'HTML error response', + rawBody: body, + }; +} + /** - * Turns XML and HTML error responses into service exceptions, as cloudserver - * and the S3C nginx proxy may return them regardless of the service protocol. - * - * Must run after (inside) the deserializer, on the raw HTTP response: errors - * thrown by the deserializer no longer carry the response body. + * restJson1 protocol that also understands the XML and HTML error responses + * that cloudserver and the S3C nginx proxy may return regardless of the + * service protocol. These bodies would otherwise fail JSON parsing: they are + * parsed here and handed to the regular error handling, so they map to the + * modeled error classes or to the service base exception, like JSON errors. */ -export function createCustomErrorMiddleware(config: { streamCollector: StreamCollector }) { - return (next: any) => async (args: any) => { - const result = await next(args); - const response = result?.response; - const statusCode = response?.statusCode; - if (!statusCode || statusCode < 300) { - return result; - } - - const headers = response.headers || {}; - const contentType = (headers['content-type'] || '').toLowerCase(); - const isXml = contentType.includes('application/xml') || contentType.includes('text/xml'); - const s3cNginxProxyResponse = contentType.includes('text/html'); - if (!isXml && !s3cNginxProxyResponse) { - return result; - } - - const body = new TextDecoder().decode(await config.streamCollector(response.body)); - response.body = body; - const $fault = statusCode >= 500 ? 'server' : 'client'; - const $metadata = { - httpStatusCode: statusCode, - requestId: headers['x-amz-request-id'], - extendedRequestId: headers['x-amz-id-2'], - }; - - if (isXml) { - const errorInfo = parseXmlError(body); - const xmlError: any = new CloudserverBackbeatRoutesServiceException({ - name: errorInfo.code || 'UnknownError', - message: errorInfo.message || 'XML error response', - $fault, - $metadata, - }); - Object.defineProperty(xmlError, '$response', { value: response, enumerable: false }); - xmlError.parsedXml = errorInfo; - xmlError.code = errorInfo.code; - throw xmlError; +export class CloudserverRestJsonProtocol extends AwsRestJsonProtocol { + async deserializeResponse( + operationSchema: OperationSchema, + context: HandlerExecutionContext & SerdeFunctions, + response: HttpResponse, + ): Promise { + if (response.statusCode >= 300) { + const contentType = String(response.headers['content-type'] || '').toLowerCase(); + const isXml = contentType.includes('application/xml') || contentType.includes('text/xml'); + const isHtml = contentType.includes('text/html'); + if (isXml || isHtml) { + const body = (await collectBody(response.body, context)).transformToString(); + // Keep the raw body readable on the error's $response + // eslint-disable-next-line no-param-reassign + response.body = body; + const errorData = isXml ? parseXmlError(body) : parseHtmlError(body, response); + return this.handleError(operationSchema, context, response, errorData, + this.deserializeMetadata(response)); + } } - - const title = body.match(/]*>([^<]+)<\/title>/i); - const htmlError: any = new CloudserverBackbeatRoutesServiceException({ - name: `HTML ${response.reason || 'Error'}`, - message: title && title[1] || 'HTML error response', - $fault, - $metadata, - }); - Object.defineProperty(htmlError, '$response', { value: response, enumerable: false }); - htmlError.rawBody = body; - throw htmlError; - }; + return super.deserializeResponse(operationSchema, context, response); + } } export function attachReqUids(s3req, uuid: string) { diff --git a/tests/testErrorHandling.test.ts b/tests/testErrorHandling.test.ts index 0abe0d82..0e59c82e 100644 --- a/tests/testErrorHandling.test.ts +++ b/tests/testErrorHandling.test.ts @@ -1,7 +1,5 @@ import { BackbeatRoutesClient, - MultipleBackendDeleteObjectInput, - MultipleBackendDeleteObjectCommand, GetObjectInput, GetObjectCommand, } from '../src/index'; @@ -31,22 +29,4 @@ describeForMongoBackend('CloudServer test error handling', () => { assert.strictEqual(err.$metadata?.httpStatusCode, 404); } }); - - it.skip('should test html parsing', async () => { - // Run CloudServer with : S3VAULT=mem S3METADATA=mem S3DATA=mem REMOTE_MANAGEMENT_DISABLE=true yarn start - // Needs to tamper with Cloudserver response manually to return an html error - try { - const deleteInput: MultipleBackendDeleteObjectInput = { - Bucket: 'testConfig.bucketName', - Key: 'notAKey', - StorageClass: 'us-east-1', - StorageType: 'file' - }; - const commandDelete = new MultipleBackendDeleteObjectCommand(deleteInput); - await backbeatRoutesClient.send(commandDelete); - } catch (err: any) { - assert.strictEqual(err.name, 'NoSuchKey'); - assert.strictEqual(err.$metadata?.httpStatusCode, 404); - } - }); }); diff --git a/tests/testErrorProtocol.test.ts b/tests/testErrorProtocol.test.ts new file mode 100644 index 00000000..48a234b8 --- /dev/null +++ b/tests/testErrorProtocol.test.ts @@ -0,0 +1,164 @@ +import http, { Server } from 'http'; +import { AddressInfo } from 'net'; +import { promisify } from 'util'; +import { + BackbeatRoutesClient, + CloudserverBackbeatRoutesServiceException, + GetObjectCommand, + GetRaftIdCommand, + PutDataCommand, + PutMetadataCommand, + StaleMicroVersionIdException, + VersionIdCollisionException, +} from '../src/index'; + +type CannedResponse = { + status: number; + headers: http.OutgoingHttpHeaders; + body: string; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function expectError(request: Promise): Promise { + try { + await request; + } catch (err) { + return err; + } + throw new Error('Expected the request to fail'); +} + +describe('Cloudserver error responses', () => { + let server: Server; + let client: BackbeatRoutesClient; + let cannedResponse: CannedResponse; + + beforeAll(async () => { + server = http.createServer((req, res) => { + req.resume(); + req.on('end', () => { + res.writeHead(cannedResponse.status, cannedResponse.headers); + res.end(cannedResponse.body); + }); + }); + await promisify(server.listen).call(server, 0, '127.0.0.1'); + const { port } = server.address() as AddressInfo; + + client = new BackbeatRoutesClient({ + endpoint: `http://127.0.0.1:${port}`, + credentials: { accessKeyId: 'a', secretAccessKey: 'b' }, + region: 'us-east-1', + maxAttempts: 1, + }); + }); + + afterAll(async () => { + client.destroy(); + await promisify(server.close).call(server); + }); + + beforeEach(() => { + cannedResponse = undefined; + }); + + it('maps an XML error to a service exception', async () => { + cannedResponse = { + status: 404, + headers: { 'Content-Type': 'application/xml', 'x-amz-request-id': 'req-1' }, + body: 'NoSuchKey' + + 'The specified key does not exist.req-1', + }; + const err = await expectError(client.send(new GetObjectCommand({ Bucket: 'bucket', Key: 'key' }))); + + expect(err).toBeInstanceOf(CloudserverBackbeatRoutesServiceException); + expect(err.name).toBe('NoSuchKey'); + expect(err.code).toBe('NoSuchKey'); + expect(err.message).toBe('The specified key does not exist.'); + expect(err.$fault).toBe('client'); + expect(err.$metadata).toMatchObject({ httpStatusCode: 404, requestId: 'req-1' }); + expect(err.$response.body).toBe(cannedResponse.body); + }); + + it('maps an XML error to its modeled error class', async () => { + cannedResponse = { + status: 409, + headers: { 'Content-Type': 'application/xml', 'x-scal-micro-version-id': 'mv-1' }, + body: 'StaleMicroVersionIdException' + + 'incoming revision is older than destination', + }; + const err = await expectError(client.send(new PutMetadataCommand({ + Bucket: 'bucket', + Key: 'key', + Body: Buffer.from('{}'), + }))); + + expect(err.constructor).toBe(StaleMicroVersionIdException); + expect(err.message).toBe('incoming revision is older than destination'); + expect(err.microVersionId).toBe('mv-1'); + expect(err.$metadata.httpStatusCode).toBe(409); + }); + + it('maps a malformed XML error to a generic service exception', async () => { + cannedResponse = { + status: 500, + headers: { 'Content-Type': 'application/xml' }, + body: '', + }; + const err = await expectError(client.send(new GetObjectCommand({ Bucket: 'bucket', Key: 'key' }))); + + expect(err).toBeInstanceOf(CloudserverBackbeatRoutesServiceException); + expect(err.name).toBe('UnknownError'); + expect(err.$fault).toBe('server'); + expect(err.$metadata.httpStatusCode).toBe(500); + }); + + it('maps an HTML proxy error to a service exception', async () => { + cannedResponse = { + status: 400, + headers: { 'Content-Type': 'text/html' }, + body: '400 Request Header Or Cookie Too Large' + + '
Request Header Or Cookie Too Large
', + }; + const err = await expectError(client.send(new GetObjectCommand({ Bucket: 'bucket', Key: 'key' }))); + + expect(err).toBeInstanceOf(CloudserverBackbeatRoutesServiceException); + expect(err.name).toBe('HTML Bad Request'); + expect(err.message).toBe('400 Request Header Or Cookie Too Large'); + expect(err.$fault).toBe('client'); + expect(err.$metadata.httpStatusCode).toBe(400); + expect(err.rawBody).toBe(cannedResponse.body); + }); + + it('still handles JSON errors', async () => { + cannedResponse = { + status: 409, + headers: { 'Content-Type': 'application/json', 'x-scal-micro-version-id': 'mv-2' }, + body: JSON.stringify({ + code: 'VersionIdCollisionException', + message: 'version id already at destination', + }), + }; + const err = await expectError(client.send(new PutDataCommand({ + Bucket: 'bucket', + Key: 'key', + ContentMD5: 'x', + CanonicalID: 'c', + Body: Buffer.from('data'), + }))); + + expect(err.constructor).toBe(VersionIdCollisionException); + expect(err.message).toBe('version id already at destination'); + expect(err.microVersionId).toBe('mv-2'); + }); + + it('still deserializes successful responses', async () => { + cannedResponse = { + status: 200, + headers: { 'Content-Type': 'text/plain' }, + body: '42', + }; + const res = await client.send(new GetRaftIdCommand({ Bucket: 'bucket' })); + + expect(res.RaftId).toBe('42'); + }); +}); From fb6dd2575cf306b3711f20b20e10bbfd45c0478a Mon Sep 17 00:00:00 2001 From: Francois Ferrand Date: Thu, 1 Oct 2026 22:12:41 +0200 Subject: [PATCH 6/6] Release 1.0.13 Issue: CLDSRVCLT-19 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 23bf44b0..19d8a425 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@scality/cloudserverclient", - "version": "1.0.12", + "version": "1.0.13", "engines": { "node": ">=20" },