diff --git a/package-lock.json b/package-lock.json index c6332079..4e1803c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,10 @@ "version": "1.11.3", "license": "Apache-2.0", "dependencies": { + "@google-cloud/sql": "^0.25.0", "@googleapis/sqladmin": "^37.0.0", + "@grpc/grpc-js": "^1.14.4", + "@grpc/proto-loader": "^0.8.1", "gaxios": "^7.3.1", "google-auth-library": "^10.9.1", "p-throttle": "^8.1.0" @@ -582,6 +585,18 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@google-cloud/sql": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@google-cloud/sql/-/sql-0.25.0.tgz", + "integrity": "sha512-OKAA98q2M25rQcqcFvbQniix4zTFcbeSyauj+PcPd+6ngn7eXvAUqpuCRVxbIGmA8r5OtE6Ru/oCJ1xwcUR2vA==", + "license": "Apache-2.0", + "dependencies": { + "google-gax": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@googleapis/sqladmin": { "version": "37.0.0", "resolved": "https://registry.npmjs.org/@googleapis/sqladmin/-/sqladmin-37.0.0.tgz", @@ -594,6 +609,95 @@ "node": ">=12.0.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -791,6 +895,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@mswjs/interceptors": { "version": "0.41.9", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.9.tgz", @@ -1242,6 +1356,63 @@ "@prisma/debug": "5.22.0" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, "node_modules/@sequelize/core": { "version": "7.0.0-alpha.48", "resolved": "https://registry.npmjs.org/@sequelize/core/-/core-7.0.0-alpha.48.tgz", @@ -2429,7 +2600,6 @@ "version": "24.13.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" @@ -4029,6 +4199,32 @@ "node": ">= 0.4" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4050,6 +4246,15 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -4129,7 +4334,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4924,7 +5128,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -5136,6 +5339,243 @@ "node": ">=18" } }, + "node_modules/google-gax": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.8.tgz", + "integrity": "sha512-M4vpZcXQIC1gqIVGQ7eaU3jXQA6zecStyTXu514TYfThlgSurYJOxHZo9fzU6hAgwPWvuEynAVHWyaIk80VeEA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "duplexify": "^4.1.3", + "google-auth-library": "10.5.0", + "google-logging-utils": "1.1.3", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "3.0.4", + "protobufjs": "^7.5.4", + "retry-request": "^8.0.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/google-gax/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/google-gax/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/google-gax/node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-gax/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/google-gax/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/google-gax/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-gax/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/google-gax/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/google-logging-utils": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", @@ -5950,7 +6390,6 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -6142,7 +6581,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -6969,6 +7407,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -7029,7 +7473,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "dev": true, "license": "Apache-2.0" }, "node_modules/loose-envify": { @@ -7823,6 +8266,15 @@ "node": ">=8" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -7846,7 +8298,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -8578,6 +9029,41 @@ "node": ">= 8" } }, + "node_modules/proto3-json-serializer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz", + "integrity": "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8928,6 +9414,15 @@ "dev": true, "license": "MIT" }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -9055,6 +9550,19 @@ "dev": true, "license": "MIT" }, + "node_modules/retry-request": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.4.tgz", + "integrity": "sha512-pI6/7eabUYkZxamkOq0g0uMxKLLGnjzhefY+vL8bVXag5rto4OU2YBTPytWLuHH7aEKD6fn7kJycQfid0Mwnkw==", + "license": "MIT", + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -9551,6 +10059,21 @@ "node": ">=8" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", @@ -9562,7 +10085,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -9703,6 +10225,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10003,6 +10531,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/teeny-request": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.4.tgz", + "integrity": "sha512-R1Cg4Vu0UULeDfHL/kjABLaTW++9yD/B6n2g48y5dJ04hsEaxcfmAqbNDzNsbqAYJyIpZafjklLG9YxRu9uzOg==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/test-exclude": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", @@ -10541,7 +11084,6 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, "license": "MIT" }, "node_modules/uri-js": { @@ -10560,6 +11102,12 @@ "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", "license": "BSD" }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/uuid": { "version": "11.1.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", @@ -10886,7 +11434,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { @@ -10955,7 +11502,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" diff --git a/package.json b/package.json index f8566d59..ed9b29af 100644 --- a/package.json +++ b/package.json @@ -87,9 +87,12 @@ "url": "git+https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector" }, "dependencies": { + "@google-cloud/sql": "^0.25.0", "@googleapis/sqladmin": "^37.0.0", "gaxios": "^7.3.1", "google-auth-library": "^10.9.1", + "@grpc/grpc-js": "^1.14.4", + "@grpc/proto-loader": "^0.8.1", "p-throttle": "^8.1.0" } } diff --git a/scripts/fixup.cjs b/scripts/fixup.cjs index baa16e53..55f80768 100644 --- a/scripts/fixup.cjs +++ b/scripts/fixup.cjs @@ -39,12 +39,12 @@ async function fixupImportFileExtensions() { const replaceExtension = source => source.replace(/(^import.*from '\.\/.*)(';)$/gm, '$1.js$2'); - const recursiveReadDir = async path => { - const dirResults = await readdir(mjsDistFolder, {withFileTypes: true}); + const recursiveReadDir = async dirPath => { + const dirResults = await readdir(dirPath, {withFileTypes: true}); for (const entry of dirResults) { - const path = resolve(mjsDistFolder, entry.name); + const path = resolve(dirPath, entry.name); if (entry.isDirectory()) { - recursiveReadDir(path); + await recursiveReadDir(path); } else if (path.endsWith('.js') || path.endsWith('.d.ts')) { mjsFilePaths.push(path); } diff --git a/src/cloud-sql-instance.ts b/src/cloud-sql-instance.ts index 8d9f0e91..e88bbee9 100644 --- a/src/cloud-sql-instance.ts +++ b/src/cloud-sql-instance.ts @@ -63,10 +63,10 @@ interface CloudSQLInstanceOptions { } interface RefreshResult { - ephemeralCert: SslCert; + ephemeralCert?: SslCert; host: string; - privateKey: string; - serverCaCert: SslCert; + privateKey?: string; + serverCaCert?: SslCert; } export class CloudSQLInstance { @@ -243,10 +243,12 @@ export class CloudSQLInstance { // then we go ahead and update values this.updateValues(nextValues); - const refreshInterval = getRefreshInterval( - /* c8 ignore next */ - String(this.ephemeralCert?.expirationTime) - ); + let refreshInterval = 3600000; // 1 hour default + if (this.ephemeralCert) { + refreshInterval = getRefreshInterval( + this.ephemeralCert.expirationTime + ); + } this.scheduleRefresh(refreshInterval); // This is the end of the successful refresh chain, so now @@ -289,30 +291,37 @@ export class CloudSQLInstance { return Promise.reject('closed'); } - const rsaKeys: RSAKeys = await generateKeys(); const metadata: InstanceMetadata = await this.sqlAdminFetcher.getInstanceMetadata(this.instanceInfo); - const ephemeralCert = await this.sqlAdminFetcher.getEphemeralCertificate( - this.instanceInfo, - rsaKeys.publicKey, - this.authType - ); - let host; - if (this.instanceInfo && this.instanceInfo.domainName) { - try { - const ips = await resolveARecord(this.instanceInfo.domainName); - if (ips && ips.length > 0) { - host = ips[0]; + let host = ''; + let ephemeralCert; + let privateKey; + + if (this.ipType !== IpAddressTypes.SQL_DATA) { + const rsaKeys: RSAKeys = await generateKeys(); + ephemeralCert = await this.sqlAdminFetcher.getEphemeralCertificate( + this.instanceInfo, + rsaKeys.publicKey, + this.authType + ); + privateKey = rsaKeys.privateKey; + + if (this.instanceInfo && this.instanceInfo.domainName) { + try { + const ips = await resolveARecord(this.instanceInfo.domainName); + if (ips && ips.length > 0) { + host = ips[0]; + } + } catch (e) { + // ignore error, fallback to metadata IP } - } catch (e) { - // ignore error, fallback to metadata IP + } + if (!host) { + host = selectIpAddress(metadata.ipAddresses, this.ipType); } } - if (!host) { - host = selectIpAddress(metadata.ipAddresses, this.ipType); - } - const privateKey = rsaKeys.privateKey; + const serverCaCert = metadata.serverCaCert; this.serverCaMode = metadata.serverCaMode; this.dnsName = metadata.dnsName; @@ -347,6 +356,9 @@ export class CloudSQLInstance { privateKey, serverCaCert, }: Partial): boolean { + if (this.ipType === IpAddressTypes.SQL_DATA) { + return true; + } if (!ephemeralCert || !host || !privateKey || !serverCaCert) { return false; } diff --git a/src/connector.ts b/src/connector.ts index a665fad5..12dbffca 100644 --- a/src/connector.ts +++ b/src/connector.ts @@ -13,7 +13,7 @@ // limitations under the License. import {createServer, Server, Socket} from 'node:net'; -import tls from 'node:tls'; + import {promisify} from 'node:util'; import {AuthClient, GoogleAuth} from 'google-auth-library'; import {CloudSQLInstance} from './cloud-sql-instance'; @@ -22,6 +22,8 @@ import {IpAddressTypes} from './ip-addresses'; import {AuthTypes} from './auth-types'; import {SQLAdminFetcher} from './sqladmin-fetcher'; import {CloudSQLConnectorError} from './errors'; +import {resolveInstanceName} from './parse-instance-connection-name'; +import {SqlDataClient} from './sql-data-client'; // These Socket types are subsets from nodejs definitely typed repo, ref: // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/ae0fe42ff0e6e820e8ae324acf4f8e944aa1b2b7/types/node/v18/net.d.ts#L437 @@ -46,6 +48,8 @@ export declare interface ConnectionOptions { domainName?: string; failoverPeriod?: number; limitRateInterval?: number; + sqlDataEndpoint?: string; + sqlDataStreamTimeout?: number; } export declare interface SocketConnectionOptions extends ConnectionOptions { @@ -53,11 +57,11 @@ export declare interface SocketConnectionOptions extends ConnectionOptions { } interface StreamFunction { - (): tls.TLSSocket; + (): Socket; } interface PromisedStreamFunction { - (): Promise; + (): Promise; } // DriverOptions is the interface describing the object returned by @@ -187,6 +191,8 @@ export interface ConnectorOptions { */ universeDomain?: string; userAgent?: string; + sqlDataEndpoint?: string; + sqlDataStreamTimeout?: number; } // The Connector class is the main public API to interact @@ -196,6 +202,10 @@ export class Connector { private readonly sqlAdminFetcher: SQLAdminFetcher; private readonly localProxies: Set; private readonly sockets: Set; + private readonly sqlDataEndpoint?: string; + private readonly sqlDataStreamTimeout?: number; + private readonly sqlDataTunnels = new Map(); + private readonly sqlDataUnsupportedInstances = new Set(); constructor(opts: ConnectorOptions = {}) { this.sqlAdminFetcher = new SQLAdminFetcher({ @@ -207,6 +217,8 @@ export class Connector { this.instances = new CloudSQLInstanceMap(this.sqlAdminFetcher); this.localProxies = new Set(); this.sockets = new Set(); + this.sqlDataEndpoint = opts.sqlDataEndpoint; + this.sqlDataStreamTimeout = opts.sqlDataStreamTimeout; } // Connector.getOptions is a method that accepts a Cloud SQL instance @@ -222,8 +234,162 @@ export class Connector { // const res = await pool.query('SELECT * FROM pg_catalog.pg_tables;') async getOptions(opts: ConnectionOptions): Promise { const {instances} = this; + + const instanceInfo = await resolveInstanceName( + opts.instanceConnectionName, + opts.domainName, + this.sqlAdminFetcher + ); + const connectionName = `${instanceInfo.projectId}:${instanceInfo.regionId}:${instanceInfo.instanceId}`; + + let ipType = opts.ipType || IpAddressTypes.PUBLIC; + + if ( + ipType === IpAddressTypes.SQL_DATA && + this.sqlDataUnsupportedInstances.has(connectionName) + ) { + ipType = IpAddressTypes.PUBLIC; + opts.ipType = ipType; + } + await instances.loadInstance(opts); + if (ipType === IpAddressTypes.SQL_DATA) { + let tunnel = this.sqlDataTunnels.get(connectionName); + if (!tunnel) { + const cloudSqlInstance = instances.getInstance(opts); + const getDirectSocket = () => { + const { + instanceInfo, + ephemeralCert, + host, + port, + privateKey, + serverCaCert, + dnsName, + } = cloudSqlInstance; + + if ( + instanceInfo && + ephemeralCert && + host && + port && + privateKey && + serverCaCert + ) { + const tlsSocket = getSocket({ + instanceInfo, + ephemeralCert, + host, + port, + privateKey, + serverCaCert, + instanceDnsName: dnsName, + serverName: instanceInfo.domainName || dnsName, + }); + tlsSocket.once('error', () => { + cloudSqlInstance.forceRefresh(); + }); + tlsSocket.once('secureConnect', async () => { + cloudSqlInstance.setEstablishedConnection(); + }); + + cloudSqlInstance.addSocket(tlsSocket); + + return tlsSocket; + } + + throw new CloudSQLConnectorError({ + message: 'Invalid Cloud SQL Instance info', + code: 'EBADINSTANCEINFO', + }); + }; + + tunnel = new SqlDataClient({ + instanceConnectionName: connectionName, + auth: this.sqlAdminFetcher.adminAuth, + endpoint: opts.sqlDataEndpoint || this.sqlDataEndpoint, + streamTimeout: opts.sqlDataStreamTimeout || this.sqlDataStreamTimeout, + getDirectSocket, + onUnsupported: () => { + this.sqlDataUnsupportedInstances.add(connectionName); + }, + }); + this.sqlDataTunnels.set(connectionName, tunnel); + } + + const tunnelPort = await tunnel.start(); + + return { + stream: () => { + if (this.sqlDataUnsupportedInstances.has(connectionName)) { + const cloudSqlInstance = instances.getInstance(opts); + const { + instanceInfo, + ephemeralCert, + host, + port, + privateKey, + serverCaCert, + dnsName, + } = cloudSqlInstance; + + if ( + instanceInfo && + ephemeralCert && + host && + port && + privateKey && + serverCaCert + ) { + const tlsSocket = getSocket({ + instanceInfo, + ephemeralCert, + host, + port, + privateKey, + serverCaCert, + instanceDnsName: dnsName, + serverName: instanceInfo.domainName || dnsName, + }); + tlsSocket.once('error', () => { + cloudSqlInstance.forceRefresh(); + }); + tlsSocket.once('secureConnect', async () => { + cloudSqlInstance.setEstablishedConnection(); + }); + + cloudSqlInstance.addSocket(tlsSocket); + + return tlsSocket; + } + + throw new CloudSQLConnectorError({ + message: 'Invalid Cloud SQL Instance info', + code: 'EBADINSTANCEINFO', + }); + } + + const socket = new Socket(); + socket.connect(tunnelPort, '127.0.0.1'); + + const cloudSqlInstance = instances.getInstance(opts); + socket.once('connect', () => { + cloudSqlInstance.setEstablishedConnection(); + }); + socket.once('error', () => { + cloudSqlInstance.forceRefresh(); + }); + + cloudSqlInstance.addSocket(socket); + + socket.connect = () => socket; + + return socket; + }, + }; + } + return { stream() { const cloudSqlInstance = instances.getInstance(opts); diff --git a/src/ip-addresses.ts b/src/ip-addresses.ts index 973e0d14..a3d7941f 100644 --- a/src/ip-addresses.ts +++ b/src/ip-addresses.ts @@ -18,6 +18,7 @@ export enum IpAddressTypes { PUBLIC = 'PUBLIC', PRIVATE = 'PRIVATE', PSC = 'PSC', + SQL_DATA = 'SQL_DATA', } export declare interface IpAddresses { @@ -67,6 +68,17 @@ export function selectIpAddress( return getPrivateIpAddress(ipAddresses); case IpAddressTypes.PSC: return getPSCIpAddress(ipAddresses); + case IpAddressTypes.SQL_DATA: + if (ipAddresses.public) { + return getPublicIpAddress(ipAddresses); + } + if (ipAddresses.private) { + return getPrivateIpAddress(ipAddresses); + } + if (ipAddresses.psc) { + return getPSCIpAddress(ipAddresses); + } + return ''; default: throw new CloudSQLConnectorError({ message: 'Cannot connect to instance, it has no supported IP addresses', diff --git a/src/sql-data-client.ts b/src/sql-data-client.ts new file mode 100644 index 00000000..dea7179e --- /dev/null +++ b/src/sql-data-client.ts @@ -0,0 +1,265 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as net from 'node:net'; +import * as grpc from '@grpc/grpc-js'; +import {AuthClient, GoogleAuth} from 'google-auth-library'; +import {v1beta4, protos} from '@google-cloud/sql'; +import {CloudSQLConnectorError} from './errors'; + +export interface SqlDataClientOptions { + instanceConnectionName: string; + auth: GoogleAuth; + endpoint?: string; + streamTimeout?: number; + channelCredentials?: grpc.ChannelCredentials; + getDirectSocket?: () => net.Socket; + onUnsupported?: () => void; +} + +export class SqlDataClient { + private readonly instanceConnectionName: string; + private readonly auth: GoogleAuth; + private readonly endpoint: string; + private readonly streamTimeout: number; + private readonly projectId: string; + private readonly regionId: string; + private readonly instanceId: string; + private readonly channelCredentials?: grpc.ChannelCredentials; + private readonly getDirectSocket?: () => net.Socket; + private readonly onUnsupported?: () => void; + private server?: net.Server; + private client?: v1beta4.SqlDataServiceClient; + private port?: number; + + constructor(opts: SqlDataClientOptions) { + this.instanceConnectionName = opts.instanceConnectionName; + this.auth = opts.auth; + this.endpoint = opts.endpoint || 'sqladmin.googleapis.com'; + this.streamTimeout = opts.streamTimeout || 2 * 60 * 60 * 1000; // 2 hours + this.channelCredentials = opts.channelCredentials; + this.getDirectSocket = opts.getDirectSocket; + this.onUnsupported = opts.onUnsupported; + + const parts = this.instanceConnectionName.split(':'); + if (parts.length !== 3) { + throw new CloudSQLConnectorError({ + message: `Invalid instance connection name: ${this.instanceConnectionName}`, + code: 'EBADINSTANCECONNECTIONNAME', + }); + } + this.projectId = parts[0]; + this.regionId = parts[1]; + this.instanceId = parts[2]; + } + + async start(): Promise { + if (this.port !== undefined) { + return this.port; + } + return new Promise((resolvePromise, rejectPromise) => { + this.server = net.createServer(async socket => { + try { + await this.handleConnection(socket); + } catch (err: unknown) { + socket.destroy(err as Error); + } + }); + + this.server.on('error', err => { + rejectPromise(err); + }); + + this.server.listen(0, '127.0.0.1', () => { + const addr = this.server?.address() as net.AddressInfo; + this.port = addr.port; + resolvePromise(this.port); + }); + }); + } + + async close(): Promise { + if (this.server) { + return new Promise(resolvePromise => { + this.server?.close(() => { + resolvePromise(); + }); + }); + } + } + + private async handleConnection(socket: net.Socket): Promise { + let isFallback = false; + let isEstablished = false; + let isClosed = false; + const clientBuffer: Buffer[] = []; + let directSocket: net.Socket | undefined; + + let servicePath = this.endpoint; + let port = 443; + if (this.endpoint.includes(':')) { + const parts = this.endpoint.split(':'); + servicePath = parts[0]; + port = parseInt(parts[1], 10); + } + + const client = new v1beta4.SqlDataServiceClient({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + auth: this.auth as any, + servicePath, + sslCreds: this.channelCredentials, + port, + }); + this.client = client; + + const instanceResource = `projects/${this.projectId}/instances/${this.instanceId}`; + const locationResource = `locations/${this.regionId}`; + + const stream = client.streamSqlData({ + otherArgs: { + headers: { + 'x-goog-request-params': `instance_id=${instanceResource}&location_id=${locationResource}`, + }, + }, + }); + + const startSessionMsg = { + startSession: { + locationId: locationResource, + instanceId: instanceResource, + }, + }; + stream.write(startSessionMsg); + + stream.on( + 'data', + (response: protos.google.cloud.sql.v1beta4.IStreamSqlDataResponse) => { + if (isFallback || isClosed) { + return; + } + if (!isEstablished) { + isEstablished = true; + clientBuffer.length = 0; + } + if (response.data && response.data.data) { + socket.write(response.data.data); + } + if (response.terminateSession) { + const status = response.terminateSession.status; + const code = status ? status.code : 'UNKNOWN'; + const msg = status ? status.message : 'Session terminated by server'; + socket.destroy( + new Error( + `gRPC Stream terminated by server: Code ${code}, Message: ${msg}` + ) + ); + } + } + ); + + stream.on('error', (err: grpc.ServiceError) => { + if (isFallback || isClosed) { + return; + } + if (err.code === 9 && !isEstablished && this.getDirectSocket) { + // FAILED_PRECONDITION: Instance does not support SQL_DATA. + isFallback = true; + this.onUnsupported?.(); + try { + stream.destroy(); + } catch { + // ignore + } + try { + directSocket = this.getDirectSocket(); + while (clientBuffer.length > 0) { + const chunk = clientBuffer.shift(); + if (chunk) { + directSocket.write(chunk); + } + } + socket.pipe(directSocket); + directSocket.pipe(socket); + + directSocket.on('error', directErr => { + socket.destroy(directErr); + }); + directSocket.on('close', () => { + socket.end(); + }); + } catch (directErr) { + socket.destroy(directErr as Error); + } + return; + } + socket.destroy(err); + }); + + stream.on('end', () => { + if (isFallback || isClosed) { + return; + } + socket.end(); + }); + + socket.on('data', chunk => { + if (isFallback) { + if (directSocket) { + directSocket.write(chunk); + } + } else { + if (!isEstablished) { + clientBuffer.push(chunk); + } + stream.write({ + data: { + firstByteOffset: 0, + data: chunk, + }, + }); + } + }); + + socket.on('end', () => { + isClosed = true; + if (isFallback) { + if (directSocket) { + directSocket.end(); + } + } else { + const terminateSessionMsg = { + terminateSession: { + status: { + code: 0, + message: 'Client closed connection', + }, + }, + }; + stream.write(terminateSessionMsg); + stream.end(); + } + }); + + socket.on('error', err => { + isClosed = true; + if (isFallback) { + if (directSocket) { + directSocket.destroy(err); + } + } else { + stream.destroy(err); + } + }); + } +} diff --git a/src/sqladmin-fetcher.ts b/src/sqladmin-fetcher.ts index 5c652000..014957b7 100644 --- a/src/sqladmin-fetcher.ts +++ b/src/sqladmin-fetcher.ts @@ -26,7 +26,7 @@ import {AuthTypes} from './auth-types'; export interface InstanceMetadata { ipAddresses: IpAddresses; - serverCaCert: SslCert; + serverCaCert?: SslCert; serverCaMode: string; dnsName: string; } @@ -88,7 +88,7 @@ export interface SQLAdminFetcherOptions { export class SQLAdminFetcher { private readonly client: sqladmin_v1beta4.Sqladmin; private readonly auth: GoogleAuth; - private readonly adminAuth: GoogleAuth; + public readonly adminAuth: GoogleAuth; constructor({ loginAuth, @@ -172,12 +172,8 @@ export class SQLAdminFetcher { ipAddresses.psc = dnsName; } - if (!ipAddresses.public && !ipAddresses.private && !ipAddresses.psc) { - throw new CloudSQLConnectorError({ - message: 'Cannot connect to instance, it has no supported IP addresses', - code: 'ENOSQLADMINIPADDRESS', - }); - } + // Do not throw here if there are no IP addresses. + // IP address validation will be performed inside selectIpAddress. return ipAddresses; } @@ -211,13 +207,7 @@ export class SQLAdminFetcher { res.data.pscEnabled ); - const {serverCaCert} = res.data; - if (!serverCaCert || !serverCaCert.cert || !serverCaCert.expirationTime) { - throw new CloudSQLConnectorError({ - message: 'Cannot connect to instance, no valid CA certificate found', - code: 'ENOSQLADMINCERT', - }); - } + const serverCaCert = res.data.serverCaCert; const {region} = res.data; if (!region) { @@ -247,10 +237,13 @@ export class SQLAdminFetcher { return { ipAddresses, - serverCaCert: { - cert: serverCaCert.cert, - expirationTime: serverCaCert.expirationTime, - }, + serverCaCert: + serverCaCert && serverCaCert.cert && serverCaCert.expirationTime + ? { + cert: serverCaCert.cert, + expirationTime: serverCaCert.expirationTime, + } + : undefined, serverCaMode: res.data.serverCaMode || '', dnsName: serverName || '', }; diff --git a/system-test/postgres-sqldata.test.ts b/system-test/postgres-sqldata.test.ts new file mode 100644 index 00000000..2688ed1d --- /dev/null +++ b/system-test/postgres-sqldata.test.ts @@ -0,0 +1,72 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import t from 'tap'; +import pg from 'pg'; +import {Connector, IpAddressTypes} from '@google-cloud/cloud-sql-connector'; +const {Client} = pg; + +t.test('open SQL_DATA connection and retrieves standard pg tables', async t => { + if (process.env.RUN_SQL_DATA_SYSTEM_TESTS !== 'true') { + t.skip( + 'Skipping SQL_DATA system test. Set RUN_SQL_DATA_SYSTEM_TESTS=true to run.' + ); + return; + } + const pass = process.env.POSTGRES_CUSTOMER_CAS_PASS; + if (!pass) { + t.skip('POSTGRES_CUSTOMER_CAS_PASS env var not set, skipping'); + return; + } + + const connectionName = + process.env.POSTGRES_CUSTOMER_CAS_CONNECTION_NAME || + 'hessjc-playground-01:us-central1:aide-poc-instance-1'; + + const connector = new Connector({ + sqlAdminAPIEndpoint: + 'https://coreltest-sqladmin.mtls.sandbox.googleapis.com', + sqlDataEndpoint: 'coreltest-sqladmin.mtls.sandbox.googleapis.com:443', + userProject: 'speckle-connector-quotaproject', + }); + + const clientOpts = await connector.getOptions({ + instanceConnectionName: connectionName, + ipType: IpAddressTypes.SQL_DATA, + sqlDataEndpoint: 'coreltest-sqladmin.mtls.sandbox.googleapis.com:443', + }); + + const client = new Client({ + ...clientOpts, + user: 'postgres', + password: pass, + database: 'postgres', + }); + + t.after(async () => { + try { + await client.end(); + } finally { + connector.close(); + } + }); + + await client.connect(); + + const { + rows: [result], + } = await client.query('SELECT NOW();'); + const returnedDate = result['now']; + t.ok(returnedDate.getTime(), 'should have valid returned date object'); +}); diff --git a/test/ip-addresses.ts b/test/ip-addresses.ts index 1f0c782d..83f96508 100644 --- a/test/ip-addresses.ts +++ b/test/ip-addresses.ts @@ -74,3 +74,43 @@ t.same( 'abcde.12345.us-central1.sql.goog', 'should select psc ip' ); + +t.same( + selectIpAddress( + { + public: '0.0.0.0', + private: '0.0.0.2', + }, + IpAddressTypes.SQL_DATA + ), + '0.0.0.0', + 'should select public ip as fallback for SQL_DATA' +); + +t.same( + selectIpAddress( + { + private: '0.0.0.2', + }, + IpAddressTypes.SQL_DATA + ), + '0.0.0.2', + 'should select private ip as fallback for SQL_DATA if public is missing' +); + +t.same( + selectIpAddress( + { + psc: 'abcde.12345.us-central1.sql.goog', + }, + IpAddressTypes.SQL_DATA + ), + 'abcde.12345.us-central1.sql.goog', + 'should select psc ip as fallback for SQL_DATA if public and private are missing' +); + +t.same( + selectIpAddress({}, IpAddressTypes.SQL_DATA), + '', + 'should return empty string if no public, private, or psc ip fallback found for SQL_DATA' +); diff --git a/test/sql-data-client.ts b/test/sql-data-client.ts new file mode 100644 index 00000000..506e91ee --- /dev/null +++ b/test/sql-data-client.ts @@ -0,0 +1,323 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import t from 'tap'; +import * as grpc from '@grpc/grpc-js'; +import * as protoLoader from '@grpc/proto-loader'; +import * as net from 'node:net'; +import {resolve} from 'node:path'; +import {SqlDataClient} from '../src/sql-data-client'; +import {AuthClient} from 'google-auth-library'; + +const protoPath = resolve( + __dirname, + '../node_modules/@google-cloud/sql/build/protos/google/cloud/sql/v1beta4/cloud_sql_data.proto' +); +const packageDefinition = protoLoader.loadSync(protoPath, { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + includeDirs: [ + resolve(__dirname, '../node_modules/@google-cloud/sql/build/protos'), + resolve(__dirname, '../node_modules/google-gax/build/protos'), + ], +}); +const protoDescriptor = grpc.loadPackageDefinition(packageDefinition); +const googleProto = protoDescriptor.google as grpc.GrpcObject; +const cloudProto = googleProto.cloud as grpc.GrpcObject; +const sqlProto = cloudProto.sql as grpc.GrpcObject; +const v1beta4 = sqlProto.v1beta4 as grpc.GrpcObject; +interface StreamSqlDataRequest { + start_session?: { + location_id: string; + instance_id: string; + session_id?: string; + }; + data?: { + first_byte_offset: number; + data: Buffer; + }; +} + +const SqlDataServiceClientClass = + v1beta4.SqlDataService as grpc.ServiceClientConstructor; + +const mockAuth = { + getRequestHeaders: async () => ({ + authorization: 'Bearer fake-token', + }), + getUniverseDomain: async () => 'googleapis.com', +} as unknown as AuthClient; + +// Helper to start fake gRPC server +function startFakeServer( + handler: ( + call: grpc.ServerDuplexStream + ) => void +): Promise<{server: grpc.Server; port: number}> { + const server = new grpc.Server(); + server.addService(SqlDataServiceClientClass.service, { + StreamSqlData: handler, + }); + return new Promise((res, rej) => { + server.bindAsync( + '127.0.0.1:0', + grpc.ServerCredentials.createInsecure(), + (err, port) => { + if (err) { + rej(err); + } else { + res({server, port}); + } + } + ); + }); +} + +t.test('SqlDataClient socket tunnel', async t => { + t.test('should transfer data bidirectionally', async t => { + let serverReceivedStartSession = false; + let serverReceivedClientData = false; + let serverReceivedEnd = false; + let clientReceivedServerData = false; + + let resolveServerEnded: () => void; + const serverEnded = new Promise(res => { + resolveServerEnded = res; + }); + + const testClientData = Buffer.from('client hello'); + const testServerData = Buffer.from('server hello'); + + const {server, port} = await startFakeServer(call => { + call.on('data', request => { + if (request.start_session) { + serverReceivedStartSession = true; + // Send SessionMetadata as handshake response + call.write({ + session_metadata: { + supported_features: ['SQL_DATA_FEATURE_UNSPECIFIED'], + }, + }); + } else if (request.data) { + const dataBuf = request.data.data; + if (dataBuf.equals(testClientData)) { + serverReceivedClientData = true; + // Respond back with server data + call.write({ + data: { + first_byte_offset: 0, + data: testServerData, + }, + }); + } + } + }); + call.on('end', () => { + serverReceivedEnd = true; + call.end(); + resolveServerEnded(); + }); + }); + + t.teardown(() => { + server.forceShutdown(); + }); + + const client = new SqlDataClient({ + instanceConnectionName: 'proj:reg:inst', + auth: mockAuth, + endpoint: `127.0.0.1:${port}`, + channelCredentials: grpc.credentials.createInsecure(), + }); + + const localPort = await client.start(); + t.ok(localPort > 0, 'should start local server and return a port'); + + // Create a local socket connection + const socket = net.connect({port: localPort, host: '127.0.0.1'}); + + await new Promise((resolve, reject) => { + socket.on('connect', () => { + socket.write(testClientData); + }); + + socket.on('data', data => { + if (data.equals(testServerData)) { + clientReceivedServerData = true; + socket.end(); // close socket + } + }); + + socket.on('end', () => { + resolve(); + }); + + socket.on('error', err => { + reject(err); + }); + }); + + await client.close(); + await serverEnded; // Wait for the gRPC stream on server to finish + + // Verify all assertions + t.ok( + serverReceivedStartSession, + 'server should have received start_session' + ); + t.ok(serverReceivedClientData, 'server should have received client data'); + t.ok(clientReceivedServerData, 'client should have received server data'); + t.ok(serverReceivedEnd, 'server should have received end stream signal'); + }); + + t.test( + 'should fallback to direct socket when server returns FAILED_PRECONDITION (9)', + async t => { + let unsupportedCalled = false; + const testClientData = Buffer.from('client hello'); + const testDirectResponse = Buffer.from('direct response'); + + // Start a mock direct backend server + const directServer = net.createServer(directConn => { + directConn.on('data', data => { + if (data.equals(testClientData)) { + directConn.write(testDirectResponse); + } + }); + }); + const directPort = await new Promise(res => { + directServer.listen(0, '127.0.0.1', () => { + res((directServer.address() as net.AddressInfo).port); + }); + }); + + t.teardown(() => { + directServer.close(); + }); + + // Start a mock gRPC server returning FAILED_PRECONDITION + const {server, port} = await startFakeServer(call => { + call.on('data', request => { + if (request.start_session) { + const err = { + code: grpc.status.FAILED_PRECONDITION, + details: 'SQL_DATA is not supported for this instance', + }; + call.emit('error', err); + } + }); + }); + + t.teardown(() => { + server.forceShutdown(); + }); + + const client = new SqlDataClient({ + instanceConnectionName: 'proj:reg:inst', + auth: mockAuth, + endpoint: `127.0.0.1:${port}`, + channelCredentials: grpc.credentials.createInsecure(), + getDirectSocket: () => + net.connect({port: directPort, host: '127.0.0.1'}), + onUnsupported: () => { + unsupportedCalled = true; + }, + }); + + const localPort = await client.start(); + const socket = net.connect({port: localPort, host: '127.0.0.1'}); + + let receivedData = Buffer.alloc(0); + await new Promise((resolve, reject) => { + socket.on('connect', () => { + socket.write(testClientData); + }); + socket.on('data', chunk => { + receivedData = Buffer.concat([receivedData, chunk]); + if (receivedData.equals(testDirectResponse)) { + socket.end(); + } + }); + socket.on('end', () => { + resolve(); + }); + socket.on('error', err => { + reject(err); + }); + }); + + await client.close(); + + t.ok(unsupportedCalled, 'onUnsupported callback should have been called'); + t.same( + receivedData, + testDirectResponse, + 'client should receive direct response via fallback' + ); + } + ); + + t.test( + 'should destroy client socket on non-FAILED_PRECONDITION error', + async t => { + const {server, port} = await startFakeServer(call => { + call.on('data', request => { + if (request.start_session) { + const err = { + code: grpc.status.UNAVAILABLE, + details: 'Service unavailable', + }; + call.emit('error', err); + } + }); + }); + + t.teardown(() => { + server.forceShutdown(); + }); + + const client = new SqlDataClient({ + instanceConnectionName: 'proj:reg:inst', + auth: mockAuth, + endpoint: `127.0.0.1:${port}`, + channelCredentials: grpc.credentials.createInsecure(), + }); + + const localPort = await client.start(); + const socket = net.connect({port: localPort, host: '127.0.0.1'}); + + let socketClosed = false; + await new Promise(resolve => { + socket.on('connect', () => { + socket.write(Buffer.from('hello')); + }); + socket.on('error', () => { + socketClosed = true; + resolve(); + }); + socket.on('close', () => { + socketClosed = true; + resolve(); + }); + }); + + await client.close(); + + t.ok(socketClosed, 'client socket should be closed on server error'); + } + ); +}); diff --git a/test/sqladmin-fetcher.ts b/test/sqladmin-fetcher.ts index 0ffed779..5fd0fc3e 100644 --- a/test/sqladmin-fetcher.ts +++ b/test/sqladmin-fetcher.ts @@ -233,13 +233,8 @@ t.test('getInstanceMetadata no ip', async t => { }); const fetcher = new SQLAdminFetcher(); - t.rejects( - fetcher.getInstanceMetadata(instanceConnectionInfo), - { - code: 'ENOSQLADMINIPADDRESS', - }, - 'should throw no ip type found' - ); + const res = await fetcher.getInstanceMetadata(instanceConnectionInfo); + t.strictSame(res.ipAddresses, {}, 'should return empty ipAddresses'); }); t.test('getInstanceMetadata no valid cert', async t => { @@ -253,12 +248,11 @@ t.test('getInstanceMetadata no valid cert', async t => { }); const fetcher = new SQLAdminFetcher(); - t.rejects( - fetcher.getInstanceMetadata(instanceConnectionInfo), - { - code: 'ENOSQLADMINCERT', - }, - 'should throw no cert error' + const res = await fetcher.getInstanceMetadata(instanceConnectionInfo); + t.strictSame( + res.serverCaCert, + undefined, + 'should return undefined serverCaCert' ); });