From e8efa8b1d501fb988233664413be6e19a5f79fc9 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Tue, 1 Sep 2026 20:22:29 +0100 Subject: [PATCH 1/2] fix(storage-resize-images): map the Off content-filter option to OFF The select option "Off (No filtering)" stored "False", which convertHarmBlockThreshold rejects, so a deployment with filtering off threw "Invalid HarmBlockThreshold: False" on every event. Map the option to "OFF", which the resolver converts to a null threshold, and delete the unused CONTENT_FILTER_OPTIONS const that recorded the intended values. Fixes #3047 --- kits/storage-resize-images/src/config.ts | 8 +-- .../tests/config.test.ts | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/kits/storage-resize-images/src/config.ts b/kits/storage-resize-images/src/config.ts index 9902641f79..b972bb4df4 100644 --- a/kits/storage-resize-images/src/config.ts +++ b/kits/storage-resize-images/src/config.ts @@ -41,12 +41,6 @@ const IMAGE_TYPE_OPTIONS = [ "false", ] as const; const MEMORY_OPTIONS = [512, 1024, 2048, 4096, 8192] as const; -const CONTENT_FILTER_OPTIONS = [ - "OFF", - "BLOCK_ONLY_HIGH", - "BLOCK_MEDIUM_AND_ABOVE", - "BLOCK_LOW_AND_ABOVE", -] as const; const ABSOLUTE_PATH_LIST_VALIDATION = { validationRegex: /^(?:(\/[^\s\/\,]+)+(\,(\/[^\s\/\,]+)+)*|)$/, validationErrorMessage: @@ -232,7 +226,7 @@ const params = { default: "OFF", input: select({ - "Off (No filtering)": "False", + "Off (No filtering)": "OFF", "Low strictness (Block only high severity content)": "BLOCK_ONLY_HIGH", "Medium strictness (Block medium and high severity content)": "BLOCK_MEDIUM_AND_ABOVE", diff --git a/kits/storage-resize-images/tests/config.test.ts b/kits/storage-resize-images/tests/config.test.ts index 7ecaeef67d..e4cf66e385 100644 --- a/kits/storage-resize-images/tests/config.test.ts +++ b/kits/storage-resize-images/tests/config.test.ts @@ -28,6 +28,9 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import type { SelectInput, StringParam } from "firebase-functions/params"; +import type { ContentFilterLevel } from "../src/export-config"; + const ENV_KEYS = [ "IMG_BUCKET", "IMG_SIZES", @@ -214,6 +217,70 @@ describe("configFromEnv", () => { }); }); +/** + * The select is the only source of CONTENT_FILTER_LEVEL values at deploy + * time, so every option it offers must be a value the resolver accepts. + */ +describe("CONTENT_FILTER_LEVEL select", () => { + const baseConfig = { + bucket: "extensions-testing.appspot.com", + sizes: "200x200", + } as const; + + async function selectOptions() { + await import("../src/config"); + const { declaredParams } = await import("firebase-functions/params"); + const param = declaredParams.find( + (declared) => declared.name === "CONTENT_FILTER_LEVEL" + ) as StringParam | undefined; + const input = param?.options.input as SelectInput | undefined; + return input?.select.options ?? []; + } + + test("offers OFF and the three block thresholds", async () => { + expect(await selectOptions()).toEqual([ + { label: "Off (No filtering)", value: "OFF" }, + { + label: "Low strictness (Block only high severity content)", + value: "BLOCK_ONLY_HIGH", + }, + { + label: "Medium strictness (Block medium and high severity content)", + value: "BLOCK_MEDIUM_AND_ABOVE", + }, + { + label: "High strictness (Block low, medium, and high severity content)", + value: "BLOCK_LOW_AND_ABOVE", + }, + ]); + }); + + test("every option resolves, and OFF disables filtering", async () => { + const { resolveResizeImagesConfig } = await import("../src/export-config"); + for (const { value } of await selectOptions()) { + const resolved = resolveResizeImagesConfig({ + ...baseConfig, + contentFilterLevel: value as ContentFilterLevel, + }); + if (value === "OFF") { + expect(resolved.contentFilterLevel).toBeNull(); + } else { + expect(resolved.contentFilterLevel).toBe(value); + } + } + }); + + test('the retired "False" option value is rejected', async () => { + const { resolveResizeImagesConfig } = await import("../src/export-config"); + expect(() => + resolveResizeImagesConfig({ + ...baseConfig, + contentFilterLevel: "False" as never, + }) + ).toThrow("Invalid HarmBlockThreshold: False"); + }); +}); + /** * The extension enforces path-list shape at install time via the * `extension.yaml` param regex. The kit has no install step, so the same From 60532811ec7ca90c5e7937daaaad58d4d55016bf Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Wed, 2 Sep 2026 12:34:35 +0100 Subject: [PATCH 2/2] fix(storage-resize-images): store lowercase false for the original image-type option The IMAGE_TYPE multiSelect stored "False" while every consumer compares "false", so picking original asked sharp for a format named False and failed every resize. Pins the multiSelect option list and that every conversion value maps to a supported output content type, removes the unreferenced IMAGE_TYPE_OPTIONS and MEMORY_OPTIONS consts for the same reason CONTENT_FILTER_OPTIONS went, and documents the stored-value upgrade caveat for CONTENT_FILTER_LEVEL and IMAGE_TYPE in the CHANGELOG. --- kits/storage-resize-images/CHANGELOG.md | 2 + kits/storage-resize-images/src/config.ts | 12 +---- .../tests/config.test.ts | 45 ++++++++++++++++++- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/kits/storage-resize-images/CHANGELOG.md b/kits/storage-resize-images/CHANGELOG.md index bddce1dca9..6729d6545c 100644 --- a/kits/storage-resize-images/CHANGELOG.md +++ b/kits/storage-resize-images/CHANGELOG.md @@ -1,2 +1,4 @@ +- fix: the "Off (No filtering)" content-filter option now stores `OFF` instead of `False`, which the resolver rejected, so every storage event threw `Invalid HarmBlockThreshold: False`. A `.env` written by an earlier deploy keeps the stored value on upgrade: if it carries `CONTENT_FILTER_LEVEL=False`, edit it to `OFF` (or re-select the option). +- fix: the "original" image-type option now stores `false` instead of `False`. The resize path treated `False` as a target format, so each resized file was uploaded as `_.False` with no content type and reported as a success. A stored `IMAGE_TYPE=["False"]` likewise needs editing to `["false"]`. - fix: restore the `us-central1` content-filter fallback. `checkImageContent` threw `FUNCTION_REGION is required for Vertex AI filtering.` when no region was available; the extension fell back to `us-central1`. The Vertex AI call now uses the function's region when known and `us-central1` otherwise, matching the extension. Normal CLI deploys were unaffected (the Firebase CLI sets `FUNCTION_REGION` on deployed functions); the throw was reachable for library consumers, emulator runs, and hand-rolled environments. - Initial release of kit, see README for differences between the legacy extension and this kit diff --git a/kits/storage-resize-images/src/config.ts b/kits/storage-resize-images/src/config.ts index b972bb4df4..63c25e0cc6 100644 --- a/kits/storage-resize-images/src/config.ts +++ b/kits/storage-resize-images/src/config.ts @@ -31,16 +31,6 @@ import { type ResizeImagesConfig, } from "./export-config"; -const IMAGE_TYPE_OPTIONS = [ - "jpeg", - "webp", - "png", - "tiff", - "gif", - "avif", - "false", -] as const; -const MEMORY_OPTIONS = [512, 1024, 2048, 4096, 8192] as const; const ABSOLUTE_PATH_LIST_VALIDATION = { validationRegex: /^(?:(\/[^\s\/\,]+)+(\,(\/[^\s\/\,]+)+)*|)$/, validationErrorMessage: @@ -160,7 +150,7 @@ const params = { tiff: "tiff", gif: "gif", avif: "avif", - original: "False", + original: "false", }), }), outputOptions: defineString("OUTPUT_OPTIONS", { diff --git a/kits/storage-resize-images/tests/config.test.ts b/kits/storage-resize-images/tests/config.test.ts index e4cf66e385..c6ce6998de 100644 --- a/kits/storage-resize-images/tests/config.test.ts +++ b/kits/storage-resize-images/tests/config.test.ts @@ -28,7 +28,12 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import type { SelectInput, StringParam } from "firebase-functions/params"; +import type { + ListParam, + MultiSelectInput, + SelectInput, + StringParam, +} from "firebase-functions/params"; import type { ContentFilterLevel } from "../src/export-config"; const ENV_KEYS = [ @@ -281,6 +286,44 @@ describe("CONTENT_FILTER_LEVEL select", () => { }); }); +/** + * The same audit for IMAGE_TYPE: every multiSelect value must be one the + * resize path accepts - `"false"` for keeping the original format, or a key + * of `SUPPORTED_IMAGE_CONTENT_TYPE_MAP` so the output content type resolves. + */ +describe("IMAGE_TYPE multiSelect", () => { + async function multiSelectOptions() { + await import("../src/config"); + const { declaredParams } = await import("firebase-functions/params"); + const param = declaredParams.find( + (declared) => declared.name === "IMAGE_TYPE" + ) as ListParam | undefined; + const input = param?.options.input as MultiSelectInput | undefined; + return input?.multiSelect.options ?? []; + } + + test('offers the six conversion formats and "false" for the original type', async () => { + expect(await multiSelectOptions()).toEqual([ + { label: "jpeg", value: "jpeg" }, + { label: "webp", value: "webp" }, + { label: "png", value: "png" }, + { label: "tiff", value: "tiff" }, + { label: "gif", value: "gif" }, + { label: "avif", value: "avif" }, + { label: "original", value: "false" }, + ]); + }); + + test("every conversion value maps to an output content type", async () => { + const { SUPPORTED_IMAGE_CONTENT_TYPE_MAP } = await import("../src/global"); + for (const { value } of await multiSelectOptions()) { + if (value !== "false") { + expect(SUPPORTED_IMAGE_CONTENT_TYPE_MAP).toHaveProperty(value); + } + } + }); +}); + /** * The extension enforces path-list shape at install time via the * `extension.yaml` param regex. The kit has no install step, so the same