@thorprovider/adapters 5.0.11 → 5.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thorprovider/adapters",
3
- "version": "5.0.11",
3
+ "version": "5.0.12",
4
4
  "description": "Framework-agnostic commerce adapters for Thor Commerce ecosystem - Multi-platform support with unified API",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",
@@ -45,4 +45,4 @@
45
45
  "directory": "packages/adapters"
46
46
  },
47
47
  "license": "MIT"
48
- }
48
+ }
@@ -1,5 +1,13 @@
1
1
  /**
2
2
  * @fileoverview Tests for MedusaAuthProvider
3
+ *
4
+ * Covers the channel-linked registration/login workflow
5
+ * (change: storefront-live-auth-orders-acceptance, task 2.4):
6
+ * - the registration token is persisted in the request-scoped MUTABLE custom
7
+ * storage before the customer-linking call, and that call carries it;
8
+ * - an auth identity without a linked customer is not treated as complete;
9
+ * - the channel-linking route being unavailable fails closed with a typed error;
10
+ * - login links the customer's channel idempotently.
3
11
  */
4
12
 
5
13
  import { describe, expect, it, vi } from 'vitest'
@@ -17,6 +25,9 @@ function makeSdkStub() {
17
25
  resetPassword: vi.fn(),
18
26
  updateProvider: vi.fn(),
19
27
  },
28
+ client: {
29
+ fetch: vi.fn(),
30
+ },
20
31
  store: {
21
32
  customer: {
22
33
  retrieve: vi.fn(),
@@ -31,20 +42,49 @@ function makeSdkStub() {
31
42
  } as unknown as Medusa
32
43
  }
33
44
 
34
- function makeStorageWithToken(token: string | null) {
45
+ /** Access the stubbed SDK client fetch as a vitest mock. */
46
+ function mockFetch(sdk: Medusa): ReturnType<typeof vi.fn> {
47
+ return sdk.client.fetch as unknown as ReturnType<typeof vi.fn>
48
+ }
49
+
50
+ /** A mutable request-scoped storage that records the order of its operations. */
51
+ function makeRecordingStorage(
52
+ token: string | null,
53
+ events: string[] = [],
54
+ ): {
55
+ storage: {
56
+ getItem: (key: string) => Promise<string | null>
57
+ setItem: (key: string, value: string) => Promise<void>
58
+ removeItem: () => Promise<void>
59
+ }
60
+ peek: () => string | null
61
+ } {
35
62
  let stored: string | null = token
36
63
  return {
37
- getItem: () => Promise.resolve(stored),
38
- setItem: (_key: string, value: string) => { stored = value; return Promise.resolve() },
39
- removeItem: () => { stored = null; return Promise.resolve() },
64
+ storage: {
65
+ getItem: () => Promise.resolve(stored),
66
+ setItem: (_key: string, value: string) => {
67
+ stored = value
68
+ events.push(`set:${value}`)
69
+ return Promise.resolve()
70
+ },
71
+ removeItem: () => {
72
+ stored = null
73
+ events.push('remove')
74
+ return Promise.resolve()
75
+ },
76
+ },
77
+ peek: () => stored,
40
78
  }
41
79
  }
42
80
 
43
- function makeAuthConfig(token: string | null = 'jwt-bearer-token') {
81
+ function makeAuthConfig(token: string | null = 'jwt-bearer-token', customStorage?: unknown) {
44
82
  return {
45
83
  method: 'jwt' as const,
46
84
  storage: 'memory' as const,
47
- customStorage: makeStorageWithToken(token),
85
+ customStorage:
86
+ (customStorage as ReturnType<typeof makeRecordingStorage>['storage']) ??
87
+ makeRecordingStorage(token).storage,
48
88
  }
49
89
  }
50
90
 
@@ -56,7 +96,10 @@ describe('MedusaAuthProvider', () => {
56
96
  it('stores jwt token and returns customer on login', async () => {
57
97
  const sdk = makeSdkStub()
58
98
  sdk.auth.login = vi.fn().mockResolvedValue('jwt-bearer-token')
59
- sdk.store.customer.retrieve = vi.fn().mockResolvedValue({ customer: { id: 'cus_1', email: 'test@example.com' } })
99
+ mockFetch(sdk).mockResolvedValue({ linked: true })
100
+ sdk.store.customer.retrieve = vi
101
+ .fn()
102
+ .mockResolvedValue({ customer: { id: 'cus_1', email: 'test@example.com' } })
60
103
 
61
104
  const provider = new MedusaAuthProvider(sdk, makeAuthConfig('jwt-bearer-token'), false)
62
105
 
@@ -70,13 +113,45 @@ describe('MedusaAuthProvider', () => {
70
113
  })
71
114
  })
72
115
 
116
+ it('links the customer channel idempotently on login', async () => {
117
+ const sdk = makeSdkStub()
118
+ sdk.auth.login = vi.fn().mockResolvedValue('jwt-bearer-token')
119
+ mockFetch(sdk).mockResolvedValue({ linked: false, reason: 'already_linked' })
120
+ sdk.store.customer.retrieve = vi
121
+ .fn()
122
+ .mockResolvedValue({ customer: { id: 'cus_1', email: 'test@example.com' } })
123
+
124
+ const provider = new MedusaAuthProvider(sdk, makeAuthConfig('jwt-bearer-token'), false)
125
+ const result = await provider.login({ email: 'test@example.com', password: 'secret' })
126
+
127
+ expect(sdk.client.fetch).toHaveBeenCalledWith(
128
+ '/store/thor/customers/me/channels',
129
+ expect.objectContaining({ method: 'POST' }),
130
+ )
131
+ expect(result.customer?.id).toBe('cus_1')
132
+ })
133
+
134
+ it('fails closed with a typed error when the channel link route is unavailable on login', async () => {
135
+ const sdk = makeSdkStub()
136
+ sdk.auth.login = vi.fn().mockResolvedValue('jwt-bearer-token')
137
+ mockFetch(sdk).mockRejectedValue(Object.assign(new Error('not found'), { status: 404 }))
138
+
139
+ const provider = new MedusaAuthProvider(sdk, makeAuthConfig('jwt-bearer-token'), false)
140
+
141
+ await expect(
142
+ provider.login({ email: 'test@example.com', password: 'secret' }),
143
+ ).rejects.toMatchObject({ provider: 'CUSTOMER_LINK_UNAVAILABLE' })
144
+ })
145
+
73
146
  it('throws ProviderAPIError with login code on auth failure', async () => {
74
147
  const sdk = makeSdkStub()
75
148
  sdk.auth.login = vi.fn().mockRejectedValue(new Error('bad credentials'))
76
149
 
77
150
  const provider = new MedusaAuthProvider(sdk, makeAuthConfig(), false)
78
151
 
79
- await expect(provider.login({ email: 'test@example.com', password: 'secret' })).rejects.toMatchObject({
152
+ await expect(
153
+ provider.login({ email: 'test@example.com', password: 'secret' }),
154
+ ).rejects.toMatchObject({
80
155
  name: 'ProviderAPIError',
81
156
  code: 'PROVIDER_API_ERROR',
82
157
  provider: 'LOGIN_FAILED',
@@ -86,7 +161,9 @@ describe('MedusaAuthProvider', () => {
86
161
 
87
162
  it('returns requiresAction when login requires verification (v2.16.0)', async () => {
88
163
  const sdk = makeSdkStub()
89
- sdk.auth.login = vi.fn().mockResolvedValue({ verification_required: true, token: 'verify-token' })
164
+ sdk.auth.login = vi
165
+ .fn()
166
+ .mockResolvedValue({ verification_required: true, token: 'verify-token' })
90
167
 
91
168
  const provider = new MedusaAuthProvider(sdk, makeAuthConfig(), false)
92
169
  const result = await provider.login({ email: 'test@example.com', password: 'secret' })
@@ -95,19 +172,22 @@ describe('MedusaAuthProvider', () => {
95
172
  expect(result.token).toBe('verify-token')
96
173
  })
97
174
 
98
- it('stores jwt token and returns customer on register (v2.16.0)', async () => {
175
+ it('persists the registration token before the channel-linking call and returns the linked customer', async () => {
99
176
  const sdk = makeSdkStub()
100
- sdk.auth.register = vi.fn().mockResolvedValue(undefined)
101
- sdk.auth.login = vi.fn().mockResolvedValue('jwt-bearer-token')
102
- sdk.store.customer.create = vi.fn().mockResolvedValue({
103
- customer: { id: 'cus_1', email: 'test@example.com', first_name: 'Test', last_name: 'User' },
104
- })
105
- sdk.store.customer.retrieve = vi.fn().mockResolvedValue({
106
- customer: { id: 'cus_1', email: 'test@example.com', first_name: 'Test', last_name: 'User' },
177
+ const events: string[] = []
178
+ const { storage } = makeRecordingStorage(null, events)
179
+ sdk.auth.register = vi.fn().mockResolvedValue('registration-token')
180
+ sdk.auth.login = vi.fn().mockResolvedValue('session-token')
181
+ mockFetch(sdk).mockImplementation(async (path: string) => {
182
+ events.push(`fetch:${path}`)
183
+ if (path === '/store/thor/customers') return { customer: { id: 'cus_1' } }
184
+ throw new Error(`unexpected ${path}`)
107
185
  })
186
+ sdk.store.customer.retrieve = vi
187
+ .fn()
188
+ .mockResolvedValue({ customer: { id: 'cus_1', email: 'test@example.com' } })
108
189
 
109
- const provider = new MedusaAuthProvider(sdk, makeAuthConfig('jwt-bearer-token'), false)
110
-
190
+ const provider = new MedusaAuthProvider(sdk, makeAuthConfig(null, storage), false)
111
191
  const result = await provider.register({
112
192
  email: 'test@example.com',
113
193
  password: 'secret',
@@ -119,12 +199,55 @@ describe('MedusaAuthProvider', () => {
119
199
  email: 'test@example.com',
120
200
  password: 'secret',
121
201
  })
122
- expect(sdk.auth.login).toHaveBeenCalledWith('customer', 'emailpass', {
123
- email: 'test@example.com',
124
- password: 'secret',
125
- })
126
- expect(result.token).toBe('jwt-bearer-token')
127
- expect(result.customer?.email).toBe('test@example.com')
202
+ // The registration token was written to the request-scoped mutable storage
203
+ // BEFORE the linking call carried it.
204
+ expect(events.indexOf('set:registration-token')).toBeGreaterThanOrEqual(0)
205
+ expect(events.indexOf('set:registration-token')).toBeLessThan(
206
+ events.indexOf('fetch:/store/thor/customers'),
207
+ )
208
+ // The linking call goes through the channel-linking route, not native create.
209
+ expect(sdk.client.fetch).toHaveBeenCalledWith(
210
+ '/store/thor/customers',
211
+ expect.objectContaining({ method: 'POST' }),
212
+ )
213
+ expect(sdk.store.customer.create).not.toHaveBeenCalled()
214
+ expect(result.token).toBe('session-token')
215
+ expect(result.customer?.id).toBe('cus_1')
216
+ })
217
+
218
+ it('does not treat an auth identity without a linked customer as complete', async () => {
219
+ const sdk = makeSdkStub()
220
+ sdk.auth.register = vi.fn().mockResolvedValue('registration-token')
221
+ // The linking route returns no customer id -> incomplete.
222
+ mockFetch(sdk).mockResolvedValue({ customer: {} })
223
+
224
+ const provider = new MedusaAuthProvider(sdk, makeAuthConfig(null), false)
225
+
226
+ await expect(
227
+ provider.register({
228
+ email: 'test@example.com',
229
+ password: 'secret',
230
+ first_name: 'Test',
231
+ last_name: 'User',
232
+ }),
233
+ ).rejects.toMatchObject({ provider: 'CUSTOMER_LINK_INCOMPLETE' })
234
+ })
235
+
236
+ it('fails closed with a typed unavailable error when the linking route is missing', async () => {
237
+ const sdk = makeSdkStub()
238
+ sdk.auth.register = vi.fn().mockResolvedValue('registration-token')
239
+ mockFetch(sdk).mockRejectedValue(Object.assign(new Error('not found'), { status: 404 }))
240
+
241
+ const provider = new MedusaAuthProvider(sdk, makeAuthConfig(null), false)
242
+
243
+ await expect(
244
+ provider.register({
245
+ email: 'test@example.com',
246
+ password: 'secret',
247
+ first_name: 'Test',
248
+ last_name: 'User',
249
+ }),
250
+ ).rejects.toMatchObject({ provider: 'CUSTOMER_LINK_UNAVAILABLE' })
128
251
  })
129
252
 
130
253
  it('requires email when resetting password', async () => {
@@ -179,7 +179,13 @@ export class MedusaAuthProvider implements AuthProvider {
179
179
  // Note: For session-based auth, the SDK automatically handles cookies
180
180
  // when auth.login() is called with auth.type='session' in SDK config
181
181
 
182
- // Step 3: Fetch customer data
182
+ // Step 3: Link the customer to the publishable-key sales channel
183
+ // (idempotent). The order-history route is channel-scoped, so login must
184
+ // ensure the customer's channel link before any history read; a route that
185
+ // is unavailable is surfaced as a typed error rather than silently skipped.
186
+ await this.linkCustomerChannel();
187
+
188
+ // Step 4: Fetch customer data
183
189
  const customer = await this.getCurrentCustomer();
184
190
 
185
191
  this.logger.info('Login successful', { customerId: customer?.id });
@@ -189,6 +195,8 @@ export class MedusaAuthProvider implements AuthProvider {
189
195
  customer: customer ?? undefined,
190
196
  };
191
197
  } catch (error: any) {
198
+ // A typed channel-account failure is truthful and must not be masked.
199
+ if (error instanceof ProviderAPIError) throw error;
192
200
  this.logger.error('Login failed', error);
193
201
  throw new ProviderAPIError(
194
202
  `Authentication failed: ${error.message}`,
@@ -202,24 +210,29 @@ export class MedusaAuthProvider implements AuthProvider {
202
210
  this.logger.debug('register', { email: data.email });
203
211
 
204
212
  try {
205
- // Step 1: Register auth identity with Medusa
206
- // v2.16.0: register no longer accepts returnVerification option.
207
- // The SDK auto-stores the registration JWT token for subsequent requests.
208
- await this.sdk.auth.register('customer', 'emailpass', {
213
+ // Step 1: Register auth identity with Medusa.
214
+ // v2.16.0: register no longer accepts returnVerification option. It
215
+ // returns the registration JWT token and writes it into the SDK's
216
+ // request-scoped token storage (the caller's mutable custom storage).
217
+ const registrationToken = await this.sdk.auth.register('customer', 'emailpass', {
209
218
  email: data.email,
210
219
  password: data.password,
211
220
  });
212
221
 
213
- // Step 2: Create customer profile
214
- // The registration token is automatically included in the header by the SDK.
215
- const { customer } = await this.sdk.store.customer.create({
216
- email: data.email,
217
- first_name: data.first_name,
218
- last_name: data.last_name,
219
- phone: data.phone,
220
- });
222
+ // Step 2: Persist the registration token explicitly through the
223
+ // request-scoped MUTABLE custom storage so the customer-linking call
224
+ // carries it. The storage's `setItem` genuinely mutates the per-request
225
+ // holder (it is not a no-op), so the linking call is authenticated.
226
+ if (this.authConfig.method === 'jwt' && typeof registrationToken === 'string') {
227
+ await this.storageManager.setToken(registrationToken);
228
+ }
229
+
230
+ // Step 3: Create + link the customer to the publishable-key sales channel
231
+ // through the channel-linking route. The native `POST /store/customers`
232
+ // is NOT used: it does not link a channel and would mis-scope history.
233
+ const linked = await this.createChannelLinkedCustomer(data);
221
234
 
222
- // Step 3: Login to obtain the actual session/token
235
+ // Step 4: Login to obtain the actual session/token.
223
236
  // v2.16.0: verification status is now returned by login, not register.
224
237
  const loginResult = await this.sdk.auth.login('customer', 'emailpass', {
225
238
  email: data.email,
@@ -250,19 +263,31 @@ export class MedusaAuthProvider implements AuthProvider {
250
263
 
251
264
  const token = loginResult;
252
265
 
253
- // Step 4: Store actual auth token based on auth method
266
+ // Step 5: Store actual auth token based on auth method
254
267
  if (this.authConfig.method === 'jwt') {
255
268
  await this.storageManager.setToken(token);
256
269
  }
257
270
  // Note: For session-based auth, the SDK automatically handles cookies
258
271
 
272
+ // Step 6: An auth identity alone is NOT a complete customer. Resolve the
273
+ // linked customer and require it to be the one just created/linked.
274
+ const customer = await this.getCurrentCustomer();
275
+ if (!customer || customer.id !== linked.id) {
276
+ throw new ProviderAPIError(
277
+ 'Registration created an auth identity without a linked customer.',
278
+ 'CUSTOMER_LINK_INCOMPLETE',
279
+ );
280
+ }
281
+
259
282
  this.logger.info('Registration successful', { customerId: customer.id });
260
283
 
261
284
  return {
262
285
  token: this.authConfig.method === 'jwt' ? token : undefined,
263
- customer: transformCustomer(customer, customer.email),
286
+ customer,
264
287
  };
265
288
  } catch (error: any) {
289
+ // A typed channel-account failure is truthful and must not be masked.
290
+ if (error instanceof ProviderAPIError) throw error;
266
291
  this.logger.error('Registration failed', error);
267
292
  throw new ProviderAPIError(
268
293
  `Registration failed: ${error.message}`,
@@ -271,6 +296,94 @@ export class MedusaAuthProvider implements AuthProvider {
271
296
  );
272
297
  }
273
298
  }
299
+
300
+ /**
301
+ * Create and channel-link a customer through `POST /store/thor/customers`.
302
+ *
303
+ * The request carries the registration token (read by the SDK from the
304
+ * request-scoped custom storage) and the publishable key; the backend links
305
+ * the customer to the publishable-key sales channel. A route that is
306
+ * unavailable fails closed with a typed error — there is no silent native
307
+ * fallback that would leave the customer unlinked.
308
+ */
309
+ private async createChannelLinkedCustomer(data: RegisterData): Promise<{ id: string }> {
310
+ const response = await this.fetchChannelAccountRoute<{ customer?: { id?: string } }>(
311
+ '/store/thor/customers',
312
+ {
313
+ method: 'POST',
314
+ body: {
315
+ email: data.email,
316
+ first_name: data.first_name,
317
+ last_name: data.last_name,
318
+ phone: data.phone,
319
+ },
320
+ },
321
+ );
322
+ const id = response?.customer?.id;
323
+ if (typeof id !== 'string' || id.length === 0) {
324
+ throw new ProviderAPIError(
325
+ 'Channel-linked customer creation returned no customer id.',
326
+ 'CUSTOMER_LINK_INCOMPLETE',
327
+ );
328
+ }
329
+ return { id };
330
+ }
331
+
332
+ /**
333
+ * Link the authenticated customer to the publishable-key sales channel
334
+ * through `POST /store/thor/customers/me/channels` (idempotent). Called after
335
+ * login so history is channel-scoped for the current storefront.
336
+ */
337
+ private async linkCustomerChannel(): Promise<void> {
338
+ const response = await this.fetchChannelAccountRoute<{
339
+ linked?: boolean;
340
+ reason?: string;
341
+ }>('/store/thor/customers/me/channels', { method: 'POST' });
342
+
343
+ // `linked:true` means the link was created now; `linked:false` is only valid
344
+ // when the link already existed. Any other shape is an incomplete link.
345
+ if (response?.linked === false && response?.reason !== 'already_linked') {
346
+ throw new ProviderAPIError(
347
+ 'Customer channel link did not complete.',
348
+ 'CUSTOMER_LINK_INCOMPLETE',
349
+ );
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Call a Thor channel account route through the SDK client, which attaches
355
+ * the request-scoped customer/registration token and the publishable key. A
356
+ * missing client or an unavailable route (404/405/501) fails closed with a
357
+ * typed `CUSTOMER_LINK_UNAVAILABLE` error.
358
+ */
359
+ private async fetchChannelAccountRoute<T>(
360
+ path: string,
361
+ init: { method: 'GET' | 'POST'; body?: unknown; query?: Record<string, unknown> },
362
+ ): Promise<T> {
363
+ const client = (this.sdk as unknown as { client?: { fetch?: unknown } }).client;
364
+ if (!client || typeof client.fetch !== 'function') {
365
+ throw new ProviderAPIError(
366
+ `The channel account route ${path} is unavailable in this SDK.`,
367
+ 'CUSTOMER_LINK_UNAVAILABLE',
368
+ );
369
+ }
370
+ try {
371
+ return await (
372
+ client.fetch as (p: string, i: typeof init) => Promise<T>
373
+ )(path, init);
374
+ } catch (error: any) {
375
+ const status = error?.status || error?.statusCode;
376
+ if (status === 404 || status === 405 || status === 501) {
377
+ throw new ProviderAPIError(
378
+ `The channel account route ${path} is unavailable (status ${status}).`,
379
+ 'CUSTOMER_LINK_UNAVAILABLE',
380
+ status,
381
+ );
382
+ }
383
+ throw error;
384
+ }
385
+ }
386
+
274
387
 
275
388
  async logout(): Promise<void> {
276
389
  this.logger.debug('logout');
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @fileoverview Channel-scoped customer orders tests (MedusaProvider).
3
+ *
4
+ * Covers the account channel-API selection
5
+ * (change: storefront-live-auth-orders-acceptance, tasks 2.1–2.2, 2.5):
6
+ * - the paginated call goes to `GET /store/thor/orders` (customer + publishable
7
+ * key sales channel) carrying the request's token, and no client-side
8
+ * `filterByChannel` runs;
9
+ * - `getOrders` keeps its legacy `Order[]` return via the same channel route;
10
+ * - a foreign-channel order (a multi-channel key silently scoped to a first id)
11
+ * fails closed with a typed error (AC-AUTH-4.5).
12
+ */
13
+
14
+ import { describe, expect, it } from 'vitest'
15
+ import { http, HttpResponse } from 'msw'
16
+ import { server } from '../stelorder/client.test-helpers'
17
+ import { MedusaProvider } from './index'
18
+
19
+ const BASE = 'https://medusa.example.com'
20
+ const PUB_KEY = 'pk_test_channel'
21
+ const STORE_CHANNEL = 'sc_store'
22
+ const TOKEN = 'request-scoped-customer-token'
23
+
24
+ /** A minimal Medusa order the transform understands. */
25
+ function medusaOrder(id: string, salesChannelId: string, displayId: number) {
26
+ return {
27
+ id,
28
+ display_id: displayId,
29
+ status: 'pending',
30
+ created_at: '2026-01-01T00:00:00.000Z',
31
+ updated_at: '2026-01-01T00:00:00.000Z',
32
+ email: 'buyer@example.com',
33
+ currency_code: 'usd',
34
+ total: 1000,
35
+ subtotal: 1000,
36
+ sales_channel_id: salesChannelId,
37
+ items: [],
38
+ }
39
+ }
40
+
41
+ /** A request-scoped custom storage holding exactly the request token. */
42
+ function makeProvider() {
43
+ return new MedusaProvider({
44
+ baseUrl: BASE,
45
+ publishableKey: PUB_KEY,
46
+ currencyCode: 'usd',
47
+ debug: false,
48
+ auth: {
49
+ method: 'jwt',
50
+ storage: 'memory',
51
+ customStorage: {
52
+ getItem: () => TOKEN,
53
+ setItem: () => {},
54
+ removeItem: () => {},
55
+ },
56
+ },
57
+ })
58
+ }
59
+
60
+ describe('MedusaProvider — channel-scoped customer orders', () => {
61
+ it('calls GET /store/thor/orders with the request token and paginated window', async () => {
62
+ const captured: { url: string; auth: string | null }[] = []
63
+ server.use(
64
+ http.get(`${BASE}/store/thor/orders`, ({ request }) => {
65
+ captured.push({
66
+ url: request.url,
67
+ auth: request.headers.get('authorization'),
68
+ })
69
+ return HttpResponse.json({
70
+ orders: [medusaOrder('order_1', STORE_CHANNEL, 1)],
71
+ count: 12,
72
+ limit: 5,
73
+ offset: 5,
74
+ })
75
+ }),
76
+ )
77
+
78
+ const provider = makeProvider()
79
+ const page = await provider.customerOrdersPage({
80
+ page: 2,
81
+ pageSize: 5,
82
+ salesChannelId: STORE_CHANNEL,
83
+ })
84
+
85
+ expect(captured).toHaveLength(1)
86
+ const params = new URL(captured[0].url).searchParams
87
+ expect(new URL(captured[0].url).pathname).toBe('/store/thor/orders')
88
+ expect(params.get('limit')).toBe('5')
89
+ expect(params.get('offset')).toBe('5')
90
+ // The SDK attaches the request-scoped token as a bearer header.
91
+ expect(captured[0].auth).toBe(`Bearer ${TOKEN}`)
92
+ // The backend's real count is passed through, not a page length.
93
+ expect(page.count).toBe(12)
94
+ expect(page.limit).toBe(5)
95
+ expect(page.offset).toBe(5)
96
+ expect(page.orders).toHaveLength(1)
97
+ })
98
+
99
+ it('keeps the legacy getOrders surface array-returning through the channel route', async () => {
100
+ let calledPath = ''
101
+ server.use(
102
+ http.get(`${BASE}/store/thor/orders`, ({ request }) => {
103
+ calledPath = new URL(request.url).pathname
104
+ return HttpResponse.json({
105
+ orders: [
106
+ medusaOrder('order_1', STORE_CHANNEL, 1),
107
+ medusaOrder('order_2', STORE_CHANNEL, 2),
108
+ ],
109
+ count: 2,
110
+ limit: 100,
111
+ offset: 0,
112
+ })
113
+ }),
114
+ )
115
+
116
+ const provider = makeProvider()
117
+ const orders = await provider.getOrders({ salesChannelId: STORE_CHANNEL })
118
+
119
+ expect(calledPath).toBe('/store/thor/orders')
120
+ expect(Array.isArray(orders)).toBe(true)
121
+ expect(orders).toHaveLength(2)
122
+ expect(orders.map((o) => o.id)).toEqual(['order_1', 'order_2'])
123
+ })
124
+
125
+ it('fails closed when the backend returns a foreign-channel order', async () => {
126
+ server.use(
127
+ http.get(`${BASE}/store/thor/orders`, () =>
128
+ HttpResponse.json({
129
+ orders: [medusaOrder('order_foreign', 'sc_other', 9)],
130
+ count: 1,
131
+ limit: 10,
132
+ offset: 0,
133
+ }),
134
+ ),
135
+ )
136
+
137
+ const provider = makeProvider()
138
+ await expect(
139
+ provider.customerOrdersPage({ page: 1, pageSize: 10, salesChannelId: STORE_CHANNEL }),
140
+ ).rejects.toMatchObject({ name: 'ProviderAPIError' })
141
+ })
142
+ })