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
6 changes: 5 additions & 1 deletion packages/store/src/cli/services/store/auth/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
25 changes: 4 additions & 21 deletions packages/store/src/cli/services/store/auth/session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,26 +212,6 @@ function readRawStoreSessionStorage(storage: LocalStorage<StoreSessionSchema>):
return (storage as unknown as {config?: {store?: RawStoreSessionStorage}}).config?.store ?? {}
}

function collectCurrentStoredStoreAppSessions(
storage: LocalStorage<StoreSessionSchema>,
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.
Expand All @@ -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
Expand Down
33 changes: 33 additions & 0 deletions packages/store/src/cli/services/store/auth/stored-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, unknown>>({cwd})

setStoredStoreAppSession(buildSession(), storage as any)

const rawStorage = (storage as unknown as {config: {store: Record<string, unknown>}}).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<Record<string, unknown>>({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<Record<string, unknown>>({cwd})
Expand Down
Loading