diff --git a/kits/delete-user-data/README.md b/kits/delete-user-data/README.md index 3cc0ec9e3..256b5d9ca 100644 --- a/kits/delete-user-data/README.md +++ b/kits/delete-user-data/README.md @@ -182,12 +182,14 @@ The extension deployed to the location you picked at install time. This kit sets no region, so its functions deploy to your codebase's default (`us-central1` unless you have changed it). -### Pub/Sub handlers are 2nd gen - -`handleSearch` and `handleDeletion` are now 2nd gen functions. `clearData` -stays 1st gen, because the Firebase Auth `user.delete` trigger has no 2nd gen -equivalent. This mainly matters if you have infrastructure or alerting keyed to -function generation. +### All functions are 2nd gen + +`clearData`, `handleSearch` and `handleDeletion` are all 2nd gen functions. Kits +do not support 1st gen endpoints, so `clearData` listens on the 2nd gen Firebase +Auth event `google.firebase.auth.user.v2.deleted` instead of the 1st gen +`user.delete` trigger. That event type is still beta in `firebase-functions`. +This mainly matters if you have infrastructure or alerting keyed to function +generation. ### Empty search fields no longer error diff --git a/kits/delete-user-data/src/identity-shim.d.ts b/kits/delete-user-data/src/identity-shim.d.ts new file mode 100644 index 000000000..25217d9b6 --- /dev/null +++ b/kits/delete-user-data/src/identity-shim.d.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { UserRecord } from "firebase-functions/v1/auth"; +import type { CloudEvent, CloudFunction } from "firebase-functions/v2"; + +/** + * `onUserDeleted` ships in firebase-functions 7.3.2 and emits a `gcfv2` + * endpoint, but is marked `@beta @internal` upstream and so is stripped from + * the published type declarations. It is the only 2nd gen Firebase Auth + * deletion trigger, and kits reject 1st gen endpoints. Keep `firebase-functions` + * pinned exactly: this signature carries no stability guarantee. Delete this + * file once the export is public. + */ +declare module "firebase-functions/v2/identity" { + export function onUserDeleted( + handler: (event: CloudEvent) => unknown + ): CloudFunction>; +} diff --git a/kits/delete-user-data/src/index.ts b/kits/delete-user-data/src/index.ts index bd6012502..5c39e70ff 100644 --- a/kits/delete-user-data/src/index.ts +++ b/kits/delete-user-data/src/index.ts @@ -17,9 +17,9 @@ import { PubSub } from "@google-cloud/pubsub"; import * as admin from "firebase-admin"; import { getFirestore } from "firebase-admin/firestore"; -import * as functionsV1 from "firebase-functions/v1"; import type { Role } from "firebase-functions/v2"; import { requiresRole } from "firebase-functions/v2"; +import { onUserDeleted } from "firebase-functions/v2/identity"; import { onMessagePublished } from "firebase-functions/v2/pubsub"; import { CONFIG_EXPRESSIONS, configFromEnv } from "./config"; import * as events from "./events"; @@ -81,8 +81,15 @@ function getContext(): HandlerContext { return ctx; } -export const clearData = functionsV1.auth.user().onDelete((user) => { - return handleClear(user.uid, getContext()); +export const clearData = onUserDeleted((event) => { + // The Auth event delivers no user record when the payload envelope is empty, + // so bail before getContext() rather than initialising the SDKs for nothing. + const uid = event.data?.uid; + if (!uid) { + logs.deletionEventMissingUid(event.id); + return; + } + return handleClear(uid, getContext()); }); export const handleSearch = onMessagePublished( diff --git a/kits/delete-user-data/src/logs.ts b/kits/delete-user-data/src/logs.ts index 4145c8ac0..cbacbc8dd 100644 --- a/kits/delete-user-data/src/logs.ts +++ b/kits/delete-user-data/src/logs.ts @@ -120,6 +120,12 @@ export const customFunctionError = (err: Error) => { logger.error(`Call to custom hook function threw an error`, err); }; +export const deletionEventMissingUid = (eventId: string) => { + logger.error( + `Auth deletion event ${eventId} carried no user id, so no data was deleted` + ); +}; + export function warnInvalidPaths(invalidPathCount: number, uid: string) { logger.warn( `Attempted to delete ${invalidPathCount} invalid paths for deleted user ${uid}` diff --git a/kits/delete-user-data/tests/index.test.ts b/kits/delete-user-data/tests/index.test.ts new file mode 100644 index 000000000..b031ab638 --- /dev/null +++ b/kits/delete-user-data/tests/index.test.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { beforeEach, describe, expect, test, vi } from "vitest"; + +vi.mock("../src/logs"); +vi.mock("../src/handlers", async (importOriginal) => ({ + ...(await importOriginal()), + handleClear: vi.fn(), +})); + +import { handleClear } from "../src/handlers"; +import { clearData } from "../src/index"; +import * as logs from "../src/logs"; + +function deletionEvent(data: unknown) { + return { + specversion: "1.0", + id: "event-id", + type: "google.firebase.auth.user.v2.deleted", + source: "//identitytoolkit.googleapis.com/projects/test-project", + time: "2026-01-01T00:00:00.000Z", + data, + } as any; +} + +describe("clearData", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + // Kits reject gen1 endpoints, so the deploy fails if this regresses. + test("registers a gen2 Firebase Auth deletion trigger", () => { + const endpoint = (clearData as any).__endpoint; + + expect(endpoint.platform).toBe("gcfv2"); + expect(endpoint.eventTrigger.eventType).toBe( + "google.firebase.auth.user.v2.deleted" + ); + }); + + test("logs and skips deletion when the event carries no user record", () => { + expect(() => clearData(deletionEvent(undefined))).not.toThrow(); + + expect(handleClear).not.toHaveBeenCalled(); + expect(logs.deletionEventMissingUid).toHaveBeenCalledWith("event-id"); + }); + + test("logs and skips deletion when the user record has no uid", () => { + expect(() => + clearData(deletionEvent({ email: "user@example.com" })) + ).not.toThrow(); + + expect(handleClear).not.toHaveBeenCalled(); + expect(logs.deletionEventMissingUid).toHaveBeenCalledWith("event-id"); + }); +});