From f97de3f1cc755a7c560defdeb69db72d770d368e Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Mon, 24 Aug 2026 11:38:30 +0200 Subject: [PATCH] fix(gdpr): use connect.sid for Substack credentials --- .../deletion-queue/DeletionQueueContent.tsx | 6 +-- .../deletion-substack-credential.test.ts | 42 +++++++++++++++---- .../deletion-substack-credential.ts | 17 +++++--- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/apps/web/src/app/admin/deletion-queue/DeletionQueueContent.tsx b/apps/web/src/app/admin/deletion-queue/DeletionQueueContent.tsx index f355c2b54a..8bbf98c246 100644 --- a/apps/web/src/app/admin/deletion-queue/DeletionQueueContent.tsx +++ b/apps/web/src/app/admin/deletion-queue/DeletionQueueContent.tsx @@ -679,11 +679,11 @@ function SubstackCredentialDialog({ value={material} onChange={event => setMaterial(event.target.value)} className="min-h-28 font-mono" - placeholder="substack.sid=…" + placeholder="connect.sid=…" />

- Paste the substack.sid cookie from a logged-in - Substack session. + Paste the full connect.sid=… cookie string from a + logged-in Substack session.

diff --git a/apps/web/src/lib/user/deletion-queue/deletion-substack-credential.test.ts b/apps/web/src/lib/user/deletion-queue/deletion-substack-credential.test.ts index b33495afcc..82f06f7e39 100644 --- a/apps/web/src/lib/user/deletion-queue/deletion-substack-credential.test.ts +++ b/apps/web/src/lib/user/deletion-queue/deletion-substack-credential.test.ts @@ -6,6 +6,7 @@ import { cookieFromCredential, deleteSubstackCredential, getSubstackCredentialMeta, + getSubstackPublicationUrl, replaceSubstackCredential, testSubstackCredentialMaterial, } from '@/lib/user/deletion-queue/deletion-substack-credential'; @@ -13,11 +14,15 @@ import { insertTestUser } from '@/tests/helpers/user.helper'; describe('cookieFromCredential', () => { it('builds a sid cookie from JSON sid material', () => { - expect(cookieFromCredential('{"sid":"abc123"}')).toBe('substack.sid=abc123'); + expect(cookieFromCredential('{"sid":"abc123"}')).toBe('connect.sid=abc123'); }); it('returns a raw cookie string unchanged', () => { - expect(cookieFromCredential('substack.sid=raw-cookie')).toBe('substack.sid=raw-cookie'); + expect(cookieFromCredential('connect.sid=raw-cookie')).toBe('connect.sid=raw-cookie'); + }); + + it('uses connect.sid for a bare session value', () => { + expect(cookieFromCredential('bare-session-value')).toBe('connect.sid=bare-session-value'); }); it('returns null for empty material', () => { @@ -52,12 +57,12 @@ describe('testSubstackCredentialMaterial', () => { ) ); - const result = await testSubstackCredentialMaterial('{"sid":"abc123"}'); + const result = await testSubstackCredentialMaterial('connect.sid=abc123'); expect(result).toEqual({ status: 'healthy', handle: 'jane', name: 'Jane Doe' }); expect(JSON.stringify(result)).not.toContain('secret@example.com'); expect(fetchSpy).toHaveBeenCalledWith(`${publication}/api/v1/user/profile/self`, { - headers: { Cookie: 'substack.sid=abc123', Accept: 'application/json' }, + headers: { Cookie: 'connect.sid=abc123', Accept: 'application/json' }, signal: expect.any(AbortSignal), }); }); @@ -65,7 +70,7 @@ describe('testSubstackCredentialMaterial', () => { it('returns expired on 401', async () => { jest.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('', { status: 401 })); - await expect(testSubstackCredentialMaterial('substack.sid=expired')).resolves.toEqual({ + await expect(testSubstackCredentialMaterial('connect.sid=expired')).resolves.toEqual({ status: 'expired', }); }); @@ -90,14 +95,37 @@ describe('testSubstackCredentialMaterial', () => { 1, `${publication}/api/v1/user/profile/self`, expect.objectContaining({ - headers: { Cookie: 'substack.sid=sid-only', Accept: 'application/json' }, + headers: { Cookie: 'connect.sid=sid-only', Accept: 'application/json' }, }) ); expect(fetchSpy).toHaveBeenNthCalledWith( 2, 'https://substack.com/api/v1/user/profile/self', expect.objectContaining({ - headers: { Cookie: 'substack.sid=sid-only', Accept: 'application/json' }, + headers: { Cookie: 'connect.sid=sid-only', Accept: 'application/json' }, + }) + ); + }); + + it('uses the blog.kilo.ai publication when no override is configured', async () => { + delete process.env.SUBSTACK_PUBLICATION_URL; + const fetchSpy = jest.spyOn(globalThis, 'fetch').mockResolvedValue( + new Response(JSON.stringify({ handle: 'default-publication', name: 'Default Publication' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ); + + await expect(testSubstackCredentialMaterial('connect.sid=abc123')).resolves.toEqual({ + status: 'healthy', + handle: 'default-publication', + name: 'Default Publication', + }); + expect(getSubstackPublicationUrl()).toBe('https://blog.kilo.ai'); + expect(fetchSpy).toHaveBeenCalledWith( + 'https://blog.kilo.ai/api/v1/user/profile/self', + expect.objectContaining({ + headers: { Cookie: 'connect.sid=abc123', Accept: 'application/json' }, }) ); }); diff --git a/apps/web/src/lib/user/deletion-queue/deletion-substack-credential.ts b/apps/web/src/lib/user/deletion-queue/deletion-substack-credential.ts index 41c967ae38..fe1dbe1865 100644 --- a/apps/web/src/lib/user/deletion-queue/deletion-substack-credential.ts +++ b/apps/web/src/lib/user/deletion-queue/deletion-substack-credential.ts @@ -3,6 +3,7 @@ import { user_deletion_provider_credentials } from '@kilocode/db/schema'; import { UserDeletionProviderScope } from '@kilocode/db/schema-types'; import { getEnvVariable } from '@/lib/dotenvx'; import { db } from '@/lib/drizzle'; +import { USER_DELETION_DEFAULT_SUBSTACK_PUBLICATION_URL } from '@/lib/user/deletion-queue/deletion-constants'; import { decryptDeletionCredential, DeletionCryptoError, @@ -24,7 +25,7 @@ export function cookieFromCredential(material: string): string | null { try { const parsed: unknown = JSON.parse(trimmed); if (isRecord(parsed) && typeof parsed.sid === 'string' && parsed.sid) { - return `substack.sid=${parsed.sid}`; + return `connect.sid=${parsed.sid}`; } if (isRecord(parsed) && typeof parsed.cookie === 'string' && parsed.cookie) { return parsed.cookie; @@ -34,16 +35,20 @@ export function cookieFromCredential(material: string): string | null { } return null; } - return trimmed.includes('=') ? trimmed : `substack.sid=${trimmed}`; + return trimmed.includes('=') ? trimmed : `connect.sid=${trimmed}`; +} + +export function getSubstackPublicationUrl(): string { + return ( + getEnvVariable('SUBSTACK_PUBLICATION_URL').trim().replace(/\/$/, '') || + USER_DELETION_DEFAULT_SUBSTACK_PUBLICATION_URL + ); } export async function testSubstackCredentialMaterial( material: string ): Promise { - const publication = getEnvVariable('SUBSTACK_PUBLICATION_URL').trim().replace(/\/$/, ''); - if (!publication) { - return { status: 'error', errorCode: 'configuration_missing' }; - } + const publication = getSubstackPublicationUrl(); const cookie = cookieFromCredential(material); if (!cookie) {