Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ INTERNAL_API_SECRET=changeme
# Access disable happens later in worker preflight; pending requests can be cancelled.
# Keep production values off preview deployments; rotate Cloud+CSA together.
SUPPORT_API_SECRET=changeme
# CSA Vercel Deployment Protection automation bypass (header only, never a query param).
# Required when CSA has Vercel Authentication enabled.
CSA_VERCEL_PROTECTION_BYPASS=
# Git token service persisted-authorization disconnect
GIT_TOKEN_SERVICE_API_URL=http://localhost:8802
# Optional public HTTPS origin for Bitbucket Code Reviewer webhooks.
Expand Down
1 change: 1 addition & 0 deletions ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Manage shared web env var additions and rotations with `pnpm web:env set <VARIAB
- `CUSTOMERIO_TRACK_BASE` - Optional Customer.io Track API base override for local user-deletion cleanup; defaults to `https://track.customer.io`. [SERVER]
- `SUBSTACK_PUBLICATION_URL` - Substack publication origin used by user-deletion subscriber cleanup; defaults to `https://blog.kilo.ai`. Must be `blog.kilo.ai` or a `*.substack.com` host. The Substack admin search URL is hardcoded to `https://kilocode.substack.com/publish/subscribers`, not this publication. [SERVER]
- `CSA_APP_BASE_URL` - CSA origin used by the Cloud deletion worker to call `POST /api/internal/cloud/users/gdpr-scrub`. Example: the production CSA app URL. [SERVER]
- `CSA_VERCEL_PROTECTION_BYPASS` - CSA Vercel Deployment Protection automation bypass. Cloud sends it as the `x-vercel-protection-bypass` header on Cloud → CSA `POST /api/internal/cloud/users/gdpr-scrub`, never as a query parameter. Required when CSA has Vercel Authentication enabled; without it Vercel returns 401 before the CSA route. Distinct from `SUPPORT_API_SECRET`. `[SECRET]`
- `SENTRY_ORG` - Sentry organization slug for source map uploads; used in `apps/web/next.config.mjs`. `[SECRET]`
- `SENTRY_PROJECT` - Sentry project slug for source map uploads; used in `apps/web/next.config.mjs`. `[SECRET]`
- `SENTRY_AUTH_TOKEN` - Sentry auth token for source map uploads; used in `apps/web/next.config.mjs`. `[SECRET]`
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/lib/user/deletion-queue/deletion-hints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ describe('deletionAttentionHint', () => {
expect(deletionAttentionHint('usage_prefix_progress_invalid')?.action).not.toMatch(/Mark done/);
expect(deletionAttentionHint('delete_ready_missing')?.action).toMatch(/delete-ready/);
expect(deletionAttentionHint('csa_unauthorized')?.title).toMatch(/CSA/);
expect(deletionAttentionHint('csa_unauthorized')?.action).toMatch(
/CSA_VERCEL_PROTECTION_BYPASS/
);
});

it('falls back for other HTTP statuses', () => {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/lib/user/deletion-queue/deletion-hints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,8 @@ const KNOWN_HINTS: Record<string, DeletionAttentionHint> = {
},
csa_unauthorized: {
title: 'CSA rejected the support-DB scrub',
action: 'Confirm SUPPORT_API_SECRET matches CSA KILO_SUPPORT_API_SECRET, then Retry.',
action:
'Confirm SUPPORT_API_SECRET matches CSA KILO_SUPPORT_API_SECRET and CSA_VERCEL_PROTECTION_BYPASS matches CSA Vercel automation bypass, then Retry.',
},
csa_blocked_email: {
title: 'CSA refused this email as a relay or internal target',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { handleCsaSupportDb } from '@/lib/user/deletion-queue/handlers/csa-suppo
describe('handleCsaSupportDb', () => {
const originalSecret = process.env.SUPPORT_API_SECRET;
const originalBase = process.env.CSA_APP_BASE_URL;
const originalBypass = process.env.CSA_VERCEL_PROTECTION_BYPASS;

beforeEach(() => {
process.env.SUPPORT_API_SECRET = 'shared-support-secret';
process.env.CSA_APP_BASE_URL = 'https://csa.example.test';
delete process.env.CSA_VERCEL_PROTECTION_BYPASS;
});

afterEach(() => {
Expand All @@ -18,6 +20,8 @@ describe('handleCsaSupportDb', () => {
else process.env.SUPPORT_API_SECRET = originalSecret;
if (originalBase === undefined) delete process.env.CSA_APP_BASE_URL;
else process.env.CSA_APP_BASE_URL = originalBase;
if (originalBypass === undefined) delete process.env.CSA_VERCEL_PROTECTION_BYPASS;
else process.env.CSA_VERCEL_PROTECTION_BYPASS = originalBypass;
});

it('succeeds when CSA returns updated', async () => {
Expand Down Expand Up @@ -76,6 +80,27 @@ describe('handleCsaSupportDb', () => {
const init = fetchSpy.mock.calls[0]?.[1] as RequestInit;
expect((init.headers as Record<string, string>)['X-Actor-Email']).toBeUndefined();
});

it('omits the Vercel protection bypass header when unset', async () => {
const fetchSpy = mockCsa({ status: 200, body: { status: 'updated' } });
await handleCsaSupportDb(handlerArgs());
const init = fetchSpy.mock.calls[0]?.[1] as RequestInit;
expect((init.headers as Record<string, string>)['x-vercel-protection-bypass']).toBeUndefined();
});

it('sends the Vercel protection bypass header when configured', async () => {
process.env.CSA_VERCEL_PROTECTION_BYPASS = 'csa-vercel-bypass';
const fetchSpy = mockCsa({ status: 200, body: { status: 'updated' } });
const outcome = await handleCsaSupportDb(handlerArgs());
expect(outcome).toEqual({ kind: 'succeeded' });
const init = fetchSpy.mock.calls[0]?.[1] as RequestInit;
expect((init.headers as Record<string, string>)['x-vercel-protection-bypass']).toBe(
'csa-vercel-bypass'
);
expect(fetchSpy.mock.calls[0]?.[0]).toBe(
'https://csa.example.test/api/internal/cloud/users/gdpr-scrub'
);
});
});

function handlerArgs(request: Partial<UserDeletionRequest> = {}): {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export const handleCsaSupportDb: DeletionHandler = async ({ request, context })
};
const actorEmail = request.requested_by_email?.trim();
if (actorEmail) headers['X-Actor-Email'] = actorEmail;
const protectionBypass = getEnvVariable('CSA_VERCEL_PROTECTION_BYPASS').trim();
if (protectionBypass) headers['x-vercel-protection-bypass'] = protectionBypass;

const result = await deletionFetch(context, `${baseUrl}/api/internal/cloud/users/gdpr-scrub`, {
method: 'POST',
Expand Down
Loading