brainerce 1.22.0 → 1.23.11

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.js CHANGED
@@ -51,10 +51,12 @@ __export(index_exports, {
51
51
  getStockStatus: () => getStockStatus,
52
52
  getVariantOptions: () => getVariantOptions,
53
53
  getVariantPrice: () => getVariantPrice,
54
+ isAllowedPaymentUrl: () => isAllowedPaymentUrl,
54
55
  isCouponApplicableToProduct: () => isCouponApplicableToProduct,
55
56
  isHtmlDescription: () => isHtmlDescription,
56
57
  isWebhookEventType: () => isWebhookEventType,
57
58
  parseWebhookEvent: () => parseWebhookEvent,
59
+ safePaymentRedirect: () => safePaymentRedirect,
58
60
  verifyWebhook: () => verifyWebhook
59
61
  });
60
62
  module.exports = __toCommonJS(index_exports);
@@ -258,23 +260,31 @@ var BrainerceClient = class {
258
260
  // These methods store cart data in localStorage - NO API calls!
259
261
  // Use for guest users in vibe-coded sites
260
262
  this.LOCAL_CART_KEY = "brainerce_cart";
261
- if (!options.apiKey && !options.storeId && !options.connectionId) {
262
- throw new Error("BrainerceClient: either connectionId, apiKey, or storeId is required");
263
+ const resolvedSalesChannelId = options.salesChannelId ?? options.connectionId;
264
+ if (!options.apiKey && !options.storeId && !resolvedSalesChannelId) {
265
+ throw new Error(
266
+ "BrainerceClient: either salesChannelId, apiKey, or storeId is required"
267
+ );
263
268
  }
264
269
  if (options.apiKey && !options.apiKey.startsWith("brainerce_")) {
265
270
  console.warn('BrainerceClient: apiKey should start with "brainerce_"');
266
271
  }
267
- if (options.connectionId && !options.connectionId.startsWith("vc_")) {
268
- console.warn('BrainerceClient: connectionId should start with "vc_"');
272
+ if (resolvedSalesChannelId && !resolvedSalesChannelId.startsWith("vc_")) {
273
+ console.warn('BrainerceClient: salesChannelId should start with "vc_"');
274
+ }
275
+ if (!options.salesChannelId && options.connectionId) {
276
+ console.warn(
277
+ "BrainerceClient: `connectionId` is deprecated \u2014 use `salesChannelId` instead. `connectionId` will be removed in SDK 2.0."
278
+ );
269
279
  }
270
280
  if (options.apiKey && typeof window !== "undefined") {
271
281
  console.warn(
272
- "BrainerceClient: WARNING - API key detected in browser environment. This is a security risk! Use connectionId or storeId for frontend applications."
282
+ "BrainerceClient: WARNING - API key detected in browser environment. This is a security risk! Use salesChannelId or storeId for frontend applications."
273
283
  );
274
284
  }
275
285
  this.apiKey = options.apiKey;
276
286
  this.storeId = options.storeId;
277
- this.connectionId = options.connectionId;
287
+ this.connectionId = resolvedSalesChannelId;
278
288
  let resolvedBase = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
279
289
  if (resolvedBase.startsWith("/") && typeof window !== "undefined" && window.location?.origin) {
280
290
  resolvedBase = window.location.origin + resolvedBase;
@@ -361,11 +371,17 @@ var BrainerceClient = class {
361
371
  }
362
372
  // -------------------- Mode Detection --------------------
363
373
  /**
364
- * Check if client is in vibe-coded mode (using connectionId)
374
+ * Check if client is in sales-channel mode (using salesChannelId / legacy connectionId).
365
375
  */
366
- isVibeCodedMode() {
376
+ isSalesChannelMode() {
367
377
  return !!this.connectionId && !this.apiKey;
368
378
  }
379
+ /**
380
+ * @deprecated Use `isSalesChannelMode()` instead. Kept as a backwards-compatible alias.
381
+ */
382
+ isVibeCodedMode() {
383
+ return this.isSalesChannelMode();
384
+ }
369
385
  /**
370
386
  * Check if client is in storefront mode (using storeId)
371
387
  */
@@ -677,6 +693,18 @@ var BrainerceClient = class {
677
693
  const categories = Array.isArray(params?.categories) ? params.categories.join(",") : params?.categories;
678
694
  const brands = Array.isArray(params?.brands) ? params.brands.join(",") : params?.brands;
679
695
  const tags = Array.isArray(params?.tags) ? params.tags.join(",") : params?.tags;
696
+ let metafields;
697
+ if (params?.metafields && Object.keys(params.metafields).length > 0) {
698
+ const normalized = {};
699
+ for (const [key, raw] of Object.entries(params.metafields)) {
700
+ const arr = Array.isArray(raw) ? raw : [raw];
701
+ const strings = arr.map((v) => String(v)).filter(Boolean);
702
+ if (strings.length > 0) normalized[key] = strings;
703
+ }
704
+ if (Object.keys(normalized).length > 0) {
705
+ metafields = JSON.stringify(normalized);
706
+ }
707
+ }
680
708
  const queryParams = {
681
709
  page: params?.page,
682
710
  limit: params?.limit,
@@ -687,6 +715,7 @@ var BrainerceClient = class {
687
715
  tags,
688
716
  minPrice: params?.minPrice,
689
717
  maxPrice: params?.maxPrice,
718
+ metafields,
690
719
  sortBy: params?.sortBy,
691
720
  sortOrder: params?.sortOrder,
692
721
  // Admin-only params
@@ -1616,6 +1645,58 @@ var BrainerceClient = class {
1616
1645
  async getCustomerByEmail(email) {
1617
1646
  return this.request("GET", "/api/v1/customers/by-email", void 0, { email });
1618
1647
  }
1648
+ /**
1649
+ * List a customer's saved payment methods (vaulted cards).
1650
+ *
1651
+ * Returns display-only metadata — last4, brand, expiry, default flag,
1652
+ * status. The underlying provider token is encrypted at rest and
1653
+ * NEVER returned through this API.
1654
+ *
1655
+ * Apps mode requires `customers:read` and `payments:read` scopes.
1656
+ *
1657
+ * @param storeId - The store this customer belongs to
1658
+ * @param customerId - The customer's ID
1659
+ * @returns Array of saved payment methods
1660
+ *
1661
+ * @example
1662
+ * ```typescript
1663
+ * const methods = await client.listSavedPaymentMethods(storeId, customerId);
1664
+ * methods.forEach((m) => {
1665
+ * console.log(`${m.brand} ending in ${m.last4} (expires ${m.expMonth}/${m.expYear})`);
1666
+ * });
1667
+ * ```
1668
+ */
1669
+ async listSavedPaymentMethods(storeId, customerId) {
1670
+ return this.request(
1671
+ "GET",
1672
+ `/api/stores/${storeId}/customers/${customerId}/payment-methods`
1673
+ );
1674
+ }
1675
+ /**
1676
+ * Remove a customer's saved payment method.
1677
+ *
1678
+ * Hard-deletes the row. The provider may still hold the underlying
1679
+ * token internally — we don't issue a delete-at-provider call because
1680
+ * not every provider supports it. From the platform's perspective the
1681
+ * token is gone; subsequent charges will fail.
1682
+ *
1683
+ * Apps mode requires `customers:write` scope.
1684
+ *
1685
+ * @param storeId - The store this customer belongs to
1686
+ * @param customerId - The customer's ID
1687
+ * @param paymentMethodId - The saved payment method ID to remove
1688
+ *
1689
+ * @example
1690
+ * ```typescript
1691
+ * await client.removeSavedPaymentMethod(storeId, customerId, methodId);
1692
+ * ```
1693
+ */
1694
+ async removeSavedPaymentMethod(storeId, customerId, paymentMethodId) {
1695
+ return this.request(
1696
+ "DELETE",
1697
+ `/api/stores/${storeId}/customers/${customerId}/payment-methods/${paymentMethodId}`
1698
+ );
1699
+ }
1619
1700
  /**
1620
1701
  * Login an existing customer (returns JWT token)
1621
1702
  * Works in vibe-coded, storefront, and admin mode
@@ -2468,6 +2549,82 @@ var BrainerceClient = class {
2468
2549
  "cart"
2469
2550
  );
2470
2551
  }
2552
+ /**
2553
+ * Recalculate cart totals against current product/variant prices and
2554
+ * discount rules. Idempotent — does NOT mutate the per-item snapshot
2555
+ * `unitPrice`. The returned cart's `hasPriceChanges` / `hasUnavailableItems`
2556
+ * flags reflect the live state. Call this on cart load if you want to
2557
+ * surface drift to the customer before they reach checkout.
2558
+ *
2559
+ * @example
2560
+ * ```typescript
2561
+ * const cart = await client.recalculateCart('cart_123');
2562
+ * if (cart.hasPriceChanges) {
2563
+ * // show "prices have changed" banner
2564
+ * }
2565
+ * ```
2566
+ */
2567
+ async recalculateCart(cartId) {
2568
+ if (cartId === this.VIRTUAL_LOCAL_CART_ID) {
2569
+ return this.withGuards(this.localCartToCart(this.getLocalCart()), "cart");
2570
+ }
2571
+ if (this.isVibeCodedMode()) {
2572
+ return this.withGuards(
2573
+ this.vibeCodedRequest("POST", `/cart/${cartId}/recalculate`),
2574
+ "cart"
2575
+ );
2576
+ }
2577
+ if (this.storeId && !this.apiKey) {
2578
+ return this.withGuards(
2579
+ this.storefrontRequest("POST", `/cart/${cartId}/recalculate`),
2580
+ "cart"
2581
+ );
2582
+ }
2583
+ return this.withGuards(
2584
+ this.adminRequest("POST", `/api/v1/cart/${cartId}/recalculate`),
2585
+ "cart"
2586
+ );
2587
+ }
2588
+ /**
2589
+ * Refresh per-item `unitPrice` snapshots to current live prices. Use this
2590
+ * after the customer accepts new prices in the drift-reconfirm flow. The
2591
+ * subsequent `createCheckout` call will then succeed (it would otherwise
2592
+ * throw `PRICE_DRIFT`).
2593
+ *
2594
+ * @example
2595
+ * ```typescript
2596
+ * try {
2597
+ * await client.createCheckout(cartId);
2598
+ * } catch (err) {
2599
+ * if (err.code === 'PRICE_DRIFT') {
2600
+ * // ask user to confirm new prices, then:
2601
+ * await client.refreshCartSnapshots(cartId);
2602
+ * await client.createCheckout(cartId);
2603
+ * }
2604
+ * }
2605
+ * ```
2606
+ */
2607
+ async refreshCartSnapshots(cartId) {
2608
+ if (cartId === this.VIRTUAL_LOCAL_CART_ID) {
2609
+ return this.withGuards(this.localCartToCart(this.getLocalCart()), "cart");
2610
+ }
2611
+ if (this.isVibeCodedMode()) {
2612
+ return this.withGuards(
2613
+ this.vibeCodedRequest("POST", `/cart/${cartId}/refresh-snapshots`),
2614
+ "cart"
2615
+ );
2616
+ }
2617
+ if (this.storeId && !this.apiKey) {
2618
+ return this.withGuards(
2619
+ this.storefrontRequest("POST", `/cart/${cartId}/refresh-snapshots`),
2620
+ "cart"
2621
+ );
2622
+ }
2623
+ return this.withGuards(
2624
+ this.adminRequest("POST", `/api/v1/cart/${cartId}/refresh-snapshots`),
2625
+ "cart"
2626
+ );
2627
+ }
2471
2628
  /**
2472
2629
  * Link a cart to the currently logged-in customer.
2473
2630
  * Use this after customer logs in to associate their guest cart with their account.
@@ -2708,7 +2865,8 @@ var BrainerceClient = class {
2708
2865
  * ```typescript
2709
2866
  * const { bundles } = await client.getCartBundles('cart_123');
2710
2867
  * bundles.forEach(b => {
2711
- * console.log(`Add ${b.bundleProduct.name} and save! Was ${b.originalPrice}, now ${b.discountedPrice}`);
2868
+ * const names = b.offeredProducts.map(p => p.name).join(', ');
2869
+ * console.log(`Add ${names} and save! Was ${b.totalOriginalPrice}, now ${b.totalDiscountedPrice}`);
2712
2870
  * });
2713
2871
  * ```
2714
2872
  */
@@ -2771,24 +2929,31 @@ var BrainerceClient = class {
2771
2929
  throw new BrainerceError("removeOrderBump() requires vibe-coded or storefront mode", 400);
2772
2930
  }
2773
2931
  /**
2774
- * Add a bundle offer product to the cart with its discount applied.
2932
+ * Accept an N-product bundle offer: every offered product not yet in cart
2933
+ * is added with the bundle discount applied.
2775
2934
  *
2776
2935
  * @param cartId - Cart ID
2777
2936
  * @param bundleOfferId - Bundle offer ID
2778
- * @param variantId - Optional variant ID (required when bundle product has variants and no admin-locked variant)
2937
+ * @param variantSelections - Optional map of `productId variantId` for offered products with variants
2779
2938
  * @returns Updated cart
2780
2939
  *
2781
2940
  * @example
2782
2941
  * ```typescript
2783
2942
  * const { bundles } = await client.getCartBundles('cart_123');
2784
- * // Simple product or locked variant:
2785
- * const cart = await client.addBundleToCart('cart_123', bundles[0].id);
2786
- * // Product with variants (customer selects):
2787
- * const cart = await client.addBundleToCart('cart_123', bundles[0].id, selectedVariantId);
2943
+ * const bundle = bundles[0];
2944
+ * // Simple products only:
2945
+ * await client.addBundleToCart('cart_123', bundle.id);
2946
+ * // Some offered products have variants:
2947
+ * await client.addBundleToCart('cart_123', bundle.id, {
2948
+ * [variantProductId]: selectedVariantId,
2949
+ * });
2788
2950
  * ```
2789
2951
  */
2790
- async addBundleToCart(cartId, bundleOfferId, variantId) {
2791
- const body = { bundleOfferId, ...variantId && { variantId } };
2952
+ async addBundleToCart(cartId, bundleOfferId, variantSelections) {
2953
+ const body = {
2954
+ bundleOfferId,
2955
+ ...variantSelections && Object.keys(variantSelections).length > 0 ? { variantSelections } : {}
2956
+ };
2792
2957
  if (this.isVibeCodedMode()) {
2793
2958
  return this.vibeCodedRequest("POST", `/cart/${cartId}/bundle`, body);
2794
2959
  }
@@ -2904,6 +3069,9 @@ var BrainerceClient = class {
2904
3069
  couponCode: null,
2905
3070
  items: [],
2906
3071
  itemCount: 0,
3072
+ hasPriceChanges: false,
3073
+ hasUnavailableItems: false,
3074
+ unavailableItemIds: [],
2907
3075
  expiresAt: null,
2908
3076
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2909
3077
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -3140,23 +3308,21 @@ var BrainerceClient = class {
3140
3308
  * ```
3141
3309
  */
3142
3310
  async smartAddToCart(item) {
3311
+ const payload = {
3312
+ productId: item.productId,
3313
+ variantId: item.variantId,
3314
+ quantity: item.quantity,
3315
+ metadata: item.metadata,
3316
+ ...item.selections && item.selections.length > 0 ? { selections: item.selections } : {},
3317
+ ...item.nestedByModifierId ? { nestedByModifierId: item.nestedByModifierId } : {}
3318
+ };
3143
3319
  if (this.isCustomerLoggedIn()) {
3144
3320
  const cart = await this.getOrCreateCustomerCart();
3145
- const updated = await this.addToCart(cart.id, {
3146
- productId: item.productId,
3147
- variantId: item.variantId,
3148
- quantity: item.quantity,
3149
- metadata: item.metadata
3150
- });
3321
+ const updated = await this.addToCart(cart.id, payload);
3151
3322
  return updated;
3152
3323
  } else {
3153
3324
  const cart = await this.getOrCreateSessionCart();
3154
- const updated = await this.addToCart(cart.id, {
3155
- productId: item.productId,
3156
- variantId: item.variantId,
3157
- quantity: item.quantity,
3158
- metadata: item.metadata
3159
- });
3325
+ const updated = await this.addToCart(cart.id, payload);
3160
3326
  this.updateSessionCartItemCount(updated.items?.length ?? 0);
3161
3327
  return updated;
3162
3328
  }
@@ -4199,6 +4365,16 @@ var BrainerceClient = class {
4199
4365
  variantId: item.variantId || null,
4200
4366
  quantity: item.quantity,
4201
4367
  unitPrice: item.price || "0",
4368
+ // Local carts never reach the server, so they have no live price to
4369
+ // compare against. Surface the snapshot as both values and flag
4370
+ // everything as "unchanged + available" — the migration to a server
4371
+ // cart on next load will run the real recalc.
4372
+ currentUnitPrice: item.price || "0",
4373
+ priceChanged: false,
4374
+ priceDelta: "0",
4375
+ priceDirection: "unchanged",
4376
+ isAvailable: true,
4377
+ unavailableReason: null,
4202
4378
  discountAmount: "0",
4203
4379
  promoDiscountAmount: "0",
4204
4380
  promoSource: null,
@@ -4221,6 +4397,9 @@ var BrainerceClient = class {
4221
4397
  updatedAt: item.addedAt
4222
4398
  })),
4223
4399
  itemCount: localCart.items.reduce((sum, i) => sum + i.quantity, 0),
4400
+ hasPriceChanges: false,
4401
+ hasUnavailableItems: false,
4402
+ unavailableItemIds: [],
4224
4403
  expiresAt: null,
4225
4404
  createdAt: localCart.updatedAt || (/* @__PURE__ */ new Date()).toISOString(),
4226
4405
  updatedAt: localCart.updatedAt || (/* @__PURE__ */ new Date()).toISOString()
@@ -5567,6 +5746,204 @@ var BrainerceClient = class {
5567
5746
  `/api/v1/attributes/${attributeId}/options/${optionId}`
5568
5747
  );
5569
5748
  }
5749
+ // -------------------- Modifier Groups (Admin) --------------------
5750
+ // These methods require Admin mode (apiKey).
5751
+ // Routes: /api/stores/:storeId/modifier-groups[/:id]
5752
+ // /api/stores/:storeId/modifier-groups/:groupId/modifiers[/:modifierId]
5753
+ // /api/stores/:storeId/products/:productId/modifier-groups[/:attachmentId]
5754
+ //
5755
+ // Server-side validation failures arrive as a structured 400 envelope:
5756
+ // { code: 'MODIFIER_VALIDATION_FAILED', errors: ModifierValidationError[] }
5757
+ // surfaced via BrainerceError.details.
5758
+ /**
5759
+ * List modifier groups in a store, paginated.
5760
+ * Requires Admin mode (apiKey).
5761
+ *
5762
+ * @example
5763
+ * ```typescript
5764
+ * const groups = await client.listModifierGroups('store_123', {
5765
+ * page: 1,
5766
+ * limit: 20,
5767
+ * search: 'pizza',
5768
+ * status: 'active',
5769
+ * });
5770
+ * ```
5771
+ */
5772
+ async listModifierGroups(storeId, params) {
5773
+ return this.adminRequest(
5774
+ "GET",
5775
+ `/api/stores/${storeId}/modifier-groups`,
5776
+ void 0,
5777
+ params
5778
+ );
5779
+ }
5780
+ /**
5781
+ * Fetch a single modifier group (with its modifiers).
5782
+ * Requires Admin mode (apiKey).
5783
+ *
5784
+ * Admin fetches include `internalName`; storefront responses strip it
5785
+ * (PRD §5.2 invariant).
5786
+ */
5787
+ async getModifierGroup(storeId, groupId) {
5788
+ return this.adminRequest(
5789
+ "GET",
5790
+ `/api/stores/${storeId}/modifier-groups/${groupId}`
5791
+ );
5792
+ }
5793
+ /**
5794
+ * Create a modifier group. Modifiers themselves are created separately via
5795
+ * `createModifier(storeId, groupId, …)`.
5796
+ * Requires Admin mode (apiKey).
5797
+ *
5798
+ * @example
5799
+ * ```typescript
5800
+ * const group = await client.createModifierGroup('store_123', {
5801
+ * name: 'Toppings',
5802
+ * internalName: 'Pizza toppings',
5803
+ * selectionType: 'MULTIPLE',
5804
+ * minSelections: 0,
5805
+ * maxSelections: 8,
5806
+ * freeQuantity: 3,
5807
+ * freeAllocationPolicy: 'EXPENSIVE_FREE',
5808
+ * });
5809
+ * ```
5810
+ */
5811
+ async createModifierGroup(storeId, data) {
5812
+ return this.adminRequest("POST", `/api/stores/${storeId}/modifier-groups`, data);
5813
+ }
5814
+ /**
5815
+ * Update a modifier group's metadata or selection rules. Pass `status: 'archived'`
5816
+ * to soft-delete (the group becomes hidden from product attachments).
5817
+ * Requires Admin mode (apiKey).
5818
+ */
5819
+ async updateModifierGroup(storeId, groupId, data) {
5820
+ return this.adminRequest(
5821
+ "PATCH",
5822
+ `/api/stores/${storeId}/modifier-groups/${groupId}`,
5823
+ data
5824
+ );
5825
+ }
5826
+ /**
5827
+ * Hard-delete a modifier group. Server rejects deletion when the group is
5828
+ * still attached to any product (detach the attachments first, or use
5829
+ * `updateModifierGroup(..., { status: 'archived' })` to keep the row).
5830
+ * Requires Admin mode (apiKey).
5831
+ */
5832
+ async deleteModifierGroup(storeId, groupId) {
5833
+ await this.adminRequest("DELETE", `/api/stores/${storeId}/modifier-groups/${groupId}`);
5834
+ }
5835
+ /**
5836
+ * Create a modifier inside a group (e.g., "Olives" inside the "Toppings" group).
5837
+ * Requires Admin mode (apiKey).
5838
+ *
5839
+ * `priceDelta` is a decimal string. Negatives are accepted for downsell
5840
+ * modifiers (e.g., `"-2.00"` for "no bread"); the server still enforces
5841
+ * `unitPrice >= 0` at cart-line resolution and rejects negative deltas
5842
+ * combined with a `referencedProductId`.
5843
+ *
5844
+ * @example
5845
+ * ```typescript
5846
+ * const olives = await client.createModifier('store_123', 'mg_toppings', {
5847
+ * name: 'Olives',
5848
+ * priceDelta: '5.00',
5849
+ * isDefault: false,
5850
+ * available: true,
5851
+ * });
5852
+ * ```
5853
+ */
5854
+ async createModifier(storeId, groupId, data) {
5855
+ return this.adminRequest(
5856
+ "POST",
5857
+ `/api/stores/${storeId}/modifier-groups/${groupId}/modifiers`,
5858
+ data
5859
+ );
5860
+ }
5861
+ /**
5862
+ * Update a modifier. Pass `status: 'archived'` to soft-delete.
5863
+ * Requires Admin mode (apiKey).
5864
+ */
5865
+ async updateModifier(storeId, groupId, modifierId, data) {
5866
+ return this.adminRequest(
5867
+ "PATCH",
5868
+ `/api/stores/${storeId}/modifier-groups/${groupId}/modifiers/${modifierId}`,
5869
+ data
5870
+ );
5871
+ }
5872
+ /**
5873
+ * Hard-delete a modifier. Use `updateModifier(..., { status: 'archived' })`
5874
+ * if existing line items / order snapshots reference it.
5875
+ * Requires Admin mode (apiKey).
5876
+ */
5877
+ async deleteModifier(storeId, groupId, modifierId) {
5878
+ await this.adminRequest(
5879
+ "DELETE",
5880
+ `/api/stores/${storeId}/modifier-groups/${groupId}/modifiers/${modifierId}`
5881
+ );
5882
+ }
5883
+ /**
5884
+ * Flip the sold-out toggle on a modifier. Operational endpoint kept separate
5885
+ * from `updateModifier` because the daily ops bar is intentionally lower
5886
+ * (PRD §7.1 — STAFF role once the granular `TOGGLE_MODIFIER_AVAILABILITY`
5887
+ * permission ships).
5888
+ * Requires Admin mode (apiKey).
5889
+ *
5890
+ * @example
5891
+ * ```typescript
5892
+ * // Mark the egg modifier as out of stock during a busy lunch service
5893
+ * await client.toggleModifierAvailability('store_123', 'mg_toppings', 'm_egg', false);
5894
+ * ```
5895
+ */
5896
+ async toggleModifierAvailability(storeId, groupId, modifierId, available) {
5897
+ return this.adminRequest(
5898
+ "POST",
5899
+ `/api/stores/${storeId}/modifier-groups/${groupId}/modifiers/${modifierId}/availability-toggle`,
5900
+ { available }
5901
+ );
5902
+ }
5903
+ /**
5904
+ * Attach a modifier group to a product. Three patterns (PRD §5.4):
5905
+ *
5906
+ * - `variantId` omitted → default attach (applies to all variants).
5907
+ * - `variantId` set + matching default attach already exists → variant override row.
5908
+ * - `variantId` set + no default attach → variant-only group.
5909
+ *
5910
+ * Override fields use `null` to mean inherit; any non-null value (including
5911
+ * `0` or `false`) wins. The disable-for-variant convention is `maxOverride: 0` —
5912
+ * the resolver returns the group with `effectiveMax=0` and the validator
5913
+ * silently skips it on cart add.
5914
+ *
5915
+ * Requires Admin mode (apiKey).
5916
+ */
5917
+ async attachModifierGroup(storeId, productId, data) {
5918
+ return this.adminRequest(
5919
+ "POST",
5920
+ `/api/stores/${storeId}/products/${productId}/modifier-groups`,
5921
+ data
5922
+ );
5923
+ }
5924
+ /**
5925
+ * Update an existing attachment's overrides or position. The `modifierGroupId`
5926
+ * and `variantId` are immutable — to swap groups or move between default /
5927
+ * per-variant rows, detach and re-attach.
5928
+ * Requires Admin mode (apiKey).
5929
+ */
5930
+ async updateAttachment(storeId, productId, attachmentId, data) {
5931
+ return this.adminRequest(
5932
+ "PATCH",
5933
+ `/api/stores/${storeId}/products/${productId}/modifier-groups/${attachmentId}`,
5934
+ data
5935
+ );
5936
+ }
5937
+ /**
5938
+ * Detach a modifier group from a product (or remove a per-variant override row).
5939
+ * Requires Admin mode (apiKey).
5940
+ */
5941
+ async detachModifierGroup(storeId, productId, attachmentId) {
5942
+ await this.adminRequest(
5943
+ "DELETE",
5944
+ `/api/stores/${storeId}/products/${productId}/modifier-groups/${attachmentId}`
5945
+ );
5946
+ }
5570
5947
  // -------------------- Shipping: Zones and Rates (Admin) --------------------
5571
5948
  // These methods require Admin mode (apiKey)
5572
5949
  /**
@@ -5746,7 +6123,10 @@ var BrainerceClient = class {
5746
6123
  type: d.type,
5747
6124
  required: d.required,
5748
6125
  enumValues: d.enumValues || void 0,
5749
- position: d.position
6126
+ position: d.position,
6127
+ isCustomerInput: d.isCustomerInput,
6128
+ appliesToAllProducts: d.appliesToAllProducts,
6129
+ filterable: d.filterable
5750
6130
  }))
5751
6131
  };
5752
6132
  }
@@ -5802,6 +6182,107 @@ var BrainerceClient = class {
5802
6182
  async deleteMetafieldDefinition(definitionId) {
5803
6183
  await this.adminRequest("DELETE", `/api/v1/metafield-definitions/${definitionId}`);
5804
6184
  }
6185
+ /**
6186
+ * Replace the platform publishing configuration on a metafield definition.
6187
+ * `publishedOn` is the source of truth for which sales channels the field
6188
+ * appears on; `platformMetadata` carries the per-platform mapping config
6189
+ * required for sync.
6190
+ *
6191
+ * Requires Admin mode (apiKey).
6192
+ *
6193
+ * @example
6194
+ * ```typescript
6195
+ * await client.setMetafieldPlatforms('def_123', {
6196
+ * publishedOn: ['SHOPIFY', 'WOOCOMMERCE'],
6197
+ * platformMetadata: {
6198
+ * SHOPIFY: { namespace: 'custom', key: 'warranty' },
6199
+ * WOOCOMMERCE: { key: '_warranty_info' },
6200
+ * },
6201
+ * });
6202
+ * ```
6203
+ */
6204
+ async setMetafieldPlatforms(definitionId, data) {
6205
+ return this.adminRequest(
6206
+ "PUT",
6207
+ `/api/v1/metafield-definitions/${definitionId}/platforms`,
6208
+ data
6209
+ );
6210
+ }
6211
+ // -------------- Vibe-coded site publishing (Admin Mode) --------------
6212
+ // Publish/unpublish category/tag/brand/metafield definitions to specific
6213
+ // vibe-coded sites. Mirrors the per-channel control already available for
6214
+ // Products/Coupons. When no publishes exist for an entity, it remains
6215
+ // visible to all vibe-coded sites of the store (legacy default).
6216
+ /**
6217
+ * Publish a metafield definition to a vibe-coded site (admin mode).
6218
+ * @example
6219
+ * ```typescript
6220
+ * await client.publishMetafieldDefinitionToVibeCodedSite('def_123', 'conn_456');
6221
+ * ```
6222
+ */
6223
+ async publishMetafieldDefinitionToVibeCodedSite(definitionId, vibeCodedConnectionId) {
6224
+ return this.adminRequest(
6225
+ "POST",
6226
+ `/api/v1/metafield-definitions/${definitionId}/publish-vibe-coded`,
6227
+ { vibeCodedConnectionId }
6228
+ );
6229
+ }
6230
+ /** Unpublish a metafield definition from a vibe-coded site (admin mode). */
6231
+ async unpublishMetafieldDefinitionFromVibeCodedSite(definitionId, vibeCodedConnectionId) {
6232
+ return this.adminRequest(
6233
+ "POST",
6234
+ `/api/v1/metafield-definitions/${definitionId}/unpublish-vibe-coded`,
6235
+ { vibeCodedConnectionId }
6236
+ );
6237
+ }
6238
+ /** Publish a category to a vibe-coded site (admin mode). */
6239
+ async publishCategoryToVibeCodedSite(categoryId, vibeCodedConnectionId) {
6240
+ return this.adminRequest(
6241
+ "POST",
6242
+ `/api/v1/categories/${categoryId}/publish-vibe-coded`,
6243
+ { vibeCodedConnectionId }
6244
+ );
6245
+ }
6246
+ /** Unpublish a category from a vibe-coded site (admin mode). */
6247
+ async unpublishCategoryFromVibeCodedSite(categoryId, vibeCodedConnectionId) {
6248
+ return this.adminRequest(
6249
+ "POST",
6250
+ `/api/v1/categories/${categoryId}/unpublish-vibe-coded`,
6251
+ { vibeCodedConnectionId }
6252
+ );
6253
+ }
6254
+ /** Publish a tag to a vibe-coded site (admin mode). */
6255
+ async publishTagToVibeCodedSite(tagId, vibeCodedConnectionId) {
6256
+ return this.adminRequest(
6257
+ "POST",
6258
+ `/api/v1/tags/${tagId}/publish-vibe-coded`,
6259
+ { vibeCodedConnectionId }
6260
+ );
6261
+ }
6262
+ /** Unpublish a tag from a vibe-coded site (admin mode). */
6263
+ async unpublishTagFromVibeCodedSite(tagId, vibeCodedConnectionId) {
6264
+ return this.adminRequest(
6265
+ "POST",
6266
+ `/api/v1/tags/${tagId}/unpublish-vibe-coded`,
6267
+ { vibeCodedConnectionId }
6268
+ );
6269
+ }
6270
+ /** Publish a brand to a vibe-coded site (admin mode). */
6271
+ async publishBrandToVibeCodedSite(brandId, vibeCodedConnectionId) {
6272
+ return this.adminRequest(
6273
+ "POST",
6274
+ `/api/v1/brands/${brandId}/publish-vibe-coded`,
6275
+ { vibeCodedConnectionId }
6276
+ );
6277
+ }
6278
+ /** Unpublish a brand from a vibe-coded site (admin mode). */
6279
+ async unpublishBrandFromVibeCodedSite(brandId, vibeCodedConnectionId) {
6280
+ return this.adminRequest(
6281
+ "POST",
6282
+ `/api/v1/brands/${brandId}/unpublish-vibe-coded`,
6283
+ { vibeCodedConnectionId }
6284
+ );
6285
+ }
5805
6286
  /**
5806
6287
  * Replace the list of products a (customer-input) metafield definition is
5807
6288
  * attached to. Diff-scoped: only rows for this definition are touched, so
@@ -6371,6 +6852,55 @@ function createWebhookHandler(handlers) {
6371
6852
  };
6372
6853
  }
6373
6854
 
6855
+ // src/payment-url.ts
6856
+ var ALLOWED_PAYMENT_HOSTS = [
6857
+ // Stripe
6858
+ "checkout.stripe.com",
6859
+ "js.stripe.com",
6860
+ "hooks.stripe.com",
6861
+ // PayPal
6862
+ "www.paypal.com",
6863
+ "www.sandbox.paypal.com",
6864
+ // Cardcom
6865
+ "secure.cardcom.solutions",
6866
+ // Meshulam
6867
+ "meshulam.co.il",
6868
+ // Grow (Linktech)
6869
+ "grow.link",
6870
+ "grow.security",
6871
+ // CreditGuard
6872
+ "creditguard.co.il",
6873
+ // Brainerce-hosted payment embeds (backend payment-embed proxy at
6874
+ // `/api/payment/embed/...` that fronts provider apps' embed shells —
6875
+ // e.g. cardcom-payments OpenFields wrapper). The match also covers
6876
+ // subdomains like `api.brainerce.com`, `staging.brainerce.com`.
6877
+ "brainerce.com"
6878
+ ];
6879
+ function isAllowedPaymentUrl(url, options) {
6880
+ if (!url || typeof url !== "string") return false;
6881
+ let parsed;
6882
+ try {
6883
+ parsed = new URL(url);
6884
+ } catch {
6885
+ return false;
6886
+ }
6887
+ const hostname = parsed.hostname.toLowerCase();
6888
+ if (options?.allowLocalhost && parsed.protocol === "http:" && (hostname === "localhost" || hostname === "127.0.0.1")) {
6889
+ return true;
6890
+ }
6891
+ if (parsed.protocol !== "https:") return false;
6892
+ const allowed = options?.extraHosts ? [...ALLOWED_PAYMENT_HOSTS, ...options.extraHosts] : ALLOWED_PAYMENT_HOSTS;
6893
+ return allowed.some((host) => hostname === host || hostname.endsWith("." + host));
6894
+ }
6895
+ function safePaymentRedirect(url, options) {
6896
+ if (!isAllowedPaymentUrl(url, options)) {
6897
+ throw new Error("Payment redirect URL is not in the allowlist");
6898
+ }
6899
+ if (typeof window !== "undefined") {
6900
+ window.location.href = url;
6901
+ }
6902
+ }
6903
+
6374
6904
  // src/types.ts
6375
6905
  function isHtmlDescription(product) {
6376
6906
  if (product?.descriptionFormat === "html") return true;
@@ -6611,9 +7141,11 @@ function isCouponApplicableToProduct(coupon, productId) {
6611
7141
  getStockStatus,
6612
7142
  getVariantOptions,
6613
7143
  getVariantPrice,
7144
+ isAllowedPaymentUrl,
6614
7145
  isCouponApplicableToProduct,
6615
7146
  isHtmlDescription,
6616
7147
  isWebhookEventType,
6617
7148
  parseWebhookEvent,
7149
+ safePaymentRedirect,
6618
7150
  verifyWebhook
6619
7151
  });