@thorprovider/adapters 4.0.0 → 4.1.0

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,10 +1,10 @@
1
1
  {
2
2
  "name": "@thorprovider/adapters",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
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",
7
- "access": "restricted"
7
+ "access": "public"
8
8
  },
9
9
  "main": "dist/index.js",
10
10
  "types": "dist/index.d.ts",
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "@medusajs/js-sdk": "2.11.3",
28
28
  "@medusajs/types": "2.11.3",
29
- "@thorprovider/types": "workspace:*",
29
+ "@thorprovider/types": "^3.0.0",
30
30
  "bcryptjs": "^3.0.3",
31
31
  "jose": "^6.2.2"
32
32
  },
@@ -0,0 +1,101 @@
1
+ import type { AuthConfig } from '@thorprovider/types';
2
+
3
+ type StoredValue = string | null;
4
+
5
+ function isBrowserStorageAvailable(): boolean {
6
+ return typeof window !== 'undefined';
7
+ }
8
+
9
+ function getStorage(config: AuthConfig): Storage | null {
10
+ if (!isBrowserStorageAvailable()) {
11
+ return null;
12
+ }
13
+
14
+ switch (config.storage) {
15
+ case 'localStorage':
16
+ return window.localStorage;
17
+ case 'sessionStorage':
18
+ return window.sessionStorage;
19
+ default:
20
+ return null;
21
+ }
22
+ }
23
+
24
+ export class AuthStorage {
25
+ private memory = new Map<string, string>();
26
+
27
+ constructor(
28
+ private readonly config: AuthConfig,
29
+ private readonly namespace: string,
30
+ ) {}
31
+
32
+ private key(suffix: string): string {
33
+ return `${this.namespace}:${suffix}`;
34
+ }
35
+
36
+ async getString(suffix: string): Promise<StoredValue> {
37
+ const key = this.key(suffix);
38
+
39
+ if (this.config.customStorage) {
40
+ return (await this.config.customStorage.getItem(key)) ?? null;
41
+ }
42
+
43
+ const browserStorage = getStorage(this.config);
44
+ if (browserStorage) {
45
+ return browserStorage.getItem(key);
46
+ }
47
+
48
+ return this.memory.get(key) ?? null;
49
+ }
50
+
51
+ async setString(suffix: string, value: string): Promise<void> {
52
+ const key = this.key(suffix);
53
+
54
+ if (this.config.customStorage) {
55
+ await this.config.customStorage.setItem(key, value);
56
+ return;
57
+ }
58
+
59
+ const browserStorage = getStorage(this.config);
60
+ if (browserStorage) {
61
+ browserStorage.setItem(key, value);
62
+ return;
63
+ }
64
+
65
+ this.memory.set(key, value);
66
+ }
67
+
68
+ async remove(suffix: string): Promise<void> {
69
+ const key = this.key(suffix);
70
+
71
+ if (this.config.customStorage) {
72
+ await this.config.customStorage.removeItem(key);
73
+ return;
74
+ }
75
+
76
+ const browserStorage = getStorage(this.config);
77
+ if (browserStorage) {
78
+ browserStorage.removeItem(key);
79
+ return;
80
+ }
81
+
82
+ this.memory.delete(key);
83
+ }
84
+
85
+ async getJson<T>(suffix: string): Promise<T | null> {
86
+ const value = await this.getString(suffix);
87
+ if (!value) {
88
+ return null;
89
+ }
90
+
91
+ try {
92
+ return JSON.parse(value) as T;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ async setJson(suffix: string, value: unknown): Promise<void> {
99
+ await this.setString(suffix, JSON.stringify(value));
100
+ }
101
+ }
@@ -0,0 +1,402 @@
1
+ import type {
2
+ Address,
3
+ AuthConfig,
4
+ AuthProvider,
5
+ AuthResponse,
6
+ CreateAddressData,
7
+ Customer,
8
+ LoginCredentials,
9
+ RegisterData,
10
+ UpdateCustomerData,
11
+ } from '@thorprovider/types';
12
+ import { createLogger } from '../../core/logger';
13
+ import { ProviderAPIError, extractErrorMessage } from '../../core/errors';
14
+ import { AuthStorage } from '../../lib/auth-storage';
15
+ import {
16
+ CUSTOMER_ACCESS_TOKEN_CREATE,
17
+ CUSTOMER_ACCESS_TOKEN_DELETE,
18
+ CUSTOMER_ADDRESS_CREATE,
19
+ CUSTOMER_ADDRESS_DELETE,
20
+ CUSTOMER_ADDRESS_UPDATE,
21
+ CUSTOMER_CREATE,
22
+ CUSTOMER_RECOVER,
23
+ CUSTOMER_RESET,
24
+ CUSTOMER_UPDATE,
25
+ } from './mutations';
26
+ import { GET_CUSTOMER } from './queries';
27
+ import { transformAddress, transformCustomer, transformCustomerAddresses } from './transforms';
28
+
29
+ type ShopifyGraphqlRequester = <T = any>(gql: string, variables?: Record<string, unknown>) => Promise<T>;
30
+
31
+ interface ShopifyCustomerToken {
32
+ accessToken: string;
33
+ expiresAt?: string;
34
+ }
35
+
36
+ interface ShopifyAuthDependencies {
37
+ query: ShopifyGraphqlRequester;
38
+ authConfig: AuthConfig;
39
+ debug?: boolean;
40
+ }
41
+
42
+ export class ShopifyAuthProvider implements AuthProvider {
43
+ private readonly logger;
44
+ private readonly storage: AuthStorage;
45
+
46
+ constructor(private readonly deps: ShopifyAuthDependencies) {
47
+ this.logger = createLogger('ShopifyAuthProvider', deps.debug);
48
+ this.storage = new AuthStorage(deps.authConfig, 'thor:shopify:auth');
49
+ }
50
+
51
+ private async readToken(): Promise<ShopifyCustomerToken | null> {
52
+ const token = await this.storage.getJson<ShopifyCustomerToken>('customer-access-token');
53
+ if (!token?.accessToken) {
54
+ return null;
55
+ }
56
+
57
+ if (token.expiresAt && new Date(token.expiresAt).getTime() <= Date.now()) {
58
+ await this.storage.remove('customer-access-token');
59
+ return null;
60
+ }
61
+
62
+ return token;
63
+ }
64
+
65
+ async getStoredToken(): Promise<ShopifyCustomerToken | null> {
66
+ return this.readToken();
67
+ }
68
+
69
+ private async writeToken(token: ShopifyCustomerToken | null): Promise<void> {
70
+ if (!token) {
71
+ await this.storage.remove('customer-access-token');
72
+ return;
73
+ }
74
+
75
+ await this.storage.setJson('customer-access-token', token);
76
+ }
77
+
78
+ private async requireToken(): Promise<ShopifyCustomerToken> {
79
+ const token = await this.readToken();
80
+ if (!token) {
81
+ throw new ProviderAPIError('Shopify customer access token is required.', 'shopify', 401);
82
+ }
83
+ return token;
84
+ }
85
+
86
+ private async fetchCurrentCustomer(): Promise<any | null> {
87
+ const token = await this.readToken();
88
+ if (!token) {
89
+ return null;
90
+ }
91
+
92
+ try {
93
+ const data = await this.deps.query<{ customer: any }>(GET_CUSTOMER, {
94
+ customerAccessToken: token.accessToken,
95
+ });
96
+
97
+ return data.customer ?? null;
98
+ } catch (error) {
99
+ await this.writeToken(null);
100
+ this.logger.warn('Failed to fetch current Shopify customer', extractErrorMessage(error));
101
+ return null;
102
+ }
103
+ }
104
+
105
+ async login(credentials: LoginCredentials): Promise<AuthResponse> {
106
+ try {
107
+ const data = await this.deps.query<{
108
+ customerAccessTokenCreate: {
109
+ customerAccessToken?: ShopifyCustomerToken;
110
+ customerUserErrors?: Array<{ message: string }>;
111
+ };
112
+ }>(CUSTOMER_ACCESS_TOKEN_CREATE, {
113
+ input: {
114
+ email: credentials.email,
115
+ password: credentials.password,
116
+ },
117
+ });
118
+
119
+ const result = data.customerAccessTokenCreate;
120
+ if (result.customerUserErrors?.length || !result.customerAccessToken?.accessToken) {
121
+ throw new ProviderAPIError(
122
+ result.customerUserErrors?.map((entry) => entry.message).join('; ') || 'Shopify login failed.',
123
+ 'shopify',
124
+ 400,
125
+ );
126
+ }
127
+
128
+ await this.writeToken(result.customerAccessToken);
129
+ const customer = await this.getCurrentCustomer();
130
+
131
+ return {
132
+ token: result.customerAccessToken.accessToken,
133
+ customer: customer ?? undefined,
134
+ };
135
+ } catch (error) {
136
+ throw new ProviderAPIError(`Shopify login failed: ${extractErrorMessage(error)}`, 'shopify', 400);
137
+ }
138
+ }
139
+
140
+ async register(data: RegisterData): Promise<AuthResponse> {
141
+ try {
142
+ const createResult = await this.deps.query<{
143
+ customerCreate: {
144
+ customer?: any;
145
+ customerUserErrors?: Array<{ message: string }>;
146
+ };
147
+ }>(CUSTOMER_CREATE, {
148
+ input: {
149
+ firstName: data.first_name,
150
+ lastName: data.last_name,
151
+ email: data.email,
152
+ phone: data.phone,
153
+ password: data.password,
154
+ },
155
+ });
156
+
157
+ const result = createResult.customerCreate;
158
+ if (result.customerUserErrors?.length) {
159
+ throw new ProviderAPIError(
160
+ result.customerUserErrors.map((entry) => entry.message).join('; '),
161
+ 'shopify',
162
+ 400,
163
+ );
164
+ }
165
+
166
+ return this.login({ email: data.email, password: data.password });
167
+ } catch (error) {
168
+ throw new ProviderAPIError(`Shopify registration failed: ${extractErrorMessage(error)}`, 'shopify', 400);
169
+ }
170
+ }
171
+
172
+ async logout(): Promise<void> {
173
+ try {
174
+ const token = await this.readToken();
175
+ if (token?.accessToken) {
176
+ await this.deps.query(CUSTOMER_ACCESS_TOKEN_DELETE, {
177
+ customerAccessToken: token.accessToken,
178
+ });
179
+ }
180
+ } catch (error) {
181
+ this.logger.warn('Shopify logout returned an error', extractErrorMessage(error));
182
+ } finally {
183
+ await this.writeToken(null);
184
+ }
185
+ }
186
+
187
+ async requestPasswordReset(email: string): Promise<void> {
188
+ try {
189
+ const data = await this.deps.query<{
190
+ customerRecover: {
191
+ customerUserErrors?: Array<{ message: string }>;
192
+ };
193
+ }>(CUSTOMER_RECOVER, { email });
194
+
195
+ if (data.customerRecover.customerUserErrors?.length) {
196
+ throw new ProviderAPIError(
197
+ data.customerRecover.customerUserErrors.map((entry) => entry.message).join('; '),
198
+ 'shopify',
199
+ 400,
200
+ );
201
+ }
202
+ } catch (error) {
203
+ throw new ProviderAPIError(`Shopify password reset request failed: ${extractErrorMessage(error)}`, 'shopify', 400);
204
+ }
205
+ }
206
+
207
+ async resetPassword(token: string, password: string, metadata?: Record<string, unknown>): Promise<void> {
208
+ const customerId = typeof metadata?.customerId === 'string' ? metadata.customerId : null;
209
+ if (!customerId) {
210
+ throw new ProviderAPIError('Shopify password reset requires metadata.customerId.', 'shopify', 400);
211
+ }
212
+
213
+ try {
214
+ const data = await this.deps.query<{
215
+ customerReset: {
216
+ customerAccessToken?: ShopifyCustomerToken;
217
+ customerUserErrors?: Array<{ message: string }>;
218
+ };
219
+ }>(CUSTOMER_RESET, {
220
+ id: customerId,
221
+ input: {
222
+ resetToken: token,
223
+ password,
224
+ },
225
+ });
226
+
227
+ if (data.customerReset.customerUserErrors?.length) {
228
+ throw new ProviderAPIError(
229
+ data.customerReset.customerUserErrors.map((entry) => entry.message).join('; '),
230
+ 'shopify',
231
+ 400,
232
+ );
233
+ }
234
+
235
+ if (data.customerReset.customerAccessToken?.accessToken) {
236
+ await this.writeToken(data.customerReset.customerAccessToken);
237
+ }
238
+ } catch (error) {
239
+ throw new ProviderAPIError(`Shopify password reset failed: ${extractErrorMessage(error)}`, 'shopify', 400);
240
+ }
241
+ }
242
+
243
+ async getCurrentCustomer(): Promise<Customer | null> {
244
+ const customer = await this.fetchCurrentCustomer();
245
+ return customer ? transformCustomer(customer) : null;
246
+ }
247
+
248
+ async updateCustomer(data: UpdateCustomerData): Promise<Customer> {
249
+ const token = await this.requireToken();
250
+
251
+ try {
252
+ const payload: Record<string, unknown> = {};
253
+ if (data.first_name !== undefined) payload.firstName = data.first_name;
254
+ if (data.last_name !== undefined) payload.lastName = data.last_name;
255
+ if (data.email !== undefined) payload.email = data.email;
256
+ if (data.phone !== undefined && data.phone !== null) payload.phone = data.phone;
257
+ if (data.password !== undefined) payload.password = data.password;
258
+
259
+ const response = await this.deps.query<{
260
+ customerUpdate: {
261
+ customer?: any;
262
+ customerAccessToken?: ShopifyCustomerToken;
263
+ customerUserErrors?: Array<{ message: string }>;
264
+ };
265
+ }>(CUSTOMER_UPDATE, {
266
+ customerAccessToken: token.accessToken,
267
+ customer: payload,
268
+ });
269
+
270
+ const result = response.customerUpdate;
271
+ if (result.customerUserErrors?.length || !result.customer) {
272
+ throw new ProviderAPIError(
273
+ result.customerUserErrors?.map((entry) => entry.message).join('; ') || 'Shopify customer update failed.',
274
+ 'shopify',
275
+ 400,
276
+ );
277
+ }
278
+
279
+ if (result.customerAccessToken?.accessToken) {
280
+ await this.writeToken(result.customerAccessToken);
281
+ }
282
+
283
+ return transformCustomer(result.customer);
284
+ } catch (error) {
285
+ throw new ProviderAPIError(`Shopify update customer failed: ${extractErrorMessage(error)}`, 'shopify', 400);
286
+ }
287
+ }
288
+
289
+ async isAuthenticated(): Promise<boolean> {
290
+ return (await this.getCurrentCustomer()) !== null;
291
+ }
292
+
293
+ getAuthToken(): string | null {
294
+ return null;
295
+ }
296
+
297
+ async getAddresses(): Promise<Address[]> {
298
+ const customer = await this.fetchCurrentCustomer();
299
+ return customer ? transformCustomerAddresses(customer) : [];
300
+ }
301
+
302
+ private toMailingAddressInput(data: Partial<CreateAddressData>): Record<string, unknown> {
303
+ return {
304
+ firstName: data.firstName,
305
+ lastName: data.lastName,
306
+ company: data.company,
307
+ address1: data.address1,
308
+ address2: data.address2,
309
+ city: data.city,
310
+ province: data.province,
311
+ country: data.countryCode,
312
+ zip: data.postalCode,
313
+ phone: data.phone,
314
+ };
315
+ }
316
+
317
+ async createAddress(data: CreateAddressData): Promise<Address> {
318
+ const token = await this.requireToken();
319
+
320
+ try {
321
+ const response = await this.deps.query<{
322
+ customerAddressCreate: {
323
+ customerAddress?: any;
324
+ customerUserErrors?: Array<{ message: string }>;
325
+ };
326
+ }>(CUSTOMER_ADDRESS_CREATE, {
327
+ customerAccessToken: token.accessToken,
328
+ address: this.toMailingAddressInput(data),
329
+ });
330
+
331
+ if (response.customerAddressCreate.customerUserErrors?.length || !response.customerAddressCreate.customerAddress) {
332
+ throw new ProviderAPIError(
333
+ response.customerAddressCreate.customerUserErrors?.map((entry) => entry.message).join('; ') || 'Shopify address create failed.',
334
+ 'shopify',
335
+ 400,
336
+ );
337
+ }
338
+
339
+ return transformAddress(response.customerAddressCreate.customerAddress);
340
+ } catch (error) {
341
+ throw new ProviderAPIError(`Shopify create address failed: ${extractErrorMessage(error)}`, 'shopify', 400);
342
+ }
343
+ }
344
+
345
+ async updateAddress(addressId: string, data: Partial<CreateAddressData>): Promise<Address> {
346
+ const token = await this.requireToken();
347
+
348
+ try {
349
+ const response = await this.deps.query<{
350
+ customerAddressUpdate: {
351
+ customerAddress?: any;
352
+ customerUserErrors?: Array<{ message: string }>;
353
+ };
354
+ }>(CUSTOMER_ADDRESS_UPDATE, {
355
+ customerAccessToken: token.accessToken,
356
+ id: addressId,
357
+ address: this.toMailingAddressInput(data),
358
+ });
359
+
360
+ if (response.customerAddressUpdate.customerUserErrors?.length || !response.customerAddressUpdate.customerAddress) {
361
+ throw new ProviderAPIError(
362
+ response.customerAddressUpdate.customerUserErrors?.map((entry) => entry.message).join('; ') || 'Shopify address update failed.',
363
+ 'shopify',
364
+ 400,
365
+ );
366
+ }
367
+
368
+ return transformAddress(response.customerAddressUpdate.customerAddress);
369
+ } catch (error) {
370
+ throw new ProviderAPIError(`Shopify update address failed: ${extractErrorMessage(error)}`, 'shopify', 400);
371
+ }
372
+ }
373
+
374
+ async deleteAddress(addressId: string): Promise<void> {
375
+ const token = await this.requireToken();
376
+
377
+ try {
378
+ const response = await this.deps.query<{
379
+ customerAddressDelete: {
380
+ customerUserErrors?: Array<{ message: string }>;
381
+ };
382
+ }>(CUSTOMER_ADDRESS_DELETE, {
383
+ customerAccessToken: token.accessToken,
384
+ id: addressId,
385
+ });
386
+
387
+ if (response.customerAddressDelete.customerUserErrors?.length) {
388
+ throw new ProviderAPIError(
389
+ response.customerAddressDelete.customerUserErrors.map((entry) => entry.message).join('; '),
390
+ 'shopify',
391
+ 400,
392
+ );
393
+ }
394
+ } catch (error) {
395
+ throw new ProviderAPIError(`Shopify delete address failed: ${extractErrorMessage(error)}`, 'shopify', 400);
396
+ }
397
+ }
398
+
399
+ async deleteCustomerAccount(): Promise<void> {
400
+ throw new ProviderAPIError('Shopify customer self-service deletion requires Admin API and is not supported.', 'shopify', 501);
401
+ }
402
+ }
@@ -100,3 +100,148 @@ export const CUSTOMER_ACCESS_TOKEN_CREATE = `
100
100
  }
101
101
  }
102
102
  `;
103
+
104
+ export const CUSTOMER_ACCESS_TOKEN_DELETE = `
105
+ mutation CustomerAccessTokenDelete($customerAccessToken: String!) {
106
+ customerAccessTokenDelete(customerAccessToken: $customerAccessToken) {
107
+ deletedAccessToken
108
+ deletedCustomerAccessTokenId
109
+ userErrors {
110
+ field
111
+ message
112
+ }
113
+ }
114
+ }
115
+ `;
116
+
117
+ export const CUSTOMER_CREATE = `
118
+ mutation CustomerCreate($input: CustomerCreateInput!) {
119
+ customerCreate(input: $input) {
120
+ customer {
121
+ id
122
+ email
123
+ firstName
124
+ lastName
125
+ }
126
+ customerUserErrors {
127
+ field
128
+ message
129
+ code
130
+ }
131
+ }
132
+ }
133
+ `;
134
+
135
+ export const CUSTOMER_UPDATE = `
136
+ mutation CustomerUpdate($customerAccessToken: String!, $customer: CustomerUpdateInput!) {
137
+ customerUpdate(customerAccessToken: $customerAccessToken, customer: $customer) {
138
+ customer {
139
+ id
140
+ email
141
+ firstName
142
+ lastName
143
+ phone
144
+ }
145
+ customerAccessToken {
146
+ accessToken
147
+ expiresAt
148
+ }
149
+ customerUserErrors {
150
+ field
151
+ message
152
+ code
153
+ }
154
+ }
155
+ }
156
+ `;
157
+
158
+ export const CUSTOMER_RECOVER = `
159
+ mutation CustomerRecover($email: String!) {
160
+ customerRecover(email: $email) {
161
+ customerUserErrors {
162
+ field
163
+ message
164
+ code
165
+ }
166
+ }
167
+ }
168
+ `;
169
+
170
+ export const CUSTOMER_RESET = `
171
+ mutation CustomerReset($id: ID!, $input: CustomerResetInput!) {
172
+ customerReset(id: $id, input: $input) {
173
+ customerAccessToken {
174
+ accessToken
175
+ expiresAt
176
+ }
177
+ customerUserErrors {
178
+ field
179
+ message
180
+ code
181
+ }
182
+ }
183
+ }
184
+ `;
185
+
186
+ export const CUSTOMER_ADDRESS_CREATE = `
187
+ mutation CustomerAddressCreate($customerAccessToken: String!, $address: MailingAddressInput!) {
188
+ customerAddressCreate(customerAccessToken: $customerAccessToken, address: $address) {
189
+ customerAddress {
190
+ id
191
+ firstName
192
+ lastName
193
+ address1
194
+ address2
195
+ city
196
+ province
197
+ country
198
+ zip
199
+ phone
200
+ company
201
+ }
202
+ customerUserErrors {
203
+ field
204
+ message
205
+ code
206
+ }
207
+ }
208
+ }
209
+ `;
210
+
211
+ export const CUSTOMER_ADDRESS_UPDATE = `
212
+ mutation CustomerAddressUpdate($customerAccessToken: String!, $id: ID!, $address: MailingAddressInput!) {
213
+ customerAddressUpdate(customerAccessToken: $customerAccessToken, id: $id, address: $address) {
214
+ customerAddress {
215
+ id
216
+ firstName
217
+ lastName
218
+ address1
219
+ address2
220
+ city
221
+ province
222
+ country
223
+ zip
224
+ phone
225
+ company
226
+ }
227
+ customerUserErrors {
228
+ field
229
+ message
230
+ code
231
+ }
232
+ }
233
+ }
234
+ `;
235
+
236
+ export const CUSTOMER_ADDRESS_DELETE = `
237
+ mutation CustomerAddressDelete($customerAccessToken: String!, $id: ID!) {
238
+ customerAddressDelete(customerAccessToken: $customerAccessToken, id: $id) {
239
+ deletedCustomerAddressId
240
+ customerUserErrors {
241
+ field
242
+ message
243
+ code
244
+ }
245
+ }
246
+ }
247
+ `;
@@ -171,7 +171,7 @@ export function transformCart(raw: any): Cart {
171
171
  // Customer Transforms
172
172
  // ============================================
173
173
 
174
- function transformAddress(raw: any): Address {
174
+ export function transformAddress(raw: any): Address {
175
175
  return {
176
176
  id: raw.id ?? '',
177
177
  firstName: raw.firstName ?? '',
@@ -198,6 +198,11 @@ export function transformCustomer(raw: any): Customer {
198
198
  };
199
199
  }
200
200
 
201
+ export function transformCustomerAddresses(raw: any): Address[] {
202
+ const edges = raw.addresses?.edges ?? [];
203
+ return edges.map((edge: any) => transformAddress(edge.node));
204
+ }
205
+
201
206
  // ============================================
202
207
  // Order Transforms
203
208
  // ============================================