From cda291043a4b1f880d026fc69baea2df8cd12a1c Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Tue, 7 Apr 2026 16:22:08 +0200 Subject: [PATCH 1/7] fix(node-sdk): replace deprecated google cloud storage client --- .changeset/tasty-rabbits-wave.md | 5 + packages/node-sdk/package.json | 2 +- .../node-sdk/src/flagsFallbackProvider.ts | 139 ++++++- .../test/flagsFallbackProvider.test.ts | 73 ++++ yarn.lock | 353 ++++++------------ 5 files changed, 305 insertions(+), 267 deletions(-) create mode 100644 .changeset/tasty-rabbits-wave.md diff --git a/.changeset/tasty-rabbits-wave.md b/.changeset/tasty-rabbits-wave.md new file mode 100644 index 00000000..9072ee46 --- /dev/null +++ b/.changeset/tasty-rabbits-wave.md @@ -0,0 +1,5 @@ +--- +"@reflag/node-sdk": patch +--- + +Replace the built-in GCS fallback provider's default client dependency with `@googleapis/storage`, removing the deprecated `@google-cloud/storage` dependency and its vulnerable transitive request stack. diff --git a/packages/node-sdk/package.json b/packages/node-sdk/package.json index f6d77322..a74a17a2 100644 --- a/packages/node-sdk/package.json +++ b/packages/node-sdk/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.888.0", - "@google-cloud/storage": "^7.19.0", + "@googleapis/storage": "^21.2.0", "@redis/client": "^5.11.0", "@reflag/flag-evaluation": "1.0.0" } diff --git a/packages/node-sdk/src/flagsFallbackProvider.ts b/packages/node-sdk/src/flagsFallbackProvider.ts index 0bb72876..d0bee634 100644 --- a/packages/node-sdk/src/flagsFallbackProvider.ts +++ b/packages/node-sdk/src/flagsFallbackProvider.ts @@ -66,6 +66,19 @@ export type GCSFallbackProviderOptions = { keyPrefix?: string; }; +type LegacyGCSClient = NonNullable; + +type GCSObjectStore = { + exists(bucket: string, path: string): Promise; + download(bucket: string, path: string): Promise; + save( + bucket: string, + path: string, + body: string, + options: { contentType: string }, + ): Promise; +}; + export type RedisFallbackProviderOptions = { /** * Optional Redis client. When omitted, a client is created using `REDIS_URL`. @@ -153,6 +166,12 @@ function isFlagApiResponse(value: unknown): value is FlagAPIResponse { async function readBodyAsString(body: unknown) { if (typeof body === "string") return body; if (body instanceof Uint8Array) return Buffer.from(body).toString("utf-8"); + if (body instanceof ArrayBuffer) return Buffer.from(body).toString("utf-8"); + if (ArrayBuffer.isView(body)) { + return Buffer.from(body.buffer, body.byteOffset, body.byteLength).toString( + "utf-8", + ); + } if (body && typeof body === "object") { if ( "transformToString" in body && @@ -169,6 +188,15 @@ function parseSnapshot(raw: string) { return isFlagsFallbackSnapshot(parsed) ? parsed : undefined; } +function isNotFoundError(error: any) { + return ( + error?.code === 404 || + error?.status === 404 || + error?.response?.status === 404 || + error?.$metadata?.httpStatusCode === 404 + ); +} + function staticFlagApiResponse( key: string, isEnabled: boolean, @@ -195,9 +223,83 @@ async function createDefaultS3Client() { return new S3Client({}); } -async function createDefaultGCSClient() { - const { Storage } = await import("@google-cloud/storage"); - return new Storage(); +function createGCSObjectStore(client: LegacyGCSClient): GCSObjectStore { + return { + async exists(bucket, path) { + const [exists] = await client.bucket(bucket).file(path).exists(); + return exists; + }, + + async download(bucket, path) { + const [contents] = await client.bucket(bucket).file(path).download(); + return contents; + }, + + async save(bucket, path, body, options) { + return client.bucket(bucket).file(path).save(body, options); + }, + }; +} + +async function createDefaultGCSObjectStore(): Promise { + const { auth, storage } = await import("@googleapis/storage"); + const gcs = storage({ + version: "v1", + auth: new auth.GoogleAuth({ + scopes: ["https://www.googleapis.com/auth/devstorage.read_write"], + }), + }); + + return { + async exists(bucket, path) { + try { + await gcs.objects.get({ + bucket, + object: path, + }); + return true; + } catch (error) { + if (isNotFoundError(error)) { + return false; + } + throw error; + } + }, + + async download(bucket, path) { + const response = await gcs.objects.get( + { + bucket, + object: path, + alt: "media", + }, + { + responseType: "arraybuffer", + }, + ); + + if (response.data instanceof Uint8Array) { + return response.data; + } + if (response.data instanceof ArrayBuffer) { + return new Uint8Array(response.data); + } + + throw new TypeError("Unexpected GCS download response body format"); + }, + + async save(bucket, path, body, options) { + await gcs.objects.insert({ + bucket, + name: path, + uploadType: "media", + media: { + mimeType: options.contentType, + body, + }, + }); + }, + }; } export function createStaticFallbackProvider({ @@ -281,10 +383,7 @@ export function createS3FallbackProvider({ return parseSnapshot(body); } catch (error: any) { - if ( - error?.name === "NoSuchKey" || - error?.$metadata?.httpStatusCode === 404 - ) { + if (error?.name === "NoSuchKey" || isNotFoundError(error)) { return undefined; } throw error; @@ -312,36 +411,38 @@ export function createGCSFallbackProvider({ client, keyPrefix, }: GCSFallbackProviderOptions): FlagsFallbackProvider { - let defaultClient: GCSFallbackProviderOptions["client"] | undefined; + let defaultClient: GCSObjectStore | undefined; const getClient = async () => { - defaultClient ??= client ?? (await createDefaultGCSClient()); + defaultClient ??= client + ? createGCSObjectStore(client) + : await createDefaultGCSObjectStore(); return defaultClient; }; return { async load(context) { const storage = await getClient(); - const file = storage - .bucket(bucket) - .file(snapshotObjectKey(context, keyPrefix)); - const [exists] = await file.exists(); + const objectKey = snapshotObjectKey(context, keyPrefix); + const exists = await storage.exists(bucket, objectKey); if (!exists) { return undefined; } - const [contents] = await file.download(); + const contents = await storage.download(bucket, objectKey); return parseSnapshot(Buffer.from(contents).toString("utf-8")); }, async save(context, snapshot) { const storage = await getClient(); - await storage - .bucket(bucket) - .file(snapshotObjectKey(context, keyPrefix)) - .save(JSON.stringify(snapshot), { + await storage.save( + bucket, + snapshotObjectKey(context, keyPrefix), + JSON.stringify(snapshot), + { contentType: "application/json", - }); + }, + ); }, }; } diff --git a/packages/node-sdk/test/flagsFallbackProvider.test.ts b/packages/node-sdk/test/flagsFallbackProvider.test.ts index b2c0362e..d3fcdc32 100644 --- a/packages/node-sdk/test/flagsFallbackProvider.test.ts +++ b/packages/node-sdk/test/flagsFallbackProvider.test.ts @@ -241,6 +241,79 @@ describe("flagsFallbackProvider", () => { }); }); + it("creates the default GCS client from @googleapis/storage", async () => { + vi.resetModules(); + + const get = vi + .fn() + .mockResolvedValueOnce({ data: { kind: "storage#object" } }) + .mockResolvedValueOnce({ + data: Buffer.from(JSON.stringify(snapshot), "utf-8"), + }); + const insert = vi.fn().mockResolvedValue({}); + const storage = vi.fn().mockReturnValue({ + objects: { + get, + insert, + }, + }); + const GoogleAuth = vi.fn(); + + vi.doMock("@googleapis/storage", () => ({ + auth: { GoogleAuth }, + storage, + })); + + try { + const { createGCSFallbackProvider } = await import( + "../src/flagsFallbackProvider" + ); + const provider = createGCSFallbackProvider({ + bucket: "bucket-name", + keyPrefix: "reflag/flags-fallback///", + }); + + await expect(provider.load(context)).resolves.toEqual(snapshot); + await provider.save(context, snapshot); + + expect(GoogleAuth).toHaveBeenCalledWith({ + scopes: ["https://www.googleapis.com/auth/devstorage.read_write"], + }); + expect(storage).toHaveBeenCalledWith( + expect.objectContaining({ + version: "v1", + }), + ); + expect(get).toHaveBeenNthCalledWith(1, { + bucket: "bucket-name", + object: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, + }); + expect(get).toHaveBeenNthCalledWith( + 2, + { + bucket: "bucket-name", + object: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, + alt: "media", + }, + { + responseType: "arraybuffer", + }, + ); + expect(insert).toHaveBeenCalledWith({ + bucket: "bucket-name", + name: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, + uploadType: "media", + media: { + mimeType: "application/json", + body: JSON.stringify(snapshot), + }, + }); + } finally { + vi.doUnmock("@googleapis/storage"); + vi.resetModules(); + } + }); + it("loads Redis snapshots", async () => { const get = vi.fn().mockResolvedValue(JSON.stringify(snapshot)); const set = vi.fn(); diff --git a/yarn.lock b/yarn.lock index 08d6995e..92c22edc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4464,50 +4464,12 @@ __metadata: languageName: node linkType: hard -"@google-cloud/paginator@npm:^5.0.0": - version: 5.0.2 - resolution: "@google-cloud/paginator@npm:5.0.2" - dependencies: - arrify: "npm:^2.0.0" - extend: "npm:^3.0.2" - checksum: 10c0/aac4ed986c2b274ac9fdca3f68d5ba6ee95f4c35370b11db25c288bf485352e2ec5df16bf9c3cff554a2e73a07e62f10044d273788df61897b81fe47bb18106d - languageName: node - linkType: hard - -"@google-cloud/projectify@npm:^4.0.0": - version: 4.0.0 - resolution: "@google-cloud/projectify@npm:4.0.0" - checksum: 10c0/0d0a6ceca76a138973fcb3ad577f209acdbd9d9aed1c645b09f98d5e5a258053dbbe6c1f13e6f85310cc0d9308f5f3a84f8fa4f1a132549a68d86174fb21067f - languageName: node - linkType: hard - -"@google-cloud/promisify@npm:<4.1.0": - version: 4.0.0 - resolution: "@google-cloud/promisify@npm:4.0.0" - checksum: 10c0/4332cbd923d7c6943ecdf46f187f1417c84bb9c801525cd74d719c766bfaad650f7964fb74576345f6537b6d6273a4f2992c8d79ebec6c8b8401b23d626b8dd3 - languageName: node - linkType: hard - -"@google-cloud/storage@npm:^7.19.0": - version: 7.19.0 - resolution: "@google-cloud/storage@npm:7.19.0" +"@googleapis/storage@npm:^21.2.0": + version: 21.2.0 + resolution: "@googleapis/storage@npm:21.2.0" dependencies: - "@google-cloud/paginator": "npm:^5.0.0" - "@google-cloud/projectify": "npm:^4.0.0" - "@google-cloud/promisify": "npm:<4.1.0" - abort-controller: "npm:^3.0.0" - async-retry: "npm:^1.3.3" - duplexify: "npm:^4.1.3" - fast-xml-parser: "npm:^5.3.4" - gaxios: "npm:^6.0.2" - google-auth-library: "npm:^9.6.3" - html-entities: "npm:^2.5.2" - mime: "npm:^3.0.0" - p-limit: "npm:^3.0.1" - retry-request: "npm:^7.0.0" - teeny-request: "npm:^9.0.0" - uuid: "npm:^8.0.0" - checksum: 10c0/2951e4a0b3c2f90c28917a9b313a981722a9e5648ca2b6d04f384f816e9107e1637b00c32c5e71ed8d25c7e1840898b616f015b29c2cc1d8d8a22c8630778d8c + googleapis-common: "npm:^8.0.0" + checksum: 10c0/ff7e758d4a14e19ae05343d80994e91a09ad82be3ab42cc903b123b30e7e57a78e7c54c7eb21398f015216d82968a60f674f5f5db0776433b31912ecd5beb023 languageName: node linkType: hard @@ -7232,7 +7194,7 @@ __metadata: dependencies: "@aws-sdk/client-s3": "npm:^3.888.0" "@babel/core": "npm:~7.24.7" - "@google-cloud/storage": "npm:^7.19.0" + "@googleapis/storage": "npm:^21.2.0" "@redis/client": "npm:^5.11.0" "@reflag/eslint-config": "npm:~0.0.2" "@reflag/flag-evaluation": "npm:1.0.0" @@ -8790,13 +8752,6 @@ __metadata: languageName: node linkType: hard -"@types/caseless@npm:*": - version: 0.12.5 - resolution: "@types/caseless@npm:0.12.5" - checksum: 10c0/b1f8b8a38ce747b643115d37a40ea824c658bd7050e4b69427a10e9d12d1606ed17a0f6018241c08291cd59f70aeb3c1f3754ad61e45f8dbba708ec72dde7ec8 - languageName: node - linkType: hard - "@types/connect@npm:*": version: 3.4.38 resolution: "@types/connect@npm:3.4.38" @@ -9153,18 +9108,6 @@ __metadata: languageName: node linkType: hard -"@types/request@npm:^2.48.8": - version: 2.48.13 - resolution: "@types/request@npm:2.48.13" - dependencies: - "@types/caseless": "npm:*" - "@types/node": "npm:*" - "@types/tough-cookie": "npm:*" - form-data: "npm:^2.5.5" - checksum: 10c0/1c6798d926a6577f213dbc04aa09945590f260ea367537c20824ff337b0a49d56e5199a6a6029e625568d97c3bbb98908bdb8d9158eb421f70a0d03ae230ff72 - languageName: node - linkType: hard - "@types/semver@npm:^7.7.0": version: 7.7.0 resolution: "@types/semver@npm:7.7.0" @@ -11076,7 +11019,7 @@ __metadata: languageName: node linkType: hard -"arrify@npm:^2.0.0, arrify@npm:^2.0.1": +"arrify@npm:^2.0.1": version: 2.0.1 resolution: "arrify@npm:2.0.1" checksum: 10c0/3fb30b5e7c37abea1907a60b28a554d2f0fc088757ca9bf5b684786e583fdf14360721eb12575c1ce6f995282eab936712d3c4389122682eafab0e0b57f78dbb @@ -11143,15 +11086,6 @@ __metadata: languageName: node linkType: hard -"async-retry@npm:^1.3.3": - version: 1.3.3 - resolution: "async-retry@npm:1.3.3" - dependencies: - retry: "npm:0.13.1" - checksum: 10c0/cabced4fb46f8737b95cc88dc9c0ff42656c62dc83ce0650864e891b6c155a063af08d62c446269b51256f6fbcb69a6563b80e76d0ea4a5117b0c0377b6b19d8 - languageName: node - linkType: hard - "async@npm:^2.6.4": version: 2.6.4 resolution: "async@npm:2.6.4" @@ -13025,6 +12959,13 @@ __metadata: languageName: node linkType: hard +"data-uri-to-buffer@npm:^4.0.0": + version: 4.0.1 + resolution: "data-uri-to-buffer@npm:4.0.1" + checksum: 10c0/20a6b93107597530d71d4cb285acee17f66bcdfc03fd81040921a81252f19db27588d87fc8fc69e1950c55cfb0bf8ae40d0e5e21d907230813eb5d5a7f9eb45b + languageName: node + linkType: hard + "data-uri-to-buffer@npm:^6.0.2": version: 6.0.2 resolution: "data-uri-to-buffer@npm:6.0.2" @@ -13594,18 +13535,6 @@ __metadata: languageName: node linkType: hard -"duplexify@npm:^4.1.3": - version: 4.1.3 - resolution: "duplexify@npm:4.1.3" - dependencies: - end-of-stream: "npm:^1.4.1" - inherits: "npm:^2.0.3" - readable-stream: "npm:^3.1.1" - stream-shift: "npm:^1.0.2" - checksum: 10c0/8a7621ae95c89f3937f982fe36d72ea997836a708471a75bb2a0eecde3330311b1e128a6dad510e0fd64ace0c56bff3484ed2e82af0e465600c82117eadfbda5 - languageName: node - linkType: hard - "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -15764,15 +15693,6 @@ __metadata: languageName: node linkType: hard -"fast-xml-builder@npm:^1.1.3": - version: 1.1.3 - resolution: "fast-xml-builder@npm:1.1.3" - dependencies: - path-expression-matcher: "npm:^1.1.3" - checksum: 10c0/353a2b3695f66f2b0717fcb1a59d2c4108a3b29e7e4f125a51accaccade509dc6fb7e2777da62fb3b339487753ece01eaa270542b10a68dfd43f48d607a6b10e - languageName: node - linkType: hard - "fast-xml-parser@npm:5.4.1": version: 5.4.1 resolution: "fast-xml-parser@npm:5.4.1" @@ -15796,19 +15716,6 @@ __metadata: languageName: node linkType: hard -"fast-xml-parser@npm:^5.3.4": - version: 5.5.5 - resolution: "fast-xml-parser@npm:5.5.5" - dependencies: - fast-xml-builder: "npm:^1.1.3" - path-expression-matcher: "npm:^1.1.3" - strnum: "npm:^2.1.2" - bin: - fxparser: src/cli/cli.js - checksum: 10c0/d159b331e7c3ea27e388ece304988f8dc57e8cbd9d91f4ab824c78c1ed7915f10b77032ec72ed4a9ddb872db4261b5ee0699d2b6a4a80e06dfc95174dde35e93 - languageName: node - linkType: hard - "fastest-levenshtein@npm:^1.0.12": version: 1.0.12 resolution: "fastest-levenshtein@npm:1.0.12" @@ -15870,6 +15777,16 @@ __metadata: languageName: node linkType: hard +"fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": + version: 3.2.0 + resolution: "fetch-blob@npm:3.2.0" + dependencies: + node-domexception: "npm:^1.0.0" + web-streams-polyfill: "npm:^3.0.3" + checksum: 10c0/60054bf47bfa10fb0ba6cb7742acec2f37c1f56344f79a70bb8b1c48d77675927c720ff3191fa546410a0442c998d27ab05e9144c32d530d8a52fbe68f843b69 + languageName: node + linkType: hard + "figures@npm:3.2.0, figures@npm:^3.0.0, figures@npm:^3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -16119,20 +16036,6 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^2.5.5": - version: 2.5.5 - resolution: "form-data@npm:2.5.5" - dependencies: - asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.8" - es-set-tostringtag: "npm:^2.1.0" - hasown: "npm:^2.0.2" - mime-types: "npm:^2.1.35" - safe-buffer: "npm:^5.2.1" - checksum: 10c0/7fb70447849fc9bce4d01fe9a626f6587441f85779a2803b67f803e1ab52b0bd78db0a7acd80d944c665f68ca90936c327f1244b730719b638a0219e98b20488 - languageName: node - linkType: hard - "form-data@npm:^4.0.0": version: 4.0.4 resolution: "form-data@npm:4.0.4" @@ -16159,6 +16062,15 @@ __metadata: languageName: node linkType: hard +"formdata-polyfill@npm:^4.0.10": + version: 4.0.10 + resolution: "formdata-polyfill@npm:4.0.10" + dependencies: + fetch-blob: "npm:^3.1.2" + checksum: 10c0/5392ec484f9ce0d5e0d52fb5a78e7486637d516179b0eb84d81389d7eccf9ca2f663079da56f761355c0a65792810e3b345dc24db9a8bbbcf24ef3c8c88570c6 + languageName: node + linkType: hard + "formidable@npm:^3.5.4": version: 3.5.4 resolution: "formidable@npm:3.5.4" @@ -16386,27 +16298,25 @@ __metadata: languageName: node linkType: hard -"gaxios@npm:^6.0.0, gaxios@npm:^6.0.2, gaxios@npm:^6.1.1": - version: 6.7.1 - resolution: "gaxios@npm:6.7.1" +"gaxios@npm:^7.0.0, gaxios@npm:^7.0.0-rc.4, gaxios@npm:^7.1.4": + version: 7.1.4 + resolution: "gaxios@npm:7.1.4" dependencies: extend: "npm:^3.0.2" https-proxy-agent: "npm:^7.0.1" - is-stream: "npm:^2.0.0" - node-fetch: "npm:^2.6.9" - uuid: "npm:^9.0.1" - checksum: 10c0/53e92088470661c5bc493a1de29d05aff58b1f0009ec5e7903f730f892c3642a93e264e61904383741ccbab1ce6e519f12a985bba91e13527678b32ee6d7d3fd + node-fetch: "npm:^3.3.2" + checksum: 10c0/147adf5f2606442945d8b19df1e9fe2833a5ec30af00743d0c44292899c5eef1c0a77b74ff07d9dfdc6b009c08af1f3f3d1d5d772109fde50c92435533795803 languageName: node linkType: hard -"gcp-metadata@npm:^6.1.0": - version: 6.1.1 - resolution: "gcp-metadata@npm:6.1.1" +"gcp-metadata@npm:8.1.2": + version: 8.1.2 + resolution: "gcp-metadata@npm:8.1.2" dependencies: - gaxios: "npm:^6.1.1" - google-logging-utils: "npm:^0.0.2" + gaxios: "npm:^7.0.0" + google-logging-utils: "npm:^1.0.0" json-bigint: "npm:^1.0.0" - checksum: 10c0/71f6ad4800aa622c246ceec3955014c0c78cdcfe025971f9558b9379f4019f5e65772763428ee8c3244fa81b8631977316eaa71a823493f82e5c44d7259ffac8 + checksum: 10c0/15a61231a9410dc11c2828d2c9fdc8b0a939f1af746195c44edc6f2ffea0acab52cef3a7b9828069a36fd5d68bda730f7328a415fe42a01258f6e249dfba6908 languageName: node linkType: hard @@ -16917,24 +16827,37 @@ __metadata: languageName: node linkType: hard -"google-auth-library@npm:^9.6.3": - version: 9.15.1 - resolution: "google-auth-library@npm:9.15.1" +"google-auth-library@npm:^10.1.0": + version: 10.6.2 + resolution: "google-auth-library@npm:10.6.2" dependencies: base64-js: "npm:^1.3.0" ecdsa-sig-formatter: "npm:^1.0.11" - gaxios: "npm:^6.1.1" - gcp-metadata: "npm:^6.1.0" - gtoken: "npm:^7.0.0" + gaxios: "npm:^7.1.4" + gcp-metadata: "npm:8.1.2" + google-logging-utils: "npm:1.1.3" jws: "npm:^4.0.0" - checksum: 10c0/6eef36d9a9cb7decd11e920ee892579261c6390104b3b24d3e0f3889096673189fe2ed0ee43fd563710e2560de98e63ad5aa4967b91e7f4e69074a422d5f7b65 + checksum: 10c0/4878d9070e751202eff8adca7a78a41f045c460f611a62d8c0c14ac4bd33d66afc5d788ef82225873dadc7cde401d47f223f3c109f1a192564164fdd44a36614 languageName: node linkType: hard -"google-logging-utils@npm:^0.0.2": - version: 0.0.2 - resolution: "google-logging-utils@npm:0.0.2" - checksum: 10c0/9a4bbd470dd101c77405e450fffca8592d1d7114f245a121288d04a957aca08c9dea2dd1a871effe71e41540d1bb0494731a0b0f6fea4358e77f06645e4268c1 +"google-logging-utils@npm:1.1.3, google-logging-utils@npm:^1.0.0": + version: 1.1.3 + resolution: "google-logging-utils@npm:1.1.3" + checksum: 10c0/e65201c7e96543bd1423b9324013736646b9eed60941e0bfa47b9bfd146d2f09cf3df1c99ca60b7d80a726075263ead049ee72de53372cb8458c3bc55c2c1e59 + languageName: node + linkType: hard + +"googleapis-common@npm:^8.0.0": + version: 8.0.1 + resolution: "googleapis-common@npm:8.0.1" + dependencies: + extend: "npm:^3.0.2" + gaxios: "npm:^7.0.0-rc.4" + google-auth-library: "npm:^10.1.0" + qs: "npm:^6.7.0" + url-template: "npm:^2.0.8" + checksum: 10c0/ee939fdfcaea32c528e7be64621b3e6b35e612f2456c8d4aca835e0a01ccec52f4ea2ac9509b083128398631a6cd66512c400adb4d0375a4638c55888ccdea7f languageName: node linkType: hard @@ -16982,16 +16905,6 @@ __metadata: languageName: node linkType: hard -"gtoken@npm:^7.0.0": - version: 7.1.0 - resolution: "gtoken@npm:7.1.0" - dependencies: - gaxios: "npm:^6.0.0" - jws: "npm:^4.0.0" - checksum: 10c0/0a3dcacb1a3c4578abe1ee01c7d0bf20bffe8ded3ee73fc58885d53c00f6eb43b4e1372ff179f0da3ed5cfebd5b7c6ab8ae2776f1787e90d943691b4fe57c716 - languageName: node - linkType: hard - "handlebars@npm:^4.7.7": version: 4.7.9 resolution: "handlebars@npm:4.7.9" @@ -17261,13 +17174,6 @@ __metadata: languageName: node linkType: hard -"html-entities@npm:^2.5.2": - version: 2.6.0 - resolution: "html-entities@npm:2.6.0" - checksum: 10c0/7c8b15d9ea0cd00dc9279f61bab002ba6ca8a7a0f3c36ed2db3530a67a9621c017830d1d2c1c65beb9b8e3436ea663e9cf8b230472e0e413359399413b27c8b7 - languageName: node - linkType: hard - "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -20206,7 +20112,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:^2.1.35, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.12, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -20242,15 +20148,6 @@ __metadata: languageName: node linkType: hard -"mime@npm:^3.0.0": - version: 3.0.0 - resolution: "mime@npm:3.0.0" - bin: - mime: cli.js - checksum: 10c0/402e792a8df1b2cc41cb77f0dcc46472b7944b7ec29cb5bbcd398624b6b97096728f1239766d3fdeb20551dd8d94738344c195a6ea10c4f906eb0356323b0531 - languageName: node - linkType: hard - "mimic-fn@npm:^1.0.0": version: 1.2.0 resolution: "mimic-fn@npm:1.2.0" @@ -21137,6 +21034,13 @@ __metadata: languageName: node linkType: hard +"node-domexception@npm:^1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: 10c0/5e5d63cda29856402df9472335af4bb13875e1927ad3be861dc5ebde38917aecbf9ae337923777af52a48c426b70148815e890a5d72760f1b4d758cc671b1a2b + languageName: node + linkType: hard + "node-fetch@npm:2.6.7": version: 2.6.7 resolution: "node-fetch@npm:2.6.7" @@ -21151,7 +21055,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9": +"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -21165,6 +21069,17 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^3.3.2": + version: 3.3.2 + resolution: "node-fetch@npm:3.3.2" + dependencies: + data-uri-to-buffer: "npm:^4.0.0" + fetch-blob: "npm:^3.1.4" + formdata-polyfill: "npm:^4.0.10" + checksum: 10c0/f3d5e56190562221398c9f5750198b34cf6113aa304e34ee97c94fd300ec578b25b2c2906edba922050fce983338fde0d5d34fcb0fc3336ade5bd0e429ad7538 + languageName: node + linkType: hard + "node-forge@npm:^1.3.3": version: 1.4.0 resolution: "node-forge@npm:1.4.0" @@ -22060,7 +21975,7 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.0.1, p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": +"p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": version: 3.1.0 resolution: "p-limit@npm:3.1.0" dependencies: @@ -22381,13 +22296,6 @@ __metadata: languageName: node linkType: hard -"path-expression-matcher@npm:^1.1.3": - version: 1.1.3 - resolution: "path-expression-matcher@npm:1.1.3" - checksum: 10c0/45c01471bc62c5f38d069418aec831763e6f45bb85f9520b08de441e6cd14f84b3098ecb66255e819c2af21102abcd2b45550dc1285996717ce9292802df2bc5 - languageName: node - linkType: hard - "path-is-absolute@npm:^1.0.0": version: 1.0.1 resolution: "path-is-absolute@npm:1.0.1" @@ -23493,7 +23401,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.14.1": +"qs@npm:^6.14.1, qs@npm:^6.7.0": version: 6.15.0 resolution: "qs@npm:6.15.0" dependencies: @@ -24382,24 +24290,6 @@ __metadata: languageName: node linkType: hard -"retry-request@npm:^7.0.0": - version: 7.0.2 - resolution: "retry-request@npm:7.0.2" - dependencies: - "@types/request": "npm:^2.48.8" - extend: "npm:^3.0.2" - teeny-request: "npm:^9.0.0" - checksum: 10c0/c79936695a43db1bc82a7bad348a1e0be1c363799be2e1fa87b8c3aeb5dabf0ccb023b811aa5000c000ee73e196b88febff7d3e22cbb63a77175228514256155 - languageName: node - linkType: hard - -"retry@npm:0.13.1": - version: 0.13.1 - resolution: "retry@npm:0.13.1" - checksum: 10c0/9ae822ee19db2163497e074ea919780b1efa00431d197c7afdb950e42bf109196774b92a49fc9821f0b8b328a98eea6017410bfc5e8a0fc19c85c6d11adb3772 - languageName: node - linkType: hard - "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -24643,7 +24533,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 @@ -25601,22 +25491,6 @@ __metadata: languageName: node linkType: hard -"stream-events@npm:^1.0.5": - version: 1.0.5 - resolution: "stream-events@npm:1.0.5" - dependencies: - stubs: "npm:^3.0.0" - checksum: 10c0/5d235a5799a483e94ea8829526fe9d95d76460032d5e78555fe4f801949ac6a27ea2212e4e0827c55f78726b3242701768adf2d33789465f51b31ed8ebd6b086 - languageName: node - linkType: hard - -"stream-shift@npm:^1.0.2": - version: 1.0.3 - resolution: "stream-shift@npm:1.0.3" - checksum: 10c0/939cd1051ca750d240a0625b106a2b988c45fb5a3be0cebe9a9858cb01bc1955e8c7b9fac17a9462976bea4a7b704e317c5c2200c70f0ca715a3363b9aa4fd3b - languageName: node - linkType: hard - "streamsearch@npm:^1.1.0": version: 1.1.0 resolution: "streamsearch@npm:1.1.0" @@ -25974,13 +25848,6 @@ __metadata: languageName: node linkType: hard -"stubs@npm:^3.0.0": - version: 3.0.0 - resolution: "stubs@npm:3.0.0" - checksum: 10c0/841a4ab8c76795d34aefe129185763b55fbf2e4693208215627caea4dd62e1299423dcd96f708d3128e3dfa0e669bae2cb912e6e906d7d81eaf6493196570923 - languageName: node - linkType: hard - "styled-jsx@npm:5.1.1": version: 5.1.1 resolution: "styled-jsx@npm:5.1.1" @@ -26215,19 +26082,6 @@ __metadata: languageName: node linkType: hard -"teeny-request@npm:^9.0.0": - version: 9.0.0 - resolution: "teeny-request@npm:9.0.0" - dependencies: - http-proxy-agent: "npm:^5.0.0" - https-proxy-agent: "npm:^5.0.0" - node-fetch: "npm:^2.6.9" - stream-events: "npm:^1.0.5" - uuid: "npm:^9.0.0" - checksum: 10c0/1c51a284075b57b7b7f970fc8d855d611912f0e485aa1d1dfda3c0be3f2df392e4ce83b1b39877134041abb7c255f3777f175b27323ef5bf008839e42a1958bc - languageName: node - linkType: hard - "temp-dir@npm:1.0.0": version: 1.0.0 resolution: "temp-dir@npm:1.0.0" @@ -27436,6 +27290,13 @@ __metadata: languageName: node linkType: hard +"url-template@npm:^2.0.8": + version: 2.0.8 + resolution: "url-template@npm:2.0.8" + checksum: 10c0/56a15057eacbcf05d52b0caed8279c8451b3dd9d32856a1fdd91c6dc84dcb1646f12bafc756b7ade62ca5b1564da8efd7baac5add35868bafb43eb024c62805b + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -27459,16 +27320,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^8.0.0": - version: 8.3.2 - resolution: "uuid@npm:8.3.2" - bin: - uuid: dist/bin/uuid - checksum: 10c0/bcbb807a917d374a49f475fae2e87fdca7da5e5530820ef53f65ba1d12131bd81a92ecf259cc7ce317cbe0f289e7d79fdfebcef9bfa3087c8c8a2fa304c9be54 - languageName: node - linkType: hard - -"uuid@npm:^9.0.0, uuid@npm:^9.0.1": +"uuid@npm:^9.0.0": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: @@ -28111,6 +27963,13 @@ __metadata: languageName: node linkType: hard +"web-streams-polyfill@npm:^3.0.3": + version: 3.3.3 + resolution: "web-streams-polyfill@npm:3.3.3" + checksum: 10c0/64e855c47f6c8330b5436147db1c75cb7e7474d924166800e8e2aab5eb6c76aac4981a84261dd2982b3e754490900b99791c80ae1407a9fa0dcff74f82ea3a7f + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" From 6028757212a3dd5dbad7c5be251e5738cfe7810d Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Tue, 7 Apr 2026 16:23:02 +0200 Subject: [PATCH 2/7] chore(node-sdk): note gcs client cleanup for next major --- packages/node-sdk/src/flagsFallbackProvider.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/node-sdk/src/flagsFallbackProvider.ts b/packages/node-sdk/src/flagsFallbackProvider.ts index d0bee634..d6d84640 100644 --- a/packages/node-sdk/src/flagsFallbackProvider.ts +++ b/packages/node-sdk/src/flagsFallbackProvider.ts @@ -47,6 +47,10 @@ export type GCSFallbackProviderOptions = { /** * Optional GCS client. A default client is created when omitted. + * + * TODO(next major): Replace this legacy `bucket().file()` client shape with + * a simpler object-store interface that doesn't mirror the deprecated + * `@google-cloud/storage` API. */ client?: { bucket(name: string): { From 5e51df146d2ef12658533bd8802eacde66b64a1d Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Tue, 7 Apr 2026 16:24:41 +0200 Subject: [PATCH 3/7] chore(node-sdk): pin @googleapis/storage to 21.2.0 --- packages/node-sdk/package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/node-sdk/package.json b/packages/node-sdk/package.json index a74a17a2..421b988f 100644 --- a/packages/node-sdk/package.json +++ b/packages/node-sdk/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.888.0", - "@googleapis/storage": "^21.2.0", + "@googleapis/storage": "21.2.0", "@redis/client": "^5.11.0", "@reflag/flag-evaluation": "1.0.0" } diff --git a/yarn.lock b/yarn.lock index 92c22edc..3c8fa14d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4464,7 +4464,7 @@ __metadata: languageName: node linkType: hard -"@googleapis/storage@npm:^21.2.0": +"@googleapis/storage@npm:21.2.0": version: 21.2.0 resolution: "@googleapis/storage@npm:21.2.0" dependencies: @@ -7194,7 +7194,7 @@ __metadata: dependencies: "@aws-sdk/client-s3": "npm:^3.888.0" "@babel/core": "npm:~7.24.7" - "@googleapis/storage": "npm:^21.2.0" + "@googleapis/storage": "npm:21.2.0" "@redis/client": "npm:^5.11.0" "@reflag/eslint-config": "npm:~0.0.2" "@reflag/flag-evaluation": "npm:1.0.0" From 0d4282f276a220eb8191d28a17fdbb9af8657853 Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Tue, 7 Apr 2026 16:33:24 +0200 Subject: [PATCH 4/7] fix(node-sdk): use storage-control for gcs fallback --- .changeset/tasty-rabbits-wave.md | 2 +- packages/node-sdk/package.json | 2 +- .../node-sdk/src/flagsFallbackProvider.ts | 139 ++++++-- .../test/flagsFallbackProvider.test.ts | 109 ++++--- yarn.lock | 304 ++++++++++++++++-- 5 files changed, 448 insertions(+), 108 deletions(-) diff --git a/.changeset/tasty-rabbits-wave.md b/.changeset/tasty-rabbits-wave.md index 9072ee46..c837fdfd 100644 --- a/.changeset/tasty-rabbits-wave.md +++ b/.changeset/tasty-rabbits-wave.md @@ -2,4 +2,4 @@ "@reflag/node-sdk": patch --- -Replace the built-in GCS fallback provider's default client dependency with `@googleapis/storage`, removing the deprecated `@google-cloud/storage` dependency and its vulnerable transitive request stack. +Replace the built-in GCS fallback provider's default client dependency with `@google-cloud/storage-control`, removing the deprecated `@google-cloud/storage` dependency and its vulnerable transitive request stack. diff --git a/packages/node-sdk/package.json b/packages/node-sdk/package.json index 421b988f..ee96326b 100644 --- a/packages/node-sdk/package.json +++ b/packages/node-sdk/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.888.0", - "@googleapis/storage": "21.2.0", + "@google-cloud/storage-control": "0.8.2", "@redis/client": "^5.11.0", "@reflag/flag-evaluation": "1.0.0" } diff --git a/packages/node-sdk/src/flagsFallbackProvider.ts b/packages/node-sdk/src/flagsFallbackProvider.ts index d6d84640..63dbe20b 100644 --- a/packages/node-sdk/src/flagsFallbackProvider.ts +++ b/packages/node-sdk/src/flagsFallbackProvider.ts @@ -83,6 +83,33 @@ type GCSObjectStore = { ): Promise; }; +type GCSStorageControlReadResponse = { + checksummedData?: { + content?: string | Uint8Array | ArrayBuffer; + }; +}; + +type GCSStorageControlStub = { + getObject(request: { bucket: string; object: string }): Promise<[unknown]>; + readObject(request: { bucket: string; object: string }): { + on(event: "data", listener: (response: GCSStorageControlReadResponse) => void): unknown; + on(event: "error", listener: (error: unknown) => void): unknown; + on(event: "end", listener: () => void): unknown; + }; + writeObject( + callback: (error: unknown, response?: unknown) => void, + ): { + on(event: "error", listener: (error: unknown) => void): unknown; + write(chunk: unknown): unknown; + end(): unknown; + }; +}; + +type GCSStorageControlClient = { + bucketPath(project: string, bucket: string): string; + initialize(): Promise; +}; + export type RedisFallbackProviderOptions = { /** * Optional Redis client. When omitted, a client is created using `REDIS_URL`. @@ -194,6 +221,7 @@ function parseSnapshot(raw: string) { function isNotFoundError(error: any) { return ( + error?.code === 5 || error?.code === 404 || error?.status === 404 || error?.response?.status === 404 || @@ -246,19 +274,17 @@ function createGCSObjectStore(client: LegacyGCSClient): GCSObjectStore { } async function createDefaultGCSObjectStore(): Promise { - const { auth, storage } = await import("@googleapis/storage"); - const gcs = storage({ - version: "v1", - auth: new auth.GoogleAuth({ - scopes: ["https://www.googleapis.com/auth/devstorage.read_write"], - }), - }); + const { v2 } = await import("@google-cloud/storage-control"); + const gcs = new v2.StorageClient() as unknown as GCSStorageControlClient; + const stub = await gcs.initialize(); + + const bucketResourceName = (bucket: string) => gcs.bucketPath("_", bucket); return { async exists(bucket, path) { try { - await gcs.objects.get({ - bucket, + await stub.getObject({ + bucket: bucketResourceName(bucket), object: path, }); return true; @@ -271,36 +297,81 @@ async function createDefaultGCSObjectStore(): Promise { }, async download(bucket, path) { - const response = await gcs.objects.get( - { - bucket, - object: path, - alt: "media", - }, - { - responseType: "arraybuffer", - }, - ); + const stream = stub.readObject({ + bucket: bucketResourceName(bucket), + object: path, + }); - if (response.data instanceof Uint8Array) { - return response.data; - } - if (response.data instanceof ArrayBuffer) { - return new Uint8Array(response.data); - } + return await new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + let settled = false; + + const fail = (error: unknown) => { + if (settled) return; + settled = true; + reject(error); + }; + + stream.on("data", (response) => { + const content = response?.checksummedData?.content; + if (content === undefined || content === null) { + return; + } + + if (typeof content === "string") { + chunks.push(Buffer.from(content, "utf-8")); + return; + } + if (content instanceof Uint8Array) { + chunks.push(content); + return; + } + if (content instanceof ArrayBuffer) { + chunks.push(new Uint8Array(content)); + return; + } + + fail(new TypeError("Unexpected GCS download response body format")); + }); - throw new TypeError("Unexpected GCS download response body format"); + stream.on("error", fail); + stream.on("end", () => { + if (settled) return; + settled = true; + resolve(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)))); + }); + }); }, async save(bucket, path, body, options) { - await gcs.objects.insert({ - bucket, - name: path, - uploadType: "media", - media: { - mimeType: options.contentType, - body, - }, + const content = Buffer.from(body, "utf-8"); + + await new Promise((resolve, reject) => { + const stream = stub.writeObject((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + + stream.on("error", reject); + stream.write({ + writeObjectSpec: { + resource: { + bucket: bucketResourceName(bucket), + name: path, + contentType: options.contentType, + }, + objectSize: content.byteLength, + }, + writeOffset: 0, + checksummedData: { + content, + }, + finishWrite: true, + }); + stream.end(); }); }, }; diff --git a/packages/node-sdk/test/flagsFallbackProvider.test.ts b/packages/node-sdk/test/flagsFallbackProvider.test.ts index d3fcdc32..5476a030 100644 --- a/packages/node-sdk/test/flagsFallbackProvider.test.ts +++ b/packages/node-sdk/test/flagsFallbackProvider.test.ts @@ -1,3 +1,4 @@ +import { EventEmitter } from "events"; import { mkdtemp, readFile, rm } from "fs/promises"; import os from "os"; import path from "path"; @@ -241,27 +242,55 @@ describe("flagsFallbackProvider", () => { }); }); - it("creates the default GCS client from @googleapis/storage", async () => { + it("creates the default GCS client from @google-cloud/storage-control", async () => { vi.resetModules(); - const get = vi - .fn() - .mockResolvedValueOnce({ data: { kind: "storage#object" } }) - .mockResolvedValueOnce({ - data: Buffer.from(JSON.stringify(snapshot), "utf-8"), + const getObject = vi.fn().mockResolvedValue([{}]); + const readObject = vi.fn().mockImplementation(() => { + const stream = new EventEmitter(); + queueMicrotask(() => { + stream.emit("data", { + checksummedData: { + content: Buffer.from(JSON.stringify(snapshot), "utf-8"), + }, + }); + stream.emit("end"); }); - const insert = vi.fn().mockResolvedValue({}); - const storage = vi.fn().mockReturnValue({ - objects: { - get, - insert, - }, + return stream; + }); + const write = vi.fn(); + const end = vi.fn().mockImplementation(function (this: EventEmitter) { + queueMicrotask(() => callback(null, {})); + this.emit("finish"); + }); + let callback: ((error: unknown, response?: unknown) => void) | undefined; + const writeObject = vi.fn().mockImplementation((cb) => { + callback = cb; + const stream = new EventEmitter() as EventEmitter & { + write: typeof write; + end: typeof end; + }; + stream.write = write; + stream.end = end; + return stream; }); - const GoogleAuth = vi.fn(); + const initialize = vi.fn().mockResolvedValue({ + getObject, + readObject, + writeObject, + }); + const bucketPath = vi + .fn() + .mockImplementation((project: string, bucket: string) => + `projects/${project}/buckets/${bucket}`, + ); + const StorageClient = vi.fn().mockImplementation(() => ({ + initialize, + bucketPath, + })); - vi.doMock("@googleapis/storage", () => ({ - auth: { GoogleAuth }, - storage, + vi.doMock("@google-cloud/storage-control", () => ({ + v2: { StorageClient }, })); try { @@ -276,40 +305,34 @@ describe("flagsFallbackProvider", () => { await expect(provider.load(context)).resolves.toEqual(snapshot); await provider.save(context, snapshot); - expect(GoogleAuth).toHaveBeenCalledWith({ - scopes: ["https://www.googleapis.com/auth/devstorage.read_write"], + expect(StorageClient).toHaveBeenCalledWith(); + expect(initialize).toHaveBeenCalledTimes(1); + expect(bucketPath).toHaveBeenCalledWith("_", "bucket-name"); + expect(getObject).toHaveBeenCalledWith({ + bucket: "projects/_/buckets/bucket-name", + object: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, }); - expect(storage).toHaveBeenCalledWith( - expect.objectContaining({ - version: "v1", - }), - ); - expect(get).toHaveBeenNthCalledWith(1, { - bucket: "bucket-name", + expect(readObject).toHaveBeenCalledWith({ + bucket: "projects/_/buckets/bucket-name", object: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, }); - expect(get).toHaveBeenNthCalledWith( - 2, - { - bucket: "bucket-name", - object: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, - alt: "media", - }, - { - responseType: "arraybuffer", + expect(write).toHaveBeenCalledWith({ + writeObjectSpec: { + resource: { + bucket: "projects/_/buckets/bucket-name", + name: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, + contentType: "application/json", + }, + objectSize: Buffer.byteLength(JSON.stringify(snapshot), "utf-8"), }, - ); - expect(insert).toHaveBeenCalledWith({ - bucket: "bucket-name", - name: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, - uploadType: "media", - media: { - mimeType: "application/json", - body: JSON.stringify(snapshot), + writeOffset: 0, + checksummedData: { + content: Buffer.from(JSON.stringify(snapshot), "utf-8"), }, + finishWrite: true, }); } finally { - vi.doUnmock("@googleapis/storage"); + vi.doUnmock("@google-cloud/storage-control"); vi.resetModules(); } }); diff --git a/yarn.lock b/yarn.lock index 3c8fa14d..618f0ee6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4464,12 +4464,36 @@ __metadata: languageName: node linkType: hard -"@googleapis/storage@npm:21.2.0": - version: 21.2.0 - resolution: "@googleapis/storage@npm:21.2.0" +"@google-cloud/storage-control@npm:0.8.2": + version: 0.8.2 + resolution: "@google-cloud/storage-control@npm:0.8.2" dependencies: - googleapis-common: "npm:^8.0.0" - checksum: 10c0/ff7e758d4a14e19ae05343d80994e91a09ad82be3ab42cc903b123b30e7e57a78e7c54c7eb21398f015216d82968a60f674f5f5db0776433b31912ecd5beb023 + google-gax: "npm:^5.0.0" + checksum: 10c0/94aed7aeb1e07f7c55908f52f08bfdf220bb4a229e3630db52d8c47252c441fb8f860718f1cef0f5ac4e31c775d231ad7fe47604d0e9ce2dd138fa51c590982a + languageName: node + linkType: hard + +"@grpc/grpc-js@npm:^1.12.6": + version: 1.14.3 + resolution: "@grpc/grpc-js@npm:1.14.3" + dependencies: + "@grpc/proto-loader": "npm:^0.8.0" + "@js-sdsl/ordered-map": "npm:^4.4.2" + checksum: 10c0/f41f06a311b93cca8c472d56e21387e0f7b57bb2337a91d15ea4279bac8ec4fa0de6bd0d881201229ab800c0f0c55277911ecb850e057f20a828d0ddd623551d + languageName: node + linkType: hard + +"@grpc/proto-loader@npm:^0.8.0": + version: 0.8.0 + resolution: "@grpc/proto-loader@npm:0.8.0" + dependencies: + lodash.camelcase: "npm:^4.3.0" + long: "npm:^5.0.0" + protobufjs: "npm:^7.5.3" + yargs: "npm:^17.7.2" + bin: + proto-loader-gen-types: build/bin/proto-loader-gen-types.js + checksum: 10c0/a27da3b85d5d17bab956d536786c717287eae46ca264ea9ec774db90ff571955bae2705809f431b4622fbf3be9951d7c7bbb1360b2015ee88abe1587cf3d6fe0 languageName: node linkType: hard @@ -5391,6 +5415,13 @@ __metadata: languageName: node linkType: hard +"@js-sdsl/ordered-map@npm:^4.4.2": + version: 4.4.2 + resolution: "@js-sdsl/ordered-map@npm:4.4.2" + checksum: 10c0/cc7e15dc4acf6d9ef663757279600bab70533d847dcc1ab01332e9e680bd30b77cdf9ad885cc774276f51d98b05a013571c940e5b360985af5eb798dc1a2ee2b + languageName: node + linkType: hard + "@lerna/create@npm:8.1.3": version: 8.1.3 resolution: "@lerna/create@npm:8.1.3" @@ -6640,6 +6671,79 @@ __metadata: languageName: node linkType: hard +"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/aspromise@npm:1.1.2" + checksum: 10c0/a83343a468ff5b5ec6bff36fd788a64c839e48a07ff9f4f813564f58caf44d011cd6504ed2147bf34835bd7a7dd2107052af755961c6b098fd8902b4f6500d0f + languageName: node + linkType: hard + +"@protobufjs/base64@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/base64@npm:1.1.2" + checksum: 10c0/eec925e681081af190b8ee231f9bad3101e189abbc182ff279da6b531e7dbd2a56f1f306f37a80b1be9e00aa2d271690d08dcc5f326f71c9eed8546675c8caf6 + languageName: node + linkType: hard + +"@protobufjs/codegen@npm:^2.0.4": + version: 2.0.4 + resolution: "@protobufjs/codegen@npm:2.0.4" + checksum: 10c0/26ae337c5659e41f091606d16465bbcc1df1f37cc1ed462438b1f67be0c1e28dfb2ca9f294f39100c52161aef82edf758c95d6d75650a1ddf31f7ddee1440b43 + languageName: node + linkType: hard + +"@protobufjs/eventemitter@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/eventemitter@npm:1.1.0" + checksum: 10c0/1eb0a75180e5206d1033e4138212a8c7089a3d418c6dfa5a6ce42e593a4ae2e5892c4ef7421f38092badba4040ea6a45f0928869989411001d8c1018ea9a6e70 + languageName: node + linkType: hard + +"@protobufjs/fetch@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/fetch@npm:1.1.0" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.1" + "@protobufjs/inquire": "npm:^1.1.0" + checksum: 10c0/cda6a3dc2d50a182c5865b160f72077aac197046600091dbb005dd0a66db9cce3c5eaed6d470ac8ed49d7bcbeef6ee5f0bc288db5ff9a70cbd003e5909065233 + languageName: node + linkType: hard + +"@protobufjs/float@npm:^1.0.2": + version: 1.0.2 + resolution: "@protobufjs/float@npm:1.0.2" + checksum: 10c0/18f2bdede76ffcf0170708af15c9c9db6259b771e6b84c51b06df34a9c339dbbeec267d14ce0bddd20acc142b1d980d983d31434398df7f98eb0c94a0eb79069 + languageName: node + linkType: hard + +"@protobufjs/inquire@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/inquire@npm:1.1.0" + checksum: 10c0/64372482efcba1fb4d166a2664a6395fa978b557803857c9c03500e0ac1013eb4b1aacc9ed851dd5fc22f81583670b4f4431bae186f3373fedcfde863ef5921a + languageName: node + linkType: hard + +"@protobufjs/path@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/path@npm:1.1.2" + checksum: 10c0/cece0a938e7f5dfd2fa03f8c14f2f1cf8b0d6e13ac7326ff4c96ea311effd5fb7ae0bba754fbf505312af2e38500250c90e68506b97c02360a43793d88a0d8b4 + languageName: node + linkType: hard + +"@protobufjs/pool@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/pool@npm:1.1.0" + checksum: 10c0/eda2718b7f222ac6e6ad36f758a92ef90d26526026a19f4f17f668f45e0306a5bd734def3f48f51f8134ae0978b6262a5c517c08b115a551756d1a3aadfcf038 + languageName: node + linkType: hard + +"@protobufjs/utf8@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/utf8@npm:1.1.0" + checksum: 10c0/a3fe31fe3fa29aa3349e2e04ee13dc170cc6af7c23d92ad49e3eeaf79b9766264544d3da824dba93b7855bd6a2982fb40032ef40693da98a136d835752beb487 + languageName: node + linkType: hard + "@react-native-async-storage/async-storage@npm:^2.2.0": version: 2.2.0 resolution: "@react-native-async-storage/async-storage@npm:2.2.0" @@ -7194,7 +7298,7 @@ __metadata: dependencies: "@aws-sdk/client-s3": "npm:^3.888.0" "@babel/core": "npm:~7.24.7" - "@googleapis/storage": "npm:21.2.0" + "@google-cloud/storage-control": "npm:0.8.2" "@redis/client": "npm:^5.11.0" "@reflag/eslint-config": "npm:~0.0.2" "@reflag/flag-evaluation": "npm:1.0.0" @@ -8991,6 +9095,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:>=13.7.0": + version: 25.5.2 + resolution: "@types/node@npm:25.5.2" + dependencies: + undici-types: "npm:~7.18.0" + checksum: 10c0/11e41a85401724cd1a4de6fb7bd4264ec46db10c09fc8cf8d41de4ede0a7063db458348f859ead4ec0929906aa26aaf45a5fef3aa59742ca0521cda9cee52377 + languageName: node + linkType: hard + "@types/node@npm:^12.7.1": version: 12.20.55 resolution: "@types/node@npm:12.20.55" @@ -13535,6 +13648,18 @@ __metadata: languageName: node linkType: hard +"duplexify@npm:^4.1.3": + version: 4.1.3 + resolution: "duplexify@npm:4.1.3" + dependencies: + end-of-stream: "npm:^1.4.1" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.1.1" + stream-shift: "npm:^1.0.2" + checksum: 10c0/8a7621ae95c89f3937f982fe36d72ea997836a708471a75bb2a0eecde3330311b1e128a6dad510e0fd64ace0c56bff3484ed2e82af0e465600c82117eadfbda5 + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -16298,7 +16423,7 @@ __metadata: languageName: node linkType: hard -"gaxios@npm:^7.0.0, gaxios@npm:^7.0.0-rc.4, gaxios@npm:^7.1.4": +"gaxios@npm:^7.0.0, gaxios@npm:^7.1.4": version: 7.1.4 resolution: "gaxios@npm:7.1.4" dependencies: @@ -16668,6 +16793,22 @@ __metadata: languageName: node linkType: hard +"glob@npm:^10.3.7": + version: 10.5.0 + resolution: "glob@npm:10.5.0" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + package-json-from-dist: "npm:^1.0.0" + path-scurry: "npm:^1.11.1" + bin: + glob: dist/esm/bin.mjs + checksum: 10c0/100705eddbde6323e7b35e1d1ac28bcb58322095bd8e63a7d0bef1a2cdafe0d0f7922a981b2b48369a4f8c1b077be5c171804534c3509dfe950dde15fbe6d828 + languageName: node + linkType: hard + "glob@npm:^10.4.1": version: 10.4.2 resolution: "glob@npm:10.4.2" @@ -16841,23 +16982,29 @@ __metadata: languageName: node linkType: hard -"google-logging-utils@npm:1.1.3, google-logging-utils@npm:^1.0.0": - version: 1.1.3 - resolution: "google-logging-utils@npm:1.1.3" - checksum: 10c0/e65201c7e96543bd1423b9324013736646b9eed60941e0bfa47b9bfd146d2f09cf3df1c99ca60b7d80a726075263ead049ee72de53372cb8458c3bc55c2c1e59 +"google-gax@npm:^5.0.0": + version: 5.0.6 + resolution: "google-gax@npm:5.0.6" + dependencies: + "@grpc/grpc-js": "npm:^1.12.6" + "@grpc/proto-loader": "npm:^0.8.0" + duplexify: "npm:^4.1.3" + google-auth-library: "npm:^10.1.0" + google-logging-utils: "npm:^1.1.1" + node-fetch: "npm:^3.3.2" + object-hash: "npm:^3.0.0" + proto3-json-serializer: "npm:^3.0.0" + protobufjs: "npm:^7.5.3" + retry-request: "npm:^8.0.0" + rimraf: "npm:^5.0.1" + checksum: 10c0/1b368602340a80362d3a520727882feeabf7aa747097bf347de6b422d5b7dfa933e501ad18e4e44c84cde6fb6690038c1c4455ae885d68ce6db598e9e459cdff languageName: node linkType: hard -"googleapis-common@npm:^8.0.0": - version: 8.0.1 - resolution: "googleapis-common@npm:8.0.1" - dependencies: - extend: "npm:^3.0.2" - gaxios: "npm:^7.0.0-rc.4" - google-auth-library: "npm:^10.1.0" - qs: "npm:^6.7.0" - url-template: "npm:^2.0.8" - checksum: 10c0/ee939fdfcaea32c528e7be64621b3e6b35e612f2456c8d4aca835e0a01ccec52f4ea2ac9509b083128398631a6cd66512c400adb4d0375a4638c55888ccdea7f +"google-logging-utils@npm:1.1.3, google-logging-utils@npm:^1.0.0, google-logging-utils@npm:^1.1.1": + version: 1.1.3 + resolution: "google-logging-utils@npm:1.1.3" + checksum: 10c0/e65201c7e96543bd1423b9324013736646b9eed60941e0bfa47b9bfd146d2f09cf3df1c99ca60b7d80a726075263ead049ee72de53372cb8458c3bc55c2c1e59 languageName: node linkType: hard @@ -19414,6 +19561,13 @@ __metadata: languageName: node linkType: hard +"lodash.camelcase@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.camelcase@npm:4.3.0" + checksum: 10c0/fcba15d21a458076dd309fce6b1b4bf611d84a0ec252cb92447c948c533ac250b95d2e00955801ebc367e5af5ed288b996d75d37d2035260a937008e14eaf432 + languageName: node + linkType: hard + "lodash.debounce@npm:^4.0.8": version: 4.0.8 resolution: "lodash.debounce@npm:4.0.8" @@ -19519,6 +19673,13 @@ __metadata: languageName: node linkType: hard +"long@npm:^5.0.0": + version: 5.3.2 + resolution: "long@npm:5.3.2" + checksum: 10c0/7130fe1cbce2dca06734b35b70d380ca3f70271c7f8852c922a7c62c86c4e35f0c39290565eca7133c625908d40e126ac57c02b1b1a4636b9457d77e1e60b981 + languageName: node + linkType: hard + "loose-envify@npm:^1.0.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" @@ -23308,6 +23469,35 @@ __metadata: languageName: node linkType: hard +"proto3-json-serializer@npm:^3.0.0": + version: 3.0.4 + resolution: "proto3-json-serializer@npm:3.0.4" + dependencies: + protobufjs: "npm:^7.4.0" + checksum: 10c0/59150d5e6b396e00c0374e71eb4ebd6f33acd4212c0ad2acdda12181b08f923b733559d74ad78455085223efe24168d4d42887a8b8f15f7663f20debe5796081 + languageName: node + linkType: hard + +"protobufjs@npm:^7.4.0, protobufjs@npm:^7.5.3": + version: 7.5.4 + resolution: "protobufjs@npm:7.5.4" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.2" + "@protobufjs/base64": "npm:^1.1.2" + "@protobufjs/codegen": "npm:^2.0.4" + "@protobufjs/eventemitter": "npm:^1.1.0" + "@protobufjs/fetch": "npm:^1.1.0" + "@protobufjs/float": "npm:^1.0.2" + "@protobufjs/inquire": "npm:^1.1.0" + "@protobufjs/path": "npm:^1.1.2" + "@protobufjs/pool": "npm:^1.1.0" + "@protobufjs/utf8": "npm:^1.1.0" + "@types/node": "npm:>=13.7.0" + long: "npm:^5.0.0" + checksum: 10c0/913b676109ffb3c05d3d31e03a684e569be91f3bba8613da4a683d69d9dba948daa2afd7d2e7944d1aa6c417890c35d9d9a8883c1160affafb0f9670d59ef722 + languageName: node + linkType: hard + "protocols@npm:^2.0.0, protocols@npm:^2.0.1": version: 2.0.1 resolution: "protocols@npm:2.0.1" @@ -23401,7 +23591,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.14.1, qs@npm:^6.7.0": +"qs@npm:^6.14.1": version: 6.15.0 resolution: "qs@npm:6.15.0" dependencies: @@ -24290,6 +24480,16 @@ __metadata: languageName: node linkType: hard +"retry-request@npm:^8.0.0": + version: 8.0.2 + resolution: "retry-request@npm:8.0.2" + dependencies: + extend: "npm:^3.0.2" + teeny-request: "npm:^10.0.0" + checksum: 10c0/e0b44950d69fe8f51d4e808650cd0d06383cd711adcc71e7764b28b2f10a9ac3b2209f0c9ac0547df0745031810a259730b9d13b0070d69df9806c37432456a9 + languageName: node + linkType: hard + "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -24326,6 +24526,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^5.0.1": + version: 5.0.10 + resolution: "rimraf@npm:5.0.10" + dependencies: + glob: "npm:^10.3.7" + bin: + rimraf: dist/esm/bin.mjs + checksum: 10c0/7da4fd0e15118ee05b918359462cfa1e7fe4b1228c7765195a45b55576e8c15b95db513b8466ec89129666f4af45ad978a3057a02139afba1a63512a2d9644cc + languageName: node + linkType: hard + "rollup-preserve-directives@npm:^1.1.2": version: 1.1.2 resolution: "rollup-preserve-directives@npm:1.1.2" @@ -25491,6 +25702,22 @@ __metadata: languageName: node linkType: hard +"stream-events@npm:^1.0.5": + version: 1.0.5 + resolution: "stream-events@npm:1.0.5" + dependencies: + stubs: "npm:^3.0.0" + checksum: 10c0/5d235a5799a483e94ea8829526fe9d95d76460032d5e78555fe4f801949ac6a27ea2212e4e0827c55f78726b3242701768adf2d33789465f51b31ed8ebd6b086 + languageName: node + linkType: hard + +"stream-shift@npm:^1.0.2": + version: 1.0.3 + resolution: "stream-shift@npm:1.0.3" + checksum: 10c0/939cd1051ca750d240a0625b106a2b988c45fb5a3be0cebe9a9858cb01bc1955e8c7b9fac17a9462976bea4a7b704e317c5c2200c70f0ca715a3363b9aa4fd3b + languageName: node + linkType: hard + "streamsearch@npm:^1.1.0": version: 1.1.0 resolution: "streamsearch@npm:1.1.0" @@ -25848,6 +26075,13 @@ __metadata: languageName: node linkType: hard +"stubs@npm:^3.0.0": + version: 3.0.0 + resolution: "stubs@npm:3.0.0" + checksum: 10c0/841a4ab8c76795d34aefe129185763b55fbf2e4693208215627caea4dd62e1299423dcd96f708d3128e3dfa0e669bae2cb912e6e906d7d81eaf6493196570923 + languageName: node + linkType: hard + "styled-jsx@npm:5.1.1": version: 5.1.1 resolution: "styled-jsx@npm:5.1.1" @@ -26082,6 +26316,18 @@ __metadata: languageName: node linkType: hard +"teeny-request@npm:^10.0.0": + version: 10.1.2 + resolution: "teeny-request@npm:10.1.2" + dependencies: + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + node-fetch: "npm:^3.3.2" + stream-events: "npm:^1.0.5" + checksum: 10c0/32cebdd723bc0bf7a10e2e6c0c7870b3e48895b8780949eab1aec8c2f7a10ccf50f2f2f66b4d520a490081b3d791b1d9746faaa510fb1fcff0eaeb1058c1fc08 + languageName: node + linkType: hard + "temp-dir@npm:1.0.0": version: 1.0.0 resolution: "temp-dir@npm:1.0.0" @@ -27078,6 +27324,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~7.18.0": + version: 7.18.2 + resolution: "undici-types@npm:7.18.2" + checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d + languageName: node + linkType: hard + "undici@npm:7.24.4": version: 7.24.4 resolution: "undici@npm:7.24.4" @@ -27290,13 +27543,6 @@ __metadata: languageName: node linkType: hard -"url-template@npm:^2.0.8": - version: 2.0.8 - resolution: "url-template@npm:2.0.8" - checksum: 10c0/56a15057eacbcf05d52b0caed8279c8451b3dd9d32856a1fdd91c6dc84dcb1646f12bafc756b7ade62ca5b1564da8efd7baac5add35868bafb43eb024c62805b - languageName: node - linkType: hard - "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" From 8c18ca8c09b60f00b89b443123d8624cda088b3c Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Tue, 7 Apr 2026 20:02:38 +0200 Subject: [PATCH 5/7] fix(node-sdk): switch gcs fallback back to googleapis storage --- .changeset/tasty-rabbits-wave.md | 2 +- packages/node-sdk/package.json | 2 +- .../node-sdk/src/flagsFallbackProvider.ts | 139 ++------ .../test/flagsFallbackProvider.test.ts | 109 +++---- yarn.lock | 304 ++---------------- 5 files changed, 108 insertions(+), 448 deletions(-) diff --git a/.changeset/tasty-rabbits-wave.md b/.changeset/tasty-rabbits-wave.md index c837fdfd..9072ee46 100644 --- a/.changeset/tasty-rabbits-wave.md +++ b/.changeset/tasty-rabbits-wave.md @@ -2,4 +2,4 @@ "@reflag/node-sdk": patch --- -Replace the built-in GCS fallback provider's default client dependency with `@google-cloud/storage-control`, removing the deprecated `@google-cloud/storage` dependency and its vulnerable transitive request stack. +Replace the built-in GCS fallback provider's default client dependency with `@googleapis/storage`, removing the deprecated `@google-cloud/storage` dependency and its vulnerable transitive request stack. diff --git a/packages/node-sdk/package.json b/packages/node-sdk/package.json index ee96326b..421b988f 100644 --- a/packages/node-sdk/package.json +++ b/packages/node-sdk/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.888.0", - "@google-cloud/storage-control": "0.8.2", + "@googleapis/storage": "21.2.0", "@redis/client": "^5.11.0", "@reflag/flag-evaluation": "1.0.0" } diff --git a/packages/node-sdk/src/flagsFallbackProvider.ts b/packages/node-sdk/src/flagsFallbackProvider.ts index 63dbe20b..d6d84640 100644 --- a/packages/node-sdk/src/flagsFallbackProvider.ts +++ b/packages/node-sdk/src/flagsFallbackProvider.ts @@ -83,33 +83,6 @@ type GCSObjectStore = { ): Promise; }; -type GCSStorageControlReadResponse = { - checksummedData?: { - content?: string | Uint8Array | ArrayBuffer; - }; -}; - -type GCSStorageControlStub = { - getObject(request: { bucket: string; object: string }): Promise<[unknown]>; - readObject(request: { bucket: string; object: string }): { - on(event: "data", listener: (response: GCSStorageControlReadResponse) => void): unknown; - on(event: "error", listener: (error: unknown) => void): unknown; - on(event: "end", listener: () => void): unknown; - }; - writeObject( - callback: (error: unknown, response?: unknown) => void, - ): { - on(event: "error", listener: (error: unknown) => void): unknown; - write(chunk: unknown): unknown; - end(): unknown; - }; -}; - -type GCSStorageControlClient = { - bucketPath(project: string, bucket: string): string; - initialize(): Promise; -}; - export type RedisFallbackProviderOptions = { /** * Optional Redis client. When omitted, a client is created using `REDIS_URL`. @@ -221,7 +194,6 @@ function parseSnapshot(raw: string) { function isNotFoundError(error: any) { return ( - error?.code === 5 || error?.code === 404 || error?.status === 404 || error?.response?.status === 404 || @@ -274,17 +246,19 @@ function createGCSObjectStore(client: LegacyGCSClient): GCSObjectStore { } async function createDefaultGCSObjectStore(): Promise { - const { v2 } = await import("@google-cloud/storage-control"); - const gcs = new v2.StorageClient() as unknown as GCSStorageControlClient; - const stub = await gcs.initialize(); - - const bucketResourceName = (bucket: string) => gcs.bucketPath("_", bucket); + const { auth, storage } = await import("@googleapis/storage"); + const gcs = storage({ + version: "v1", + auth: new auth.GoogleAuth({ + scopes: ["https://www.googleapis.com/auth/devstorage.read_write"], + }), + }); return { async exists(bucket, path) { try { - await stub.getObject({ - bucket: bucketResourceName(bucket), + await gcs.objects.get({ + bucket, object: path, }); return true; @@ -297,81 +271,36 @@ async function createDefaultGCSObjectStore(): Promise { }, async download(bucket, path) { - const stream = stub.readObject({ - bucket: bucketResourceName(bucket), - object: path, - }); + const response = await gcs.objects.get( + { + bucket, + object: path, + alt: "media", + }, + { + responseType: "arraybuffer", + }, + ); - return await new Promise((resolve, reject) => { - const chunks: Uint8Array[] = []; - let settled = false; - - const fail = (error: unknown) => { - if (settled) return; - settled = true; - reject(error); - }; - - stream.on("data", (response) => { - const content = response?.checksummedData?.content; - if (content === undefined || content === null) { - return; - } - - if (typeof content === "string") { - chunks.push(Buffer.from(content, "utf-8")); - return; - } - if (content instanceof Uint8Array) { - chunks.push(content); - return; - } - if (content instanceof ArrayBuffer) { - chunks.push(new Uint8Array(content)); - return; - } - - fail(new TypeError("Unexpected GCS download response body format")); - }); + if (response.data instanceof Uint8Array) { + return response.data; + } + if (response.data instanceof ArrayBuffer) { + return new Uint8Array(response.data); + } - stream.on("error", fail); - stream.on("end", () => { - if (settled) return; - settled = true; - resolve(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)))); - }); - }); + throw new TypeError("Unexpected GCS download response body format"); }, async save(bucket, path, body, options) { - const content = Buffer.from(body, "utf-8"); - - await new Promise((resolve, reject) => { - const stream = stub.writeObject((error) => { - if (error) { - reject(error); - return; - } - resolve(); - }); - - stream.on("error", reject); - stream.write({ - writeObjectSpec: { - resource: { - bucket: bucketResourceName(bucket), - name: path, - contentType: options.contentType, - }, - objectSize: content.byteLength, - }, - writeOffset: 0, - checksummedData: { - content, - }, - finishWrite: true, - }); - stream.end(); + await gcs.objects.insert({ + bucket, + name: path, + uploadType: "media", + media: { + mimeType: options.contentType, + body, + }, }); }, }; diff --git a/packages/node-sdk/test/flagsFallbackProvider.test.ts b/packages/node-sdk/test/flagsFallbackProvider.test.ts index 5476a030..d3fcdc32 100644 --- a/packages/node-sdk/test/flagsFallbackProvider.test.ts +++ b/packages/node-sdk/test/flagsFallbackProvider.test.ts @@ -1,4 +1,3 @@ -import { EventEmitter } from "events"; import { mkdtemp, readFile, rm } from "fs/promises"; import os from "os"; import path from "path"; @@ -242,55 +241,27 @@ describe("flagsFallbackProvider", () => { }); }); - it("creates the default GCS client from @google-cloud/storage-control", async () => { + it("creates the default GCS client from @googleapis/storage", async () => { vi.resetModules(); - const getObject = vi.fn().mockResolvedValue([{}]); - const readObject = vi.fn().mockImplementation(() => { - const stream = new EventEmitter(); - queueMicrotask(() => { - stream.emit("data", { - checksummedData: { - content: Buffer.from(JSON.stringify(snapshot), "utf-8"), - }, - }); - stream.emit("end"); + const get = vi + .fn() + .mockResolvedValueOnce({ data: { kind: "storage#object" } }) + .mockResolvedValueOnce({ + data: Buffer.from(JSON.stringify(snapshot), "utf-8"), }); - return stream; - }); - const write = vi.fn(); - const end = vi.fn().mockImplementation(function (this: EventEmitter) { - queueMicrotask(() => callback(null, {})); - this.emit("finish"); - }); - let callback: ((error: unknown, response?: unknown) => void) | undefined; - const writeObject = vi.fn().mockImplementation((cb) => { - callback = cb; - const stream = new EventEmitter() as EventEmitter & { - write: typeof write; - end: typeof end; - }; - stream.write = write; - stream.end = end; - return stream; - }); - const initialize = vi.fn().mockResolvedValue({ - getObject, - readObject, - writeObject, + const insert = vi.fn().mockResolvedValue({}); + const storage = vi.fn().mockReturnValue({ + objects: { + get, + insert, + }, }); - const bucketPath = vi - .fn() - .mockImplementation((project: string, bucket: string) => - `projects/${project}/buckets/${bucket}`, - ); - const StorageClient = vi.fn().mockImplementation(() => ({ - initialize, - bucketPath, - })); + const GoogleAuth = vi.fn(); - vi.doMock("@google-cloud/storage-control", () => ({ - v2: { StorageClient }, + vi.doMock("@googleapis/storage", () => ({ + auth: { GoogleAuth }, + storage, })); try { @@ -305,34 +276,40 @@ describe("flagsFallbackProvider", () => { await expect(provider.load(context)).resolves.toEqual(snapshot); await provider.save(context, snapshot); - expect(StorageClient).toHaveBeenCalledWith(); - expect(initialize).toHaveBeenCalledTimes(1); - expect(bucketPath).toHaveBeenCalledWith("_", "bucket-name"); - expect(getObject).toHaveBeenCalledWith({ - bucket: "projects/_/buckets/bucket-name", - object: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, + expect(GoogleAuth).toHaveBeenCalledWith({ + scopes: ["https://www.googleapis.com/auth/devstorage.read_write"], }); - expect(readObject).toHaveBeenCalledWith({ - bucket: "projects/_/buckets/bucket-name", + expect(storage).toHaveBeenCalledWith( + expect.objectContaining({ + version: "v1", + }), + ); + expect(get).toHaveBeenNthCalledWith(1, { + bucket: "bucket-name", object: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, }); - expect(write).toHaveBeenCalledWith({ - writeObjectSpec: { - resource: { - bucket: "projects/_/buckets/bucket-name", - name: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, - contentType: "application/json", - }, - objectSize: Buffer.byteLength(JSON.stringify(snapshot), "utf-8"), + expect(get).toHaveBeenNthCalledWith( + 2, + { + bucket: "bucket-name", + object: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, + alt: "media", }, - writeOffset: 0, - checksummedData: { - content: Buffer.from(JSON.stringify(snapshot), "utf-8"), + { + responseType: "arraybuffer", + }, + ); + expect(insert).toHaveBeenCalledWith({ + bucket: "bucket-name", + name: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, + uploadType: "media", + media: { + mimeType: "application/json", + body: JSON.stringify(snapshot), }, - finishWrite: true, }); } finally { - vi.doUnmock("@google-cloud/storage-control"); + vi.doUnmock("@googleapis/storage"); vi.resetModules(); } }); diff --git a/yarn.lock b/yarn.lock index 618f0ee6..3c8fa14d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4464,36 +4464,12 @@ __metadata: languageName: node linkType: hard -"@google-cloud/storage-control@npm:0.8.2": - version: 0.8.2 - resolution: "@google-cloud/storage-control@npm:0.8.2" +"@googleapis/storage@npm:21.2.0": + version: 21.2.0 + resolution: "@googleapis/storage@npm:21.2.0" dependencies: - google-gax: "npm:^5.0.0" - checksum: 10c0/94aed7aeb1e07f7c55908f52f08bfdf220bb4a229e3630db52d8c47252c441fb8f860718f1cef0f5ac4e31c775d231ad7fe47604d0e9ce2dd138fa51c590982a - languageName: node - linkType: hard - -"@grpc/grpc-js@npm:^1.12.6": - version: 1.14.3 - resolution: "@grpc/grpc-js@npm:1.14.3" - dependencies: - "@grpc/proto-loader": "npm:^0.8.0" - "@js-sdsl/ordered-map": "npm:^4.4.2" - checksum: 10c0/f41f06a311b93cca8c472d56e21387e0f7b57bb2337a91d15ea4279bac8ec4fa0de6bd0d881201229ab800c0f0c55277911ecb850e057f20a828d0ddd623551d - languageName: node - linkType: hard - -"@grpc/proto-loader@npm:^0.8.0": - version: 0.8.0 - resolution: "@grpc/proto-loader@npm:0.8.0" - dependencies: - lodash.camelcase: "npm:^4.3.0" - long: "npm:^5.0.0" - protobufjs: "npm:^7.5.3" - yargs: "npm:^17.7.2" - bin: - proto-loader-gen-types: build/bin/proto-loader-gen-types.js - checksum: 10c0/a27da3b85d5d17bab956d536786c717287eae46ca264ea9ec774db90ff571955bae2705809f431b4622fbf3be9951d7c7bbb1360b2015ee88abe1587cf3d6fe0 + googleapis-common: "npm:^8.0.0" + checksum: 10c0/ff7e758d4a14e19ae05343d80994e91a09ad82be3ab42cc903b123b30e7e57a78e7c54c7eb21398f015216d82968a60f674f5f5db0776433b31912ecd5beb023 languageName: node linkType: hard @@ -5415,13 +5391,6 @@ __metadata: languageName: node linkType: hard -"@js-sdsl/ordered-map@npm:^4.4.2": - version: 4.4.2 - resolution: "@js-sdsl/ordered-map@npm:4.4.2" - checksum: 10c0/cc7e15dc4acf6d9ef663757279600bab70533d847dcc1ab01332e9e680bd30b77cdf9ad885cc774276f51d98b05a013571c940e5b360985af5eb798dc1a2ee2b - languageName: node - linkType: hard - "@lerna/create@npm:8.1.3": version: 8.1.3 resolution: "@lerna/create@npm:8.1.3" @@ -6671,79 +6640,6 @@ __metadata: languageName: node linkType: hard -"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/aspromise@npm:1.1.2" - checksum: 10c0/a83343a468ff5b5ec6bff36fd788a64c839e48a07ff9f4f813564f58caf44d011cd6504ed2147bf34835bd7a7dd2107052af755961c6b098fd8902b4f6500d0f - languageName: node - linkType: hard - -"@protobufjs/base64@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/base64@npm:1.1.2" - checksum: 10c0/eec925e681081af190b8ee231f9bad3101e189abbc182ff279da6b531e7dbd2a56f1f306f37a80b1be9e00aa2d271690d08dcc5f326f71c9eed8546675c8caf6 - languageName: node - linkType: hard - -"@protobufjs/codegen@npm:^2.0.4": - version: 2.0.4 - resolution: "@protobufjs/codegen@npm:2.0.4" - checksum: 10c0/26ae337c5659e41f091606d16465bbcc1df1f37cc1ed462438b1f67be0c1e28dfb2ca9f294f39100c52161aef82edf758c95d6d75650a1ddf31f7ddee1440b43 - languageName: node - linkType: hard - -"@protobufjs/eventemitter@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/eventemitter@npm:1.1.0" - checksum: 10c0/1eb0a75180e5206d1033e4138212a8c7089a3d418c6dfa5a6ce42e593a4ae2e5892c4ef7421f38092badba4040ea6a45f0928869989411001d8c1018ea9a6e70 - languageName: node - linkType: hard - -"@protobufjs/fetch@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/fetch@npm:1.1.0" - dependencies: - "@protobufjs/aspromise": "npm:^1.1.1" - "@protobufjs/inquire": "npm:^1.1.0" - checksum: 10c0/cda6a3dc2d50a182c5865b160f72077aac197046600091dbb005dd0a66db9cce3c5eaed6d470ac8ed49d7bcbeef6ee5f0bc288db5ff9a70cbd003e5909065233 - languageName: node - linkType: hard - -"@protobufjs/float@npm:^1.0.2": - version: 1.0.2 - resolution: "@protobufjs/float@npm:1.0.2" - checksum: 10c0/18f2bdede76ffcf0170708af15c9c9db6259b771e6b84c51b06df34a9c339dbbeec267d14ce0bddd20acc142b1d980d983d31434398df7f98eb0c94a0eb79069 - languageName: node - linkType: hard - -"@protobufjs/inquire@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/inquire@npm:1.1.0" - checksum: 10c0/64372482efcba1fb4d166a2664a6395fa978b557803857c9c03500e0ac1013eb4b1aacc9ed851dd5fc22f81583670b4f4431bae186f3373fedcfde863ef5921a - languageName: node - linkType: hard - -"@protobufjs/path@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/path@npm:1.1.2" - checksum: 10c0/cece0a938e7f5dfd2fa03f8c14f2f1cf8b0d6e13ac7326ff4c96ea311effd5fb7ae0bba754fbf505312af2e38500250c90e68506b97c02360a43793d88a0d8b4 - languageName: node - linkType: hard - -"@protobufjs/pool@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/pool@npm:1.1.0" - checksum: 10c0/eda2718b7f222ac6e6ad36f758a92ef90d26526026a19f4f17f668f45e0306a5bd734def3f48f51f8134ae0978b6262a5c517c08b115a551756d1a3aadfcf038 - languageName: node - linkType: hard - -"@protobufjs/utf8@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/utf8@npm:1.1.0" - checksum: 10c0/a3fe31fe3fa29aa3349e2e04ee13dc170cc6af7c23d92ad49e3eeaf79b9766264544d3da824dba93b7855bd6a2982fb40032ef40693da98a136d835752beb487 - languageName: node - linkType: hard - "@react-native-async-storage/async-storage@npm:^2.2.0": version: 2.2.0 resolution: "@react-native-async-storage/async-storage@npm:2.2.0" @@ -7298,7 +7194,7 @@ __metadata: dependencies: "@aws-sdk/client-s3": "npm:^3.888.0" "@babel/core": "npm:~7.24.7" - "@google-cloud/storage-control": "npm:0.8.2" + "@googleapis/storage": "npm:21.2.0" "@redis/client": "npm:^5.11.0" "@reflag/eslint-config": "npm:~0.0.2" "@reflag/flag-evaluation": "npm:1.0.0" @@ -9095,15 +8991,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:>=13.7.0": - version: 25.5.2 - resolution: "@types/node@npm:25.5.2" - dependencies: - undici-types: "npm:~7.18.0" - checksum: 10c0/11e41a85401724cd1a4de6fb7bd4264ec46db10c09fc8cf8d41de4ede0a7063db458348f859ead4ec0929906aa26aaf45a5fef3aa59742ca0521cda9cee52377 - languageName: node - linkType: hard - "@types/node@npm:^12.7.1": version: 12.20.55 resolution: "@types/node@npm:12.20.55" @@ -13648,18 +13535,6 @@ __metadata: languageName: node linkType: hard -"duplexify@npm:^4.1.3": - version: 4.1.3 - resolution: "duplexify@npm:4.1.3" - dependencies: - end-of-stream: "npm:^1.4.1" - inherits: "npm:^2.0.3" - readable-stream: "npm:^3.1.1" - stream-shift: "npm:^1.0.2" - checksum: 10c0/8a7621ae95c89f3937f982fe36d72ea997836a708471a75bb2a0eecde3330311b1e128a6dad510e0fd64ace0c56bff3484ed2e82af0e465600c82117eadfbda5 - languageName: node - linkType: hard - "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -16423,7 +16298,7 @@ __metadata: languageName: node linkType: hard -"gaxios@npm:^7.0.0, gaxios@npm:^7.1.4": +"gaxios@npm:^7.0.0, gaxios@npm:^7.0.0-rc.4, gaxios@npm:^7.1.4": version: 7.1.4 resolution: "gaxios@npm:7.1.4" dependencies: @@ -16793,22 +16668,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.3.7": - version: 10.5.0 - resolution: "glob@npm:10.5.0" - dependencies: - foreground-child: "npm:^3.1.0" - jackspeak: "npm:^3.1.2" - minimatch: "npm:^9.0.4" - minipass: "npm:^7.1.2" - package-json-from-dist: "npm:^1.0.0" - path-scurry: "npm:^1.11.1" - bin: - glob: dist/esm/bin.mjs - checksum: 10c0/100705eddbde6323e7b35e1d1ac28bcb58322095bd8e63a7d0bef1a2cdafe0d0f7922a981b2b48369a4f8c1b077be5c171804534c3509dfe950dde15fbe6d828 - languageName: node - linkType: hard - "glob@npm:^10.4.1": version: 10.4.2 resolution: "glob@npm:10.4.2" @@ -16982,32 +16841,26 @@ __metadata: languageName: node linkType: hard -"google-gax@npm:^5.0.0": - version: 5.0.6 - resolution: "google-gax@npm:5.0.6" - dependencies: - "@grpc/grpc-js": "npm:^1.12.6" - "@grpc/proto-loader": "npm:^0.8.0" - duplexify: "npm:^4.1.3" - google-auth-library: "npm:^10.1.0" - google-logging-utils: "npm:^1.1.1" - node-fetch: "npm:^3.3.2" - object-hash: "npm:^3.0.0" - proto3-json-serializer: "npm:^3.0.0" - protobufjs: "npm:^7.5.3" - retry-request: "npm:^8.0.0" - rimraf: "npm:^5.0.1" - checksum: 10c0/1b368602340a80362d3a520727882feeabf7aa747097bf347de6b422d5b7dfa933e501ad18e4e44c84cde6fb6690038c1c4455ae885d68ce6db598e9e459cdff - languageName: node - linkType: hard - -"google-logging-utils@npm:1.1.3, google-logging-utils@npm:^1.0.0, google-logging-utils@npm:^1.1.1": +"google-logging-utils@npm:1.1.3, google-logging-utils@npm:^1.0.0": version: 1.1.3 resolution: "google-logging-utils@npm:1.1.3" checksum: 10c0/e65201c7e96543bd1423b9324013736646b9eed60941e0bfa47b9bfd146d2f09cf3df1c99ca60b7d80a726075263ead049ee72de53372cb8458c3bc55c2c1e59 languageName: node linkType: hard +"googleapis-common@npm:^8.0.0": + version: 8.0.1 + resolution: "googleapis-common@npm:8.0.1" + dependencies: + extend: "npm:^3.0.2" + gaxios: "npm:^7.0.0-rc.4" + google-auth-library: "npm:^10.1.0" + qs: "npm:^6.7.0" + url-template: "npm:^2.0.8" + checksum: 10c0/ee939fdfcaea32c528e7be64621b3e6b35e612f2456c8d4aca835e0a01ccec52f4ea2ac9509b083128398631a6cd66512c400adb4d0375a4638c55888ccdea7f + languageName: node + linkType: hard + "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -19561,13 +19414,6 @@ __metadata: languageName: node linkType: hard -"lodash.camelcase@npm:^4.3.0": - version: 4.3.0 - resolution: "lodash.camelcase@npm:4.3.0" - checksum: 10c0/fcba15d21a458076dd309fce6b1b4bf611d84a0ec252cb92447c948c533ac250b95d2e00955801ebc367e5af5ed288b996d75d37d2035260a937008e14eaf432 - languageName: node - linkType: hard - "lodash.debounce@npm:^4.0.8": version: 4.0.8 resolution: "lodash.debounce@npm:4.0.8" @@ -19673,13 +19519,6 @@ __metadata: languageName: node linkType: hard -"long@npm:^5.0.0": - version: 5.3.2 - resolution: "long@npm:5.3.2" - checksum: 10c0/7130fe1cbce2dca06734b35b70d380ca3f70271c7f8852c922a7c62c86c4e35f0c39290565eca7133c625908d40e126ac57c02b1b1a4636b9457d77e1e60b981 - languageName: node - linkType: hard - "loose-envify@npm:^1.0.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" @@ -23469,35 +23308,6 @@ __metadata: languageName: node linkType: hard -"proto3-json-serializer@npm:^3.0.0": - version: 3.0.4 - resolution: "proto3-json-serializer@npm:3.0.4" - dependencies: - protobufjs: "npm:^7.4.0" - checksum: 10c0/59150d5e6b396e00c0374e71eb4ebd6f33acd4212c0ad2acdda12181b08f923b733559d74ad78455085223efe24168d4d42887a8b8f15f7663f20debe5796081 - languageName: node - linkType: hard - -"protobufjs@npm:^7.4.0, protobufjs@npm:^7.5.3": - version: 7.5.4 - resolution: "protobufjs@npm:7.5.4" - dependencies: - "@protobufjs/aspromise": "npm:^1.1.2" - "@protobufjs/base64": "npm:^1.1.2" - "@protobufjs/codegen": "npm:^2.0.4" - "@protobufjs/eventemitter": "npm:^1.1.0" - "@protobufjs/fetch": "npm:^1.1.0" - "@protobufjs/float": "npm:^1.0.2" - "@protobufjs/inquire": "npm:^1.1.0" - "@protobufjs/path": "npm:^1.1.2" - "@protobufjs/pool": "npm:^1.1.0" - "@protobufjs/utf8": "npm:^1.1.0" - "@types/node": "npm:>=13.7.0" - long: "npm:^5.0.0" - checksum: 10c0/913b676109ffb3c05d3d31e03a684e569be91f3bba8613da4a683d69d9dba948daa2afd7d2e7944d1aa6c417890c35d9d9a8883c1160affafb0f9670d59ef722 - languageName: node - linkType: hard - "protocols@npm:^2.0.0, protocols@npm:^2.0.1": version: 2.0.1 resolution: "protocols@npm:2.0.1" @@ -23591,7 +23401,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.14.1": +"qs@npm:^6.14.1, qs@npm:^6.7.0": version: 6.15.0 resolution: "qs@npm:6.15.0" dependencies: @@ -24480,16 +24290,6 @@ __metadata: languageName: node linkType: hard -"retry-request@npm:^8.0.0": - version: 8.0.2 - resolution: "retry-request@npm:8.0.2" - dependencies: - extend: "npm:^3.0.2" - teeny-request: "npm:^10.0.0" - checksum: 10c0/e0b44950d69fe8f51d4e808650cd0d06383cd711adcc71e7764b28b2f10a9ac3b2209f0c9ac0547df0745031810a259730b9d13b0070d69df9806c37432456a9 - languageName: node - linkType: hard - "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -24526,17 +24326,6 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^5.0.1": - version: 5.0.10 - resolution: "rimraf@npm:5.0.10" - dependencies: - glob: "npm:^10.3.7" - bin: - rimraf: dist/esm/bin.mjs - checksum: 10c0/7da4fd0e15118ee05b918359462cfa1e7fe4b1228c7765195a45b55576e8c15b95db513b8466ec89129666f4af45ad978a3057a02139afba1a63512a2d9644cc - languageName: node - linkType: hard - "rollup-preserve-directives@npm:^1.1.2": version: 1.1.2 resolution: "rollup-preserve-directives@npm:1.1.2" @@ -25702,22 +25491,6 @@ __metadata: languageName: node linkType: hard -"stream-events@npm:^1.0.5": - version: 1.0.5 - resolution: "stream-events@npm:1.0.5" - dependencies: - stubs: "npm:^3.0.0" - checksum: 10c0/5d235a5799a483e94ea8829526fe9d95d76460032d5e78555fe4f801949ac6a27ea2212e4e0827c55f78726b3242701768adf2d33789465f51b31ed8ebd6b086 - languageName: node - linkType: hard - -"stream-shift@npm:^1.0.2": - version: 1.0.3 - resolution: "stream-shift@npm:1.0.3" - checksum: 10c0/939cd1051ca750d240a0625b106a2b988c45fb5a3be0cebe9a9858cb01bc1955e8c7b9fac17a9462976bea4a7b704e317c5c2200c70f0ca715a3363b9aa4fd3b - languageName: node - linkType: hard - "streamsearch@npm:^1.1.0": version: 1.1.0 resolution: "streamsearch@npm:1.1.0" @@ -26075,13 +25848,6 @@ __metadata: languageName: node linkType: hard -"stubs@npm:^3.0.0": - version: 3.0.0 - resolution: "stubs@npm:3.0.0" - checksum: 10c0/841a4ab8c76795d34aefe129185763b55fbf2e4693208215627caea4dd62e1299423dcd96f708d3128e3dfa0e669bae2cb912e6e906d7d81eaf6493196570923 - languageName: node - linkType: hard - "styled-jsx@npm:5.1.1": version: 5.1.1 resolution: "styled-jsx@npm:5.1.1" @@ -26316,18 +26082,6 @@ __metadata: languageName: node linkType: hard -"teeny-request@npm:^10.0.0": - version: 10.1.2 - resolution: "teeny-request@npm:10.1.2" - dependencies: - http-proxy-agent: "npm:^7.0.0" - https-proxy-agent: "npm:^7.0.1" - node-fetch: "npm:^3.3.2" - stream-events: "npm:^1.0.5" - checksum: 10c0/32cebdd723bc0bf7a10e2e6c0c7870b3e48895b8780949eab1aec8c2f7a10ccf50f2f2f66b4d520a490081b3d791b1d9746faaa510fb1fcff0eaeb1058c1fc08 - languageName: node - linkType: hard - "temp-dir@npm:1.0.0": version: 1.0.0 resolution: "temp-dir@npm:1.0.0" @@ -27324,13 +27078,6 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~7.18.0": - version: 7.18.2 - resolution: "undici-types@npm:7.18.2" - checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d - languageName: node - linkType: hard - "undici@npm:7.24.4": version: 7.24.4 resolution: "undici@npm:7.24.4" @@ -27543,6 +27290,13 @@ __metadata: languageName: node linkType: hard +"url-template@npm:^2.0.8": + version: 2.0.8 + resolution: "url-template@npm:2.0.8" + checksum: 10c0/56a15057eacbcf05d52b0caed8279c8451b3dd9d32856a1fdd91c6dc84dcb1646f12bafc756b7ade62ca5b1564da8efd7baac5add35868bafb43eb024c62805b + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" From fbbcc620a258ccb973797a6bf5a819cbf420b0fe Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Wed, 8 Apr 2026 09:07:02 +0200 Subject: [PATCH 6/7] fix(node-sdk): avoid shadowing node path import --- .../node-sdk/src/flagsFallbackProvider.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/node-sdk/src/flagsFallbackProvider.ts b/packages/node-sdk/src/flagsFallbackProvider.ts index d6d84640..1990901d 100644 --- a/packages/node-sdk/src/flagsFallbackProvider.ts +++ b/packages/node-sdk/src/flagsFallbackProvider.ts @@ -73,11 +73,11 @@ export type GCSFallbackProviderOptions = { type LegacyGCSClient = NonNullable; type GCSObjectStore = { - exists(bucket: string, path: string): Promise; - download(bucket: string, path: string): Promise; + exists(bucket: string, objectPath: string): Promise; + download(bucket: string, objectPath: string): Promise; save( bucket: string, - path: string, + objectPath: string, body: string, options: { contentType: string }, ): Promise; @@ -229,18 +229,18 @@ async function createDefaultS3Client() { function createGCSObjectStore(client: LegacyGCSClient): GCSObjectStore { return { - async exists(bucket, path) { - const [exists] = await client.bucket(bucket).file(path).exists(); + async exists(bucket, objectPath) { + const [exists] = await client.bucket(bucket).file(objectPath).exists(); return exists; }, - async download(bucket, path) { - const [contents] = await client.bucket(bucket).file(path).download(); + async download(bucket, objectPath) { + const [contents] = await client.bucket(bucket).file(objectPath).download(); return contents; }, - async save(bucket, path, body, options) { - return client.bucket(bucket).file(path).save(body, options); + async save(bucket, objectPath, body, options) { + return client.bucket(bucket).file(objectPath).save(body, options); }, }; } @@ -255,11 +255,11 @@ async function createDefaultGCSObjectStore(): Promise { }); return { - async exists(bucket, path) { + async exists(bucket, objectPath) { try { await gcs.objects.get({ bucket, - object: path, + object: objectPath, }); return true; } catch (error) { @@ -270,11 +270,11 @@ async function createDefaultGCSObjectStore(): Promise { } }, - async download(bucket, path) { + async download(bucket, objectPath) { const response = await gcs.objects.get( { bucket, - object: path, + object: objectPath, alt: "media", }, { @@ -292,10 +292,10 @@ async function createDefaultGCSObjectStore(): Promise { throw new TypeError("Unexpected GCS download response body format"); }, - async save(bucket, path, body, options) { + async save(bucket, objectPath, body, options) { await gcs.objects.insert({ bucket, - name: path, + name: objectPath, uploadType: "media", media: { mimeType: options.contentType, From fad81845a9f6817998419553368a7d77b514e117 Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Wed, 8 Apr 2026 09:59:46 +0200 Subject: [PATCH 7/7] feat(node-sdk): accept googleapis gcs clients --- .../node-sdk/src/flagsFallbackProvider.ts | 115 +++++++++++++----- packages/node-sdk/src/index.ts | 3 + .../test/flagsFallbackProvider.test.ts | 54 +++++++- 3 files changed, 141 insertions(+), 31 deletions(-) diff --git a/packages/node-sdk/src/flagsFallbackProvider.ts b/packages/node-sdk/src/flagsFallbackProvider.ts index 1990901d..d373d1f1 100644 --- a/packages/node-sdk/src/flagsFallbackProvider.ts +++ b/packages/node-sdk/src/flagsFallbackProvider.ts @@ -39,6 +39,44 @@ export type S3FallbackProviderOptions = { keyPrefix?: string; }; +export type GCSLegacyClient = { + bucket(name: string): { + file(path: string): { + exists(): Promise<[boolean]>; + download(): Promise<[Uint8Array]>; + save(body: string, options: { contentType: string }): Promise; + }; + }; +}; + +export type GCSGoogleApisClient = { + objects: { + get( + params: { + bucket: string; + object: string; + alt?: string; + }, + options?: { + responseType?: "arraybuffer"; + }, + ): Promise<{ + data: unknown; + }>; + insert(params: { + bucket: string; + name: string; + uploadType: "media"; + media: { + mimeType: string; + body: string; + }; + }): Promise; + }; +}; + +export type GCSFallbackProviderClient = GCSLegacyClient | GCSGoogleApisClient; + export type GCSFallbackProviderOptions = { /** * Bucket where snapshots are stored. @@ -48,19 +86,12 @@ export type GCSFallbackProviderOptions = { /** * Optional GCS client. A default client is created when omitted. * - * TODO(next major): Replace this legacy `bucket().file()` client shape with - * a simpler object-store interface that doesn't mirror the deprecated - * `@google-cloud/storage` API. + * Accepts either a legacy `bucket().file()` client or a generated + * `@googleapis/storage` client. + * + * TODO(next major): Replace this with a simpler object-store interface. */ - client?: { - bucket(name: string): { - file(path: string): { - exists(): Promise<[boolean]>; - download(): Promise<[Uint8Array]>; - save(body: string, options: { contentType: string }): Promise; - }; - }; - }; + client?: GCSFallbackProviderClient; /** * Prefix for generated per-environment keys. @@ -70,8 +101,6 @@ export type GCSFallbackProviderOptions = { keyPrefix?: string; }; -type LegacyGCSClient = NonNullable; - type GCSObjectStore = { exists(bucket: string, objectPath: string): Promise; download(bucket: string, objectPath: string): Promise; @@ -227,7 +256,7 @@ async function createDefaultS3Client() { return new S3Client({}); } -function createGCSObjectStore(client: LegacyGCSClient): GCSObjectStore { +function createLegacyGCSObjectStore(client: GCSLegacyClient): GCSObjectStore { return { async exists(bucket, objectPath) { const [exists] = await client.bucket(bucket).file(objectPath).exists(); @@ -235,7 +264,10 @@ function createGCSObjectStore(client: LegacyGCSClient): GCSObjectStore { }, async download(bucket, objectPath) { - const [contents] = await client.bucket(bucket).file(objectPath).download(); + const [contents] = await client + .bucket(bucket) + .file(objectPath) + .download(); return contents; }, @@ -245,19 +277,13 @@ function createGCSObjectStore(client: LegacyGCSClient): GCSObjectStore { }; } -async function createDefaultGCSObjectStore(): Promise { - const { auth, storage } = await import("@googleapis/storage"); - const gcs = storage({ - version: "v1", - auth: new auth.GoogleAuth({ - scopes: ["https://www.googleapis.com/auth/devstorage.read_write"], - }), - }); - +function createGoogleApisGCSObjectStore( + client: GCSGoogleApisClient, +): GCSObjectStore { return { async exists(bucket, objectPath) { try { - await gcs.objects.get({ + await client.objects.get({ bucket, object: objectPath, }); @@ -271,7 +297,7 @@ async function createDefaultGCSObjectStore(): Promise { }, async download(bucket, objectPath) { - const response = await gcs.objects.get( + const response = await client.objects.get( { bucket, object: objectPath, @@ -293,7 +319,7 @@ async function createDefaultGCSObjectStore(): Promise { }, async save(bucket, objectPath, body, options) { - await gcs.objects.insert({ + await client.objects.insert({ bucket, name: objectPath, uploadType: "media", @@ -306,6 +332,39 @@ async function createDefaultGCSObjectStore(): Promise { }; } +function isGoogleApisGCSClient( + client: GCSFallbackProviderClient, +): client is GCSGoogleApisClient { + return ( + "objects" in client && + isObject(client.objects) && + typeof client.objects.get === "function" && + typeof client.objects.insert === "function" + ); +} + +function createGCSObjectStore( + client: GCSFallbackProviderClient, +): GCSObjectStore { + if (isGoogleApisGCSClient(client)) { + return createGoogleApisGCSObjectStore(client); + } + + return createLegacyGCSObjectStore(client); +} + +async function createDefaultGCSObjectStore(): Promise { + const { auth, storage } = await import("@googleapis/storage"); + return createGoogleApisGCSObjectStore( + storage({ + version: "v1", + auth: new auth.GoogleAuth({ + scopes: ["https://www.googleapis.com/auth/devstorage.read_write"], + }), + }), + ); +} + export function createStaticFallbackProvider({ flags, }: StaticFallbackProviderOptions): FlagsFallbackProvider { diff --git a/packages/node-sdk/src/index.ts b/packages/node-sdk/src/index.ts index 1d78d058..e6bcd752 100644 --- a/packages/node-sdk/src/index.ts +++ b/packages/node-sdk/src/index.ts @@ -10,7 +10,10 @@ export { BoundReflagClient, ReflagClient } from "./client"; export { EdgeClient, EdgeClientOptions } from "./edgeClient"; export type { FileFallbackProviderOptions, + GCSFallbackProviderClient, GCSFallbackProviderOptions, + GCSGoogleApisClient, + GCSLegacyClient, RedisFallbackProviderOptions, S3FallbackProviderOptions, StaticFallbackProviderOptions, diff --git a/packages/node-sdk/test/flagsFallbackProvider.test.ts b/packages/node-sdk/test/flagsFallbackProvider.test.ts index 0fd808d5..f94429ff 100644 --- a/packages/node-sdk/test/flagsFallbackProvider.test.ts +++ b/packages/node-sdk/test/flagsFallbackProvider.test.ts @@ -242,6 +242,55 @@ describe("flagsFallbackProvider", () => { }); }); + it("accepts a provided @googleapis/storage client", async () => { + const get = vi + .fn() + .mockResolvedValueOnce({ data: { kind: "storage#object" } }) + .mockResolvedValueOnce({ + data: Buffer.from(JSON.stringify(snapshot), "utf-8"), + }); + const insert = vi.fn().mockResolvedValue({}); + + const provider = fallbackProviders.gcs({ + bucket: "bucket-name", + client: { + objects: { + get, + insert, + }, + }, + keyPrefix: "reflag/flags-fallback///", + }); + + await expect(provider.load(context)).resolves.toEqual(snapshot); + await provider.save(context, snapshot); + + expect(get).toHaveBeenNthCalledWith(1, { + bucket: "bucket-name", + object: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, + }); + expect(get).toHaveBeenNthCalledWith( + 2, + { + bucket: "bucket-name", + object: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, + alt: "media", + }, + { + responseType: "arraybuffer", + }, + ); + expect(insert).toHaveBeenCalledWith({ + bucket: "bucket-name", + name: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`, + uploadType: "media", + media: { + mimeType: "application/json", + body: JSON.stringify(snapshot), + }, + }); + }); + it("creates the default GCS client from @googleapis/storage", async () => { vi.resetModules(); @@ -266,9 +315,8 @@ describe("flagsFallbackProvider", () => { })); try { - const { createGCSFallbackProvider } = await import( - "../src/flagsFallbackProvider" - ); + const { createGCSFallbackProvider } = + await import("../src/flagsFallbackProvider"); const provider = createGCSFallbackProvider({ bucket: "bucket-name", keyPrefix: "reflag/flags-fallback///",