Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
12 changes: 9 additions & 3 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ CALLBACK_TOKEN_SECRET=
# BYOK encryption key (generate: openssl rand -base64 32)
# Used for Bring-Your-Own-Key encryption of sensitive app data.
BYOK_ENCRYPTION_KEY=
# User-deletion HMAC key. Required for sign-in (identity fence).
# Generate: openssl rand -base64 32
USER_DELETION_AUDIT_HMAC_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
# User-deletion AES key (generate: openssl rand -base64 32)
USER_DELETION_ENCRYPTION_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
# ============================================================================
# REQUIRED - Security & Turnstile
# ============================================================================
Expand Down Expand Up @@ -103,9 +108,10 @@ STRIPE_KILOCLAW_2026_05_10_COMMIT_PRICE_ID=price_test_kiloclaw_2026_05_10_commit
# ============================================================================
# Internal API secret (generate: openssl rand -base64 32)
INTERNAL_API_SECRET=changeme
# Support service API (CSA GDPR web only). Distinct from INTERNAL_API_SECRET.
# Leak = enumerate any email + wipe any non-admin, non-bot, non-live-sub customer.
# Generate: openssl rand -base64 32
# Customer Support Automation internal API bearer token (generate: openssl rand -base64 32)
Comment thread
eshurakov marked this conversation as resolved.
# Leak = look up any email and enqueue deletion for non-admin, non-bot, non-live-sub customers.
# 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
# Git token service persisted-authorization disconnect
GIT_TOKEN_SERVICE_API_URL=http://localhost:8802
Expand Down
12 changes: 11 additions & 1 deletion ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ Manage shared web env var additions and rotations with `pnpm web:env set <VARIAB

- `NEXT_PUBLIC_POSTHOG_KEY` - PostHog public API key for client-side analytics; used in `apps/web/src/components/PostHogProvider.tsx`. [PUBLIC]
- `NEXT_PUBLIC_POSTHOG_DEBUG` - Enables PostHog debug logging; checked in `apps/web/src/components/PostHogProvider.tsx` and `apps/web/src/lib/stytch.ts`. [PUBLIC]
- `POSTHOG_PERSONAL_API_KEY` - PostHog personal API key used by user-deletion PostHog cleanup. `[SECRET]`
- `POSTHOG_ENVIRONMENT_ID` - PostHog project/environment ID used by user-deletion PostHog cleanup. [SERVER]
- `POSTHOG_HOST` - Optional PostHog API host override for user-deletion cleanup; defaults to `https://us.posthog.com`. [SERVER]
- `PYLON_API_KEY` - Pylon REST API key used by user-deletion contact and reply cleanup. `[SECRET]`
- `PYLON_HOST` - Optional Pylon API host override for local user-deletion cleanup; defaults to `https://api.usepylon.com`. [SERVER]
- `PYLON_FINAL_EMAIL_AUTHOR_USER_ID` - Optional Pylon staff user id used to match an already-posted deletion reply. [SERVER]
- `CUSTOMERIO_TRACK_BASE` - Optional Customer.io Track API base override for local user-deletion cleanup; defaults to `https://track.customer.io`. [SERVER]
Comment thread
eshurakov marked this conversation as resolved.
- `SUBSTACK_PUBLICATION_URL` - Substack publication origin used by user-deletion subscriber cleanup; no default. Required for the Substack deletion step. [SERVER]
- `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 Expand Up @@ -69,7 +77,7 @@ Manage shared web env var additions and rotations with `pnpm web:env set <VARIAB
- `STYTCH_PROJECT_SECRET` - Stytch project secret. `[SECRET]`
- `STYTCH_PUBLIC_TOKEN` - Stytch legacy public token alias used in some test fixtures. [PUBLIC]
- `INTERNAL_API_SECRET` - Shared secret for internal API calls between services; used in `apps/web/src/lib/kiloclaw/cli-runs.test.ts`, `kiloclaw-router.test.ts`, dev seed scripts, and other service routers. `[SECRET]`
- `SUPPORT_API_SECRET` - Dedicated shared secret for the customer-support-automation GDPR web app. Accepted only on `GET /api/internal/support/users` and `POST /api/internal/support/users/gdpr-removal` via `Authorization: Bearer`. Not a user JWT and not `INTERNAL_API_SECRET`. Leak = enumerate any email + wipe any non-admin, non-bot, non-live-subscription customer. Distinct values for Development / Staging / Production. Do not put the production value on Cloud preview deployments. Rotate by updating Cloud and CSA together. `[SECRET]`
- `SUPPORT_API_SECRET` - Shared bearer token for Customer Support Automation (CSA) internal API calls; used in `apps/web/src/app/api/internal/support/`. Leak can look up any email and enqueue deletion for non-admin, non-bot, non-live-subscription customers; access disable is deferred to worker preflight and pending requests can be cancelled. Keep production values off preview deployments; rotate Cloud and CSA together. `[SECRET]`
- `CALLBACK_TOKEN_SECRET` - Secret for signing callback tokens. Required for local development. `[SECRET]`
- `INTERNAL_SECRET` - Alias/fallback for `INTERNAL_API_SECRET`; used in KiloClaw E2E scripts (`services/kiloclaw/e2e/`). `[SECRET]`

Expand Down Expand Up @@ -188,6 +196,8 @@ Manage shared web env var additions and rotations with `pnpm web:env set <VARIAB
### Encryption & Secrets

- `BYOK_ENCRYPTION_KEY` - Base64 encryption key for Bring-Your-Own-Key encryption of sensitive app data. `[SECRET]`
- `USER_DELETION_AUDIT_HMAC_KEY` - Base64 32-byte HMAC key for user-deletion email hashes and audit subjects. `[SECRET]`
- `USER_DELETION_ENCRYPTION_KEY` - Base64 32-byte AES key for user-deletion effect checkpoints and provider credentials. `[SECRET]`
- `BYTEPLUS_CODING_PLAN_ACCESS_KEY_ID` - Server-only BytePlus access key ID for Coding Plan seat resolution and quota usage APIs. Optional at startup; install with `pnpm web:env set BYTEPLUS_CODING_PLAN_ACCESS_KEY_ID`. `[SECRET]`
- `BYTEPLUS_CODING_PLAN_SECRET_ACCESS_KEY` - Server-only BytePlus secret access key for Coding Plan seat resolution and quota usage APIs. Optional at startup; install with `pnpm web:env set BYTEPLUS_CODING_PLAN_SECRET_ACCESS_KEY`. `[SECRET]`
- `CREDIT_CATEGORIES_ENCRYPTION_KEY` - Encryption key for credit category labels/values. `[SECRET]`
Expand Down
4 changes: 3 additions & 1 deletion apps/web/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,15 @@ AUTO_TRIAGE_AUTH_TOKEN='mock-key'
AUTO_FIX_URL='mock-url'
AUTO_FIX_AUTH_TOKEN='mock-key'
INTERNAL_API_SECRET='mock-secret'
SUPPORT_API_SECRET='mock-support-api-secret'
CALLBACK_TOKEN_SECRET='mock-callback-token-secret'
CRON_SECRET='cron-secret'
ARTIFICIAL_ANALYSIS_API_KEY='mock-key'
QDRANT_HOST=
QDRANT_API_KEY=
BYOK_ENCRYPTION_KEY='qF/nUaiikTlbnhMXhU1p3mexS/zqmJtpRFtZ82dKotk='
USER_DELETION_AUDIT_HMAC_KEY='0zabvwKNmMl21UHwGC4jeIxe0AY8b/mwhm5UnIgESq8='
USER_DELETION_ENCRYPTION_KEY='Zd9KlErQK2lvGl6Oqx/IPOXWUS8mng1X+8sisxCBeGs='
SUPPORT_API_SECRET='test-support-secret'
SECURITY_AGENT_AUDIT_RELIABLE_COVERAGE_START=2026-06-17T00:00:00.000Z

# Test RSA public key for agent environment profile secrets (base64 encoded)
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/app/admin/api/backfills/blocked-at/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,17 @@ describe('blockedAtBackfillCandidates', () => {
const softDeleted = await insertTestUser({
blocked_reason: 'soft-deleted at 2026-01-15T12:00:00.000Z',
});
const deletionInProgress = await insertTestUser({
blocked_reason: 'deletion-in-progress at 2026-01-15T12:00:00.000Z',
});

const matches = await missingBlockedAtUserIds();

expect(matches).toContain(blocked.id);
expect(matches).not.toContain(unblocked.id);
expect(matches).not.toContain(alreadyBackfilled.id);
expect(matches).not.toContain(softDeleted.id);
expect(matches).not.toContain(deletionInProgress.id);
});
});

Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/app/admin/api/backfills/blocked-at/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { NextResponse } from 'next/server';
import { getUserFromAuth } from '@/lib/user/server';
import { db } from '@/lib/drizzle';
import { kilocode_users } from '@kilocode/db';
import { and, count, inArray, isNotNull, isNull, like, not, sql } from 'drizzle-orm';
import { goneOrDeletingBlockedReasonSql } from '@kilocode/db/user-soft-delete';
import { and, count, inArray, isNotNull, isNull, not, sql } from 'drizzle-orm';

export const blockedAtBackfillCandidates = and(
isNotNull(kilocode_users.blocked_reason),
isNull(kilocode_users.blocked_at),
not(like(kilocode_users.blocked_reason, 'soft-deleted at %'))
not(goneOrDeletingBlockedReasonSql(kilocode_users.blocked_reason))
);

export type BlockedAtCountsResponse = {
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/app/admin/api/backfills/email-domain/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ describe('emailDomainBackfillCandidates', () => {
expect(rows.map(r => r.id)).not.toContain(user.id);
});

it('excludes deletion-in-progress users so backfill does not race with deletion', async () => {
const user = await insertTestUser({
email_domain: null,
blocked_reason: 'deletion-in-progress at 2026-08-11T12:00:00.000Z',
});

const rows = await db
.select({ id: kilocode_users.id })
.from(kilocode_users)
.where(emailDomainBackfillCandidates);

expect(rows.map(r => r.id)).not.toContain(user.id);
});

it('still includes users blocked for other reasons', async () => {
const user = await insertTestUser({
email_domain: null,
Expand Down
12 changes: 6 additions & 6 deletions apps/web/src/app/admin/api/backfills/email-domain/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import { NextResponse } from 'next/server';
import { getUserFromAuth } from '@/lib/user/server';
import { db } from '@/lib/drizzle';
import { kilocode_users } from '@kilocode/db';
import { and, isNull, count, not, or, sql, like } from 'drizzle-orm';
import { goneOrDeletingBlockedReasonSql } from '@kilocode/db/user-soft-delete';
import { and, isNull, count, not, or, sql } from 'drizzle-orm';
import { extractEmailDomain } from '@/lib/email-domain';

// Exclude soft-deleted users: softDeleteUser anonymizes them to
// `deleted+<id>@deleted.invalid` and sets `blocked_reason` to a string starting
// with `soft-deleted at`. Filling email_domain for those rows would undo the
// GDPR nulling invariant.
// Skip soft-deleted and deletion-in-progress users. Soft-deleted emails are
// already anonymized; in-progress emails are still real, but backfill must not
// race with deletion.
export const emailDomainBackfillCandidates = and(
isNull(kilocode_users.email_domain),
or(
isNull(kilocode_users.blocked_reason),
not(like(kilocode_users.blocked_reason, 'soft-deleted at %'))
not(goneOrDeletingBlockedReasonSql(kilocode_users.blocked_reason))
)
);

Expand Down
Loading
Loading