From 6bc6e165b28a184a2050912bb6ec3f6ccd972372 Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Fri, 19 Jun 2026 13:07:24 +0200 Subject: [PATCH] Escape store auth session keys Assisted-By: devx/a802aefd-9486-4d1e-bf5d-9541c093b99d --- .../src/cli/services/store/auth/config.ts | 6 +++- .../cli/services/store/auth/session-store.ts | 25 +++----------- .../services/store/auth/stored-auth.test.ts | 33 +++++++++++++++++++ 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/packages/store/src/cli/services/store/auth/config.ts b/packages/store/src/cli/services/store/auth/config.ts index 94ce1f1ea5f..46b72e6a7da 100644 --- a/packages/store/src/cli/services/store/auth/config.ts +++ b/packages/store/src/cli/services/store/auth/config.ts @@ -7,7 +7,11 @@ export function storeAuthRedirectUri(port: number): string { } export function storeAuthSessionKey(store: string): string { - return `${STORE_AUTH_APP_CLIENT_ID}::${store}` + return `${STORE_AUTH_APP_CLIENT_ID}::${escapeStoreAuthSessionKeySegment(store)}` +} + +function escapeStoreAuthSessionKeySegment(value: string): string { + return value.replace(/\./g, '\\.') } export function maskToken(token: string): string { diff --git a/packages/store/src/cli/services/store/auth/session-store.ts b/packages/store/src/cli/services/store/auth/session-store.ts index a039c08edf1..0acdb46232a 100644 --- a/packages/store/src/cli/services/store/auth/session-store.ts +++ b/packages/store/src/cli/services/store/auth/session-store.ts @@ -212,26 +212,6 @@ function readRawStoreSessionStorage(storage: LocalStorage): return (storage as unknown as {config?: {store?: RawStoreSessionStorage}}).config?.store ?? {} } -function collectCurrentStoredStoreAppSessions( - storage: LocalStorage, - store: string, - value: unknown, - sessions: StoredStoreAppSession[], -): void { - if (!value || typeof value !== 'object' || Array.isArray(value)) return - - const bucket = sanitizeStoredStoreAppSessionBucket(store, value, storage) - if (bucket) { - const session = bucket.sessionsByUserId[bucket.currentUserId] - if (session) sessions.push(session) - return - } - - for (const [childKey, childValue] of Object.entries(value as RawStoreSessionStorage)) { - collectCurrentStoredStoreAppSessions(storage, `${store}.${childKey}`, childValue, sessions) - } -} - /** * Internal persistence helper for projecting the current session for every * store that has locally stored store auth. @@ -244,7 +224,10 @@ export function listCurrentStoredStoreAppSessions( for (const [key, value] of Object.entries(readRawStoreSessionStorage(storage))) { if (!key.startsWith(keyPrefix)) continue - collectCurrentStoredStoreAppSessions(storage, key.slice(keyPrefix.length), value, sessions) + + const bucket = sanitizeStoredStoreAppSessionBucket(key.slice(keyPrefix.length), value, storage) + const session = bucket?.sessionsByUserId[bucket.currentUserId] + if (session) sessions.push(session) } return sessions diff --git a/packages/store/src/cli/services/store/auth/stored-auth.test.ts b/packages/store/src/cli/services/store/auth/stored-auth.test.ts index f1b8ba69671..c2d1a0f960f 100644 --- a/packages/store/src/cli/services/store/auth/stored-auth.test.ts +++ b/packages/store/src/cli/services/store/auth/stored-auth.test.ts @@ -66,6 +66,39 @@ describe('listStoredStoreAuthSummaries', () => { }) }) + test('persists dotted store domains as a single top-level key', async () => { + await inTemporaryDirectory((cwd) => { + const storage = new LocalStorage>({cwd}) + + setStoredStoreAppSession(buildSession(), storage as any) + + const rawStorage = (storage as unknown as {config: {store: Record}}).config.store + expect(Object.keys(rawStorage)).toContain(`${STORE_AUTH_APP_CLIENT_ID}::shop.myshopify.com`) + expect(listStoredStoreAuthSummaries(storage as any)).toEqual([ + { + store: 'shop.myshopify.com', + userId: '42', + scopes: ['read_products'], + acquiredAt: '2026-03-27T00:00:00.000Z', + }, + ]) + }) + }) + + test('ignores legacy nested buckets written with unescaped dotted domains', async () => { + await inTemporaryDirectory((cwd) => { + const storage = new LocalStorage>({cwd}) + storage.set(`${STORE_AUTH_APP_CLIENT_ID}::legacy.myshopify.com`, { + currentUserId: '42', + sessionsByUserId: { + '42': buildSession({store: 'legacy.myshopify.com'}), + }, + }) + + expect(listStoredStoreAuthSummaries(storage as any)).toEqual([]) + }) + }) + test('sorts stores alphabetically when auth timestamps match', async () => { await inTemporaryDirectory((cwd) => { const storage = new LocalStorage>({cwd})