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
Original file line number Diff line number Diff line change
Expand Up @@ -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=…"
/>
<p className="text-muted-foreground text-xs">
Paste the <span className="font-mono">substack.sid</span> cookie from a logged-in
Substack session.
Paste the full <span className="font-mono">connect.sid=…</span> cookie string from a
logged-in Substack session.
</p>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@ import {
cookieFromCredential,
deleteSubstackCredential,
getSubstackCredentialMeta,
getSubstackPublicationUrl,
replaceSubstackCredential,
testSubstackCredentialMaterial,
} from '@/lib/user/deletion-queue/deletion-substack-credential';
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', () => {
Expand Down Expand Up @@ -52,20 +57,20 @@ 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),
});
});

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',
});
});
Expand All @@ -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' },
})
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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}`;

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.

bot: Legacy stored substack.sid cookies are not migrated to connect.sid.

Suggested fix: Recognize and rewrite the legacy cookie name in raw cookie material before returning it (including when it appears in a multi-cookie header), then add a regression test for substack.sid=.... Add or update a handler-level test that stores the legacy value and asserts the outbound deletion request sends connect.sid=.... This is needed because the deletion handler calls this normalizer for persisted credentials, while its existing test setup still stores substack.sid=test-cookie.

}

export function getSubstackPublicationUrl(): string {
return (
getEnvVariable('SUBSTACK_PUBLICATION_URL').trim().replace(/\/$/, '') ||
USER_DELETION_DEFAULT_SUBSTACK_PUBLICATION_URL
);
}

export async function testSubstackCredentialMaterial(
material: string
): Promise<SubstackCredentialTestResult> {
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) {
Expand Down