diff --git a/packages/client-google-chat/package-lock.json b/packages/client-google-chat/package-lock.json index 16537ded..91c956f1 100644 --- a/packages/client-google-chat/package-lock.json +++ b/packages/client-google-chat/package-lock.json @@ -422,22 +422,17 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.16.tgz", - "integrity": "sha512-Nasoyb5K4jfvncTKQyA13q55xHoz9as01NVYP05B0Kzux/X5UhMn3qXsZDyWOSXkfSCAIrMBKmVVWbI0vUapdQ==", + "version": "3.974.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.15.tgz", + "integrity": "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw==", "dependencies": { - "@aws-sdk/types": "^3.973.4", - "@aws-sdk/xml-builder": "^3.972.9", - "@smithy/core": "^3.23.7", - "@smithy/node-config-provider": "^4.3.10", - "@smithy/property-provider": "^4.2.10", - "@smithy/protocol-http": "^5.3.10", - "@smithy/signature-v4": "^5.3.10", - "@smithy/smithy-client": "^4.12.1", - "@smithy/types": "^4.13.0", - "@smithy/util-base64": "^4.3.1", - "@smithy/util-middleware": "^4.2.10", - "@smithy/util-utf8": "^4.2.1", + "@aws-sdk/types": "^3.973.9", + "@aws-sdk/xml-builder": "^3.972.26", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.5", + "@smithy/signature-v4": "^5.4.5", + "@smithy/types": "^4.14.2", + "bowser": "^2.11.0", "tslib": "^2.6.2" }, "engines": { @@ -457,14 +452,14 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.14.tgz", - "integrity": "sha512-PvnBY9rwBuLh9MEsAng28DG+WKl+txerKgf4BU9IPAqYI7FBIo1x6q/utLf4KLyQYgSy1TLQnbQuXx5xfBGASg==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.41.tgz", + "integrity": "sha512-n1EbJ98yvPWWdHZZv8bRBMqqDQJrtgtxyJ4xLy2Uqrh25BCOZQ7nnS1CsFXvuH8r0b0KVHDZEGEH5FxmEMP8jg==", "dependencies": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -472,19 +467,16 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.16.tgz", - "integrity": "sha512-m/QAcvw5OahqGPjeAnKtgfWgjLxeWOYj7JSmxKK6PLyKp2S/t2TAHI6EELEzXnIz28RMgbQLukJkVAqPASVAGQ==", + "version": "3.972.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.43.tgz", + "integrity": "sha512-TT76RN1NkI9WoyZqCNxOw6/WBMF7pYOTJcXbMokNFU+euSG40Kaf/t/FhDACVZWP+43wEM6ZynIPIkzS1wR1iA==", "dependencies": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/types": "^3.973.4", - "@smithy/fetch-http-handler": "^5.3.12", - "@smithy/node-http-handler": "^4.4.13", - "@smithy/property-provider": "^4.2.10", - "@smithy/protocol-http": "^5.3.10", - "@smithy/smithy-client": "^4.12.1", - "@smithy/types": "^4.13.0", - "@smithy/util-stream": "^4.5.16", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/fetch-http-handler": "^5.4.5", + "@smithy/node-http-handler": "^4.7.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -492,23 +484,22 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.14.tgz", - "integrity": "sha512-EGA7ufqNpZKZcD0RwM6gRDEQgwAf19wQ99R1ptdWYDJAnpcMcWiFyT0RIrgiZFLD28CwJmYjnra75hChnEveWA==", + "version": "3.972.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.46.tgz", + "integrity": "sha512-hvcgcwOiS0nb2XFb5Op1Pz/vYaWz5K8kKullziGpdNRuG0NwzRXseuPt2CoBqknHGaSPVesu1aOn2OcctEYdCA==", "dependencies": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/credential-provider-env": "^3.972.14", - "@aws-sdk/credential-provider-http": "^3.972.16", - "@aws-sdk/credential-provider-login": "^3.972.14", - "@aws-sdk/credential-provider-process": "^3.972.14", - "@aws-sdk/credential-provider-sso": "^3.972.14", - "@aws-sdk/credential-provider-web-identity": "^3.972.14", - "@aws-sdk/nested-clients": "^3.996.4", - "@aws-sdk/types": "^3.973.4", - "@smithy/credential-provider-imds": "^4.2.10", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/credential-provider-env": "^3.972.41", + "@aws-sdk/credential-provider-http": "^3.972.43", + "@aws-sdk/credential-provider-login": "^3.972.45", + "@aws-sdk/credential-provider-process": "^3.972.41", + "@aws-sdk/credential-provider-sso": "^3.972.45", + "@aws-sdk/credential-provider-web-identity": "^3.972.45", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/credential-provider-imds": "^4.3.6", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -516,17 +507,15 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.14.tgz", - "integrity": "sha512-P2kujQHAoV7irCTv6EGyReKFofkHCjIK+F0ZYf5UxeLeecrCwtrDkHoO2Vjsv/eRUumaKblD8czuk3CLlzwGDw==", + "version": "3.972.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.45.tgz", + "integrity": "sha512-MZQv4SNjByk1iOKmrqmzcUF/uCB05wjvEHyXKxmGQTUANTIVayX6HPUF0bzkWLvtnkH7sAn9kUCfkXbSpj9sDA==", "dependencies": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/nested-clients": "^3.996.4", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/protocol-http": "^5.3.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -534,21 +523,20 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.15.tgz", - "integrity": "sha512-59NBJgTcQ2FC94T+SWkN5UQgViFtrLnkswSKhG5xbjPAotOXnkEF2Bf0bfUV1F3VaXzqAPZJoZ3bpg4rr8XD5Q==", - "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.14", - "@aws-sdk/credential-provider-http": "^3.972.16", - "@aws-sdk/credential-provider-ini": "^3.972.14", - "@aws-sdk/credential-provider-process": "^3.972.14", - "@aws-sdk/credential-provider-sso": "^3.972.14", - "@aws-sdk/credential-provider-web-identity": "^3.972.14", - "@aws-sdk/types": "^3.973.4", - "@smithy/credential-provider-imds": "^4.2.10", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "version": "3.972.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.48.tgz", + "integrity": "sha512-QIbtJP0olSLZ2ImEu636pP+7JJbPfaL3xSJIFXhu472CWuondCc4bGOa8OeyhOFet8z4H1D/ZFKXc39FboWwYA==", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.41", + "@aws-sdk/credential-provider-http": "^3.972.43", + "@aws-sdk/credential-provider-ini": "^3.972.46", + "@aws-sdk/credential-provider-process": "^3.972.41", + "@aws-sdk/credential-provider-sso": "^3.972.45", + "@aws-sdk/credential-provider-web-identity": "^3.972.45", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/credential-provider-imds": "^4.3.6", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -556,15 +544,14 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.14.tgz", - "integrity": "sha512-KAF5LBkJInUPaR9dJDw8LqmbPDRTLyXyRoWVGcJQ+DcN9rxVKBRzAK+O4dTIvQtQ7xaIDZ2kY7zUmDlz6CCXdw==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.41.tgz", + "integrity": "sha512-7I/n1zkysouLOWvkEhjNEP4vMnD2v4kzzr3/3QBdrripEpn7ap1/I5DF3Hou1SUqkKWo1f3oPGMyFAA1FAMvsQ==", "dependencies": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -572,17 +559,16 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.14.tgz", - "integrity": "sha512-LQzIYrNABnZzkyuIguFa3VVOox9UxPpRW6PL+QYtRHaGl1Ux/+Zi54tAVK31VdeBKPKU3cxqeu8dbOgNqy+naw==", + "version": "3.972.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.45.tgz", + "integrity": "sha512-oHgbz/eFD8IKiksqDsz9ZMU4A59BpQq4QwJedBnGD80ZqYcHPPHZBwjBnxLVkB7iRVVHWpDclR8yWdD2PkQIUA==", "dependencies": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/nested-clients": "^3.996.4", - "@aws-sdk/token-providers": "3.1001.0", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/token-providers": "3.1056.0", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -590,16 +576,15 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.14.tgz", - "integrity": "sha512-rOwB3vXHHHnGvAOjTgQETxVAsWjgF61XlbGd/ulvYo7EpdXs8cbIHE3PGih9tTj/65ZOegSqZGFqLaKntaI9Kw==", + "version": "3.972.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.45.tgz", + "integrity": "sha512-CDhzKdb2onv5bpnjn/acgdNmJOQthPDLsPizU7rZflsEcgMMp8Mlri+U5hdxf8ldvZJpvM3vLU6D56vfJm5AMQ==", "dependencies": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/nested-clients": "^3.996.4", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -771,47 +756,19 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.4.tgz", - "integrity": "sha512-NowB1HfOnWC4kwZOnTg8E8rSL0U+RSjSa++UtEV4ipoH6JOjMLnHyGilqwl+Pe1f0Al6v9yMkSJ/8Ot0f578CQ==", + "version": "3.997.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.13.tgz", + "integrity": "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/middleware-host-header": "^3.972.6", - "@aws-sdk/middleware-logger": "^3.972.6", - "@aws-sdk/middleware-recursion-detection": "^3.972.6", - "@aws-sdk/middleware-user-agent": "^3.972.16", - "@aws-sdk/region-config-resolver": "^3.972.6", - "@aws-sdk/types": "^3.973.4", - "@aws-sdk/util-endpoints": "^3.996.3", - "@aws-sdk/util-user-agent-browser": "^3.972.6", - "@aws-sdk/util-user-agent-node": "^3.973.1", - "@smithy/config-resolver": "^4.4.9", - "@smithy/core": "^3.23.7", - "@smithy/fetch-http-handler": "^5.3.12", - "@smithy/hash-node": "^4.2.10", - "@smithy/invalid-dependency": "^4.2.10", - "@smithy/middleware-content-length": "^4.2.10", - "@smithy/middleware-endpoint": "^4.4.21", - "@smithy/middleware-retry": "^4.4.38", - "@smithy/middleware-serde": "^4.2.11", - "@smithy/middleware-stack": "^4.2.10", - "@smithy/node-config-provider": "^4.3.10", - "@smithy/node-http-handler": "^4.4.13", - "@smithy/protocol-http": "^5.3.10", - "@smithy/smithy-client": "^4.12.1", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.10", - "@smithy/util-base64": "^4.3.1", - "@smithy/util-body-length-browser": "^4.2.1", - "@smithy/util-body-length-node": "^4.2.2", - "@smithy/util-defaults-mode-browser": "^4.3.37", - "@smithy/util-defaults-mode-node": "^4.2.40", - "@smithy/util-endpoints": "^3.3.1", - "@smithy/util-middleware": "^4.2.10", - "@smithy/util-retry": "^4.2.10", - "@smithy/util-utf8": "^4.2.1", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/signature-v4-multi-region": "^3.996.30", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/fetch-http-handler": "^5.4.5", + "@smithy/node-http-handler": "^4.7.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -852,15 +809,13 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.4.tgz", - "integrity": "sha512-MGa8ro0onekYIiesHX60LwKdkxK3Kd61p7TTbLwZemBqlnD9OLrk9sXZdFOIxXanJ+3AaJnV/jiX866eD/4PDg==", + "version": "3.996.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.30.tgz", + "integrity": "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw==", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.16", - "@aws-sdk/types": "^3.973.4", - "@smithy/protocol-http": "^5.3.10", - "@smithy/signature-v4": "^5.3.10", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.9", + "@smithy/signature-v4": "^5.4.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -868,16 +823,15 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1001.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1001.0.tgz", - "integrity": "sha512-09XAq/uIYgeZhohuGRrR/R+ek3+ljFNdzWCXdqb9rlIERDjSfNiLjTtpHgSK1xTPmC5G4yWoEAyMfTXiggS6wA==", + "version": "3.1056.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1056.0.tgz", + "integrity": "sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA==", "dependencies": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/nested-clients": "^3.996.4", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -885,11 +839,11 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.4.tgz", - "integrity": "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==", + "version": "3.973.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.9.tgz", + "integrity": "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==", "dependencies": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -982,12 +936,12 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.9.tgz", - "integrity": "sha512-ItnlMgSqkPrUfJs7EsvU/01zw5UeIb2tNPhD09LBLHbg+g+HDiKibSLwpkuz/ZIlz4F2IMn+5XgE4AK/pfPuog==", + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.26.tgz", + "integrity": "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g==", "dependencies": { - "@smithy/types": "^4.13.0", - "fast-xml-parser": "5.4.1", + "@smithy/types": "^4.14.2", + "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" }, "engines": { @@ -2355,6 +2309,17 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@nodable/entities": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz", + "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ] + }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", @@ -2477,19 +2442,12 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.7", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.7.tgz", - "integrity": "sha512-/+ldRdtiO5Cb26afAZOG1FZM0x7D4AYdjpyOv2OScJw+4C7X+OLdRnNKF5UyUE0VpPgSKr3rnF/kvprRA4h2kg==", + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.6.tgz", + "integrity": "sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug==", "dependencies": { - "@smithy/middleware-serde": "^4.2.11", - "@smithy/protocol-http": "^5.3.10", - "@smithy/types": "^4.13.0", - "@smithy/util-base64": "^4.3.1", - "@smithy/util-body-length-browser": "^4.2.1", - "@smithy/util-middleware": "^4.2.10", - "@smithy/util-stream": "^4.5.16", - "@smithy/util-utf8": "^4.2.1", - "@smithy/uuid": "^1.1.1", + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -2497,14 +2455,12 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.10.tgz", - "integrity": "sha512-3bsMLJJLTZGZqVGGeBVFfLzuRulVsGTj12BzRKODTHqUABpIr0jMN1vN3+u6r2OfyhAQ2pXaMZWX/swBK5I6PQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.7.tgz", + "integrity": "sha512-xj8gq/bjFABAh6qWPSDCYcY3kzQIm4b561C+YnHH4zGq8rOgzQ3Shk+JGlpUxSd41UGiO6FkLdUCtNX1FAeHgg==", "dependencies": { - "@smithy/node-config-provider": "^4.3.10", - "@smithy/property-provider": "^4.2.10", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.10", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -2577,14 +2533,12 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.12.tgz", - "integrity": "sha512-muS5tFw+A/uo+U+yig06vk1776UFM+aAp9hFM8efI4ZcHhTcgv6NTeK4x7ltHeMPBwnhEjcf0MULTyxNkSNxDw==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.6.tgz", + "integrity": "sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g==", "dependencies": { - "@smithy/protocol-http": "^5.3.10", - "@smithy/querystring-builder": "^4.2.10", - "@smithy/types": "^4.13.0", - "@smithy/util-base64": "^4.3.1", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -2758,14 +2712,12 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.13.tgz", - "integrity": "sha512-o8CP8w6tlUA0lk+Qfwm6Ed0jCWk3bEY6iBOJjdBaowbXKCSClk8zIHQvUL6RUZMvuNafF27cbRCMYqw6O1v4aA==", + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.6.tgz", + "integrity": "sha512-3fya8i7GrJilQouk4cZJKdy5k8MWQBpjfXrRNaXDedH8r779tr0jcxyH3+yoTmsluc2+vF4S343yFbnvu8ExDQ==", "dependencies": { - "@smithy/abort-controller": "^4.2.10", - "@smithy/protocol-http": "^5.3.10", - "@smithy/querystring-builder": "^4.2.10", - "@smithy/types": "^4.13.0", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -2845,17 +2797,12 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.10.tgz", - "integrity": "sha512-Wab3wW8468WqTKIxI+aZe3JYO52/RYT/8sDOdzkUhjnLakLe9qoQqIcfih/qxcF4qWEFoWBszY0mj5uxffaVXA==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.6.tgz", + "integrity": "sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ==", "dependencies": { - "@smithy/is-array-buffer": "^4.2.1", - "@smithy/protocol-http": "^5.3.10", - "@smithy/types": "^4.13.0", - "@smithy/util-hex-encoding": "^4.2.1", - "@smithy/util-middleware": "^4.2.10", - "@smithy/util-uri-escape": "^4.2.1", - "@smithy/util-utf8": "^4.2.1", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -2880,9 +2827,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz", - "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.3.tgz", + "integrity": "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ==", "dependencies": { "tslib": "^2.6.2" }, @@ -4683,20 +4630,24 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, "node_modules/fast-xml-builder": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", - "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } - ] + ], + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } }, "node_modules/fast-xml-parser": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", - "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", "funding": [ { "type": "github", @@ -4704,8 +4655,10 @@ } ], "dependencies": { - "fast-xml-builder": "^1.0.0", - "strnum": "^2.1.2" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" @@ -6559,6 +6512,20 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -7546,9 +7513,9 @@ } }, "node_modules/strnum": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", - "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", "funding": [ { "type": "github", @@ -8390,6 +8357,20 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -8827,22 +8808,17 @@ } }, "@aws-sdk/core": { - "version": "3.973.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.16.tgz", - "integrity": "sha512-Nasoyb5K4jfvncTKQyA13q55xHoz9as01NVYP05B0Kzux/X5UhMn3qXsZDyWOSXkfSCAIrMBKmVVWbI0vUapdQ==", + "version": "3.974.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.15.tgz", + "integrity": "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw==", "requires": { - "@aws-sdk/types": "^3.973.4", - "@aws-sdk/xml-builder": "^3.972.9", - "@smithy/core": "^3.23.7", - "@smithy/node-config-provider": "^4.3.10", - "@smithy/property-provider": "^4.2.10", - "@smithy/protocol-http": "^5.3.10", - "@smithy/signature-v4": "^5.3.10", - "@smithy/smithy-client": "^4.12.1", - "@smithy/types": "^4.13.0", - "@smithy/util-base64": "^4.3.1", - "@smithy/util-middleware": "^4.2.10", - "@smithy/util-utf8": "^4.2.1", + "@aws-sdk/types": "^3.973.9", + "@aws-sdk/xml-builder": "^3.972.26", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.5", + "@smithy/signature-v4": "^5.4.5", + "@smithy/types": "^4.14.2", + "bowser": "^2.11.0", "tslib": "^2.6.2" } }, @@ -8856,128 +8832,118 @@ } }, "@aws-sdk/credential-provider-env": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.14.tgz", - "integrity": "sha512-PvnBY9rwBuLh9MEsAng28DG+WKl+txerKgf4BU9IPAqYI7FBIo1x6q/utLf4KLyQYgSy1TLQnbQuXx5xfBGASg==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.41.tgz", + "integrity": "sha512-n1EbJ98yvPWWdHZZv8bRBMqqDQJrtgtxyJ4xLy2Uqrh25BCOZQ7nnS1CsFXvuH8r0b0KVHDZEGEH5FxmEMP8jg==", "requires": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "@aws-sdk/credential-provider-http": { - "version": "3.972.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.16.tgz", - "integrity": "sha512-m/QAcvw5OahqGPjeAnKtgfWgjLxeWOYj7JSmxKK6PLyKp2S/t2TAHI6EELEzXnIz28RMgbQLukJkVAqPASVAGQ==", + "version": "3.972.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.43.tgz", + "integrity": "sha512-TT76RN1NkI9WoyZqCNxOw6/WBMF7pYOTJcXbMokNFU+euSG40Kaf/t/FhDACVZWP+43wEM6ZynIPIkzS1wR1iA==", "requires": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/types": "^3.973.4", - "@smithy/fetch-http-handler": "^5.3.12", - "@smithy/node-http-handler": "^4.4.13", - "@smithy/property-provider": "^4.2.10", - "@smithy/protocol-http": "^5.3.10", - "@smithy/smithy-client": "^4.12.1", - "@smithy/types": "^4.13.0", - "@smithy/util-stream": "^4.5.16", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/fetch-http-handler": "^5.4.5", + "@smithy/node-http-handler": "^4.7.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "@aws-sdk/credential-provider-ini": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.14.tgz", - "integrity": "sha512-EGA7ufqNpZKZcD0RwM6gRDEQgwAf19wQ99R1ptdWYDJAnpcMcWiFyT0RIrgiZFLD28CwJmYjnra75hChnEveWA==", + "version": "3.972.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.46.tgz", + "integrity": "sha512-hvcgcwOiS0nb2XFb5Op1Pz/vYaWz5K8kKullziGpdNRuG0NwzRXseuPt2CoBqknHGaSPVesu1aOn2OcctEYdCA==", "requires": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/credential-provider-env": "^3.972.14", - "@aws-sdk/credential-provider-http": "^3.972.16", - "@aws-sdk/credential-provider-login": "^3.972.14", - "@aws-sdk/credential-provider-process": "^3.972.14", - "@aws-sdk/credential-provider-sso": "^3.972.14", - "@aws-sdk/credential-provider-web-identity": "^3.972.14", - "@aws-sdk/nested-clients": "^3.996.4", - "@aws-sdk/types": "^3.973.4", - "@smithy/credential-provider-imds": "^4.2.10", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/credential-provider-env": "^3.972.41", + "@aws-sdk/credential-provider-http": "^3.972.43", + "@aws-sdk/credential-provider-login": "^3.972.45", + "@aws-sdk/credential-provider-process": "^3.972.41", + "@aws-sdk/credential-provider-sso": "^3.972.45", + "@aws-sdk/credential-provider-web-identity": "^3.972.45", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/credential-provider-imds": "^4.3.6", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "@aws-sdk/credential-provider-login": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.14.tgz", - "integrity": "sha512-P2kujQHAoV7irCTv6EGyReKFofkHCjIK+F0ZYf5UxeLeecrCwtrDkHoO2Vjsv/eRUumaKblD8czuk3CLlzwGDw==", + "version": "3.972.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.45.tgz", + "integrity": "sha512-MZQv4SNjByk1iOKmrqmzcUF/uCB05wjvEHyXKxmGQTUANTIVayX6HPUF0bzkWLvtnkH7sAn9kUCfkXbSpj9sDA==", "requires": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/nested-clients": "^3.996.4", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/protocol-http": "^5.3.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "@aws-sdk/credential-provider-node": { - "version": "3.972.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.15.tgz", - "integrity": "sha512-59NBJgTcQ2FC94T+SWkN5UQgViFtrLnkswSKhG5xbjPAotOXnkEF2Bf0bfUV1F3VaXzqAPZJoZ3bpg4rr8XD5Q==", - "requires": { - "@aws-sdk/credential-provider-env": "^3.972.14", - "@aws-sdk/credential-provider-http": "^3.972.16", - "@aws-sdk/credential-provider-ini": "^3.972.14", - "@aws-sdk/credential-provider-process": "^3.972.14", - "@aws-sdk/credential-provider-sso": "^3.972.14", - "@aws-sdk/credential-provider-web-identity": "^3.972.14", - "@aws-sdk/types": "^3.973.4", - "@smithy/credential-provider-imds": "^4.2.10", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "version": "3.972.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.48.tgz", + "integrity": "sha512-QIbtJP0olSLZ2ImEu636pP+7JJbPfaL3xSJIFXhu472CWuondCc4bGOa8OeyhOFet8z4H1D/ZFKXc39FboWwYA==", + "requires": { + "@aws-sdk/credential-provider-env": "^3.972.41", + "@aws-sdk/credential-provider-http": "^3.972.43", + "@aws-sdk/credential-provider-ini": "^3.972.46", + "@aws-sdk/credential-provider-process": "^3.972.41", + "@aws-sdk/credential-provider-sso": "^3.972.45", + "@aws-sdk/credential-provider-web-identity": "^3.972.45", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/credential-provider-imds": "^4.3.6", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "@aws-sdk/credential-provider-process": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.14.tgz", - "integrity": "sha512-KAF5LBkJInUPaR9dJDw8LqmbPDRTLyXyRoWVGcJQ+DcN9rxVKBRzAK+O4dTIvQtQ7xaIDZ2kY7zUmDlz6CCXdw==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.41.tgz", + "integrity": "sha512-7I/n1zkysouLOWvkEhjNEP4vMnD2v4kzzr3/3QBdrripEpn7ap1/I5DF3Hou1SUqkKWo1f3oPGMyFAA1FAMvsQ==", "requires": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "@aws-sdk/credential-provider-sso": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.14.tgz", - "integrity": "sha512-LQzIYrNABnZzkyuIguFa3VVOox9UxPpRW6PL+QYtRHaGl1Ux/+Zi54tAVK31VdeBKPKU3cxqeu8dbOgNqy+naw==", + "version": "3.972.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.45.tgz", + "integrity": "sha512-oHgbz/eFD8IKiksqDsz9ZMU4A59BpQq4QwJedBnGD80ZqYcHPPHZBwjBnxLVkB7iRVVHWpDclR8yWdD2PkQIUA==", "requires": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/nested-clients": "^3.996.4", - "@aws-sdk/token-providers": "3.1001.0", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/token-providers": "3.1056.0", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "@aws-sdk/credential-provider-web-identity": { - "version": "3.972.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.14.tgz", - "integrity": "sha512-rOwB3vXHHHnGvAOjTgQETxVAsWjgF61XlbGd/ulvYo7EpdXs8cbIHE3PGih9tTj/65ZOegSqZGFqLaKntaI9Kw==", + "version": "3.972.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.45.tgz", + "integrity": "sha512-CDhzKdb2onv5bpnjn/acgdNmJOQthPDLsPizU7rZflsEcgMMp8Mlri+U5hdxf8ldvZJpvM3vLU6D56vfJm5AMQ==", "requires": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/nested-clients": "^3.996.4", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, @@ -9116,47 +9082,19 @@ } }, "@aws-sdk/nested-clients": { - "version": "3.996.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.4.tgz", - "integrity": "sha512-NowB1HfOnWC4kwZOnTg8E8rSL0U+RSjSa++UtEV4ipoH6JOjMLnHyGilqwl+Pe1f0Al6v9yMkSJ/8Ot0f578CQ==", + "version": "3.997.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.13.tgz", + "integrity": "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg==", "requires": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/middleware-host-header": "^3.972.6", - "@aws-sdk/middleware-logger": "^3.972.6", - "@aws-sdk/middleware-recursion-detection": "^3.972.6", - "@aws-sdk/middleware-user-agent": "^3.972.16", - "@aws-sdk/region-config-resolver": "^3.972.6", - "@aws-sdk/types": "^3.973.4", - "@aws-sdk/util-endpoints": "^3.996.3", - "@aws-sdk/util-user-agent-browser": "^3.972.6", - "@aws-sdk/util-user-agent-node": "^3.973.1", - "@smithy/config-resolver": "^4.4.9", - "@smithy/core": "^3.23.7", - "@smithy/fetch-http-handler": "^5.3.12", - "@smithy/hash-node": "^4.2.10", - "@smithy/invalid-dependency": "^4.2.10", - "@smithy/middleware-content-length": "^4.2.10", - "@smithy/middleware-endpoint": "^4.4.21", - "@smithy/middleware-retry": "^4.4.38", - "@smithy/middleware-serde": "^4.2.11", - "@smithy/middleware-stack": "^4.2.10", - "@smithy/node-config-provider": "^4.3.10", - "@smithy/node-http-handler": "^4.4.13", - "@smithy/protocol-http": "^5.3.10", - "@smithy/smithy-client": "^4.12.1", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.10", - "@smithy/util-base64": "^4.3.1", - "@smithy/util-body-length-browser": "^4.2.1", - "@smithy/util-body-length-node": "^4.2.2", - "@smithy/util-defaults-mode-browser": "^4.3.37", - "@smithy/util-defaults-mode-node": "^4.2.40", - "@smithy/util-endpoints": "^3.3.1", - "@smithy/util-middleware": "^4.2.10", - "@smithy/util-retry": "^4.2.10", - "@smithy/util-utf8": "^4.2.1", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/signature-v4-multi-region": "^3.996.30", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/fetch-http-handler": "^5.4.5", + "@smithy/node-http-handler": "^4.7.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, @@ -9188,38 +9126,35 @@ } }, "@aws-sdk/signature-v4-multi-region": { - "version": "3.996.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.4.tgz", - "integrity": "sha512-MGa8ro0onekYIiesHX60LwKdkxK3Kd61p7TTbLwZemBqlnD9OLrk9sXZdFOIxXanJ+3AaJnV/jiX866eD/4PDg==", + "version": "3.996.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.30.tgz", + "integrity": "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw==", "requires": { - "@aws-sdk/middleware-sdk-s3": "^3.972.16", - "@aws-sdk/types": "^3.973.4", - "@smithy/protocol-http": "^5.3.10", - "@smithy/signature-v4": "^5.3.10", - "@smithy/types": "^4.13.0", + "@aws-sdk/types": "^3.973.9", + "@smithy/signature-v4": "^5.4.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "@aws-sdk/token-providers": { - "version": "3.1001.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1001.0.tgz", - "integrity": "sha512-09XAq/uIYgeZhohuGRrR/R+ek3+ljFNdzWCXdqb9rlIERDjSfNiLjTtpHgSK1xTPmC5G4yWoEAyMfTXiggS6wA==", + "version": "3.1056.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1056.0.tgz", + "integrity": "sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA==", "requires": { - "@aws-sdk/core": "^3.973.16", - "@aws-sdk/nested-clients": "^3.996.4", - "@aws-sdk/types": "^3.973.4", - "@smithy/property-provider": "^4.2.10", - "@smithy/shared-ini-file-loader": "^4.4.5", - "@smithy/types": "^4.13.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "@aws-sdk/types": { - "version": "3.973.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.4.tgz", - "integrity": "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==", + "version": "3.973.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.9.tgz", + "integrity": "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==", "requires": { - "@smithy/types": "^4.13.0", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, @@ -9286,12 +9221,12 @@ } }, "@aws-sdk/xml-builder": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.9.tgz", - "integrity": "sha512-ItnlMgSqkPrUfJs7EsvU/01zw5UeIb2tNPhD09LBLHbg+g+HDiKibSLwpkuz/ZIlz4F2IMn+5XgE4AK/pfPuog==", + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.26.tgz", + "integrity": "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g==", "requires": { - "@smithy/types": "^4.13.0", - "fast-xml-parser": "5.4.1", + "@smithy/types": "^4.14.2", + "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, @@ -10206,6 +10141,11 @@ "@tybys/wasm-util": "^0.10.0" } }, + "@nodable/entities": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz", + "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==" + }, "@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", @@ -10306,31 +10246,22 @@ } }, "@smithy/core": { - "version": "3.23.7", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.7.tgz", - "integrity": "sha512-/+ldRdtiO5Cb26afAZOG1FZM0x7D4AYdjpyOv2OScJw+4C7X+OLdRnNKF5UyUE0VpPgSKr3rnF/kvprRA4h2kg==", + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.6.tgz", + "integrity": "sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug==", "requires": { - "@smithy/middleware-serde": "^4.2.11", - "@smithy/protocol-http": "^5.3.10", - "@smithy/types": "^4.13.0", - "@smithy/util-base64": "^4.3.1", - "@smithy/util-body-length-browser": "^4.2.1", - "@smithy/util-middleware": "^4.2.10", - "@smithy/util-stream": "^4.5.16", - "@smithy/util-utf8": "^4.2.1", - "@smithy/uuid": "^1.1.1", + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "@smithy/credential-provider-imds": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.10.tgz", - "integrity": "sha512-3bsMLJJLTZGZqVGGeBVFfLzuRulVsGTj12BzRKODTHqUABpIr0jMN1vN3+u6r2OfyhAQ2pXaMZWX/swBK5I6PQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.7.tgz", + "integrity": "sha512-xj8gq/bjFABAh6qWPSDCYcY3kzQIm4b561C+YnHH4zGq8rOgzQ3Shk+JGlpUxSd41UGiO6FkLdUCtNX1FAeHgg==", "requires": { - "@smithy/node-config-provider": "^4.3.10", - "@smithy/property-provider": "^4.2.10", - "@smithy/types": "^4.13.0", - "@smithy/url-parser": "^4.2.10", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, @@ -10385,14 +10316,12 @@ } }, "@smithy/fetch-http-handler": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.12.tgz", - "integrity": "sha512-muS5tFw+A/uo+U+yig06vk1776UFM+aAp9hFM8efI4ZcHhTcgv6NTeK4x7ltHeMPBwnhEjcf0MULTyxNkSNxDw==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.6.tgz", + "integrity": "sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g==", "requires": { - "@smithy/protocol-http": "^5.3.10", - "@smithy/querystring-builder": "^4.2.10", - "@smithy/types": "^4.13.0", - "@smithy/util-base64": "^4.3.1", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, @@ -10527,14 +10456,12 @@ } }, "@smithy/node-http-handler": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.13.tgz", - "integrity": "sha512-o8CP8w6tlUA0lk+Qfwm6Ed0jCWk3bEY6iBOJjdBaowbXKCSClk8zIHQvUL6RUZMvuNafF27cbRCMYqw6O1v4aA==", + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.6.tgz", + "integrity": "sha512-3fya8i7GrJilQouk4cZJKdy5k8MWQBpjfXrRNaXDedH8r779tr0jcxyH3+yoTmsluc2+vF4S343yFbnvu8ExDQ==", "requires": { - "@smithy/abort-controller": "^4.2.10", - "@smithy/protocol-http": "^5.3.10", - "@smithy/querystring-builder": "^4.2.10", - "@smithy/types": "^4.13.0", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, @@ -10593,17 +10520,12 @@ } }, "@smithy/signature-v4": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.10.tgz", - "integrity": "sha512-Wab3wW8468WqTKIxI+aZe3JYO52/RYT/8sDOdzkUhjnLakLe9qoQqIcfih/qxcF4qWEFoWBszY0mj5uxffaVXA==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.6.tgz", + "integrity": "sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ==", "requires": { - "@smithy/is-array-buffer": "^4.2.1", - "@smithy/protocol-http": "^5.3.10", - "@smithy/types": "^4.13.0", - "@smithy/util-hex-encoding": "^4.2.1", - "@smithy/util-middleware": "^4.2.10", - "@smithy/util-uri-escape": "^4.2.1", - "@smithy/util-utf8": "^4.2.1", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, @@ -10622,9 +10544,9 @@ } }, "@smithy/types": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz", - "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.3.tgz", + "integrity": "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ==", "requires": { "tslib": "^2.6.2" } @@ -11952,17 +11874,23 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, "fast-xml-builder": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", - "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "requires": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } }, "fast-xml-parser": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", - "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", "requires": { - "fast-xml-builder": "^1.0.0", - "strnum": "^2.1.2" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" } }, "fb-watchman": { @@ -13309,6 +13237,11 @@ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, + "path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==" + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -14002,9 +13935,9 @@ "dev": true }, "strnum": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", - "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==" }, "supports-color": { "version": "7.2.0", @@ -14578,6 +14511,11 @@ "signal-exit": "^4.0.1" } }, + "xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/packages/client-google-chat/src/app.ts b/packages/client-google-chat/src/app.ts index 852e6655..c9dcc4f3 100644 --- a/packages/client-google-chat/src/app.ts +++ b/packages/client-google-chat/src/app.ts @@ -4,6 +4,9 @@ import { Config, getConfigFromEnv } from './config/config.js'; import { Logger } from './utils/logger.js'; import { createStorageProvider, type StorageProvider } from './storage/index.js'; import { OIDCClient } from './services/oidcClient.js'; +import { registerInstallations } from './services/installationRegistrar.js'; +import { AwsSsmInstallationSecretService } from './services/awsSsmInstallationSecretService.js'; +import { SSMClient } from '@aws-sdk/client-ssm'; import { UserAuthService } from './services/userAuthService.js'; import { A2AClientService } from './services/a2aClientService.js'; import { FileStorageService } from './services/fileStorageService.js'; @@ -148,6 +151,12 @@ function setupServerTimeouts(server: Server, config: Config) { // OIDC client const oidcClient = new OIDCClient(config); + // Per-installation notification secrets backed by AWS SSM Parameter Store. + const installationSecretService = new AwsSsmInstallationSecretService( + new SSMClient({ region: config.aws.region }), + config.installationSecret.ssmPrefix + ); + // User auth service const userAuthService = new UserAuthService(storage.userAuth, oidcClient, config, storage.oauthState); @@ -425,7 +434,7 @@ function setupServerTimeouts(server: Server, config: Config) { app.post( '/api/v1/a2a/callback', - createA2ANotificationAuthMiddleware(config.googleChatConfigs), + createA2ANotificationAuthMiddleware(config.googleChatConfigs, installationSecretService), async (req: Request, res: Response) => { const task = req.body as Task; const projectId = res.locals.projectNumber as string; @@ -468,6 +477,12 @@ function setupServerTimeouts(server: Server, config: Config) { }); setupServerTimeouts(server, config); + // Self-register each Google Chat project as a delivery channel with console-backend. + // Failures are isolated and never block startup. + registerInstallations({ config, oidcClient, installationSecretService }).catch((error) => { + logger.error(error, `Delivery-channel self-registration failed: ${error}`); + }); + // ----------------------------------------------------------------------- // Task recovery // ----------------------------------------------------------------------- diff --git a/packages/client-google-chat/src/config/config.ts b/packages/client-google-chat/src/config/config.ts index 13e5b1ab..b887ee85 100644 --- a/packages/client-google-chat/src/config/config.ts +++ b/packages/client-google-chat/src/config/config.ts @@ -17,8 +17,8 @@ export interface Config { readonly googleChatConfigs: { projectName: string; projectNumber: string; // GCP project number for verifying Google-signed tokens + botName: string; // Bot display name; used as the installation_id for delivery-channel registration googleApplicationCredentials: any; - a2aNotificationSecret?: string; // Secret for validating A2A push notifications }[]; readonly storage: StorageConfig; readonly aws: { @@ -41,6 +41,9 @@ export interface Config { url: string; audience: string; }; + readonly installationSecret: { + ssmPrefix: string; + }; } export async function getConfigFromEnv(): Promise { @@ -80,17 +83,16 @@ export async function getConfigFromEnv(): Promise { } const googleChatConfigs: Config['googleChatConfigs'] = []; - for (const project of JSON.parse(process.env.GCP_CHAT_PROJECTS) as { name: string; google_chat_app_id: string }[]) { + for (const project of JSON.parse(process.env.GCP_CHAT_PROJECTS) as { name: string; google_chat_app_id: string; bot_name: string }[]) { const envVarName = `GCP_SA_JSON_KEY_${project.name.toUpperCase().replace(/-/g, '_')}`; if (!process.env[envVarName]) { throw new Error(`Please provide ${envVarName}`); } - const secretEnvVar = `A2A_NOTIFICATION_SECRET_${project.name.toUpperCase().replace(/-/g, '_')}`; googleChatConfigs.push({ projectName: project.name, projectNumber: project.google_chat_app_id, + botName: project.bot_name, googleApplicationCredentials: JSON.parse(process.env[envVarName]!), - a2aNotificationSecret: process.env[secretEnvVar], }); } @@ -158,5 +160,10 @@ export async function getConfigFromEnv(): Promise { audience: process.env.OIDC_CONSOLE_BACKEND_AUDIENCE || 'agent-console', } : undefined, + installationSecret: { + ssmPrefix: + process.env.INSTALLATION_SECRET_SSM_PREFIX || + `/nannos/${environment}/client-google-chat/installation-secrets`, + }, }; } diff --git a/packages/client-google-chat/src/middleware/a2aNotificationAuth.ts b/packages/client-google-chat/src/middleware/a2aNotificationAuth.ts index 167f4bfa..95fcc5fc 100644 --- a/packages/client-google-chat/src/middleware/a2aNotificationAuth.ts +++ b/packages/client-google-chat/src/middleware/a2aNotificationAuth.ts @@ -1,18 +1,22 @@ import { Request, Response, NextFunction } from 'express'; import { Logger } from '../utils/logger.js'; import { Config } from '../config/config.js'; +import { InstallationSecretService } from '../services/installationSecretService.js'; const logger = Logger.getLogger('A2ANotificationAuth'); /** * Middleware to validate A2A push notification tokens. * - * Matches the X-A2A-Notification-Token header against per-project secrets - * configured via A2A_NOTIFICATION_SECRET_ env vars. - * On success, sets res.locals.projectNumber + * Matches the X-A2A-Notification-Token header against the per-installation + * secret stored in AWS SSM Parameter Store (one secret per Google Chat + * bot, keyed by `botName`). On success, sets `res.locals.projectNumber`. */ -export function createA2ANotificationAuthMiddleware(googleChatConfigs: Config['googleChatConfigs']) { - return (req: Request, res: Response, next: NextFunction): void => { +export function createA2ANotificationAuthMiddleware( + googleChatConfigs: Config['googleChatConfigs'], + installationSecretService: InstallationSecretService +) { + return async (req: Request, res: Response, next: NextFunction): Promise => { const notificationToken = req.headers['x-a2a-notification-token'] as string | undefined; if (!notificationToken) { logger.warn('[A2ANotificationAuth] Missing X-A2A-Notification-Token header'); @@ -20,12 +24,16 @@ export function createA2ANotificationAuthMiddleware(googleChatConfigs: Config['g return; } - // Resolve project by matching the secret for (const project of googleChatConfigs) { - if (project.a2aNotificationSecret && project.a2aNotificationSecret === notificationToken) { - res.locals.projectNumber = project.projectNumber; - next(); - return; + try { + const secret = await installationSecretService.get(project.botName); + if (secret && secret === notificationToken) { + res.locals.projectNumber = project.projectNumber; + next(); + return; + } + } catch (err) { + logger.warn(`Failed to resolve secret for bot=${project.botName}: ${err}`); } } diff --git a/packages/client-google-chat/src/services/awsSsmInstallationSecretService.ts b/packages/client-google-chat/src/services/awsSsmInstallationSecretService.ts new file mode 100644 index 00000000..54f57d78 --- /dev/null +++ b/packages/client-google-chat/src/services/awsSsmInstallationSecretService.ts @@ -0,0 +1,58 @@ +/** + * AwsSsmInstallationSecretService + * -------------------------------- + * AWS SSM Parameter Store-backed implementation of InstallationSecretService. + * Stores secrets as SecureString parameters at `${prefix}/${sanitizedId}` and + * generates a 32-byte hex secret on first access. + */ + +import { SSMClient, GetParameterCommand, PutParameterCommand } from '@aws-sdk/client-ssm'; +import { randomBytes } from 'node:crypto'; +import { InstallationSecretService } from './installationSecretService.js'; +import { Logger } from '../utils/logger.js'; + +const logger = Logger.getLogger('AwsSsmInstallationSecretService'); + +export class AwsSsmInstallationSecretService extends InstallationSecretService { + constructor( + private readonly client: SSMClient, + private readonly prefix: string + ) { + super(); + } + + protected async resolve(installationId: string): Promise { + const existing = await this.read(installationId); + if (existing) return existing; + + const name = this.parameterName(installationId); + const generated = randomBytes(32).toString('hex'); + await this.client.send( + new PutParameterCommand({ + Name: name, + Value: generated, + Type: 'SecureString', + Overwrite: false, + Description: `Notification secret for installation ${installationId}`, + }) + ); + logger.info(`Generated and stored notification secret in SSM for installation_id=${installationId}`); + return generated; + } + + protected async read(installationId: string): Promise { + const name = this.parameterName(installationId); + try { + const res = await this.client.send(new GetParameterCommand({ Name: name, WithDecryption: true })); + return res.Parameter?.Value ?? null; + } catch (err) { + if ((err as { name?: string })?.name === 'ParameterNotFound') return null; + throw err; + } + } + + private parameterName(installationId: string): string { + const sanitized = installationId.replace(/[^a-zA-Z0-9_.\-/]/g, '_'); + return `${this.prefix.replace(/\/$/, '')}/${sanitized}`; + } +} diff --git a/packages/client-google-chat/src/services/installationRegistrar.ts b/packages/client-google-chat/src/services/installationRegistrar.ts new file mode 100644 index 00000000..dfd7c01f --- /dev/null +++ b/packages/client-google-chat/src/services/installationRegistrar.ts @@ -0,0 +1,100 @@ +/** + * InstallationRegistrar + * --------------------- + * Self-registers each tenant (Google Chat project) as a delivery channel with + * console-backend on startup. Idempotent: each registration is keyed by a + * deterministic `installation_id` so repeated boots never create duplicates. + * + * Authentication: server-to-server OAuth2 client_credentials grant against + * Keycloak; + * + * Failures are logged but never thrown — bot startup must not depend on + * console-backend availability. + */ + +import { Config } from '../config/config.js'; +import { OIDCClient } from './oidcClient.js'; +import { InstallationSecretService } from './installationSecretService.js'; +import { Logger } from '../utils/logger.js'; + +const logger = Logger.getLogger('InstallationRegistrar'); + +interface DeliveryChannelCreateBody { + name: string; + description?: string; + webhook_url: string; + secret: string; + group_ids: number[]; + installation_id: string; +} + +export interface InstallationRegistrarDeps { + config: Config; + oidcClient: OIDCClient; + installationSecretService: InstallationSecretService; +} + +export async function registerInstallations(deps: InstallationRegistrarDeps): Promise { + const { config } = deps; + + if (!config.consoleBackend) { + logger.info('CONSOLE_BACKEND_URL not set — skipping delivery-channel self-registration'); + return; + } + + if (config.googleChatConfigs.length === 0) { + logger.info('No Google Chat projects configured — nothing to register'); + return; + } + + for (const project of config.googleChatConfigs) { + try { + await registerOne(deps, { + installationId: project.botName, + name: `Google Chat ${project.botName} (${project.projectName})`, + description: `Google Chat project ${project.projectName} via ${project.botName}`, + }); + } catch (error) { + logger.error(error, `Failed to register delivery channel for project=${project.projectName}: ${error}`); + } + } +} + +export async function registerOne( + deps: InstallationRegistrarDeps, + opts: { installationId: string; name: string; description?: string } +): Promise { + const { config, oidcClient, installationSecretService } = deps; + if (!config.consoleBackend) return; + + const secret = await installationSecretService.getOrCreate(opts.installationId); + const token = await oidcClient.getServiceToken(config.consoleBackend.audience); + const webhookUrl = new URL('/api/v1/a2a/callback', config.baseUrl).toString(); + + const body: DeliveryChannelCreateBody = { + name: opts.name, + description: opts.description, + webhook_url: webhookUrl, + secret, + group_ids: [], + installation_id: opts.installationId, + }; + + const url = new URL('/api/v1/delivery-channels', config.consoleBackend!.url).toString(); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const text = await response.text().catch(() => ''); + throw new Error(`Console-backend returned ${response.status} ${response.statusText}: ${text}`); + } + + const created = response.status === 201; + logger.info(`Delivery channel ${created ? 'created' : 'updated'} for installation_id=${opts.installationId}`); +} diff --git a/packages/client-google-chat/src/services/installationSecretService.ts b/packages/client-google-chat/src/services/installationSecretService.ts new file mode 100644 index 00000000..2c532d57 --- /dev/null +++ b/packages/client-google-chat/src/services/installationSecretService.ts @@ -0,0 +1,65 @@ +/** + * InstallationSecretService + * ------------------------- + * Abstract per-installation notification-secret service. Concrete subclasses + * implement `resolve(installationId)` against a particular backend (AWS SSM, + * GCP Secret Manager, Vault, ...). The abstract base provides an in-memory + * Promise cache so callers can invoke `getOrCreate` repeatedly without + * re-hitting the backend. + * + * Implementations: + * - AwsSsmInstallationSecretService — see ./awsSsmInstallationSecretService.ts + * + * The same module is duplicated verbatim in client-slack — keep them in sync. + */ + +export abstract class InstallationSecretService { + private readonly cache = new Map(); + private readonly inflightRead = new Map>(); + + /** + * Resolve the notification secret for `installationId`, generating and + * persisting one if it does not yet exist. Successful results are cached + * for the process lifetime. Intended to be called serially at startup. + */ + async getOrCreate(installationId: string): Promise { + const cached = this.cache.get(installationId); + if (cached) return cached; + const value = await this.resolve(installationId); + this.cache.set(installationId, value); + return value; + } + + /** + * Read-only lookup. Returns the existing secret for `installationId`, or + * `null` if none has been provisioned. Successful reads are cached; misses + * are not, so a later registration becomes visible without restart. + * Concurrent callers share a single in-flight backend request. + */ + async get(installationId: string): Promise { + const cached = this.cache.get(installationId); + if (cached) return cached; + + let pending = this.inflightRead.get(installationId); + if (!pending) { + pending = (async () => { + try { + const value = await this.read(installationId); + if (value !== null) this.cache.set(installationId, value); + return value; + } finally { + this.inflightRead.delete(installationId); + } + })(); + this.inflightRead.set(installationId, pending); + } + return pending; + } + + /** Backend-specific get-or-create: read if present, otherwise generate, persist, and return. */ + protected abstract resolve(installationId: string): Promise; + + /** Backend-specific read-only lookup; returns `null` when no secret exists. */ + protected abstract read(installationId: string): Promise; +} + diff --git a/packages/client-google-chat/src/services/oidcClient.ts b/packages/client-google-chat/src/services/oidcClient.ts index dff71ee3..73b762b5 100644 --- a/packages/client-google-chat/src/services/oidcClient.ts +++ b/packages/client-google-chat/src/services/oidcClient.ts @@ -145,6 +145,21 @@ export class OIDCClient { } } + /** + * Acquire a server-to-server access token via the OAuth2 client_credentials grant. + */ + async getServiceToken(audience: string): Promise { + const config = await this.getConfiguration(); + this.logger.info(`Requesting client_credentials token for audience=${audience}`); + + const response = await client.clientCredentialsGrant(config, { + audience, + scope: 'openid', + }); + + return response.access_token; + } + /** * Map openid-client token set to UserAuthToken */ diff --git a/packages/client-slack/package-lock.json b/packages/client-slack/package-lock.json index ad754fab..6b7e0f74 100644 --- a/packages/client-slack/package-lock.json +++ b/packages/client-slack/package-lock.json @@ -1482,10 +1482,9 @@ } }, "node_modules/@aws/lambda-invoke-store": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.1.tgz", - "integrity": "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww==", - "license": "Apache-2.0", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", "engines": { "node": ">=18.0.0" } @@ -3293,20 +3292,12 @@ } }, "node_modules/@smithy/core": { - "version": "3.18.5", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.5.tgz", - "integrity": "sha512-6gnIz3h+PEPQGDj8MnRSjDvKBah042jEoPgjFGJ4iJLBE78L4lY/n98x14XyPF4u3lN179Ub/ZKFY5za9GeLQw==", - "license": "Apache-2.0", + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.6.tgz", + "integrity": "sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug==", "dependencies": { - "@smithy/middleware-serde": "^4.2.6", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-stream": "^4.5.6", - "@smithy/util-utf8": "^4.2.0", - "@smithy/uuid": "^1.1.0", + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -3314,15 +3305,12 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", - "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", - "license": "Apache-2.0", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.7.tgz", + "integrity": "sha512-xj8gq/bjFABAh6qWPSDCYcY3kzQIm4b561C+YnHH4zGq8rOgzQ3Shk+JGlpUxSd41UGiO6FkLdUCtNX1FAeHgg==", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -3400,15 +3388,12 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", - "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", - "license": "Apache-2.0", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.6.tgz", + "integrity": "sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g==", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", - "@smithy/util-base64": "^4.3.0", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -3594,15 +3579,12 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", - "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", - "license": "Apache-2.0", + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.6.tgz", + "integrity": "sha512-3fya8i7GrJilQouk4cZJKdy5k8MWQBpjfXrRNaXDedH8r779tr0jcxyH3+yoTmsluc2+vF4S343yFbnvu8ExDQ==", "dependencies": { - "@smithy/abort-controller": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -3688,18 +3670,12 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", - "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", - "license": "Apache-2.0", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.6.tgz", + "integrity": "sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ==", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-uri-escape": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -3725,10 +3701,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", - "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", - "license": "Apache-2.0", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.3.tgz", + "integrity": "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ==", "dependencies": { "tslib": "^2.6.2" }, @@ -9299,16 +9274,15 @@ } }, "node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } - ], - "license": "MIT" + ] }, "node_modules/supports-color": { "version": "7.2.0", diff --git a/packages/client-slack/sqlmigrations/ddl/006_user_auth_oidc_sub.sql b/packages/client-slack/sqlmigrations/ddl/006_user_auth_oidc_sub.sql new file mode 100644 index 00000000..a46c1f7a --- /dev/null +++ b/packages/client-slack/sqlmigrations/ddl/006_user_auth_oidc_sub.sql @@ -0,0 +1,10 @@ +-- rambler up +alter table user_auth + add column oidc_sub text; + +create index idx_user_auth_oidc_sub on user_auth(oidc_sub); + +-- rambler down +drop index if exists idx_user_auth_oidc_sub; +alter table user_auth + drop column if exists oidc_sub; diff --git a/packages/client-slack/src/config/config.ts b/packages/client-slack/src/config/config.ts index ec97dc38..385a15a5 100644 --- a/packages/client-slack/src/config/config.ts +++ b/packages/client-slack/src/config/config.ts @@ -48,6 +48,9 @@ export interface Config { url: string; audience: string; }; + readonly installationSecret: { + ssmPrefix: string; + }; readonly adminGroup: string; readonly v2CookieSecret: string; readonly sessionTtlSeconds: number; @@ -119,7 +122,7 @@ export async function getConfigFromEnv(): Promise { throw new Error('Please provide OIDC_ISSUER_URL'); } - // Validate that we have either direct OIDC client secret or SSM key + // Validate that we have either direct OIDC client secret or SSM reference const hasOidcClientSecret = process.env.OIDC_CLIENT_SECRET || process.env.OIDC_CLIENT_SECRET_SSM_KEY; if (!hasOidcClientSecret) { throw new Error('Please provide OIDC_CLIENT_SECRET or OIDC_CLIENT_SECRET_SSM_KEY'); @@ -214,6 +217,11 @@ export async function getConfigFromEnv(): Promise { audience: process.env.OIDC_CONSOLE_BACKEND_AUDIENCE || 'agent-console', } : undefined, + installationSecret: { + ssmPrefix: + process.env.INSTALLATION_SECRET_SSM_PREFIX || + `/nannos/${environment}/client-slack/installation-secrets`, + }, adminGroup: process.env.ADMIN_GROUP!, v2CookieSecret: process.env.V2_COOKIE_SECRET!, sessionTtlSeconds: Number(process.env.SESSION_TTL_SECONDS) || 86400, diff --git a/packages/client-slack/src/handlers/a2aNotificationHandler.ts b/packages/client-slack/src/handlers/a2aNotificationHandler.ts new file mode 100644 index 00000000..9cb050c1 --- /dev/null +++ b/packages/client-slack/src/handlers/a2aNotificationHandler.ts @@ -0,0 +1,112 @@ +/** + * Handler for A2A push notification callbacks from scheduled agent runs. + * + * Flow: + * 1. Scheduler engine sends a task with pushNotificationConfig (url + secret token) + * 2. When the task completes/fails, agent-runner POSTs the Task object to this callback + * 3. The callback route validates X-A2A-Notification-Token against the configured secret + * 4. We look up the Slack user by their OIDC sub (from task metadata) + * 5. We send the notification as a DM to the user + */ + +import { WebClient } from '@slack/web-api'; +import { Task } from '@a2a-js/sdk'; +import { Logger } from '../utils/logger.js'; +import type { IUserAuthStorage, BotInstallation } from '../storage/types.js'; + +const logger = Logger.getLogger('a2aNotificationHandler'); + +interface SchedulerPayload { + scheduler_status: string; + agent_message: string; + user_sub: string; +} + +function getSchedulerPayload(task: Task): SchedulerPayload | undefined { + if (!task.status.message || task.status.message.parts.length === 0) { + logger.warn(`[A2ACallback] No task.status.message (taskId=${task.id})`); + return undefined; + } + + if (task.status.message.parts[0].kind !== 'text' || !('text' in task.status.message.parts[0])) { + logger.warn(`[A2ACallback] No task.status.message.parts[0].kind='text' (taskId=${task.id})`); + return undefined; + } + + try { + return JSON.parse(task.status.message.parts[0].text) as SchedulerPayload; + } catch (e) { + logger.warn(`[A2ACallback] Error during parsing scheduler payload '${task.status.message.parts[0].text}'`); + } + + return undefined; +} + +export interface A2ANotificationDeps { + userAuthStorage: IUserAuthStorage; +} + +/** + * Handle incoming A2A push notification callback + */ +export async function handleA2ANotification( + task: Task, + botInstallation: BotInstallation, + deps: A2ANotificationDeps, +): Promise { + const { userAuthStorage } = deps; + + const schedulerPayload = getSchedulerPayload(task); + if (!schedulerPayload) { + logger.warn(`[A2ACallback] No scheduler payload (taskId=${task.id})`); + return; + } + + if (schedulerPayload.scheduler_status === 'condition_not_met') { + logger.info(`[A2ACallback] Condition is not met (taskId=${task.id})`); + return; + } + + // Look up the Slack user by OIDC sub scoped to the authenticated team + const userAuth = await userAuthStorage.findByOidcSubAndTeam( + schedulerPayload.user_sub, + botInstallation.teamId + ); + if (!userAuth) { + logger.warn( + `[A2ACallback] No Slack user found for oidcSub=${schedulerPayload.user_sub} in team=${botInstallation.teamId}` + ); + return; + } + + if (!botInstallation.botToken) { + logger.warn( + `[A2ACallback] Bot installation ${botInstallation.botName} (team=${botInstallation.teamId}) has no botToken` + ); + return; + } + + // Send DM notification to the user via the authenticated team's bot + try { + const slackClient = new WebClient(botInstallation.botToken); + + const dmResult = await slackClient.conversations.open({ users: userAuth.userId }); + if (!dmResult.ok || !dmResult.channel?.id) { + logger.warn( + `[A2ACallback] Could not open DM with user ${userAuth.userId} in team ${botInstallation.teamId}` + ); + return; + } + + await slackClient.chat.postMessage({ + channel: dmResult.channel.id, + text: schedulerPayload.agent_message, + }); + + logger.info( + `[A2ACallback] Sent notification to user ${userAuth.userId} in team ${botInstallation.teamId}` + ); + } catch (error) { + logger.error(error, `[A2ACallback] Failed to send DM notification: ${error}`); + } +} diff --git a/packages/client-slack/src/services/awsSsmInstallationSecretService.ts b/packages/client-slack/src/services/awsSsmInstallationSecretService.ts new file mode 100644 index 00000000..54f57d78 --- /dev/null +++ b/packages/client-slack/src/services/awsSsmInstallationSecretService.ts @@ -0,0 +1,58 @@ +/** + * AwsSsmInstallationSecretService + * -------------------------------- + * AWS SSM Parameter Store-backed implementation of InstallationSecretService. + * Stores secrets as SecureString parameters at `${prefix}/${sanitizedId}` and + * generates a 32-byte hex secret on first access. + */ + +import { SSMClient, GetParameterCommand, PutParameterCommand } from '@aws-sdk/client-ssm'; +import { randomBytes } from 'node:crypto'; +import { InstallationSecretService } from './installationSecretService.js'; +import { Logger } from '../utils/logger.js'; + +const logger = Logger.getLogger('AwsSsmInstallationSecretService'); + +export class AwsSsmInstallationSecretService extends InstallationSecretService { + constructor( + private readonly client: SSMClient, + private readonly prefix: string + ) { + super(); + } + + protected async resolve(installationId: string): Promise { + const existing = await this.read(installationId); + if (existing) return existing; + + const name = this.parameterName(installationId); + const generated = randomBytes(32).toString('hex'); + await this.client.send( + new PutParameterCommand({ + Name: name, + Value: generated, + Type: 'SecureString', + Overwrite: false, + Description: `Notification secret for installation ${installationId}`, + }) + ); + logger.info(`Generated and stored notification secret in SSM for installation_id=${installationId}`); + return generated; + } + + protected async read(installationId: string): Promise { + const name = this.parameterName(installationId); + try { + const res = await this.client.send(new GetParameterCommand({ Name: name, WithDecryption: true })); + return res.Parameter?.Value ?? null; + } catch (err) { + if ((err as { name?: string })?.name === 'ParameterNotFound') return null; + throw err; + } + } + + private parameterName(installationId: string): string { + const sanitized = installationId.replace(/[^a-zA-Z0-9_.\-/]/g, '_'); + return `${this.prefix.replace(/\/$/, '')}/${sanitized}`; + } +} diff --git a/packages/client-slack/src/services/installationRegistrar.ts b/packages/client-slack/src/services/installationRegistrar.ts new file mode 100644 index 00000000..0c6c7dee --- /dev/null +++ b/packages/client-slack/src/services/installationRegistrar.ts @@ -0,0 +1,113 @@ +/** + * InstallationRegistrar + * --------------------- + * Self-registers each tenant (Slack workspace) as a delivery channel with + * console-backend on startup. Idempotent: each registration is keyed by a + * deterministic `installation_id` so repeated boots never create duplicates. + * + * Authentication: server-to-server OAuth2 client_credentials grant against + * Keycloak; the `azp` claim of the issued token becomes the channel's + * owner (`client_id` column in delivery_channels). + * + * Failures are logged but never thrown — bot startup must not depend on + * console-backend availability. + */ + +import { Config } from '../config/config.js'; +import { OIDCClient } from './oidcClient.js'; +import { InstallationSecretService } from './installationSecretService.js'; +import { IBotInstallationStore } from '../storage/types.js'; +import { Logger } from '../utils/logger.js'; + +const logger = Logger.getLogger('InstallationRegistrar'); + +interface DeliveryChannelCreateBody { + name: string; + description?: string; + webhook_url: string; + secret: string; + group_ids: number[]; + installation_id: string; +} + +export interface InstallationRegistrarDeps { + config: Config; + oidcClient: OIDCClient; + botInstallationStore: IBotInstallationStore; + installationSecretService: InstallationSecretService; +} + +export async function registerInstallations(deps: InstallationRegistrarDeps): Promise { + const { config, botInstallationStore } = deps; + + if (!config.consoleBackend) { + logger.info('CONSOLE_BACKEND_URL not set — skipping delivery-channel self-registration'); + return; + } + + let installations; + try { + installations = await botInstallationStore.listAll(); + } catch (error) { + logger.error(error, `Failed to list bot installations: ${error}`); + return; + } + + const active = installations.filter((b) => b.isActive); + if (active.length === 0) { + logger.info('No active bot installations — nothing to register'); + return; + } + + for (const bot of active) { + try { + await registerOne(deps, { + installationId: bot.botName, + name: `Slack ${bot.botName} (${bot.teamId})`, + description: `Slack workspace ${bot.teamId} via ${bot.botName} (${bot.slashCommand})`, + }); + } catch (error) { + // Per-installation isolation — keep going. + logger.error(error, `Failed to register delivery channel for appId=${bot.appId}: ${error}`); + } + } +} + +export async function registerOne( + deps: InstallationRegistrarDeps, + opts: { installationId: string; name: string; description?: string } +): Promise { + const { config, oidcClient, installationSecretService } = deps; + if (!config.consoleBackend) return; + + const secret = await installationSecretService.getOrCreate(opts.installationId); + const token = await oidcClient.getServiceToken(config.consoleBackend.audience); + const webhookUrl = new URL('/api/v1/a2a/callback', config.baseUrl).toString(); + + const body: DeliveryChannelCreateBody = { + name: opts.name, + description: opts.description, + webhook_url: webhookUrl, + secret, + group_ids: [], + installation_id: opts.installationId, + }; + + const url = new URL('/api/v1/delivery-channels', config.consoleBackend.url).toString(); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const text = await response.text().catch(() => ''); + throw new Error(`Console-backend returned ${response.status} ${response.statusText}: ${text}`); + } + + const created = response.status === 201; + logger.info(`Delivery channel ${created ? 'created' : 'updated'} for installation_id=${opts.installationId}`); +} diff --git a/packages/client-slack/src/services/installationSecretService.ts b/packages/client-slack/src/services/installationSecretService.ts new file mode 100644 index 00000000..8e895dd6 --- /dev/null +++ b/packages/client-slack/src/services/installationSecretService.ts @@ -0,0 +1,65 @@ +/** + * InstallationSecretService + * ------------------------- + * Abstract per-installation notification-secret service. Concrete subclasses + * implement `resolve(installationId)` against a particular backend (AWS SSM, + * GCP Secret Manager, Vault, ...). The abstract base provides an in-memory + * Promise cache so callers can invoke `getOrCreate` repeatedly without + * re-hitting the backend. + * + * Implementations: + * - AwsSsmInstallationSecretService — see ./awsSsmInstallationSecretService.ts + * + * The same module is duplicated verbatim in client-google-chat — keep them in sync. + */ + +export abstract class InstallationSecretService { + private readonly cache = new Map(); + private readonly inflightRead = new Map>(); + + /** + * Resolve the notification secret for `installationId`, generating and + * persisting one if it does not yet exist. Successful results are cached + * for the process lifetime. Intended to be called serially at startup. + */ + async getOrCreate(installationId: string): Promise { + const cached = this.cache.get(installationId); + if (cached) return cached; + const value = await this.resolve(installationId); + this.cache.set(installationId, value); + return value; + } + + /** + * Read-only lookup. Returns the existing secret for `installationId`, or + * `null` if none has been provisioned. Successful reads are cached; misses + * are not, so a later registration becomes visible without restart. + * Concurrent callers share a single in-flight backend request. + */ + async get(installationId: string): Promise { + const cached = this.cache.get(installationId); + if (cached) return cached; + + let pending = this.inflightRead.get(installationId); + if (!pending) { + pending = (async () => { + try { + const value = await this.read(installationId); + if (value !== null) this.cache.set(installationId, value); + return value; + } finally { + this.inflightRead.delete(installationId); + } + })(); + this.inflightRead.set(installationId, pending); + } + return pending; + } + + /** Backend-specific get-or-create: read if present, otherwise generate, persist, and return. */ + protected abstract resolve(installationId: string): Promise; + + /** Backend-specific read-only lookup; returns `null` when no secret exists. */ + protected abstract read(installationId: string): Promise; +} + diff --git a/packages/client-slack/src/services/oidcClient.ts b/packages/client-slack/src/services/oidcClient.ts index d93b8021..fb1f483b 100644 --- a/packages/client-slack/src/services/oidcClient.ts +++ b/packages/client-slack/src/services/oidcClient.ts @@ -230,15 +230,30 @@ export class OIDCClient { } } + /** + * Acquire a server-to-server access token via the OAuth2 client_credentials grant. + */ + async getServiceToken(audience: string): Promise { + const config = await this.getConfiguration(); + this.logger.info(`Requesting client_credentials token for audience=${audience}`); + + const response = await client.clientCredentialsGrant(config, { + audience, + scope: 'openid', + }); + + return response.access_token; + } + /** * Map openid-client token set to UserAuthToken */ - private mapTokenSet(tokens: client.TokenEndpointResponse): Omit { + private mapTokenSet(tokens: client.TokenEndpointResponse & client.TokenEndpointResponseHelpers): Omit { const now = Date.now(); const expiresIn = tokens.expires_in ?? 3600; // Default to 1 hour if not provided const expiresAt = now + expiresIn * 1000; - return { + const result: Omit = { accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt, @@ -248,5 +263,11 @@ export class OIDCClient { createdAt: now, updatedAt: now, }; + + if (tokens.id_token) { + result.oidcSub = tokens.claims()?.sub; + } + + return result; } } diff --git a/packages/client-slack/src/services/userAuthService.ts b/packages/client-slack/src/services/userAuthService.ts index 3410a276..8cb0e063 100644 --- a/packages/client-slack/src/services/userAuthService.ts +++ b/packages/client-slack/src/services/userAuthService.ts @@ -104,6 +104,7 @@ ai * Check if user is authorized (has valid token, refreshing if needed) refreshToken: refreshedToken.refreshToken, expiresAt: refreshedToken.expiresAt, idToken: refreshedToken.idToken, + oidcSub: refreshedToken.oidcSub, }); return refreshedToken.accessToken; diff --git a/packages/client-slack/src/slackApp.ts b/packages/client-slack/src/slackApp.ts index 0a2fcf01..9e7ea9db 100644 --- a/packages/client-slack/src/slackApp.ts +++ b/packages/client-slack/src/slackApp.ts @@ -13,6 +13,11 @@ import { handleOAuthCallback, generateCallbackHTML } from './utils/oauthCallback import { processPendingRequest } from './utils/processPendingRequest.js'; import { recoverOrphanedTasks } from './utils/taskRecovery.js'; import { MultiTenantHTTPReceiver } from './receivers/MultiTenantHTTPReceiver.js'; +import { handleA2ANotification } from './handlers/a2aNotificationHandler.js'; +import { registerInstallations } from './services/installationRegistrar.js'; +import { AwsSsmInstallationSecretService } from './services/awsSsmInstallationSecretService.js'; +import { SSMClient } from '@aws-sdk/client-ssm'; +import { Task } from '@a2a-js/sdk'; import { ParamsIncomingMessage } from '@slack/bolt/dist/receivers/ParamsIncomingMessage.js'; import { ServerResponse } from 'node:http'; let userAuthService: UserAuthService; @@ -71,6 +76,38 @@ export async function startSlackApp(config: Config) { } // ----- Custom routes (used by both HTTP receiver and socket-mode) ----- + // Per-installation notification secrets are stored in AWS SSM Parameter Store; + // the registrar generates them on first registration and the callback validator + // resolves them here to authenticate inbound A2A notifications. + const installationSecretService = new AwsSsmInstallationSecretService( + new SSMClient({ region: config.aws.region }), + config.installationSecret.ssmPrefix + ); + + /** + * Verify a notification token by matching it against the SSM-stored secret + * for any active bot installation. Resolves true on first match. + */ + /** + * Verify a notification token by matching it against the SSM-stored secret + * for any active bot installation. Returns the matching installation on + * success (so the caller can route the notification to the correct team) + * or `null` if no installation matched. + */ + const validateNotificationToken = async (token: string) => { + const installations = await storage.botInstallation.listAll(); + for (const bot of installations) { + if (!bot.isActive) continue; + try { + const secret = await installationSecretService.get(bot.botName); + if (secret && token === secret) return bot; + } catch (err) { + logger.warn(`Failed to resolve secret for ${bot.botName}: ${err}`); + } + } + return null; + }; + const customRoutes = [ { path: '/api/v1/health', @@ -187,8 +224,70 @@ export async function startSlackApp(config: Config) { { path: '/api/v1/a2a/callback', method: 'POST' as const, - handler: async (_req: ParamsIncomingMessage, _res: ServerResponse) => { - logger.warn('Received request on /api/v1/a2a/callback — NOT IMPLEMENTED YET'); + handler: async (req: ParamsIncomingMessage, res: ServerResponse) => { + // Read request body + const chunks: Buffer[] = []; + for await (const chunk of req) { + chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); + } + const body = Buffer.concat(chunks).toString('utf-8'); + + // Validate notification token + const notificationToken = req.headers['x-a2a-notification-token'] as string | undefined; + if (!notificationToken) { + logger.warn('[A2ACallback] Missing X-A2A-Notification-Token header'); + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Missing notification token' })); + return; + } + + const botInstallation = await validateNotificationToken(notificationToken); + if (!botInstallation) { + logger.warn('[A2ACallback] Invalid notification token'); + res.writeHead(403, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Invalid notification token' })); + return; + } + + // Parse task payload + let task: Task; + try { + task = JSON.parse(body) as Task; + } catch { + logger.warn('[A2ACallback] Invalid JSON body'); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Invalid JSON body' })); + return; + } + + if (!task || task.kind !== 'task' || !task.status) { + logger.warn('[A2ACallback] Invalid task payload'); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Invalid task payload' })); + return; + } + + // Only process completed/failed notifications + const state = task.status.state; + if (state !== 'completed' && state !== 'failed') { + logger.debug(`[A2ACallback] Ignoring notification with state=${state}`); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ acknowledged: true })); + return; + } + + logger.info(`[A2ACallback] Processing ${state} notification for taskId=${task.id}`); + + // Respond immediately to acknowledge receipt + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ acknowledged: true })); + + // Process notification asynchronously + handleA2ANotification(task, botInstallation, { + userAuthStorage: storage.userAuth, + }).catch((error) => { + logger.error(error, `[A2ACallback] Error handling notification: ${error}`); + }); }, }, ]; @@ -302,6 +401,17 @@ export async function startSlackApp(config: Config) { logger.info(`A2A Server: ${config.a2aServer.url}`); logger.info(`OIDC Issuer: ${config.oidc.issuerUrl}`); + // Self-register each Slack workspace as a delivery channel with console-backend. + // Failures are isolated and never block startup. + registerInstallations({ + config, + oidcClient, + botInstallationStore: storage.botInstallation, + installationSecretService, + }).catch((error) => { + logger.error(error, `Delivery-channel self-registration failed: ${error}`); + }); + // Task recovery configuration const RECOVERY_MIN_AGE_MS = 2 * 60 * 1000; // 2 minutes - only recover tasks older than this const RECOVERY_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes - how often to run recovery diff --git a/packages/client-slack/src/storage/providers/postgres/pgUserAuthStorage.ts b/packages/client-slack/src/storage/providers/postgres/pgUserAuthStorage.ts index 1b77d4da..4566469d 100644 --- a/packages/client-slack/src/storage/providers/postgres/pgUserAuthStorage.ts +++ b/packages/client-slack/src/storage/providers/postgres/pgUserAuthStorage.ts @@ -22,10 +22,11 @@ export class PgUserAuthStorage { await this.pool.query(SQL` INSERT INTO user_auth ( user_id, team_id, access_token, refresh_token, expires_at, - token_type, scope, id_token + token_type, scope, id_token, oidc_sub ) VALUES ( ${token.userId}, ${token.teamId}, ${token.accessToken}, ${token.refreshToken}, - ${new Date(token.expiresAt)}, ${token.tokenType}, ${token.scope}, ${token.idToken} + ${new Date(token.expiresAt)}, ${token.tokenType}, ${token.scope}, ${token.idToken}, + ${token.oidcSub} ) ON CONFLICT (user_id, team_id) DO UPDATE SET access_token = EXCLUDED.access_token, @@ -33,7 +34,8 @@ export class PgUserAuthStorage { expires_at = EXCLUDED.expires_at, token_type = EXCLUDED.token_type, scope = EXCLUDED.scope, - id_token = EXCLUDED.id_token + id_token = EXCLUDED.id_token, + oidc_sub = EXCLUDED.oidc_sub `); this.logger.info(`Saved auth token for user ${token.userId} in team ${token.teamId}`); } catch (error) { @@ -49,7 +51,7 @@ export class PgUserAuthStorage { try { const result = await this.pool.query(SQL` SELECT user_id, team_id, access_token, refresh_token, expires_at, - token_type, scope, id_token, created_at, updated_at + token_type, scope, id_token, oidc_sub, created_at, updated_at FROM user_auth WHERE user_id = ${userId} AND team_id = ${teamId} `); @@ -68,6 +70,7 @@ export class PgUserAuthStorage { tokenType: row.token_type, scope: row.scope, idToken: row.id_token, + oidcSub: row.oidc_sub, createdAt: new Date(row.created_at).getTime(), updatedAt: new Date(row.updated_at).getTime(), }; @@ -104,6 +107,7 @@ export class PgUserAuthStorage { refreshToken?: string; expiresAt?: number; idToken?: string; + oidcSub?: string; } ): Promise { const setClauses: string[] = []; @@ -126,6 +130,10 @@ export class PgUserAuthStorage { setClauses.push(`id_token = $${paramIndex++}`); values.push(updates.idToken); } + if (updates.oidcSub !== undefined) { + setClauses.push(`oidc_sub = $${paramIndex++}`); + values.push(updates.oidcSub); + } if (setClauses.length === 0) { return; // Nothing to update @@ -163,4 +171,78 @@ export class PgUserAuthStorage { this.logger.info(`Token found for userId=${userId}, teamId=${teamId}, valid=${isValid}`); return isValid; } + + /** + * Find a user auth record by OIDC subject identifier + */ + async findByOidcSub(oidcSub: string): Promise { + try { + const result = await this.pool.query(SQL` + SELECT user_id, team_id, access_token, refresh_token, expires_at, + token_type, scope, id_token, oidc_sub, created_at, updated_at + FROM user_auth + WHERE oidc_sub = ${oidcSub} + LIMIT 1 + `); + + if (result.rows.length === 0) { + return null; + } + + const row = result.rows[0]; + return { + userId: row.user_id, + teamId: row.team_id, + accessToken: row.access_token, + refreshToken: row.refresh_token, + expiresAt: new Date(row.expires_at).getTime(), + tokenType: row.token_type, + scope: row.scope, + idToken: row.id_token, + oidcSub: row.oidc_sub, + createdAt: new Date(row.created_at).getTime(), + updatedAt: new Date(row.updated_at).getTime(), + }; + } catch (error) { + this.logger.error(error, `Failed to find user by OIDC sub: ${error}`); + throw new Error(`Failed to find user by OIDC sub: ${error}`); + } + } + + /** + * Find a user auth record by OIDC subject identifier scoped to a Slack team. + */ + async findByOidcSubAndTeam(oidcSub: string, teamId: string): Promise { + try { + const result = await this.pool.query(SQL` + SELECT user_id, team_id, access_token, refresh_token, expires_at, + token_type, scope, id_token, oidc_sub, created_at, updated_at + FROM user_auth + WHERE oidc_sub = ${oidcSub} AND team_id = ${teamId} + LIMIT 1 + `); + + if (result.rows.length === 0) { + return null; + } + + const row = result.rows[0]; + return { + userId: row.user_id, + teamId: row.team_id, + accessToken: row.access_token, + refreshToken: row.refresh_token, + expiresAt: new Date(row.expires_at).getTime(), + tokenType: row.token_type, + scope: row.scope, + idToken: row.id_token, + oidcSub: row.oidc_sub, + createdAt: new Date(row.created_at).getTime(), + updatedAt: new Date(row.updated_at).getTime(), + }; + } catch (error) { + this.logger.error(error, `Failed to find user by OIDC sub and team: ${error}`); + throw new Error(`Failed to find user by OIDC sub and team: ${error}`); + } + } } diff --git a/packages/client-slack/src/storage/types.ts b/packages/client-slack/src/storage/types.ts index 74928a37..f5f102fa 100644 --- a/packages/client-slack/src/storage/types.ts +++ b/packages/client-slack/src/storage/types.ts @@ -7,6 +7,7 @@ export interface UserAuthToken { tokenType: string; // Usually "Bearer" scope?: string; // Granted scopes idToken?: string; // OIDC ID token + oidcSub?: string; // OIDC subject identifier createdAt: number; // Unix timestamp when token was created updatedAt: number; // Unix timestamp when token was last updated } @@ -72,9 +73,12 @@ export interface IUserAuthStorage { refreshToken?: string; expiresAt?: number; idToken?: string; + oidcSub?: string; } ): Promise; hasValidToken(userId: string, teamId: string): Promise; + findByOidcSub(oidcSub: string): Promise; + findByOidcSubAndTeam(oidcSub: string, teamId: string): Promise; } /** diff --git a/packages/console-backend/console_backend/models/delivery_channel.py b/packages/console-backend/console_backend/models/delivery_channel.py index ed6994df..1a390179 100644 --- a/packages/console-backend/console_backend/models/delivery_channel.py +++ b/packages/console-backend/console_backend/models/delivery_channel.py @@ -25,6 +25,14 @@ class DeliveryChannelCreate(BaseModel): group_ids: list[int] = Field( description="IDs of user groups whose members can see and use this channel.", ) + installation_id: str | None = Field( + default=None, + min_length=1, + max_length=200, + description=( + "Stable client-supplied identifier enabling idempotent re-registration. " + ), + ) class DeliveryChannelUpdate(BaseModel): @@ -47,6 +55,10 @@ class DeliveryChannelResponse(BaseModel): client_id: str = Field(description="Keycloak client ID of the A2A service that registered this channel.") registered_by: str = Field(description="OIDC subject (sub) of the token used to register this channel.") group_ids: list[int] = Field(description="IDs of groups that can use this channel.") + installation_id: str | None = Field( + default=None, + description="Stable client-supplied identifier (set when the channel was self-registered).", + ) created_at: datetime updated_at: datetime diff --git a/packages/console-backend/console_backend/repositories/delivery_channel_repository.py b/packages/console-backend/console_backend/repositories/delivery_channel_repository.py index 6bf37bea..4c917c67 100644 --- a/packages/console-backend/console_backend/repositories/delivery_channel_repository.py +++ b/packages/console-backend/console_backend/repositories/delivery_channel_repository.py @@ -28,6 +28,7 @@ def _row_to_response(row: Any, group_ids: list[int]) -> DeliveryChannelResponse: client_id=row["client_id"], registered_by=row["registered_by"], group_ids=group_ids, + installation_id=row["installation_id"], created_at=row["created_at"], updated_at=row["updated_at"], ) @@ -70,6 +71,7 @@ async def create_channel( "secret": data.secret, "client_id": client_id, "registered_by": actor.sub, + "installation_id": data.installation_id, "created_at": now, "updated_at": now, }, @@ -241,3 +243,55 @@ async def get_owner_client_id(self, db: AsyncSession, channel_id: int) -> str | async def get_channel_group_ids(self, db: AsyncSession, channel_id: int) -> list[int]: """Return the group IDs associated with a channel.""" return await _fetch_group_ids(db, channel_id) + + async def get_by_installation( + self, + db: AsyncSession, + client_id: str, + installation_id: str, + ) -> DeliveryChannelResponse | None: + """Look up a channel by its (client_id, installation_id) idempotency key.""" + result = await db.execute( + text( + "SELECT * FROM delivery_channels " + "WHERE client_id = :client_id AND installation_id = :installation_id" + ), + {"client_id": client_id, "installation_id": installation_id}, + ) + row = result.mappings().first() + if row is None: + return None + group_ids = await _fetch_group_ids(db, row["id"]) + return _row_to_response(row, group_ids) + + async def upsert_channel_by_installation( + self, + db: AsyncSession, + actor: User, + client_id: str, + data: DeliveryChannelCreate, + ) -> tuple[DeliveryChannelResponse, bool]: + """Idempotently create or update a channel keyed by ``(client_id, installation_id)``. + + Returns ``(channel, created)`` where ``created`` is True when a new row was inserted. + Mutable fields (name, description, webhook_url, secret, group_ids) are overwritten + on update. ``installation_id`` MUST be set on ``data``; callers should branch to + :meth:`create_channel` when it is not. + """ + assert data.installation_id is not None, "installation_id required for upsert" + + existing = await self.get_by_installation(db, client_id, data.installation_id) + if existing is None: + created = await self.create_channel(db=db, actor=actor, client_id=client_id, data=data) + return created, True + + update = DeliveryChannelUpdate( + name=data.name, + description=data.description, + webhook_url=data.webhook_url, + secret=data.secret, + group_ids=data.group_ids, + ) + updated = await self.update_channel(db=db, actor=actor, channel_id=existing.id, data=update) + assert updated is not None + return updated, False diff --git a/packages/console-backend/console_backend/routers/delivery_channel_router.py b/packages/console-backend/console_backend/routers/delivery_channel_router.py index 8beded42..6051d275 100644 --- a/packages/console-backend/console_backend/routers/delivery_channel_router.py +++ b/packages/console-backend/console_backend/routers/delivery_channel_router.py @@ -7,7 +7,7 @@ import logging -from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi import APIRouter, Depends, HTTPException, Request, Response, status from ..db.session import DbSession from ..dependencies import get_client_id_from_request, is_admin_mode, require_auth_or_bearer_token @@ -49,6 +49,7 @@ def _get_user_group_service(request: Request): # type: ignore[return] ) async def register_channel( request: Request, + response: Response, data: DeliveryChannelCreate, db: DbSession, current_user: User = Depends(require_auth_or_bearer_token), @@ -75,6 +76,18 @@ async def register_channel( detail=f"Group ID(s) not found: {sorted(missing)}", ) + # Idempotent path: when a Bearer-token caller supplies an installation_id, + # upsert by (client_id, installation_id). Returns 200 on update, 201 on create. + caller_client_id = await get_client_id_from_request(request) + if caller_client_id and data.installation_id: + channel, created = await repo.upsert_channel_by_installation( + db=db, actor=current_user, client_id=client_id, data=data + ) + await db.commit() + if not created: + response.status_code = status.HTTP_200_OK + return channel + channel = await repo.create_channel(db=db, actor=current_user, client_id=client_id, data=data) await db.commit() return channel diff --git a/packages/console-backend/sqlmigrations/ddl/060_add_installation_id_to_delivery_channels.sql b/packages/console-backend/sqlmigrations/ddl/060_add_installation_id_to_delivery_channels.sql new file mode 100644 index 00000000..44b1eb26 --- /dev/null +++ b/packages/console-backend/sqlmigrations/ddl/060_add_installation_id_to_delivery_channels.sql @@ -0,0 +1,15 @@ +-- rambler up + +-- Add a stable client-supplied identifier to enable idempotent self-registration +-- by bot clients (one delivery channel per tenant: Slack workspace / GChat project). +ALTER TABLE delivery_channels + ADD COLUMN installation_id TEXT NULL; + +CREATE UNIQUE INDEX delivery_channels_client_installation_uidx + ON delivery_channels (client_id, installation_id) + WHERE installation_id IS NOT NULL; + +-- rambler down + +DROP INDEX IF EXISTS delivery_channels_client_installation_uidx; +ALTER TABLE delivery_channels DROP COLUMN IF EXISTS installation_id;