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
150 changes: 150 additions & 0 deletions Node/ai-logic-sensitive-data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Firebase AI Logic: Redact Sensitive Data with Cloud Sensitive Data Protection (2nd Gen)

This sample demonstrates how to use **Firebase AI Logic triggers** (available in `firebase-functions` 7.4.0 and later) in conjunction with [**Cloud Sensitive Data Protection (DLP)**](https://cloud.google.com/security/products/sensitive-data-protection) to automatically inspect and redact sensitive data (PII) **before** sending prompts to the Gemini API and **after** receiving responses from the model.

## Introduction

[Firebase AI Logic triggers](https://firebase.google.com/docs/ai-logic/pre-and-post-request-scripts) allow you to run custom server-side scripts that intercept every `generateContent` request sent to the Gemini API via Firebase AI Logic — *without modifying your client application code*.

This sample implements two 2nd Gen Cloud Functions:

1. **`redactPrompt` (`beforeGenerateContent`)**:
Runs *before* a request reaches the Gemini API. It inspects all text parts in the prompt's `contents` (as well as `systemInstruction`, if present) for sensitive information (such as email addresses, phone numbers, credit card numbers, and US social security numbers) and replaces them with standard infoType placeholders (e.g., `[EMAIL_ADDRESS]`, `[PHONE_NUMBER]`). If sensitive data was found, the function returns the modified request. If no sensitive data was detected, it returns without modifying the request.

2. **`redactResponse` (`afterGenerateContent`)**:
Runs *after* the Gemini API returns a response and *before* that response is returned to the client app. It inspects the generated text in the response `candidates` for sensitive information and redacts any matching tokens.

### Request Flow

```
[Client App]
│ 1. generateContent request
[Firebase AI Logic Proxy]
│ 2. Triggers beforeGenerateContent
[Cloud Function: redactPrompt] ──(Cloud SDP / DLP API)──> Redacts sensitive data in prompt
│ 3. Sanitized prompt forwarded to Gemini API
[Gemini API (Google AI / Vertex AI)]
│ 4. Response generated
[Firebase AI Logic Proxy]
│ 5. Triggers afterGenerateContent
[Cloud Function: redactResponse] ──(Cloud SDP / DLP API)──> Redacts sensitive data in response
│ 6. Sanitized response returned to client
[Client App]
```

## Documentation Region Tags (`includecode`)

This sample provides region tags formatted for use with DevSite `includecode` in the Firebase AI Logic documentation ([pre-and-post-request-scripts.md](https://firebase.google.com/docs/ai-logic/pre-and-post-request-scripts)):

| Region Tag | Description |
| --- | --- |
| `ai_logic_imports` | Imports from `firebase-functions`, `firebase-functions/v2/ai`, and `@google-cloud/dlp`. |
| `ai_logic_redact_helper` | The `redactSensitiveData` utility function that calls `dlp.deidentifyContent(...)`. |
| `ai_logic_before_generate_content` | The `redactPrompt` pre-request trigger (`beforeGenerateContent`). |
| `ai_logic_after_generate_content` | The `redactResponse` post-request trigger (`afterGenerateContent`). |
| `ai_logic_pre_request` | Self-contained pre-request snippet including imports, DLP helper, and `beforeGenerateContent` trigger. |
| `ai_logic_sensitive_data_all` | Complete end-to-end sample containing imports, helper, and both triggers. |

To reference these snippets in Firebase DevSite documentation:

```markdown
<!-- Pre-request function snippet -->
{% includecode github_path="firebase/functions-samples/Node/ai-logic-sensitive-data/functions/index.js" region_tag="ai_logic_before_generate_content" adjust_indentation="auto" %}

<!-- Post-request function snippet -->
{% includecode github_path="firebase/functions-samples/Node/ai-logic-sensitive-data/functions/index.js" region_tag="ai_logic_after_generate_content" adjust_indentation="auto" %}
```

## Prerequisites

1. A **Firebase Project** upgraded to the [Blaze "pay-as-you-go" plan](https://firebase.google.com/pricing).
2. [Firebase CLI](https://firebase.google.com/docs/cli) installed and authenticated:
```bash
npm install -g firebase-tools
firebase login
```
3. Enable the **Cloud Sensitive Data Protection API** (formerly Cloud DLP):
```bash
gcloud services enable dlp.googleapis.com --project=YOUR_PROJECT_ID
```
4. **Grant IAM Permissions**:
- The default compute / Cloud Build service account requires the Cloud Build Service Account role:
```bash
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--member="serviceAccount:YOUR_PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
--role="roles/cloudbuild.builds.builder"
```
- The Cloud Functions runtime service account requires permissions to invoke Cloud Sensitive Data Protection:
```bash
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--member="serviceAccount:YOUR_PROJECT_ID@appspot.gserviceaccount.com" \
--role="roles/dlp.user"
```

## Setup & Deployment

1. Navigate to the sample directory:
```bash
cd Node/ai-logic-sensitive-data
```

2. Select your Firebase project:
```bash
firebase use --add
```

3. Install dependencies in the `functions` directory:
```bash
cd functions
npm install
```

4. Deploy the functions to Firebase:
```bash
firebase deploy --only functions
```

Once deployed, the `redactPrompt` and `redactResponse` functions are automatically registered with Firebase AI Logic in the `global` region as blocking triggers.

## Local Testing & Verification

The sample includes an automated unit test suite using Node.js's native test runner (`node:test`). The tests mock the Cloud Sensitive Data Protection client to verify:
- Sensitive infoTypes (emails, phone numbers, credit cards, SSNs) are properly detected and redacted.
- Non-sensitive text and non-text parts (e.g., inline images) are preserved untouched.
- Handlers return undefined when no sensitive data is detected, leaving requests and responses untouched.
- Proper handling of string and object `systemInstruction`.
- Both Gemini Developer API (`geminiV1Beta`) and Vertex AI (`vertexV1Beta1`) request structures.

To run the unit tests:

```bash
cd functions
npm test
```

## Customizing Redaction Rules

In [`functions/index.js`](functions/index.js), you can customize:

- **InfoTypes**: Add or remove infoTypes (such as `PERSON_NAME`, `PASSPORT`, `IP_ADDRESS`, `STREET_ADDRESS`) in `inspectConfig.infoTypes`.
- **Transformations**: Change `primitiveTransformation` from `replaceWithInfoTypeConfig: {}` to:
- `replaceConfig: { newValue: { stringValue: "[REDACTED]" } }` to use a generic replacement token.
- Character masking (e.g. masking all but the last 4 digits of a card number).
- Cryptographic hash or tokenization.
- **DLP Templates**: Use pre-configured Cloud SDP templates in Google Cloud Console with `inspectTemplateName` and `deidentifyTemplateName`.

## License

© Google, 2024. Licensed under an [Apache-2.0](../../LICENSE) license.
15 changes: 15 additions & 0 deletions Node/ai-logic-sensitive-data/firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"functions": {
"source": "functions",
"codebase": "ai-logic-sensitive-data",
"ignore": [
"node_modules",
".git",
"firebase-debug.log",
"firebase-debug.*.log"
],
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint"
]
}
}
3 changes: 3 additions & 0 deletions Node/ai-logic-sensitive-data/functions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Node.js dependency directory
node_modules/
tsconfig-compile.json
16 changes: 16 additions & 0 deletions Node/ai-logic-sensitive-data/functions/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default [
{
files: ["**/*.js", "**/*.cjs", "**/*.mjs"],
rules: {
"no-console": "off",
"no-unused-vars": "off",
"no-undef": "off",
"no-empty": "off",
"no-useless-escape": "off",
"no-prototype-builtins": "off",
"no-redeclare": "off",
"no-constant-condition": "off",
"no-case-declarations": "off"
}
Comment on lines +4 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Disabling a large number of important ESLint rules globally is not recommended as it can hide potential bugs and reduces code quality. While this might be acceptable for a simple sample, it's better to be more selective. For example, no-unused-vars and no-undef can catch critical errors.

Consider removing these global disables. If specific lines of code need to bypass a rule, use inline comments like // eslint-disable-next-line <rule-name> for those specific cases. This makes exceptions explicit and maintains a higher standard of code quality for the rest of the project.

}
];
172 changes: 172 additions & 0 deletions Node/ai-logic-sensitive-data/functions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/**
* Copyright 2024 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
*
* http://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.
*/

// [START ai_logic_sensitive_data_all]
// [START ai_logic_pre_request]
// [START ai_logic_imports]
import { logger } from "firebase-functions";
import {
beforeGenerateContent,
afterGenerateContent,
} from "firebase-functions/v2/ai";
import { DlpServiceClient } from "@google-cloud/dlp";
// [END ai_logic_imports]

// [START ai_logic_redact_helper]
let dlp = new DlpServiceClient();

/**
* Redacts sensitive data from a text string using Cloud Sensitive Data Protection (DLP).
*
* Inspects for basic sensitive infoTypes (email, phone number, credit card number, SSN)
* and replaces detected values with their infoType placeholder (e.g. "[EMAIL_ADDRESS]").
*
* @param {string} text - The raw input text string to inspect and redact.
* @returns {Promise<string>} The redacted text, or the original text if empty or unchanged.
*/
export async function redactSensitiveData(text) {
if (typeof text !== "string" || !text.trim()) {
return text;
}

const projectId = await dlp.getProjectId();
Comment on lines +29 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better performance, the projectId should be fetched only once during a cold start, not on every function invocation. The projectId for a given function deployment will not change. You can initialize the promise to get the project ID in the global scope and then await it inside the function.

let dlp = new DlpServiceClient();

// Initialize the projectId promise once in the global scope.
const projectIdPromise = dlp.getProjectId();

/**
 * Redacts sensitive data from a text string using Cloud Sensitive Data Protection (DLP).
 *
 * Inspects for basic sensitive infoTypes (email, phone number, credit card number, SSN)
 * and replaces detected values with their infoType placeholder (e.g. "[EMAIL_ADDRESS]").
 *
 * @param {string} text - The raw input text string to inspect and redact.
 * @returns {Promise<string>} The redacted text, or the original text if empty or unchanged.
 */
export async function redactSensitiveData(text) {
  if (typeof text !== "string" || !text.trim()) {
    return text;
  }

  const projectId = await projectIdPromise;


const [response] = await dlp.deidentifyContent({
parent: `projects/${projectId}/locations/global`,
item: { value: text },
inspectConfig: {
infoTypes: [
{ name: "EMAIL_ADDRESS" },
{ name: "PHONE_NUMBER" },
{ name: "CREDIT_CARD_NUMBER" },
{ name: "US_SOCIAL_SECURITY_NUMBER" },
],
},
deidentifyConfig: {
infoTypeTransformations: {
transformations: [
{
primitiveTransformation: {
replaceWithInfoTypeConfig: {},
},
},
],
},
},
});

return response?.item?.value ?? text;
}
// [END ai_logic_redact_helper]

// [START ai_logic_before_generate_content]
/**
* Pre-request trigger for Firebase AI Logic.
* Runs before each generateContent request is forwarded to the Gemini API.
* Intercepts the request and redacts sensitive data (PII) from prompt contents
* and system instructions before reaching the model.
*/
export const redactPrompt = beforeGenerateContent(async (event) => {
const request = event.data?.request;
if (!request) return;

let modified = false;

// Redact sensitive data from prompt contents
for (const content of request.contents ?? []) {
for (const part of content?.parts ?? []) {
if (part?.text) {
const redacted = await redactSensitiveData(part.text);
if (redacted !== part.text) {
part.text = redacted;
modified = true;
}
}
}
}

// Redact sensitive data from system instruction if present
if (typeof request.systemInstruction === "string") {
const redacted = await redactSensitiveData(request.systemInstruction);
if (redacted !== request.systemInstruction) {
request.systemInstruction = redacted;
modified = true;
}
} else if (request.systemInstruction) {
const parts = request.systemInstruction.parts ?? [request.systemInstruction];
for (const part of parts) {
if (part?.text) {
const redacted = await redactSensitiveData(part.text);
if (redacted !== part.text) {
part.text = redacted;
modified = true;
}
}
}
}

// Returning nothing (or undefined) leaves the request untouched.
// If modified, return the updated request object.
if (modified) {
logger.info("Redacted sensitive data from prompt request");
return request;
}
});
// [END ai_logic_before_generate_content]
// [END ai_logic_pre_request]

// [START ai_logic_after_generate_content]
/**
* Post-request trigger for Firebase AI Logic.
* Runs after the model generates a response and before it is returned to the client.
* Inspects all response candidates and redacts any sensitive data generated by the model.
*/
export const redactResponse = afterGenerateContent(async (event) => {
const response = event.data?.response;
if (!response?.candidates) return;

let modified = false;

// Redact sensitive data from model response candidates
for (const candidate of response.candidates) {
for (const part of candidate?.content?.parts ?? []) {
if (part?.text) {
const redacted = await redactSensitiveData(part.text);
if (redacted !== part.text) {
part.text = redacted;
modified = true;
}
}
}
}

// Returning nothing (or undefined) leaves the response untouched.
// If modified, return the updated response object.
if (modified) {
logger.info("Redacted sensitive data from model response candidates");
return response;
}
});
// [END ai_logic_after_generate_content]
// [END ai_logic_sensitive_data_all]

/**
* Allows injecting a custom DlpServiceClient for testing purposes.
* @internal
*/
export function setDlpClientForTesting(client) {
dlp = client;
}
Loading
Loading