@thorprovider/adapters 5.0.10 → 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.
Files changed (37) hide show
  1. package/AGENTS.md +1 -1
  2. package/dist/index.d.mts +376 -16
  3. package/dist/index.d.ts +376 -16
  4. package/dist/index.js +1687 -528
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1680 -530
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +3 -2
  9. package/src/core/errors.ts +31 -0
  10. package/src/factory/index.ts +16 -6
  11. package/src/factory/provider-types.test.ts +32 -0
  12. package/src/factory/provider-types.ts +176 -0
  13. package/src/index.ts +6 -4
  14. package/src/providers/index.ts +1 -1
  15. package/src/providers/medusa/auth.test.ts +148 -25
  16. package/src/providers/medusa/auth.ts +129 -16
  17. package/src/providers/medusa/available-filters.test.ts +280 -0
  18. package/src/providers/medusa/catalog-facet-support.test.ts +715 -0
  19. package/src/providers/medusa/catalog-query.test.ts +422 -0
  20. package/src/providers/medusa/catalog-sort.test.ts +175 -0
  21. package/src/providers/medusa/customer-orders.test.ts +142 -0
  22. package/src/providers/medusa/index.test.ts +480 -0
  23. package/src/providers/medusa/index.ts +1592 -538
  24. package/src/providers/medusa/live-smoke.test.ts +68 -0
  25. package/src/providers/medusa/transforms.test.ts +222 -0
  26. package/src/providers/medusa/transforms.ts +152 -66
  27. package/src/providers/mock/catalog-query.test.ts +491 -0
  28. package/src/providers/mock/index.test.ts +66 -0
  29. package/src/providers/mock/index.ts +43 -1
  30. package/src/providers/mock/mock.ts +513 -3
  31. package/src/types/completion.ts +57 -0
  32. package/src/types/index.ts +7 -0
  33. package/.turbo/turbo-build.log +0 -18
  34. package/.turbo/turbo-test.log +0 -372
  35. package/.turbo/turbo-type-check.log +0 -0
  36. package/dist/.tsbuildinfo +0 -1
  37. package/publish.log +0 -93
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@thorprovider/adapters",
3
- "version": "5.0.10",
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
+ "registry": "https://registry.npmjs.org",
6
7
  "access": "public"
7
8
  },
8
9
  "main": "dist/index.js",
@@ -44,4 +45,4 @@
44
45
  "directory": "packages/adapters"
45
46
  },
46
47
  "license": "MIT"
47
- }
48
+ }
@@ -37,6 +37,37 @@ export class ConfigurationError extends CommerceError {
37
37
  }
38
38
  }
39
39
 
40
+ /**
41
+ * Storefront validation error.
42
+ * Thrown when storefront configuration is invalid or missing.
43
+ *
44
+ * Moved here from `@thorprovider/types` so that package stays type-only.
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * throw new StorefrontConfigError(
49
+ * 'Medusa requires NEXT_PUBLIC_SALES_CHANNEL_ID',
50
+ * 'medusa',
51
+ * 'NEXT_PUBLIC_SALES_CHANNEL_ID',
52
+ * );
53
+ * ```
54
+ */
55
+ export class StorefrontConfigError extends Error {
56
+ public readonly platformType: string;
57
+ public readonly requiredEnvVar?: string;
58
+
59
+ constructor(
60
+ message: string,
61
+ platformType: string,
62
+ requiredEnvVar?: string,
63
+ ) {
64
+ super(message);
65
+ this.name = 'StorefrontConfigError';
66
+ this.platformType = platformType;
67
+ this.requiredEnvVar = requiredEnvVar;
68
+ }
69
+ }
70
+
40
71
  /**
41
72
  * Resource not found error
42
73
  */
@@ -2,12 +2,15 @@
2
2
  * @thorprovider/adapters v2.0
3
3
  * Provider Factory - Create commerce providers dynamically
4
4
  *
5
- * This factory is bound to SupportedProviderType from @thorprovider/types (L1).
5
+ * This factory owns the runtime provider registry (SupportedProviderType +
6
+ * metadata + guards) and is bound to the type-only ProviderTypeString union
7
+ * from @thorprovider/types (L1).
6
8
  * When adding a new provider:
7
- * 1. Update SupportedProviderType enum in packages/types/src/provider.ts
8
- * 2. Add new discriminated union case here
9
- * 3. Create packages/adapters/src/providers/[platform]/
10
- * 4. Implement CommerceProvider interface
9
+ * 1. Update ProviderTypeString in packages/types/src/provider.ts
10
+ * 2. Add the enum member + metadata in ./provider-types.ts
11
+ * 3. Add new discriminated union case here
12
+ * 4. Create packages/adapters/src/providers/[platform]/
13
+ * 5. Implement CommerceProvider interface
11
14
  */
12
15
 
13
16
  import type { CommerceProvider } from '../types';
@@ -22,8 +25,15 @@ import { MockProvider, type MockProviderConfig } from '../providers/mock';
22
25
  import { ProviderNotFoundError, ConfigurationError } from '../core/errors';
23
26
  import type { ProviderTypeString } from '@thorprovider/types';
24
27
 
28
+ export {
29
+ SupportedProviderType,
30
+ PROVIDER_METADATA,
31
+ isSupportedProviderType,
32
+ getProviderMetadata,
33
+ } from './provider-types';
34
+
25
35
  /**
26
- * Supported provider types (imported from @thorprovider/types L1)
36
+ * Supported provider types (type-only union owned by @thorprovider/types L1)
27
37
  * Kept for backward compatibility; prefer ProviderTypeString from @thorprovider/types
28
38
  */
29
39
  export type ProviderType = ProviderTypeString;
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ PROVIDER_METADATA,
4
+ SupportedProviderType,
5
+ getProviderMetadata,
6
+ isSupportedProviderType,
7
+ } from './provider-types'
8
+
9
+ describe('provider registry (moved from @thorprovider/types)', () => {
10
+ it('accepts supported provider strings and narrows the type', () => {
11
+ expect(isSupportedProviderType('medusa')).toBe(true)
12
+ expect(isSupportedProviderType('mock')).toBe(true)
13
+ expect(isSupportedProviderType('stelorder')).toBe(true)
14
+ })
15
+
16
+ it('rejects unsupported or non-string values', () => {
17
+ expect(isSupportedProviderType('bogus')).toBe(false)
18
+ expect(isSupportedProviderType(42)).toBe(false)
19
+ expect(isSupportedProviderType(undefined)).toBe(false)
20
+ })
21
+
22
+ it('has metadata for every enum member', () => {
23
+ for (const value of Object.values(SupportedProviderType)) {
24
+ expect(PROVIDER_METADATA[value]).toBeDefined()
25
+ }
26
+ })
27
+
28
+ it('returns metadata for a supported provider', () => {
29
+ expect(getProviderMetadata('medusa').name).toBe('Medusa JS')
30
+ expect(getProviderMetadata('mock').maxRetries).toBe(0)
31
+ })
32
+ })
@@ -0,0 +1,176 @@
1
+ /**
2
+ * @thorprovider/adapters — Provider registry (runtime)
3
+ *
4
+ * Runtime owner of the provider registry: the `SupportedProviderType` enum,
5
+ * its metadata, and the guards. The provider-type union contract
6
+ * (`ProviderTypeString`) stays type-only in `@thorprovider/types`.
7
+ *
8
+ * When adding a new provider:
9
+ * 1. Add the string to `ProviderTypeString` in packages/types/src/provider.ts.
10
+ * 2. Add the enum member + metadata entry here.
11
+ * 3. Create packages/adapters/src/providers/[platform]/.
12
+ */
13
+ import type { ProviderTypeString } from '@thorprovider/types';
14
+
15
+ /**
16
+ * All supported commerce platform providers.
17
+ * `mock` is the backend-optional runtime provider used for layout/design mode.
18
+ * `medusa` is fully implemented for real backend usage; others are placeholders for future expansion.
19
+ *
20
+ * @remarks Implementation status:
21
+ * - ✅ mock: Fixture-backed provider for backend-optional storefronts
22
+ * - ✅ medusa: Fully implemented
23
+ * - 🟡 stelorder: In progress (ERP adapter v1)
24
+ * - 🟡 shopify: Planned (Phase 2)
25
+ * - 🟡 bigcommerce: Planned (Phase 2)
26
+ * - 🟡 woocommerce: Planned (Phase 3)
27
+ * - 🟡 spree: Planned (Phase 3)
28
+ * - 🟡 magento: Planned (Phase 3)
29
+ */
30
+ export enum SupportedProviderType {
31
+ Mock = 'mock',
32
+ Medusa = 'medusa',
33
+ StelOrder = 'stelorder',
34
+ Shopify = 'shopify',
35
+ BigCommerce = 'bigcommerce',
36
+ WooCommerce = 'woocommerce',
37
+ Spree = 'spree',
38
+ Magento = 'magento',
39
+ }
40
+
41
+ /**
42
+ * Metadata about each provider.
43
+ * Useful for UI, error messages, validation, and documentation.
44
+ */
45
+ export const PROVIDER_METADATA: Record<
46
+ ProviderTypeString,
47
+ {
48
+ name: string;
49
+ description: string;
50
+ requiresStorefront: boolean;
51
+ requiredEnvVars: string[];
52
+ maxRetries: number;
53
+ }
54
+ > = {
55
+ mock: {
56
+ name: 'Mock',
57
+ description: 'Fixture-backed runtime provider for layout/design mode without a real backend',
58
+ requiresStorefront: false,
59
+ requiredEnvVars: [],
60
+ maxRetries: 0,
61
+ },
62
+ medusa: {
63
+ name: 'Medusa JS',
64
+ description: 'Medusa v2 commerce engine',
65
+ requiresStorefront: true,
66
+ requiredEnvVars: [
67
+ 'NEXT_PUBLIC_COMMERCE_API_URL',
68
+ 'NEXT_PUBLIC_COMMERCE_API_KEY',
69
+ 'NEXT_PUBLIC_SALES_CHANNEL_ID',
70
+ ],
71
+ maxRetries: 3,
72
+ },
73
+ stelorder: {
74
+ name: 'Stel Order',
75
+ description: 'Stel Order ERP and sales document API',
76
+ requiresStorefront: false,
77
+ requiredEnvVars: [
78
+ 'NEXT_PUBLIC_COMMERCE_API_URL',
79
+ 'NEXT_PUBLIC_COMMERCE_API_KEY',
80
+ ],
81
+ maxRetries: 2,
82
+ },
83
+ shopify: {
84
+ name: 'Shopify',
85
+ description: 'Shopify Storefront API (GraphQL)',
86
+ requiresStorefront: false,
87
+ requiredEnvVars: [
88
+ 'NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN',
89
+ 'NEXT_PUBLIC_SHOPIFY_STOREFRONT_TOKEN',
90
+ ],
91
+ maxRetries: 3,
92
+ },
93
+ bigcommerce: {
94
+ name: 'BigCommerce',
95
+ description: 'BigCommerce REST Storefront API',
96
+ requiresStorefront: false,
97
+ requiredEnvVars: [
98
+ 'BIGCOMMERCE_STORE_HASH',
99
+ 'BIGCOMMERCE_STOREFRONT_API_TOKEN',
100
+ 'BIGCOMMERCE_CHANNEL_ID',
101
+ ],
102
+ maxRetries: 3,
103
+ },
104
+ woocommerce: {
105
+ name: 'WooCommerce',
106
+ description: 'WooCommerce Store API + REST API v3',
107
+ requiresStorefront: false,
108
+ requiredEnvVars: [
109
+ 'WOOCOMMERCE_URL',
110
+ 'WOOCOMMERCE_CONSUMER_KEY',
111
+ 'WOOCOMMERCE_CONSUMER_SECRET',
112
+ ],
113
+ maxRetries: 3,
114
+ },
115
+ spree: {
116
+ name: 'Spree Commerce',
117
+ description: 'Spree API v2 Storefront (JSON:API)',
118
+ requiresStorefront: false,
119
+ requiredEnvVars: [
120
+ 'SPREE_API_URL',
121
+ ],
122
+ maxRetries: 3,
123
+ },
124
+ magento: {
125
+ name: 'Adobe Commerce (Magento)',
126
+ description: 'Adobe Commerce GraphQL + REST API',
127
+ requiresStorefront: true,
128
+ requiredEnvVars: [
129
+ 'MAGENTO_URL',
130
+ 'MAGENTO_STORE_CODE',
131
+ ],
132
+ maxRetries: 3,
133
+ },
134
+ };
135
+
136
+ /**
137
+ * Check if a string is a valid provider type.
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * if (isSupportedProviderType(process.env.COMMERCE_PROVIDER)) {
142
+ * // type is narrowed to ProviderTypeString
143
+ * }
144
+ * ```
145
+ */
146
+ export function isSupportedProviderType(
147
+ value: unknown
148
+ ): value is ProviderTypeString {
149
+ return (
150
+ typeof value === 'string' &&
151
+ (Object.values(SupportedProviderType) as string[]).includes(value)
152
+ );
153
+ }
154
+
155
+ /**
156
+ * Get metadata for a provider type.
157
+ * Useful for validation, error messages, and logging.
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * const meta = getProviderMetadata('medusa');
162
+ * console.log(`Using ${meta.name} with max ${meta.maxRetries} retries`);
163
+ * ```
164
+ */
165
+ export function getProviderMetadata(
166
+ type: ProviderTypeString
167
+ ): (typeof PROVIDER_METADATA)[ProviderTypeString] {
168
+ if (!isSupportedProviderType(type)) {
169
+ throw new Error(
170
+ `Unsupported provider: ${type}. Supported: ${Object.values(
171
+ SupportedProviderType
172
+ ).join(', ')}`
173
+ );
174
+ }
175
+ return PROVIDER_METADATA[type];
176
+ }
package/src/index.ts CHANGED
@@ -30,10 +30,6 @@ export type {
30
30
  DesignerHistoryEntry,
31
31
  } from '@thorprovider/types';
32
32
 
33
- export {
34
- StorefrontConfigError,
35
- } from '@thorprovider/types';
36
-
37
33
  // ============================================
38
34
  // Capabilities
39
35
  // ============================================
@@ -57,6 +53,7 @@ export {
57
53
  CommerceError,
58
54
  ProviderNotFoundError,
59
55
  ConfigurationError,
56
+ StorefrontConfigError,
60
57
  ResourceNotFoundError,
61
58
  ProviderAPIError,
62
59
  extractErrorMessage,
@@ -188,6 +185,10 @@ export type {
188
185
  export {
189
186
  createProvider,
190
187
  isProviderType,
188
+ SupportedProviderType,
189
+ PROVIDER_METADATA,
190
+ isSupportedProviderType,
191
+ getProviderMetadata,
191
192
  type ProviderType,
192
193
  type ProviderFactoryConfig,
193
194
  } from './factory';
@@ -201,6 +202,7 @@ export {
201
202
  StelOrderProvider,
202
203
  type StelOrderProviderConfig,
203
204
  MockProvider,
205
+ MOCK_SALES_CHANNEL_ID,
204
206
  type MockProviderConfig,
205
207
  ShopifyProvider,
206
208
  type ShopifyProviderConfig,
@@ -24,5 +24,5 @@ export type { SpreeProviderConfig } from './spree';
24
24
  export { MagentoProvider } from './magento';
25
25
  export type { MagentoProviderConfig } from './magento';
26
26
 
27
- export { MockProvider } from './mock';
27
+ export { MockProvider, MOCK_SALES_CHANNEL_ID } from './mock';
28
28
  export type { MockProviderConfig } from './mock';
@@ -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 () => {