Skip to content

Commit 8c18ca8

Browse files
committed
fix(node-sdk): switch gcs fallback back to googleapis storage
1 parent 0d4282f commit 8c18ca8

5 files changed

Lines changed: 108 additions & 448 deletions

File tree

.changeset/tasty-rabbits-wave.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@reflag/node-sdk": patch
33
---
44

5-
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.
5+
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.

packages/node-sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
},
4747
"dependencies": {
4848
"@aws-sdk/client-s3": "^3.888.0",
49-
"@google-cloud/storage-control": "0.8.2",
49+
"@googleapis/storage": "21.2.0",
5050
"@redis/client": "^5.11.0",
5151
"@reflag/flag-evaluation": "1.0.0"
5252
}

packages/node-sdk/src/flagsFallbackProvider.ts

Lines changed: 34 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -83,33 +83,6 @@ type GCSObjectStore = {
8383
): Promise<unknown>;
8484
};
8585

86-
type GCSStorageControlReadResponse = {
87-
checksummedData?: {
88-
content?: string | Uint8Array | ArrayBuffer;
89-
};
90-
};
91-
92-
type GCSStorageControlStub = {
93-
getObject(request: { bucket: string; object: string }): Promise<[unknown]>;
94-
readObject(request: { bucket: string; object: string }): {
95-
on(event: "data", listener: (response: GCSStorageControlReadResponse) => void): unknown;
96-
on(event: "error", listener: (error: unknown) => void): unknown;
97-
on(event: "end", listener: () => void): unknown;
98-
};
99-
writeObject(
100-
callback: (error: unknown, response?: unknown) => void,
101-
): {
102-
on(event: "error", listener: (error: unknown) => void): unknown;
103-
write(chunk: unknown): unknown;
104-
end(): unknown;
105-
};
106-
};
107-
108-
type GCSStorageControlClient = {
109-
bucketPath(project: string, bucket: string): string;
110-
initialize(): Promise<GCSStorageControlStub>;
111-
};
112-
11386
export type RedisFallbackProviderOptions = {
11487
/**
11588
* Optional Redis client. When omitted, a client is created using `REDIS_URL`.
@@ -221,7 +194,6 @@ function parseSnapshot(raw: string) {
221194

222195
function isNotFoundError(error: any) {
223196
return (
224-
error?.code === 5 ||
225197
error?.code === 404 ||
226198
error?.status === 404 ||
227199
error?.response?.status === 404 ||
@@ -274,17 +246,19 @@ function createGCSObjectStore(client: LegacyGCSClient): GCSObjectStore {
274246
}
275247

276248
async function createDefaultGCSObjectStore(): Promise<GCSObjectStore> {
277-
const { v2 } = await import("@google-cloud/storage-control");
278-
const gcs = new v2.StorageClient() as unknown as GCSStorageControlClient;
279-
const stub = await gcs.initialize();
280-
281-
const bucketResourceName = (bucket: string) => gcs.bucketPath("_", bucket);
249+
const { auth, storage } = await import("@googleapis/storage");
250+
const gcs = storage({
251+
version: "v1",
252+
auth: new auth.GoogleAuth({
253+
scopes: ["https://www.googleapis.com/auth/devstorage.read_write"],
254+
}),
255+
});
282256

283257
return {
284258
async exists(bucket, path) {
285259
try {
286-
await stub.getObject({
287-
bucket: bucketResourceName(bucket),
260+
await gcs.objects.get({
261+
bucket,
288262
object: path,
289263
});
290264
return true;
@@ -297,81 +271,36 @@ async function createDefaultGCSObjectStore(): Promise<GCSObjectStore> {
297271
},
298272

299273
async download(bucket, path) {
300-
const stream = stub.readObject({
301-
bucket: bucketResourceName(bucket),
302-
object: path,
303-
});
274+
const response = await gcs.objects.get(
275+
{
276+
bucket,
277+
object: path,
278+
alt: "media",
279+
},
280+
{
281+
responseType: "arraybuffer",
282+
},
283+
);
304284

305-
return await new Promise<Uint8Array>((resolve, reject) => {
306-
const chunks: Uint8Array[] = [];
307-
let settled = false;
308-
309-
const fail = (error: unknown) => {
310-
if (settled) return;
311-
settled = true;
312-
reject(error);
313-
};
314-
315-
stream.on("data", (response) => {
316-
const content = response?.checksummedData?.content;
317-
if (content === undefined || content === null) {
318-
return;
319-
}
320-
321-
if (typeof content === "string") {
322-
chunks.push(Buffer.from(content, "utf-8"));
323-
return;
324-
}
325-
if (content instanceof Uint8Array) {
326-
chunks.push(content);
327-
return;
328-
}
329-
if (content instanceof ArrayBuffer) {
330-
chunks.push(new Uint8Array(content));
331-
return;
332-
}
333-
334-
fail(new TypeError("Unexpected GCS download response body format"));
335-
});
285+
if (response.data instanceof Uint8Array) {
286+
return response.data;
287+
}
288+
if (response.data instanceof ArrayBuffer) {
289+
return new Uint8Array(response.data);
290+
}
336291

337-
stream.on("error", fail);
338-
stream.on("end", () => {
339-
if (settled) return;
340-
settled = true;
341-
resolve(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))));
342-
});
343-
});
292+
throw new TypeError("Unexpected GCS download response body format");
344293
},
345294

346295
async save(bucket, path, body, options) {
347-
const content = Buffer.from(body, "utf-8");
348-
349-
await new Promise<void>((resolve, reject) => {
350-
const stream = stub.writeObject((error) => {
351-
if (error) {
352-
reject(error);
353-
return;
354-
}
355-
resolve();
356-
});
357-
358-
stream.on("error", reject);
359-
stream.write({
360-
writeObjectSpec: {
361-
resource: {
362-
bucket: bucketResourceName(bucket),
363-
name: path,
364-
contentType: options.contentType,
365-
},
366-
objectSize: content.byteLength,
367-
},
368-
writeOffset: 0,
369-
checksummedData: {
370-
content,
371-
},
372-
finishWrite: true,
373-
});
374-
stream.end();
296+
await gcs.objects.insert({
297+
bucket,
298+
name: path,
299+
uploadType: "media",
300+
media: {
301+
mimeType: options.contentType,
302+
body,
303+
},
375304
});
376305
},
377306
};

packages/node-sdk/test/flagsFallbackProvider.test.ts

Lines changed: 43 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { EventEmitter } from "events";
21
import { mkdtemp, readFile, rm } from "fs/promises";
32
import os from "os";
43
import path from "path";
@@ -242,55 +241,27 @@ describe("flagsFallbackProvider", () => {
242241
});
243242
});
244243

245-
it("creates the default GCS client from @google-cloud/storage-control", async () => {
244+
it("creates the default GCS client from @googleapis/storage", async () => {
246245
vi.resetModules();
247246

248-
const getObject = vi.fn().mockResolvedValue([{}]);
249-
const readObject = vi.fn().mockImplementation(() => {
250-
const stream = new EventEmitter();
251-
queueMicrotask(() => {
252-
stream.emit("data", {
253-
checksummedData: {
254-
content: Buffer.from(JSON.stringify(snapshot), "utf-8"),
255-
},
256-
});
257-
stream.emit("end");
247+
const get = vi
248+
.fn()
249+
.mockResolvedValueOnce({ data: { kind: "storage#object" } })
250+
.mockResolvedValueOnce({
251+
data: Buffer.from(JSON.stringify(snapshot), "utf-8"),
258252
});
259-
return stream;
260-
});
261-
const write = vi.fn();
262-
const end = vi.fn().mockImplementation(function (this: EventEmitter) {
263-
queueMicrotask(() => callback(null, {}));
264-
this.emit("finish");
265-
});
266-
let callback: ((error: unknown, response?: unknown) => void) | undefined;
267-
const writeObject = vi.fn().mockImplementation((cb) => {
268-
callback = cb;
269-
const stream = new EventEmitter() as EventEmitter & {
270-
write: typeof write;
271-
end: typeof end;
272-
};
273-
stream.write = write;
274-
stream.end = end;
275-
return stream;
276-
});
277-
const initialize = vi.fn().mockResolvedValue({
278-
getObject,
279-
readObject,
280-
writeObject,
253+
const insert = vi.fn().mockResolvedValue({});
254+
const storage = vi.fn().mockReturnValue({
255+
objects: {
256+
get,
257+
insert,
258+
},
281259
});
282-
const bucketPath = vi
283-
.fn()
284-
.mockImplementation((project: string, bucket: string) =>
285-
`projects/${project}/buckets/${bucket}`,
286-
);
287-
const StorageClient = vi.fn().mockImplementation(() => ({
288-
initialize,
289-
bucketPath,
290-
}));
260+
const GoogleAuth = vi.fn();
291261

292-
vi.doMock("@google-cloud/storage-control", () => ({
293-
v2: { StorageClient },
262+
vi.doMock("@googleapis/storage", () => ({
263+
auth: { GoogleAuth },
264+
storage,
294265
}));
295266

296267
try {
@@ -305,34 +276,40 @@ describe("flagsFallbackProvider", () => {
305276
await expect(provider.load(context)).resolves.toEqual(snapshot);
306277
await provider.save(context, snapshot);
307278

308-
expect(StorageClient).toHaveBeenCalledWith();
309-
expect(initialize).toHaveBeenCalledTimes(1);
310-
expect(bucketPath).toHaveBeenCalledWith("_", "bucket-name");
311-
expect(getObject).toHaveBeenCalledWith({
312-
bucket: "projects/_/buckets/bucket-name",
313-
object: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`,
279+
expect(GoogleAuth).toHaveBeenCalledWith({
280+
scopes: ["https://www.googleapis.com/auth/devstorage.read_write"],
314281
});
315-
expect(readObject).toHaveBeenCalledWith({
316-
bucket: "projects/_/buckets/bucket-name",
282+
expect(storage).toHaveBeenCalledWith(
283+
expect.objectContaining({
284+
version: "v1",
285+
}),
286+
);
287+
expect(get).toHaveBeenNthCalledWith(1, {
288+
bucket: "bucket-name",
317289
object: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`,
318290
});
319-
expect(write).toHaveBeenCalledWith({
320-
writeObjectSpec: {
321-
resource: {
322-
bucket: "projects/_/buckets/bucket-name",
323-
name: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`,
324-
contentType: "application/json",
325-
},
326-
objectSize: Buffer.byteLength(JSON.stringify(snapshot), "utf-8"),
291+
expect(get).toHaveBeenNthCalledWith(
292+
2,
293+
{
294+
bucket: "bucket-name",
295+
object: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`,
296+
alt: "media",
327297
},
328-
writeOffset: 0,
329-
checksummedData: {
330-
content: Buffer.from(JSON.stringify(snapshot), "utf-8"),
298+
{
299+
responseType: "arraybuffer",
300+
},
301+
);
302+
expect(insert).toHaveBeenCalledWith({
303+
bucket: "bucket-name",
304+
name: `reflag/flags-fallback/flags-fallback-${context.secretKeyHash.slice(0, 16)}.json`,
305+
uploadType: "media",
306+
media: {
307+
mimeType: "application/json",
308+
body: JSON.stringify(snapshot),
331309
},
332-
finishWrite: true,
333310
});
334311
} finally {
335-
vi.doUnmock("@google-cloud/storage-control");
312+
vi.doUnmock("@googleapis/storage");
336313
vi.resetModules();
337314
}
338315
});

0 commit comments

Comments
 (0)