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.mjs CHANGED
@@ -197,23 +197,31 @@ var BrainerceClient = class {
197
197
  // These methods store cart data in localStorage - NO API calls!
198
198
  // Use for guest users in vibe-coded sites
199
199
  this.LOCAL_CART_KEY = "brainerce_cart";
200
- if (!options.apiKey && !options.storeId && !options.connectionId) {
201
- throw new Error("BrainerceClient: either connectionId, apiKey, or storeId is required");
200
+ const resolvedSalesChannelId = options.salesChannelId ?? options.connectionId;
201
+ if (!options.apiKey && !options.storeId && !resolvedSalesChannelId) {
202
+ throw new Error(
203
+ "BrainerceClient: either salesChannelId, apiKey, or storeId is required"
204
+ );
202
205
  }
203
206
  if (options.apiKey && !options.apiKey.startsWith("brainerce_")) {
204
207
  console.warn('BrainerceClient: apiKey should start with "brainerce_"');
205
208
  }
206
- if (options.connectionId && !options.connectionId.startsWith("vc_")) {
207
- console.warn('BrainerceClient: connectionId should start with "vc_"');
209
+ if (resolvedSalesChannelId && !resolvedSalesChannelId.startsWith("vc_")) {
210
+ console.warn('BrainerceClient: salesChannelId should start with "vc_"');
211
+ }
212
+ if (!options.salesChannelId && options.connectionId) {
213
+ console.warn(
214
+ "BrainerceClient: `connectionId` is deprecated \u2014 use `salesChannelId` instead. `connectionId` will be removed in SDK 2.0."
215
+ );
208
216
  }
209
217
  if (options.apiKey && typeof window !== "undefined") {
210
218
  console.warn(
211
- "BrainerceClient: WARNING - API key detected in browser environment. This is a security risk! Use connectionId or storeId for frontend applications."
219
+ "BrainerceClient: WARNING - API key detected in browser environment. This is a security risk! Use salesChannelId or storeId for frontend applications."
212
220
  );
213
221
  }
214
222
  this.apiKey = options.apiKey;
215
223
  this.storeId = options.storeId;
216
- this.connectionId = options.connectionId;
224
+ this.connectionId = resolvedSalesChannelId;
217
225
  let resolvedBase = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
218
226
  if (resolvedBase.startsWith("/") && typeof window !== "undefined" && window.location?.origin) {
219
227
  resolvedBase = window.location.origin + resolvedBase;
@@ -300,11 +308,17 @@ var BrainerceClient = class {
300
308
  }
301
309
  // -------------------- Mode Detection --------------------
302
310
  /**
303
- * Check if client is in vibe-coded mode (using connectionId)
311
+ * Check if client is in sales-channel mode (using salesChannelId / legacy connectionId).
304
312
  */
305
- isVibeCodedMode() {
313
+ isSalesChannelMode() {
306
314
  return !!this.connectionId && !this.apiKey;
307
315
  }
316
+ /**
317
+ * @deprecated Use `isSalesChannelMode()` instead. Kept as a backwards-compatible alias.
318
+ */
319
+ isVibeCodedMode() {
320
+ return this.isSalesChannelMode();
321
+ }
308
322
  /**
309
323
  * Check if client is in storefront mode (using storeId)
310
324
  */
@@ -616,6 +630,18 @@ var BrainerceClient = class {
616
630
  const categories = Array.isArray(params?.categories) ? params.categories.join(",") : params?.categories;
617
631
  const brands = Array.isArray(params?.brands) ? params.brands.join(",") : params?.brands;
618
632
  const tags = Array.isArray(params?.tags) ? params.tags.join(",") : params?.tags;
633
+ let metafields;
634
+ if (params?.metafields && Object.keys(params.metafields).length > 0) {
635
+ const normalized = {};
636
+ for (const [key, raw] of Object.entries(params.metafields)) {
637
+ const arr = Array.isArray(raw) ? raw : [raw];
638
+ const strings = arr.map((v) => String(v)).filter(Boolean);
639
+ if (strings.length > 0) normalized[key] = strings;
640
+ }
641
+ if (Object.keys(normalized).length > 0) {
642
+ metafields = JSON.stringify(normalized);
643
+ }
644
+ }
619
645
  const queryParams = {
620
646
  page: params?.page,
621
647
  limit: params?.limit,
@@ -626,6 +652,7 @@ var BrainerceClient = class {
626
652
  tags,
627
653
  minPrice: params?.minPrice,
628
654
  maxPrice: params?.maxPrice,
655
+ metafields,
629
656
  sortBy: params?.sortBy,
630
657
  sortOrder: params?.sortOrder,
631
658
  // Admin-only params
@@ -1555,6 +1582,58 @@ var BrainerceClient = class {
1555
1582
  async getCustomerByEmail(email) {
1556
1583
  return this.request("GET", "/api/v1/customers/by-email", void 0, { email });
1557
1584
  }
1585
+ /**
1586
+ * List a customer's saved payment methods (vaulted cards).
1587
+ *
1588
+ * Returns display-only metadata — last4, brand, expiry, default flag,
1589
+ * status. The underlying provider token is encrypted at rest and
1590
+ * NEVER returned through this API.
1591
+ *
1592
+ * Apps mode requires `customers:read` and `payments:read` scopes.
1593
+ *
1594
+ * @param storeId - The store this customer belongs to
1595
+ * @param customerId - The customer's ID
1596
+ * @returns Array of saved payment methods
1597
+ *
1598
+ * @example
1599
+ * ```typescript
1600
+ * const methods = await client.listSavedPaymentMethods(storeId, customerId);
1601
+ * methods.forEach((m) => {
1602
+ * console.log(`${m.brand} ending in ${m.last4} (expires ${m.expMonth}/${m.expYear})`);
1603
+ * });
1604
+ * ```
1605
+ */
1606
+ async listSavedPaymentMethods(storeId, customerId) {
1607
+ return this.request(
1608
+ "GET",
1609
+ `/api/stores/${storeId}/customers/${customerId}/payment-methods`
1610
+ );
1611
+ }
1612
+ /**
1613
+ * Remove a customer's saved payment method.
1614
+ *
1615
+ * Hard-deletes the row. The provider may still hold the underlying
1616
+ * token internally — we don't issue a delete-at-provider call because
1617
+ * not every provider supports it. From the platform's perspective the
1618
+ * token is gone; subsequent charges will fail.
1619
+ *
1620
+ * Apps mode requires `customers:write` scope.
1621
+ *
1622
+ * @param storeId - The store this customer belongs to
1623
+ * @param customerId - The customer's ID
1624
+ * @param paymentMethodId - The saved payment method ID to remove
1625
+ *
1626
+ * @example
1627
+ * ```typescript
1628
+ * await client.removeSavedPaymentMethod(storeId, customerId, methodId);
1629
+ * ```
1630
+ */
1631
+ async removeSavedPaymentMethod(storeId, customerId, paymentMethodId) {
1632
+ return this.request(
1633
+ "DELETE",
1634
+ `/api/stores/${storeId}/customers/${customerId}/payment-methods/${paymentMethodId}`
1635
+ );
1636
+ }
1558
1637
  /**
1559
1638
  * Login an existing customer (returns JWT token)
1560
1639
  * Works in vibe-coded, storefront, and admin mode
@@ -2407,6 +2486,82 @@ var BrainerceClient = class {
2407
2486
  "cart"
2408
2487
  );
2409
2488
  }
2489
+ /**
2490
+ * Recalculate cart totals against current product/variant prices and
2491
+ * discount rules. Idempotent — does NOT mutate the per-item snapshot
2492
+ * `unitPrice`. The returned cart's `hasPriceChanges` / `hasUnavailableItems`
2493
+ * flags reflect the live state. Call this on cart load if you want to
2494
+ * surface drift to the customer before they reach checkout.
2495
+ *
2496
+ * @example
2497
+ * ```typescript
2498
+ * const cart = await client.recalculateCart('cart_123');
2499
+ * if (cart.hasPriceChanges) {
2500
+ * // show "prices have changed" banner
2501
+ * }
2502
+ * ```
2503
+ */
2504
+ async recalculateCart(cartId) {
2505
+ if (cartId === this.VIRTUAL_LOCAL_CART_ID) {
2506
+ return this.withGuards(this.localCartToCart(this.getLocalCart()), "cart");
2507
+ }
2508
+ if (this.isVibeCodedMode()) {
2509
+ return this.withGuards(
2510
+ this.vibeCodedRequest("POST", `/cart/${cartId}/recalculate`),
2511
+ "cart"
2512
+ );
2513
+ }
2514
+ if (this.storeId && !this.apiKey) {
2515
+ return this.withGuards(
2516
+ this.storefrontRequest("POST", `/cart/${cartId}/recalculate`),
2517
+ "cart"
2518
+ );
2519
+ }
2520
+ return this.withGuards(
2521
+ this.adminRequest("POST", `/api/v1/cart/${cartId}/recalculate`),
2522
+ "cart"
2523
+ );
2524
+ }
2525
+ /**
2526
+ * Refresh per-item `unitPrice` snapshots to current live prices. Use this
2527
+ * after the customer accepts new prices in the drift-reconfirm flow. The
2528
+ * subsequent `createCheckout` call will then succeed (it would otherwise
2529
+ * throw `PRICE_DRIFT`).
2530
+ *
2531
+ * @example
2532
+ * ```typescript
2533
+ * try {
2534
+ * await client.createCheckout(cartId);
2535
+ * } catch (err) {
2536
+ * if (err.code === 'PRICE_DRIFT') {
2537
+ * // ask user to confirm new prices, then:
2538
+ * await client.refreshCartSnapshots(cartId);
2539
+ * await client.createCheckout(cartId);
2540
+ * }
2541
+ * }
2542
+ * ```
2543
+ */
2544
+ async refreshCartSnapshots(cartId) {
2545
+ if (cartId === this.VIRTUAL_LOCAL_CART_ID) {
2546
+ return this.withGuards(this.localCartToCart(this.getLocalCart()), "cart");
2547
+ }
2548
+ if (this.isVibeCodedMode()) {
2549
+ return this.withGuards(
2550
+ this.vibeCodedRequest("POST", `/cart/${cartId}/refresh-snapshots`),
2551
+ "cart"
2552
+ );
2553
+ }
2554
+ if (this.storeId && !this.apiKey) {
2555
+ return this.withGuards(
2556
+ this.storefrontRequest("POST", `/cart/${cartId}/refresh-snapshots`),
2557
+ "cart"
2558
+ );
2559
+ }
2560
+ return this.withGuards(
2561
+ this.adminRequest("POST", `/api/v1/cart/${cartId}/refresh-snapshots`),
2562
+ "cart"
2563
+ );
2564
+ }
2410
2565
  /**
2411
2566
  * Link a cart to the currently logged-in customer.
2412
2567
  * Use this after customer logs in to associate their guest cart with their account.
@@ -2647,7 +2802,8 @@ var BrainerceClient = class {
2647
2802
  * ```typescript
2648
2803
  * const { bundles } = await client.getCartBundles('cart_123');
2649
2804
  * bundles.forEach(b => {
2650
- * console.log(`Add ${b.bundleProduct.name} and save! Was ${b.originalPrice}, now ${b.discountedPrice}`);
2805
+ * const names = b.offeredProducts.map(p => p.name).join(', ');
2806
+ * console.log(`Add ${names} and save! Was ${b.totalOriginalPrice}, now ${b.totalDiscountedPrice}`);
2651
2807
  * });
2652
2808
  * ```
2653
2809
  */
@@ -2710,24 +2866,31 @@ var BrainerceClient = class {
2710
2866
  throw new BrainerceError("removeOrderBump() requires vibe-coded or storefront mode", 400);
2711
2867
  }
2712
2868
  /**
2713
- * Add a bundle offer product to the cart with its discount applied.
2869
+ * Accept an N-product bundle offer: every offered product not yet in cart
2870
+ * is added with the bundle discount applied.
2714
2871
  *
2715
2872
  * @param cartId - Cart ID
2716
2873
  * @param bundleOfferId - Bundle offer ID
2717
- * @param variantId - Optional variant ID (required when bundle product has variants and no admin-locked variant)
2874
+ * @param variantSelections - Optional map of `productId variantId` for offered products with variants
2718
2875
  * @returns Updated cart
2719
2876
  *
2720
2877
  * @example
2721
2878
  * ```typescript
2722
2879
  * const { bundles } = await client.getCartBundles('cart_123');
2723
- * // Simple product or locked variant:
2724
- * const cart = await client.addBundleToCart('cart_123', bundles[0].id);
2725
- * // Product with variants (customer selects):
2726
- * const cart = await client.addBundleToCart('cart_123', bundles[0].id, selectedVariantId);
2880
+ * const bundle = bundles[0];
2881
+ * // Simple products only:
2882
+ * await client.addBundleToCart('cart_123', bundle.id);
2883
+ * // Some offered products have variants:
2884
+ * await client.addBundleToCart('cart_123', bundle.id, {
2885
+ * [variantProductId]: selectedVariantId,
2886
+ * });
2727
2887
  * ```
2728
2888
  */
2729
- async addBundleToCart(cartId, bundleOfferId, variantId) {
2730
- const body = { bundleOfferId, ...variantId && { variantId } };
2889
+ async addBundleToCart(cartId, bundleOfferId, variantSelections) {
2890
+ const body = {
2891
+ bundleOfferId,
2892
+ ...variantSelections && Object.keys(variantSelections).length > 0 ? { variantSelections } : {}
2893
+ };
2731
2894
  if (this.isVibeCodedMode()) {
2732
2895
  return this.vibeCodedRequest("POST", `/cart/${cartId}/bundle`, body);
2733
2896
  }
@@ -2843,6 +3006,9 @@ var BrainerceClient = class {
2843
3006
  couponCode: null,
2844
3007
  items: [],
2845
3008
  itemCount: 0,
3009
+ hasPriceChanges: false,
3010
+ hasUnavailableItems: false,
3011
+ unavailableItemIds: [],
2846
3012
  expiresAt: null,
2847
3013
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2848
3014
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -3079,23 +3245,21 @@ var BrainerceClient = class {
3079
3245
  * ```
3080
3246
  */
3081
3247
  async smartAddToCart(item) {
3248
+ const payload = {
3249
+ productId: item.productId,
3250
+ variantId: item.variantId,
3251
+ quantity: item.quantity,
3252
+ metadata: item.metadata,
3253
+ ...item.selections && item.selections.length > 0 ? { selections: item.selections } : {},
3254
+ ...item.nestedByModifierId ? { nestedByModifierId: item.nestedByModifierId } : {}
3255
+ };
3082
3256
  if (this.isCustomerLoggedIn()) {
3083
3257
  const cart = await this.getOrCreateCustomerCart();
3084
- const updated = await this.addToCart(cart.id, {
3085
- productId: item.productId,
3086
- variantId: item.variantId,
3087
- quantity: item.quantity,
3088
- metadata: item.metadata
3089
- });
3258
+ const updated = await this.addToCart(cart.id, payload);
3090
3259
  return updated;
3091
3260
  } else {
3092
3261
  const cart = await this.getOrCreateSessionCart();
3093
- const updated = await this.addToCart(cart.id, {
3094
- productId: item.productId,
3095
- variantId: item.variantId,
3096
- quantity: item.quantity,
3097
- metadata: item.metadata
3098
- });
3262
+ const updated = await this.addToCart(cart.id, payload);
3099
3263
  this.updateSessionCartItemCount(updated.items?.length ?? 0);
3100
3264
  return updated;
3101
3265
  }
@@ -4138,6 +4302,16 @@ var BrainerceClient = class {
4138
4302
  variantId: item.variantId || null,
4139
4303
  quantity: item.quantity,
4140
4304
  unitPrice: item.price || "0",
4305
+ // Local carts never reach the server, so they have no live price to
4306
+ // compare against. Surface the snapshot as both values and flag
4307
+ // everything as "unchanged + available" — the migration to a server
4308
+ // cart on next load will run the real recalc.
4309
+ currentUnitPrice: item.price || "0",
4310
+ priceChanged: false,
4311
+ priceDelta: "0",
4312
+ priceDirection: "unchanged",
4313
+ isAvailable: true,
4314
+ unavailableReason: null,
4141
4315
  discountAmount: "0",
4142
4316
  promoDiscountAmount: "0",
4143
4317
  promoSource: null,
@@ -4160,6 +4334,9 @@ var BrainerceClient = class {
4160
4334
  updatedAt: item.addedAt
4161
4335
  })),
4162
4336
  itemCount: localCart.items.reduce((sum, i) => sum + i.quantity, 0),
4337
+ hasPriceChanges: false,
4338
+ hasUnavailableItems: false,
4339
+ unavailableItemIds: [],
4163
4340
  expiresAt: null,
4164
4341
  createdAt: localCart.updatedAt || (/* @__PURE__ */ new Date()).toISOString(),
4165
4342
  updatedAt: localCart.updatedAt || (/* @__PURE__ */ new Date()).toISOString()
@@ -5506,6 +5683,204 @@ var BrainerceClient = class {
5506
5683
  `/api/v1/attributes/${attributeId}/options/${optionId}`
5507
5684
  );
5508
5685
  }
5686
+ // -------------------- Modifier Groups (Admin) --------------------
5687
+ // These methods require Admin mode (apiKey).
5688
+ // Routes: /api/stores/:storeId/modifier-groups[/:id]
5689
+ // /api/stores/:storeId/modifier-groups/:groupId/modifiers[/:modifierId]
5690
+ // /api/stores/:storeId/products/:productId/modifier-groups[/:attachmentId]
5691
+ //
5692
+ // Server-side validation failures arrive as a structured 400 envelope:
5693
+ // { code: 'MODIFIER_VALIDATION_FAILED', errors: ModifierValidationError[] }
5694
+ // surfaced via BrainerceError.details.
5695
+ /**
5696
+ * List modifier groups in a store, paginated.
5697
+ * Requires Admin mode (apiKey).
5698
+ *
5699
+ * @example
5700
+ * ```typescript
5701
+ * const groups = await client.listModifierGroups('store_123', {
5702
+ * page: 1,
5703
+ * limit: 20,
5704
+ * search: 'pizza',
5705
+ * status: 'active',
5706
+ * });
5707
+ * ```
5708
+ */
5709
+ async listModifierGroups(storeId, params) {
5710
+ return this.adminRequest(
5711
+ "GET",
5712
+ `/api/stores/${storeId}/modifier-groups`,
5713
+ void 0,
5714
+ params
5715
+ );
5716
+ }
5717
+ /**
5718
+ * Fetch a single modifier group (with its modifiers).
5719
+ * Requires Admin mode (apiKey).
5720
+ *
5721
+ * Admin fetches include `internalName`; storefront responses strip it
5722
+ * (PRD §5.2 invariant).
5723
+ */
5724
+ async getModifierGroup(storeId, groupId) {
5725
+ return this.adminRequest(
5726
+ "GET",
5727
+ `/api/stores/${storeId}/modifier-groups/${groupId}`
5728
+ );
5729
+ }
5730
+ /**
5731
+ * Create a modifier group. Modifiers themselves are created separately via
5732
+ * `createModifier(storeId, groupId, …)`.
5733
+ * Requires Admin mode (apiKey).
5734
+ *
5735
+ * @example
5736
+ * ```typescript
5737
+ * const group = await client.createModifierGroup('store_123', {
5738
+ * name: 'Toppings',
5739
+ * internalName: 'Pizza toppings',
5740
+ * selectionType: 'MULTIPLE',
5741
+ * minSelections: 0,
5742
+ * maxSelections: 8,
5743
+ * freeQuantity: 3,
5744
+ * freeAllocationPolicy: 'EXPENSIVE_FREE',
5745
+ * });
5746
+ * ```
5747
+ */
5748
+ async createModifierGroup(storeId, data) {
5749
+ return this.adminRequest("POST", `/api/stores/${storeId}/modifier-groups`, data);
5750
+ }
5751
+ /**
5752
+ * Update a modifier group's metadata or selection rules. Pass `status: 'archived'`
5753
+ * to soft-delete (the group becomes hidden from product attachments).
5754
+ * Requires Admin mode (apiKey).
5755
+ */
5756
+ async updateModifierGroup(storeId, groupId, data) {
5757
+ return this.adminRequest(
5758
+ "PATCH",
5759
+ `/api/stores/${storeId}/modifier-groups/${groupId}`,
5760
+ data
5761
+ );
5762
+ }
5763
+ /**
5764
+ * Hard-delete a modifier group. Server rejects deletion when the group is
5765
+ * still attached to any product (detach the attachments first, or use
5766
+ * `updateModifierGroup(..., { status: 'archived' })` to keep the row).
5767
+ * Requires Admin mode (apiKey).
5768
+ */
5769
+ async deleteModifierGroup(storeId, groupId) {
5770
+ await this.adminRequest("DELETE", `/api/stores/${storeId}/modifier-groups/${groupId}`);
5771
+ }
5772
+ /**
5773
+ * Create a modifier inside a group (e.g., "Olives" inside the "Toppings" group).
5774
+ * Requires Admin mode (apiKey).
5775
+ *
5776
+ * `priceDelta` is a decimal string. Negatives are accepted for downsell
5777
+ * modifiers (e.g., `"-2.00"` for "no bread"); the server still enforces
5778
+ * `unitPrice >= 0` at cart-line resolution and rejects negative deltas
5779
+ * combined with a `referencedProductId`.
5780
+ *
5781
+ * @example
5782
+ * ```typescript
5783
+ * const olives = await client.createModifier('store_123', 'mg_toppings', {
5784
+ * name: 'Olives',
5785
+ * priceDelta: '5.00',
5786
+ * isDefault: false,
5787
+ * available: true,
5788
+ * });
5789
+ * ```
5790
+ */
5791
+ async createModifier(storeId, groupId, data) {
5792
+ return this.adminRequest(
5793
+ "POST",
5794
+ `/api/stores/${storeId}/modifier-groups/${groupId}/modifiers`,
5795
+ data
5796
+ );
5797
+ }
5798
+ /**
5799
+ * Update a modifier. Pass `status: 'archived'` to soft-delete.
5800
+ * Requires Admin mode (apiKey).
5801
+ */
5802
+ async updateModifier(storeId, groupId, modifierId, data) {
5803
+ return this.adminRequest(
5804
+ "PATCH",
5805
+ `/api/stores/${storeId}/modifier-groups/${groupId}/modifiers/${modifierId}`,
5806
+ data
5807
+ );
5808
+ }
5809
+ /**
5810
+ * Hard-delete a modifier. Use `updateModifier(..., { status: 'archived' })`
5811
+ * if existing line items / order snapshots reference it.
5812
+ * Requires Admin mode (apiKey).
5813
+ */
5814
+ async deleteModifier(storeId, groupId, modifierId) {
5815
+ await this.adminRequest(
5816
+ "DELETE",
5817
+ `/api/stores/${storeId}/modifier-groups/${groupId}/modifiers/${modifierId}`
5818
+ );
5819
+ }
5820
+ /**
5821
+ * Flip the sold-out toggle on a modifier. Operational endpoint kept separate
5822
+ * from `updateModifier` because the daily ops bar is intentionally lower
5823
+ * (PRD §7.1 — STAFF role once the granular `TOGGLE_MODIFIER_AVAILABILITY`
5824
+ * permission ships).
5825
+ * Requires Admin mode (apiKey).
5826
+ *
5827
+ * @example
5828
+ * ```typescript
5829
+ * // Mark the egg modifier as out of stock during a busy lunch service
5830
+ * await client.toggleModifierAvailability('store_123', 'mg_toppings', 'm_egg', false);
5831
+ * ```
5832
+ */
5833
+ async toggleModifierAvailability(storeId, groupId, modifierId, available) {
5834
+ return this.adminRequest(
5835
+ "POST",
5836
+ `/api/stores/${storeId}/modifier-groups/${groupId}/modifiers/${modifierId}/availability-toggle`,
5837
+ { available }
5838
+ );
5839
+ }
5840
+ /**
5841
+ * Attach a modifier group to a product. Three patterns (PRD §5.4):
5842
+ *
5843
+ * - `variantId` omitted → default attach (applies to all variants).
5844
+ * - `variantId` set + matching default attach already exists → variant override row.
5845
+ * - `variantId` set + no default attach → variant-only group.
5846
+ *
5847
+ * Override fields use `null` to mean inherit; any non-null value (including
5848
+ * `0` or `false`) wins. The disable-for-variant convention is `maxOverride: 0` —
5849
+ * the resolver returns the group with `effectiveMax=0` and the validator
5850
+ * silently skips it on cart add.
5851
+ *
5852
+ * Requires Admin mode (apiKey).
5853
+ */
5854
+ async attachModifierGroup(storeId, productId, data) {
5855
+ return this.adminRequest(
5856
+ "POST",
5857
+ `/api/stores/${storeId}/products/${productId}/modifier-groups`,
5858
+ data
5859
+ );
5860
+ }
5861
+ /**
5862
+ * Update an existing attachment's overrides or position. The `modifierGroupId`
5863
+ * and `variantId` are immutable — to swap groups or move between default /
5864
+ * per-variant rows, detach and re-attach.
5865
+ * Requires Admin mode (apiKey).
5866
+ */
5867
+ async updateAttachment(storeId, productId, attachmentId, data) {
5868
+ return this.adminRequest(
5869
+ "PATCH",
5870
+ `/api/stores/${storeId}/products/${productId}/modifier-groups/${attachmentId}`,
5871
+ data
5872
+ );
5873
+ }
5874
+ /**
5875
+ * Detach a modifier group from a product (or remove a per-variant override row).
5876
+ * Requires Admin mode (apiKey).
5877
+ */
5878
+ async detachModifierGroup(storeId, productId, attachmentId) {
5879
+ await this.adminRequest(
5880
+ "DELETE",
5881
+ `/api/stores/${storeId}/products/${productId}/modifier-groups/${attachmentId}`
5882
+ );
5883
+ }
5509
5884
  // -------------------- Shipping: Zones and Rates (Admin) --------------------
5510
5885
  // These methods require Admin mode (apiKey)
5511
5886
  /**
@@ -5685,7 +6060,10 @@ var BrainerceClient = class {
5685
6060
  type: d.type,
5686
6061
  required: d.required,
5687
6062
  enumValues: d.enumValues || void 0,
5688
- position: d.position
6063
+ position: d.position,
6064
+ isCustomerInput: d.isCustomerInput,
6065
+ appliesToAllProducts: d.appliesToAllProducts,
6066
+ filterable: d.filterable
5689
6067
  }))
5690
6068
  };
5691
6069
  }
@@ -5741,6 +6119,107 @@ var BrainerceClient = class {
5741
6119
  async deleteMetafieldDefinition(definitionId) {
5742
6120
  await this.adminRequest("DELETE", `/api/v1/metafield-definitions/${definitionId}`);
5743
6121
  }
6122
+ /**
6123
+ * Replace the platform publishing configuration on a metafield definition.
6124
+ * `publishedOn` is the source of truth for which sales channels the field
6125
+ * appears on; `platformMetadata` carries the per-platform mapping config
6126
+ * required for sync.
6127
+ *
6128
+ * Requires Admin mode (apiKey).
6129
+ *
6130
+ * @example
6131
+ * ```typescript
6132
+ * await client.setMetafieldPlatforms('def_123', {
6133
+ * publishedOn: ['SHOPIFY', 'WOOCOMMERCE'],
6134
+ * platformMetadata: {
6135
+ * SHOPIFY: { namespace: 'custom', key: 'warranty' },
6136
+ * WOOCOMMERCE: { key: '_warranty_info' },
6137
+ * },
6138
+ * });
6139
+ * ```
6140
+ */
6141
+ async setMetafieldPlatforms(definitionId, data) {
6142
+ return this.adminRequest(
6143
+ "PUT",
6144
+ `/api/v1/metafield-definitions/${definitionId}/platforms`,
6145
+ data
6146
+ );
6147
+ }
6148
+ // -------------- Vibe-coded site publishing (Admin Mode) --------------
6149
+ // Publish/unpublish category/tag/brand/metafield definitions to specific
6150
+ // vibe-coded sites. Mirrors the per-channel control already available for
6151
+ // Products/Coupons. When no publishes exist for an entity, it remains
6152
+ // visible to all vibe-coded sites of the store (legacy default).
6153
+ /**
6154
+ * Publish a metafield definition to a vibe-coded site (admin mode).
6155
+ * @example
6156
+ * ```typescript
6157
+ * await client.publishMetafieldDefinitionToVibeCodedSite('def_123', 'conn_456');
6158
+ * ```
6159
+ */
6160
+ async publishMetafieldDefinitionToVibeCodedSite(definitionId, vibeCodedConnectionId) {
6161
+ return this.adminRequest(
6162
+ "POST",
6163
+ `/api/v1/metafield-definitions/${definitionId}/publish-vibe-coded`,
6164
+ { vibeCodedConnectionId }
6165
+ );
6166
+ }
6167
+ /** Unpublish a metafield definition from a vibe-coded site (admin mode). */
6168
+ async unpublishMetafieldDefinitionFromVibeCodedSite(definitionId, vibeCodedConnectionId) {
6169
+ return this.adminRequest(
6170
+ "POST",
6171
+ `/api/v1/metafield-definitions/${definitionId}/unpublish-vibe-coded`,
6172
+ { vibeCodedConnectionId }
6173
+ );
6174
+ }
6175
+ /** Publish a category to a vibe-coded site (admin mode). */
6176
+ async publishCategoryToVibeCodedSite(categoryId, vibeCodedConnectionId) {
6177
+ return this.adminRequest(
6178
+ "POST",
6179
+ `/api/v1/categories/${categoryId}/publish-vibe-coded`,
6180
+ { vibeCodedConnectionId }
6181
+ );
6182
+ }
6183
+ /** Unpublish a category from a vibe-coded site (admin mode). */
6184
+ async unpublishCategoryFromVibeCodedSite(categoryId, vibeCodedConnectionId) {
6185
+ return this.adminRequest(
6186
+ "POST",
6187
+ `/api/v1/categories/${categoryId}/unpublish-vibe-coded`,
6188
+ { vibeCodedConnectionId }
6189
+ );
6190
+ }
6191
+ /** Publish a tag to a vibe-coded site (admin mode). */
6192
+ async publishTagToVibeCodedSite(tagId, vibeCodedConnectionId) {
6193
+ return this.adminRequest(
6194
+ "POST",
6195
+ `/api/v1/tags/${tagId}/publish-vibe-coded`,
6196
+ { vibeCodedConnectionId }
6197
+ );
6198
+ }
6199
+ /** Unpublish a tag from a vibe-coded site (admin mode). */
6200
+ async unpublishTagFromVibeCodedSite(tagId, vibeCodedConnectionId) {
6201
+ return this.adminRequest(
6202
+ "POST",
6203
+ `/api/v1/tags/${tagId}/unpublish-vibe-coded`,
6204
+ { vibeCodedConnectionId }
6205
+ );
6206
+ }
6207
+ /** Publish a brand to a vibe-coded site (admin mode). */
6208
+ async publishBrandToVibeCodedSite(brandId, vibeCodedConnectionId) {
6209
+ return this.adminRequest(
6210
+ "POST",
6211
+ `/api/v1/brands/${brandId}/publish-vibe-coded`,
6212
+ { vibeCodedConnectionId }
6213
+ );
6214
+ }
6215
+ /** Unpublish a brand from a vibe-coded site (admin mode). */
6216
+ async unpublishBrandFromVibeCodedSite(brandId, vibeCodedConnectionId) {
6217
+ return this.adminRequest(
6218
+ "POST",
6219
+ `/api/v1/brands/${brandId}/unpublish-vibe-coded`,
6220
+ { vibeCodedConnectionId }
6221
+ );
6222
+ }
5744
6223
  /**
5745
6224
  * Replace the list of products a (customer-input) metafield definition is
5746
6225
  * attached to. Diff-scoped: only rows for this definition are touched, so
@@ -6310,6 +6789,55 @@ function createWebhookHandler(handlers) {
6310
6789
  };
6311
6790
  }
6312
6791
 
6792
+ // src/payment-url.ts
6793
+ var ALLOWED_PAYMENT_HOSTS = [
6794
+ // Stripe
6795
+ "checkout.stripe.com",
6796
+ "js.stripe.com",
6797
+ "hooks.stripe.com",
6798
+ // PayPal
6799
+ "www.paypal.com",
6800
+ "www.sandbox.paypal.com",
6801
+ // Cardcom
6802
+ "secure.cardcom.solutions",
6803
+ // Meshulam
6804
+ "meshulam.co.il",
6805
+ // Grow (Linktech)
6806
+ "grow.link",
6807
+ "grow.security",
6808
+ // CreditGuard
6809
+ "creditguard.co.il",
6810
+ // Brainerce-hosted payment embeds (backend payment-embed proxy at
6811
+ // `/api/payment/embed/...` that fronts provider apps' embed shells —
6812
+ // e.g. cardcom-payments OpenFields wrapper). The match also covers
6813
+ // subdomains like `api.brainerce.com`, `staging.brainerce.com`.
6814
+ "brainerce.com"
6815
+ ];
6816
+ function isAllowedPaymentUrl(url, options) {
6817
+ if (!url || typeof url !== "string") return false;
6818
+ let parsed;
6819
+ try {
6820
+ parsed = new URL(url);
6821
+ } catch {
6822
+ return false;
6823
+ }
6824
+ const hostname = parsed.hostname.toLowerCase();
6825
+ if (options?.allowLocalhost && parsed.protocol === "http:" && (hostname === "localhost" || hostname === "127.0.0.1")) {
6826
+ return true;
6827
+ }
6828
+ if (parsed.protocol !== "https:") return false;
6829
+ const allowed = options?.extraHosts ? [...ALLOWED_PAYMENT_HOSTS, ...options.extraHosts] : ALLOWED_PAYMENT_HOSTS;
6830
+ return allowed.some((host) => hostname === host || hostname.endsWith("." + host));
6831
+ }
6832
+ function safePaymentRedirect(url, options) {
6833
+ if (!isAllowedPaymentUrl(url, options)) {
6834
+ throw new Error("Payment redirect URL is not in the allowlist");
6835
+ }
6836
+ if (typeof window !== "undefined") {
6837
+ window.location.href = url;
6838
+ }
6839
+ }
6840
+
6313
6841
  // src/types.ts
6314
6842
  function isHtmlDescription(product) {
6315
6843
  if (product?.descriptionFormat === "html") return true;
@@ -6549,9 +7077,11 @@ export {
6549
7077
  getStockStatus,
6550
7078
  getVariantOptions,
6551
7079
  getVariantPrice,
7080
+ isAllowedPaymentUrl,
6552
7081
  isCouponApplicableToProduct,
6553
7082
  isHtmlDescription,
6554
7083
  isWebhookEventType,
6555
7084
  parseWebhookEvent,
7085
+ safePaymentRedirect,
6556
7086
  verifyWebhook
6557
7087
  };