Skip to content
Open
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
103 changes: 8 additions & 95 deletions app/lib/privacy.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,8 @@
import { Stream } from "@/app/types/openapi";
import { getStore } from "./db";

export function redact(data: any): any {
if (typeof data !== 'object' || data === null) return data;
const redacted = { ...data };
const keysToRedact = ['signature', 'publickey', 'secret', 'password', 'token', 'email'];
for (const key in redacted) {
if (keysToRedact.includes(key.toLowerCase())) {
redacted[key] = '[REDACTED]';
} else if (typeof redacted[key] === 'object') {
redacted[key] = redact(redacted[key]);
}
}
return redacted;
}

/**
* Retention period: 7 years in milliseconds.
*/
const RETENTION_PERIOD_MS = 7 * 365 * 24 * 60 * 60 * 1000;

/**
* Mask an email address for non-admin roles.
*/
function maskEmail(email: string): string {
const [local, domain] = email.split("@");
if (!local || !domain) return "***";
return `${local[0]}***${local[local.length - 1]}@${domain}`;
}

/**
* Scrub PII from a stream object based on the requester's role.
* Role 'admin' sees everything; 'user' sees masked/redacted data.
*/
export function scrubStreamPII(stream: Stream, role: 'admin' | 'user' = 'user'): Stream {
if (role === 'admin') return stream;

return {
...stream,
email: stream.email ? maskEmail(stream.email) : undefined,
label: stream.label ? "[REDACTED]" : undefined,
memo: stream.memo ? "[REDACTED]" : undefined,
partnerId: stream.partnerId ? "[MASKED]" : undefined,
};
}

/**
* Check if a record is past its retention period.
*/
export function isPastRetention(createdAt: string): boolean {
const createdDate = new Date(createdAt).getTime();
const now = Date.now();
return now - createdDate > RETENTION_PERIOD_MS;
}

/**
* Process an account deletion request (DSR).
* Permanently scrubs PII from streams and deletes the user record.
* Legal/Audit requirement: Keep stream aggregates (IDs, amounts, status) for 7 years.
*/
export async function processDeletionRequest(walletAddress: string): Promise<{ requestId: string; status: string }> {
const { streamRepository } = getStore();
// 1. Scrub PII from all streams associated with this user
// In a real DB, we'd query by user_id. Here we check recipient and email.
const user = streamRepository.users.get(walletAddress);
const userEmail = user?.email;
const userName = user?.display_name;

for (const [id, stream] of streamRepository.streams.entries()) {
const isAssociated =
stream.email === userEmail ||
(userName && stream.recipient.includes(userName)) ||
stream.recipient.includes(walletAddress);

if (isAssociated) {
streamRepository.streams.set(id, {
...stream,
email: undefined,
label: undefined,
memo: undefined,
partnerId: undefined,
updatedAt: new Date().toISOString(),
});
}
}

// 2. Delete user record
streamRepository.users.delete(walletAddress);

return {
requestId: `dsr-${Math.random().toString(36).slice(2, 11)}`,
status: "processing",
};
}
export const redactPII = (input: any): any => {
if (typeof input !== 'string') return input;

return input
.replace(/([a-zA-Z0-9._-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/, '$1@****')
.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
.replace(/(.{4}).*(.{4})/, '$1****$2');
};