@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/dist/index.mjs CHANGED
@@ -1033,6 +1033,12 @@ function transformProduct(product, currencyCode) {
1033
1033
  title: product.collection.title ?? "",
1034
1034
  handle: product.collection.handle ?? ""
1035
1035
  }] : void 0,
1036
+ categories: product.categories?.map((category) => ({
1037
+ id: category.id,
1038
+ name: category.name ?? "",
1039
+ handle: category.handle ?? "",
1040
+ parentCategoryId: category.parent_category_id ?? void 0
1041
+ })),
1036
1042
  productType: product.type?.value ?? void 0,
1037
1043
  vendor: void 0,
1038
1044
  // Medusa doesn't have vendor field
@@ -1805,6 +1811,7 @@ var MedusaAuthProvider = class {
1805
1811
  if (this.authConfig.method === "jwt") {
1806
1812
  await this.storageManager.setToken(token);
1807
1813
  }
1814
+ await this.linkCustomerChannel();
1808
1815
  const customer = await this.getCurrentCustomer();
1809
1816
  this.logger.info("Login successful", { customerId: customer?.id });
1810
1817
  return {
@@ -1812,6 +1819,7 @@ var MedusaAuthProvider = class {
1812
1819
  customer: customer ?? void 0
1813
1820
  };
1814
1821
  } catch (error) {
1822
+ if (error instanceof ProviderAPIError) throw error;
1815
1823
  this.logger.error("Login failed", error);
1816
1824
  throw new ProviderAPIError(
1817
1825
  `Authentication failed: ${error.message}`,
@@ -1823,16 +1831,14 @@ var MedusaAuthProvider = class {
1823
1831
  async register(data) {
1824
1832
  this.logger.debug("register", { email: data.email });
1825
1833
  try {
1826
- await this.sdk.auth.register("customer", "emailpass", {
1834
+ const registrationToken = await this.sdk.auth.register("customer", "emailpass", {
1827
1835
  email: data.email,
1828
1836
  password: data.password
1829
1837
  });
1830
- const { customer } = await this.sdk.store.customer.create({
1831
- email: data.email,
1832
- first_name: data.first_name,
1833
- last_name: data.last_name,
1834
- phone: data.phone
1835
- });
1838
+ if (this.authConfig.method === "jwt" && typeof registrationToken === "string") {
1839
+ await this.storageManager.setToken(registrationToken);
1840
+ }
1841
+ const linked = await this.createChannelLinkedCustomer(data);
1836
1842
  const loginResult = await this.sdk.auth.login("customer", "emailpass", {
1837
1843
  email: data.email,
1838
1844
  password: data.password
@@ -1858,12 +1864,20 @@ var MedusaAuthProvider = class {
1858
1864
  if (this.authConfig.method === "jwt") {
1859
1865
  await this.storageManager.setToken(token);
1860
1866
  }
1867
+ const customer = await this.getCurrentCustomer();
1868
+ if (!customer || customer.id !== linked.id) {
1869
+ throw new ProviderAPIError(
1870
+ "Registration created an auth identity without a linked customer.",
1871
+ "CUSTOMER_LINK_INCOMPLETE"
1872
+ );
1873
+ }
1861
1874
  this.logger.info("Registration successful", { customerId: customer.id });
1862
1875
  return {
1863
1876
  token: this.authConfig.method === "jwt" ? token : void 0,
1864
- customer: transformCustomer(customer, customer.email)
1877
+ customer
1865
1878
  };
1866
1879
  } catch (error) {
1880
+ if (error instanceof ProviderAPIError) throw error;
1867
1881
  this.logger.error("Registration failed", error);
1868
1882
  throw new ProviderAPIError(
1869
1883
  `Registration failed: ${error.message}`,
@@ -1872,6 +1886,79 @@ var MedusaAuthProvider = class {
1872
1886
  );
1873
1887
  }
1874
1888
  }
1889
+ /**
1890
+ * Create and channel-link a customer through `POST /store/thor/customers`.
1891
+ *
1892
+ * The request carries the registration token (read by the SDK from the
1893
+ * request-scoped custom storage) and the publishable key; the backend links
1894
+ * the customer to the publishable-key sales channel. A route that is
1895
+ * unavailable fails closed with a typed error — there is no silent native
1896
+ * fallback that would leave the customer unlinked.
1897
+ */
1898
+ async createChannelLinkedCustomer(data) {
1899
+ const response = await this.fetchChannelAccountRoute(
1900
+ "/store/thor/customers",
1901
+ {
1902
+ method: "POST",
1903
+ body: {
1904
+ email: data.email,
1905
+ first_name: data.first_name,
1906
+ last_name: data.last_name,
1907
+ phone: data.phone
1908
+ }
1909
+ }
1910
+ );
1911
+ const id = response?.customer?.id;
1912
+ if (typeof id !== "string" || id.length === 0) {
1913
+ throw new ProviderAPIError(
1914
+ "Channel-linked customer creation returned no customer id.",
1915
+ "CUSTOMER_LINK_INCOMPLETE"
1916
+ );
1917
+ }
1918
+ return { id };
1919
+ }
1920
+ /**
1921
+ * Link the authenticated customer to the publishable-key sales channel
1922
+ * through `POST /store/thor/customers/me/channels` (idempotent). Called after
1923
+ * login so history is channel-scoped for the current storefront.
1924
+ */
1925
+ async linkCustomerChannel() {
1926
+ const response = await this.fetchChannelAccountRoute("/store/thor/customers/me/channels", { method: "POST" });
1927
+ if (response?.linked === false && response?.reason !== "already_linked") {
1928
+ throw new ProviderAPIError(
1929
+ "Customer channel link did not complete.",
1930
+ "CUSTOMER_LINK_INCOMPLETE"
1931
+ );
1932
+ }
1933
+ }
1934
+ /**
1935
+ * Call a Thor channel account route through the SDK client, which attaches
1936
+ * the request-scoped customer/registration token and the publishable key. A
1937
+ * missing client or an unavailable route (404/405/501) fails closed with a
1938
+ * typed `CUSTOMER_LINK_UNAVAILABLE` error.
1939
+ */
1940
+ async fetchChannelAccountRoute(path, init) {
1941
+ const client = this.sdk.client;
1942
+ if (!client || typeof client.fetch !== "function") {
1943
+ throw new ProviderAPIError(
1944
+ `The channel account route ${path} is unavailable in this SDK.`,
1945
+ "CUSTOMER_LINK_UNAVAILABLE"
1946
+ );
1947
+ }
1948
+ try {
1949
+ return await client.fetch(path, init);
1950
+ } catch (error) {
1951
+ const status = error?.status || error?.statusCode;
1952
+ if (status === 404 || status === 405 || status === 501) {
1953
+ throw new ProviderAPIError(
1954
+ `The channel account route ${path} is unavailable (status ${status}).`,
1955
+ "CUSTOMER_LINK_UNAVAILABLE",
1956
+ status
1957
+ );
1958
+ }
1959
+ throw error;
1960
+ }
1961
+ }
1875
1962
  async logout() {
1876
1963
  this.logger.debug("logout");
1877
1964
  try {
@@ -2218,6 +2305,14 @@ async function getPaymentMethods(sdk, options) {
2218
2305
 
2219
2306
  // src/providers/medusa/index.ts
2220
2307
  var MEDUSA_CART_RETRIEVE_FIELDS = "+items,+items.variant,+items.variant.product,+items.variant.product.thumbnail,+items.variant.options,+items.variant.options.option,+items.total,+items.unit_price,+items.item_subtotal,+items.item_total,+shipping_methods,+shipping_methods.id,+shipping_methods.shipping_option_id";
2308
+ var CUSTOMER_ORDERS_DEFAULT_PAGE_SIZE = 10;
2309
+ var CUSTOMER_ORDERS_MAX_PAGE_SIZE = 100;
2310
+ function normalizeCustomerOrdersPage(options) {
2311
+ const page = Number.isInteger(options.page) && options.page > 0 ? options.page : 1;
2312
+ const requested = Number.isInteger(options.pageSize) && options.pageSize > 0 ? options.pageSize : CUSTOMER_ORDERS_DEFAULT_PAGE_SIZE;
2313
+ const pageSize = Math.min(requested, CUSTOMER_ORDERS_MAX_PAGE_SIZE);
2314
+ return { page, pageSize, limit: pageSize, offset: (page - 1) * pageSize };
2315
+ }
2221
2316
  var MEDUSA_ORDER_COMPLETION_FIELDS = "*items";
2222
2317
  var DEFAULT_CATALOG_PAGE_SIZE = 50;
2223
2318
  function resolveCatalogPageSize(configured) {
@@ -2581,7 +2676,7 @@ var MedusaProvider = class {
2581
2676
  }
2582
2677
  /** Fields needed to price a product variant for the current region. */
2583
2678
  buildProductFields(regionId) {
2584
- const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+tags";
2679
+ const baseFields = "+variants,+variants.inventory_quantity,+options,+images,+tags,+categories.*";
2585
2680
  const pricingFields = ",+variants.calculated_price,+variants.prices";
2586
2681
  return regionId ? baseFields + pricingFields : baseFields;
2587
2682
  }
@@ -4139,13 +4234,17 @@ var MedusaProvider = class {
4139
4234
  // Order Operations
4140
4235
  // ============================================
4141
4236
  /**
4142
- * Get all orders for a customer (storefront).
4237
+ * Get all orders for a customer (storefront) as an array (legacy surface).
4143
4238
  *
4144
- * Note: This retrieves orders from the storefront API (store.order.list),
4145
- * which returns orders only for the authenticated customer in the JWT.
4239
+ * Delegates to the channel-scoped paginated contract
4240
+ * ({@link customerOrdersPage}) and returns that window's orders. The backend
4241
+ * (`GET /store/thor/orders`) scopes by BOTH the authenticated customer and
4242
+ * the sales channel resolved from the publishable key, so cross-channel
4243
+ * orders are excluded by the backend before pagination and **no client-side
4244
+ * channel filter runs**.
4146
4245
  *
4147
- * @param options - Order query options (supports callbacks for custom backends)
4148
- * @returns Array of orders for the authenticated customer
4246
+ * @param options - Order query options (supports a custom fetcher)
4247
+ * @returns Array of orders for the authenticated customer on the store channel
4149
4248
  *
4150
4249
  * @example
4151
4250
  * ```typescript
@@ -4159,32 +4258,68 @@ var MedusaProvider = class {
4159
4258
  this.logger.debug("Using custom fetcher for orders");
4160
4259
  return customFetcher(options);
4161
4260
  }
4162
- this.logger.info("Getting orders for authenticated customer");
4261
+ const page = await this.customerOrdersPage({
4262
+ page: 1,
4263
+ pageSize: options.limit ?? CUSTOMER_ORDERS_MAX_PAGE_SIZE,
4264
+ salesChannelId
4265
+ });
4266
+ return page.orders;
4267
+ }
4268
+ /**
4269
+ * Channel-scoped, paginated order history for the authenticated customer.
4270
+ *
4271
+ * Calls the backend route `GET /store/thor/orders`, which requires the
4272
+ * customer session and scopes `order` by `customer_id` **and** the
4273
+ * `sales_channel_id` resolved from the publishable key, returning
4274
+ * `{ orders, count, limit, offset }` with the backend's real total computed
4275
+ * before pagination.
4276
+ *
4277
+ * The `salesChannelId` option is the store's explicitly configured channel.
4278
+ * When supplied, every returned order is asserted to belong to it; a
4279
+ * foreign-channel order (for example from a multi-channel publishable key that
4280
+ * silently resolved an arbitrary first id) fails closed with a typed error
4281
+ * rather than being presented as the customer's history.
4282
+ *
4283
+ * @param options - Bounded pagination window and the store's sales channel
4284
+ * @returns The backend's `{ orders, count, limit, offset }`
4285
+ */
4286
+ async customerOrdersPage(options = {}) {
4287
+ const { page, pageSize, limit, offset } = normalizeCustomerOrdersPage(options);
4288
+ const { salesChannelId } = options;
4289
+ this.logger.info("Getting channel-scoped customer orders", { page, pageSize });
4163
4290
  try {
4164
- const result = await this.withRetry(
4165
- async () => this.sdk.store.order.list({
4166
- // Medusa v2 JS-SDK automatically includes authorization header if JWT is present
4167
- fields: "+items,+items.variant,+items.variant.product,+shipping_address,+billing_address,+customer"
4291
+ const response = await this.withRetry(
4292
+ async () => this.sdk.client.fetch("/store/thor/orders", {
4293
+ method: "GET",
4294
+ query: { limit, offset }
4168
4295
  }),
4169
- "getOrders",
4296
+ "customerOrdersPage",
4170
4297
  {}
4171
4298
  );
4172
- const orders = result.orders ?? [];
4173
- this.logger.debug(`Found ${orders.length} orders for customer`);
4174
- const transformed = orders.map((order) => transformOrder(order));
4175
- const channelId = salesChannelId ?? process.env.NEXT_PUBLIC_SALES_CHANNEL_ID;
4176
- if (channelId) {
4177
- const scoped = filterByChannel(transformed, channelId);
4178
- this.logger.debug(
4179
- `Channel scoping: ${scoped.length}/${transformed.length} orders match channel ${channelId}`
4299
+ const rawOrders = response.orders ?? [];
4300
+ this.logger.debug(`Found ${rawOrders.length} channel-scoped orders for customer`);
4301
+ if (salesChannelId) {
4302
+ const foreign = rawOrders.find(
4303
+ (order) => typeof order?.sales_channel_id === "string" && order.sales_channel_id !== salesChannelId
4180
4304
  );
4181
- return scoped;
4305
+ if (foreign) {
4306
+ throw new ProviderAPIError(
4307
+ `Channel-scoped order history returned an order outside the store's sales channel (${salesChannelId}).`,
4308
+ "medusa"
4309
+ );
4310
+ }
4182
4311
  }
4183
- return transformed;
4312
+ return {
4313
+ orders: rawOrders.map((order) => transformOrder(order)),
4314
+ count: typeof response.count === "number" ? response.count : rawOrders.length,
4315
+ limit: typeof response.limit === "number" ? response.limit : limit,
4316
+ offset: typeof response.offset === "number" ? response.offset : offset
4317
+ };
4184
4318
  } catch (error) {
4319
+ if (error instanceof ProviderAPIError) throw error;
4185
4320
  const status = error?.status || error?.statusCode;
4186
4321
  const errorMessage = extractErrorMessage(error);
4187
- this.logger.error("Error getting orders", { error: errorMessage });
4322
+ this.logger.error("Error getting channel-scoped orders", { error: errorMessage });
4188
4323
  throw new ProviderAPIError(
4189
4324
  `Failed to fetch orders: ${errorMessage}`,
4190
4325
  "medusa",
@@ -9831,7 +9966,7 @@ var categoryFixtures = [
9831
9966
  description: "Complementary products for cart and bundle flows."
9832
9967
  }
9833
9968
  ];
9834
- var productFixtures = [
9969
+ var heroProductFixtures = [
9835
9970
  {
9836
9971
  id: "mock-product-aurora-jacket",
9837
9972
  handle: "aurora-jacket",
@@ -9963,6 +10098,119 @@ var productFixtures = [
9963
10098
  ]
9964
10099
  }
9965
10100
  ];
10101
+ var generatedBlueprints = [
10102
+ {
10103
+ categoryHandle: "jackets",
10104
+ noun: "Jacket",
10105
+ productType: "outerwear",
10106
+ vendor: "Thor Atelier",
10107
+ tags: ["outerwear", "layer"],
10108
+ variantName: "Size",
10109
+ variantValues: ["S", "M"],
10110
+ names: ["Alpine", "Boreal", "Cinder", "Drift", "Ember", "Fjord", "Harbor", "Meridian", "Nomad", "Tundra", "Vantage"],
10111
+ description: "A considered layering piece in a brushed weave.",
10112
+ basePrice: 118,
10113
+ baseHue: 210
10114
+ },
10115
+ {
10116
+ categoryHandle: "dresses",
10117
+ noun: "Dress",
10118
+ productType: "dress",
10119
+ vendor: "Thor Atelier",
10120
+ tags: ["dress", "editorial"],
10121
+ variantName: "Size",
10122
+ variantValues: ["S", "M"],
10123
+ names: ["Amber", "Camille", "Dahlia", "Elara", "Iris", "Lumen", "Nova", "Rosalind", "Vera"],
10124
+ description: "Cut from a fluid cloth for editorial styling.",
10125
+ basePrice: 142,
10126
+ baseHue: 330
10127
+ },
10128
+ {
10129
+ categoryHandle: "home",
10130
+ noun: "Lamp",
10131
+ productType: "decor",
10132
+ vendor: "Thor Home",
10133
+ tags: ["home", "decor", "interior"],
10134
+ variantName: "Finish",
10135
+ variantValues: ["Brass"],
10136
+ names: ["Atlas", "Copper", "Dune", "Onyx", "Quill", "Slate", "Terra", "Verdant"],
10137
+ description: "A calm sculptural accent for modern interiors.",
10138
+ basePrice: 74,
10139
+ baseHue: 40
10140
+ },
10141
+ {
10142
+ categoryHandle: "accessories",
10143
+ noun: "Bag",
10144
+ productType: "bag",
10145
+ vendor: "Thor Atelier",
10146
+ tags: ["accessory", "everyday"],
10147
+ variantName: "Color",
10148
+ variantValues: ["Juniper"],
10149
+ names: ["Cove", "Fable", "Juniper", "Kestrel", "Marlowe", "Piper", "Rune"],
10150
+ description: "An everyday carry with a textured finish.",
10151
+ basePrice: 68,
10152
+ baseHue: 280
10153
+ }
10154
+ ];
10155
+ var GENERATED_CREATED_AT_BASE = Date.parse("2025-12-30T00:00:00.000Z");
10156
+ var GENERATED_COLLECTION_POOL = ["new-arrivals", "best-sellers", "studio-essentials"];
10157
+ function buildGeneratedProductFixtures() {
10158
+ const fixtures = [];
10159
+ let globalIndex = 0;
10160
+ for (const blueprint of generatedBlueprints) {
10161
+ const noun = blueprint.noun.toLowerCase();
10162
+ blueprint.names.forEach((name, localIndex) => {
10163
+ const handle = `${name.toLowerCase()}-${noun}`;
10164
+ const title = `${name} ${blueprint.noun}`;
10165
+ const price = blueprint.basePrice + localIndex * 7 + localIndex % 3 * 0.5;
10166
+ const quantityAvailable = globalIndex * 5 % 17;
10167
+ const primaryCollection = GENERATED_COLLECTION_POOL[globalIndex % GENERATED_COLLECTION_POOL.length];
10168
+ const collectionHandles = [primaryCollection];
10169
+ if (globalIndex % 5 === 0) {
10170
+ const secondary = GENERATED_COLLECTION_POOL[(globalIndex + 1) % GENERATED_COLLECTION_POOL.length];
10171
+ if (secondary !== primaryCollection) {
10172
+ collectionHandles.push(secondary);
10173
+ }
10174
+ }
10175
+ const createdAt = new Date(GENERATED_CREATED_AT_BASE - globalIndex * 864e5).toISOString();
10176
+ const variants = blueprint.variantValues.map((value, variantIndex) => ({
10177
+ id: `mock-variant-${handle}-${value.toLowerCase()}`,
10178
+ title: value,
10179
+ price,
10180
+ quantityAvailable: Math.max(0, quantityAvailable - variantIndex),
10181
+ selectedOptions: [{ name: blueprint.variantName, value }]
10182
+ }));
10183
+ fixtures.push({
10184
+ id: `mock-product-${handle}`,
10185
+ handle,
10186
+ title,
10187
+ description: `${title} - ${blueprint.description}`,
10188
+ imageLabel: title,
10189
+ imageHue: (blueprint.baseHue + localIndex * 13) % 360,
10190
+ collectionHandles,
10191
+ categoryHandle: blueprint.categoryHandle,
10192
+ productType: blueprint.productType,
10193
+ vendor: blueprint.vendor,
10194
+ tags: [...blueprint.tags],
10195
+ options: [
10196
+ {
10197
+ id: `${handle}-${blueprint.variantName.toLowerCase()}`,
10198
+ name: blueprint.variantName,
10199
+ values: [...blueprint.variantValues]
10200
+ }
10201
+ ],
10202
+ variants,
10203
+ createdAt
10204
+ });
10205
+ globalIndex += 1;
10206
+ });
10207
+ }
10208
+ return fixtures;
10209
+ }
10210
+ var productFixtures = [
10211
+ ...heroProductFixtures,
10212
+ ...buildGeneratedProductFixtures()
10213
+ ];
9966
10214
  var stockLocationFixture = {
9967
10215
  id: "mock-stock-primary",
9968
10216
  name: "Mock Main Warehouse",
@@ -10079,6 +10327,20 @@ function buildProducts(currencyCode) {
10079
10327
  title: collection.title,
10080
10328
  handle: collection.handle
10081
10329
  })),
10330
+ categories: (() => {
10331
+ const category = categoryFixtures.find(
10332
+ (item) => item.handle === product.categoryHandle
10333
+ );
10334
+ if (!category) return void 0;
10335
+ return [
10336
+ {
10337
+ id: category.id,
10338
+ name: category.name,
10339
+ handle: category.handle,
10340
+ parentCategoryId: category.parentCategoryId
10341
+ }
10342
+ ];
10343
+ })(),
10082
10344
  productType: product.productType,
10083
10345
  vendor: product.vendor,
10084
10346
  tags: product.tags,