From d148d0766dcc87d8d87fc99cae032c355d9273b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 13:21:48 +0000 Subject: [PATCH] Remove 2-part cursor fallback in decodeCursor() decodeCursor() silently accepted a legacy 2-part (value|id) cursor shape and coerced it to createdAt, which meant a corrupt or truncated cursor could be accepted with a fabricated sort field instead of being rejected. encodeCursor() has only ever emitted 3-part cursors, so the fallback was unreachable by construction and contradicted the documented contract that malformed cursors throw StackQueryError. Fixes #152 --- packages/sqlite-shared/src/cursor.ts | 8 ++------ packages/sqlite-shared/tests/cursor.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/sqlite-shared/src/cursor.ts b/packages/sqlite-shared/src/cursor.ts index d94f161..9eb7632 100644 --- a/packages/sqlite-shared/src/cursor.ts +++ b/packages/sqlite-shared/src/cursor.ts @@ -26,14 +26,10 @@ export const decodeCursor = (cursor: string): DecodedCursor => { throw new StackQueryError(`Invalid cursor: malformed "${cursor}"`); } const parts = decoded.split('|'); - // Three parts: field|value|id. Two parts: value|id, implying createdAt. - const [field, value, id] = - parts.length === 3 - ? (parts as [string, string, string]) - : (['createdAt', ...parts] as [string, string, string]); - if (field === undefined || value === undefined || id === undefined) { + if (parts.length !== 3) { throw new StackQueryError(`Invalid cursor: malformed "${cursor}"`); } + const [field, value, id] = parts as [string, string, string]; if (!SORT_FIELDS.includes(field as SortField)) { throw new StackQueryError(`Invalid cursor: unknown sort field "${field}"`); } diff --git a/packages/sqlite-shared/tests/cursor.test.ts b/packages/sqlite-shared/tests/cursor.test.ts index b9d9aa7..26030a9 100644 --- a/packages/sqlite-shared/tests/cursor.test.ts +++ b/packages/sqlite-shared/tests/cursor.test.ts @@ -19,9 +19,9 @@ describe('encodeCursor / decodeCursor', () => { expect(decodeCursor(cursor)).toEqual({ field: 'createdAt', value: 12345, id: 'rec01' }); }); - test('accepts the legacy 2-part (value|id) format as createdAt', () => { + test('throws StackQueryError for a 2-part (value|id) cursor', () => { const legacy = btoa('12345|rec01'); - expect(decodeCursor(legacy)).toEqual({ field: 'createdAt', value: 12345, id: 'rec01' }); + expect(() => decodeCursor(legacy)).toThrow(StackQueryError); }); test('throws StackQueryError for non-base64 garbage', () => {