Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions kits/delete-user-data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
32 changes: 32 additions & 0 deletions kits/delete-user-data/src/identity-shim.d.ts
Original file line number Diff line number Diff line change
@@ -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<UserRecord>) => unknown
): CloudFunction<CloudEvent<UserRecord>>;
}
13 changes: 10 additions & 3 deletions kits/delete-user-data/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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(
Expand Down
6 changes: 6 additions & 0 deletions kits/delete-user-data/src/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
Expand Down
70 changes: 70 additions & 0 deletions kits/delete-user-data/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof import("../src/handlers")>()),
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: "[email protected]" }))
).not.toThrow();

expect(handleClear).not.toHaveBeenCalled();
expect(logs.deletionEventMissingUid).toHaveBeenCalledWith("event-id");
});
});
Loading