Skip to content

Commit 48744ec

Browse files
committed
amend
1 parent 7f17ba0 commit 48744ec

4 files changed

Lines changed: 204 additions & 4 deletions

File tree

packages/store/src/cli/services/store/auth/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {outputContent, outputDebug, outputToken} from '@shopify/cli-kit/node/out
1313
import {AbortError} from '@shopify/cli-kit/node/error'
1414
import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn'
1515

16+
export {listStoredStoreAuthSummaries, type StoredStoreAuthSummary} from './stored-auth.js'
17+
1618
interface StoreAuthInput {
1719
store: string
1820
scopes: string

packages/store/src/cli/services/store/auth/session-store.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {storeAuthSessionKey} from './config.js'
1+
import {STORE_AUTH_APP_CLIENT_ID, storeAuthSessionKey} from './config.js'
22
import {LocalStorage} from '@shopify/cli-kit/node/local-storage'
33

44
export interface StoredStoreAppSession {
@@ -87,15 +87,18 @@ function sanitizeStoredStoreAppSession(value: unknown): StoredStoreAppSession |
8787
}
8888
}
8989

90-
function readStoredStoreAppSessionBucket(
90+
function sanitizeStoredStoreAppSessionBucket(
9191
store: string,
92+
storedBucket: unknown,
9293
storage: LocalStorage<StoreSessionSchema>,
9394
): StoredStoreAppSessionBucket | undefined {
94-
const key = storeAuthSessionKey(store)
95-
const storedBucket = storage.get(key)
9695
if (!storedBucket || typeof storedBucket !== 'object') return undefined
9796

9897
const {sessionsByUserId, currentUserId} = storedBucket as Partial<StoredStoreAppSessionBucket>
98+
const looksLikeBucket = sessionsByUserId !== undefined || currentUserId !== undefined
99+
if (!looksLikeBucket) return undefined
100+
101+
const key = storeAuthSessionKey(store)
99102
if (
100103
!sessionsByUserId ||
101104
typeof sessionsByUserId !== 'object' ||
@@ -131,6 +134,58 @@ function readStoredStoreAppSessionBucket(
131134
}
132135
}
133136

137+
function readStoredStoreAppSessionBucket(
138+
store: string,
139+
storage: LocalStorage<StoreSessionSchema>,
140+
): StoredStoreAppSessionBucket | undefined {
141+
return sanitizeStoredStoreAppSessionBucket(store, storage.get(storeAuthSessionKey(store)), storage)
142+
}
143+
144+
// `conf` persists dotted keys as nested objects. Store-auth callers should not
145+
// learn that layout directly; this helper keeps the current traversal private to
146+
// the persistence seam while higher-level code projects summaries instead.
147+
function readRawStoreSessionStorage(storage: LocalStorage<StoreSessionSchema>): Record<string, unknown> {
148+
return (storage as unknown as {config: {store: Record<string, unknown>}}).config.store ?? {}
149+
}
150+
151+
function collectCurrentStoredStoreAppSessions(
152+
storage: LocalStorage<StoreSessionSchema>,
153+
store: string,
154+
value: unknown,
155+
sessions: StoredStoreAppSession[],
156+
): void {
157+
if (!value || typeof value !== 'object' || Array.isArray(value)) return
158+
159+
const bucket = sanitizeStoredStoreAppSessionBucket(store, value, storage)
160+
if (bucket) {
161+
const session = bucket.sessionsByUserId[bucket.currentUserId]
162+
if (session) sessions.push(session)
163+
return
164+
}
165+
166+
for (const [childKey, childValue] of Object.entries(value as Record<string, unknown>)) {
167+
collectCurrentStoredStoreAppSessions(storage, `${store}.${childKey}`, childValue, sessions)
168+
}
169+
}
170+
171+
/**
172+
* Internal persistence helper for projecting the current session for every
173+
* store that has locally stored store auth.
174+
*/
175+
export function listCurrentStoredStoreAppSessions(
176+
storage: LocalStorage<StoreSessionSchema> = storeSessionStorage(),
177+
): StoredStoreAppSession[] {
178+
const sessions: StoredStoreAppSession[] = []
179+
const keyPrefix = `${STORE_AUTH_APP_CLIENT_ID}::`
180+
181+
for (const [key, value] of Object.entries(readRawStoreSessionStorage(storage))) {
182+
if (!key.startsWith(keyPrefix)) continue
183+
collectCurrentStoredStoreAppSessions(storage, key.slice(keyPrefix.length), value, sessions)
184+
}
185+
186+
return sessions
187+
}
188+
134189
export function getCurrentStoredStoreAppSession(
135190
store: string,
136191
storage: LocalStorage<StoreSessionSchema> = storeSessionStorage(),
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import {STORE_AUTH_APP_CLIENT_ID, storeAuthSessionKey} from './config.js'
2+
import {setStoredStoreAppSession, type StoredStoreAppSession} from './session-store.js'
3+
import {listStoredStoreAuthSummaries} from './stored-auth.js'
4+
import {inTemporaryDirectory} from '@shopify/cli-kit/node/fs'
5+
import {LocalStorage} from '@shopify/cli-kit/node/local-storage'
6+
import {describe, expect, test} from 'vitest'
7+
8+
function buildSession(overrides: Partial<StoredStoreAppSession> = {}): StoredStoreAppSession {
9+
return {
10+
store: 'shop.myshopify.com',
11+
clientId: STORE_AUTH_APP_CLIENT_ID,
12+
userId: '42',
13+
accessToken: 'token-1',
14+
refreshToken: 'refresh-token-1',
15+
scopes: ['read_products'],
16+
acquiredAt: '2026-03-27T00:00:00.000Z',
17+
...overrides,
18+
}
19+
}
20+
21+
describe('listStoredStoreAuthSummaries', () => {
22+
test('returns an empty array when no store auth is persisted', async () => {
23+
await inTemporaryDirectory((cwd) => {
24+
const storage = new LocalStorage<Record<string, unknown>>({cwd})
25+
26+
expect(listStoredStoreAuthSummaries(storage as any)).toEqual([])
27+
})
28+
})
29+
30+
test('returns one summary per store sorted by store using the current user session', async () => {
31+
await inTemporaryDirectory((cwd) => {
32+
const storage = new LocalStorage<Record<string, unknown>>({cwd})
33+
34+
setStoredStoreAppSession(buildSession({store: 'b-shop.myshopify.com'}), storage as any)
35+
setStoredStoreAppSession(
36+
buildSession({store: 'a-shop.myshopify.com', userId: '41', accessToken: 'token-41'}),
37+
storage as any,
38+
)
39+
setStoredStoreAppSession(
40+
buildSession({store: 'a-shop.myshopify.com', userId: '84', accessToken: 'token-84'}),
41+
storage as any,
42+
)
43+
44+
expect(listStoredStoreAuthSummaries(storage as any)).toEqual([
45+
{
46+
store: 'a-shop.myshopify.com',
47+
userId: '84',
48+
scopes: ['read_products'],
49+
acquiredAt: '2026-03-27T00:00:00.000Z',
50+
},
51+
{
52+
store: 'b-shop.myshopify.com',
53+
userId: '42',
54+
scopes: ['read_products'],
55+
acquiredAt: '2026-03-27T00:00:00.000Z',
56+
},
57+
])
58+
})
59+
})
60+
61+
test('projects associated user metadata without exposing tokens', async () => {
62+
await inTemporaryDirectory((cwd) => {
63+
const storage = new LocalStorage<Record<string, unknown>>({cwd})
64+
65+
setStoredStoreAppSession(
66+
buildSession({
67+
expiresAt: '2026-03-28T00:00:00.000Z',
68+
refreshTokenExpiresAt: '2026-04-28T00:00:00.000Z',
69+
associatedUser: {
70+
id: 42,
71+
email: 'merchant@example.com',
72+
firstName: 'Merchant',
73+
lastName: 'User',
74+
accountOwner: true,
75+
},
76+
}),
77+
storage as any,
78+
)
79+
80+
const [summary] = listStoredStoreAuthSummaries(storage as any)
81+
82+
expect(summary).toEqual({
83+
store: 'shop.myshopify.com',
84+
userId: '42',
85+
scopes: ['read_products'],
86+
acquiredAt: '2026-03-27T00:00:00.000Z',
87+
expiresAt: '2026-03-28T00:00:00.000Z',
88+
refreshTokenExpiresAt: '2026-04-28T00:00:00.000Z',
89+
associatedUser: {
90+
id: 42,
91+
email: 'merchant@example.com',
92+
firstName: 'Merchant',
93+
lastName: 'User',
94+
accountOwner: true,
95+
},
96+
})
97+
expect(summary).not.toHaveProperty('accessToken')
98+
expect(summary).not.toHaveProperty('refreshToken')
99+
})
100+
})
101+
102+
test('skips malformed persisted buckets while listing summaries', async () => {
103+
await inTemporaryDirectory((cwd) => {
104+
const storage = new LocalStorage<Record<string, unknown>>({cwd})
105+
storage.set(storeAuthSessionKey('broken-shop.myshopify.com'), {
106+
currentUserId: '42',
107+
sessionsByUserId: {
108+
'42': {userId: '42'},
109+
},
110+
})
111+
112+
expect(listStoredStoreAuthSummaries(storage as any)).toEqual([])
113+
expect(storage.get(storeAuthSessionKey('broken-shop.myshopify.com'))).toBeUndefined()
114+
})
115+
})
116+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {listCurrentStoredStoreAppSessions, type StoredStoreAppSession} from './session-store.js'
2+
3+
export interface StoredStoreAuthSummary {
4+
store: string
5+
userId: string
6+
scopes: string[]
7+
acquiredAt: string
8+
expiresAt?: string
9+
refreshTokenExpiresAt?: string
10+
associatedUser?: StoredStoreAppSession['associatedUser']
11+
}
12+
13+
type StoreSessionStorage = Parameters<typeof listCurrentStoredStoreAppSessions>[0]
14+
15+
export function listStoredStoreAuthSummaries(storage?: StoreSessionStorage): StoredStoreAuthSummary[] {
16+
return listCurrentStoredStoreAppSessions(storage)
17+
.map((session) => ({
18+
store: session.store,
19+
userId: session.userId,
20+
scopes: session.scopes,
21+
acquiredAt: session.acquiredAt,
22+
...(session.expiresAt ? {expiresAt: session.expiresAt} : {}),
23+
...(session.refreshTokenExpiresAt ? {refreshTokenExpiresAt: session.refreshTokenExpiresAt} : {}),
24+
...(session.associatedUser ? {associatedUser: session.associatedUser} : {}),
25+
}))
26+
.sort((left, right) => left.store.localeCompare(right.store))
27+
}

0 commit comments

Comments
 (0)