brainerce 1.20.2 → 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
@@ -115,7 +115,7 @@ function isDevGuardsEnabled() {
115
115
  }
116
116
 
117
117
  // src/version.ts
118
- var SDK_VERSION = "1.11.2";
118
+ var SDK_VERSION = "1.21.0";
119
119
 
120
120
  // src/client.ts
121
121
  var DEFAULT_BASE_URL = "https://api.brainerce.com";
@@ -141,27 +141,87 @@ var BrainerceClient = class {
141
141
  * This is needed because Stripe redirects lose in-memory state.
142
142
  */
143
143
  this.ACTIVE_CHECKOUT_KEY = "brainerce_active_checkout";
144
+ // -------------------- Contact Forms (schema) --------------------
145
+ /**
146
+ * List active contact forms configured for the store.
147
+ *
148
+ * Storefront (public) mode only. Useful when your site has multiple
149
+ * forms (e.g. "main", "newsletter", "whatsapp_prechat") and you need
150
+ * to pick the right one at render time.
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * const forms = await brainerce.contactForms.list();
155
+ * // → [{ key: 'main', name: 'Main Contact', isDefault: true }, ...]
156
+ * ```
157
+ */
158
+ this.contactForms = {
159
+ list: async () => {
160
+ if (this.isVibeCodedMode()) {
161
+ return this.vibeCodedRequest("GET", "/contact-forms");
162
+ }
163
+ return this.storefrontRequest("GET", "/contact-forms");
164
+ },
165
+ /**
166
+ * Fetch the full schema for a single contact form, including all
167
+ * visible fields + their localized labels/placeholders/help text.
168
+ *
169
+ * `locale` is the storefront locale at render time (e.g. `"he"`).
170
+ * Falls back to the store's default language when omitted.
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * const form = await brainerce.contactForms.get('main', 'he');
175
+ * // Render form.fields; submit via brainerce.createInquiry({ formKey: 'main', fields })
176
+ * ```
177
+ */
178
+ get: async (formKey = "main", locale) => {
179
+ const query = locale ? { locale } : void 0;
180
+ if (this.isVibeCodedMode()) {
181
+ return this.vibeCodedRequest(
182
+ "GET",
183
+ `/contact-forms/${encodeURIComponent(formKey)}`,
184
+ void 0,
185
+ query
186
+ );
187
+ }
188
+ return this.storefrontRequest(
189
+ "GET",
190
+ `/contact-forms/${encodeURIComponent(formKey)}`,
191
+ void 0,
192
+ query
193
+ );
194
+ }
195
+ };
144
196
  // -------------------- Local Cart (Client-Side for Guests) --------------------
145
197
  // These methods store cart data in localStorage - NO API calls!
146
198
  // Use for guest users in vibe-coded sites
147
199
  this.LOCAL_CART_KEY = "brainerce_cart";
148
- if (!options.apiKey && !options.storeId && !options.connectionId) {
149
- 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
+ );
150
205
  }
151
206
  if (options.apiKey && !options.apiKey.startsWith("brainerce_")) {
152
207
  console.warn('BrainerceClient: apiKey should start with "brainerce_"');
153
208
  }
154
- if (options.connectionId && !options.connectionId.startsWith("vc_")) {
155
- 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
+ );
156
216
  }
157
217
  if (options.apiKey && typeof window !== "undefined") {
158
218
  console.warn(
159
- "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."
160
220
  );
161
221
  }
162
222
  this.apiKey = options.apiKey;
163
223
  this.storeId = options.storeId;
164
- this.connectionId = options.connectionId;
224
+ this.connectionId = resolvedSalesChannelId;
165
225
  let resolvedBase = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
166
226
  if (resolvedBase.startsWith("/") && typeof window !== "undefined" && window.location?.origin) {
167
227
  resolvedBase = window.location.origin + resolvedBase;
@@ -248,11 +308,17 @@ var BrainerceClient = class {
248
308
  }
249
309
  // -------------------- Mode Detection --------------------
250
310
  /**
251
- * Check if client is in vibe-coded mode (using connectionId)
311
+ * Check if client is in sales-channel mode (using salesChannelId / legacy connectionId).
252
312
  */
253
- isVibeCodedMode() {
313
+ isSalesChannelMode() {
254
314
  return !!this.connectionId && !this.apiKey;
255
315
  }
316
+ /**
317
+ * @deprecated Use `isSalesChannelMode()` instead. Kept as a backwards-compatible alias.
318
+ */
319
+ isVibeCodedMode() {
320
+ return this.isSalesChannelMode();
321
+ }
256
322
  /**
257
323
  * Check if client is in storefront mode (using storeId)
258
324
  */
@@ -564,6 +630,18 @@ var BrainerceClient = class {
564
630
  const categories = Array.isArray(params?.categories) ? params.categories.join(",") : params?.categories;
565
631
  const brands = Array.isArray(params?.brands) ? params.brands.join(",") : params?.brands;
566
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
+ }
567
645
  const queryParams = {
568
646
  page: params?.page,
569
647
  limit: params?.limit,
@@ -574,6 +652,7 @@ var BrainerceClient = class {
574
652
  tags,
575
653
  minPrice: params?.minPrice,
576
654
  maxPrice: params?.maxPrice,
655
+ metafields,
577
656
  sortBy: params?.sortBy,
578
657
  sortOrder: params?.sortOrder,
579
658
  // Admin-only params
@@ -1503,6 +1582,58 @@ var BrainerceClient = class {
1503
1582
  async getCustomerByEmail(email) {
1504
1583
  return this.request("GET", "/api/v1/customers/by-email", void 0, { email });
1505
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
+ }
1506
1637
  /**
1507
1638
  * Login an existing customer (returns JWT token)
1508
1639
  * Works in vibe-coded, storefront, and admin mode
@@ -2009,7 +2140,7 @@ var BrainerceClient = class {
2009
2140
  * per IP on the server. Include an empty `honeypot` field on your form
2010
2141
  * and do NOT send it — bots that auto-fill every input will be rejected.
2011
2142
  *
2012
- * @example
2143
+ * **Legacy shape (kept working forever):**
2013
2144
  * ```typescript
2014
2145
  * await brainerce.createInquiry({
2015
2146
  * name: 'Jane Doe',
@@ -2019,6 +2150,16 @@ var BrainerceClient = class {
2019
2150
  * phone: '+1-555-0100',
2020
2151
  * });
2021
2152
  * ```
2153
+ *
2154
+ * **Phase 2 (multi-form / custom fields):**
2155
+ * ```typescript
2156
+ * const form = await brainerce.contactForms.get('newsletter');
2157
+ * await brainerce.createInquiry({
2158
+ * formKey: 'newsletter',
2159
+ * fields: { email: 'jane@example.com', source: 'homepage' },
2160
+ * locale: 'he',
2161
+ * });
2162
+ * ```
2022
2163
  */
2023
2164
  async createInquiry(input) {
2024
2165
  if (this.isVibeCodedMode()) {
@@ -2345,6 +2486,82 @@ var BrainerceClient = class {
2345
2486
  "cart"
2346
2487
  );
2347
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
+ }
2348
2565
  /**
2349
2566
  * Link a cart to the currently logged-in customer.
2350
2567
  * Use this after customer logs in to associate their guest cart with their account.
@@ -2585,7 +2802,8 @@ var BrainerceClient = class {
2585
2802
  * ```typescript
2586
2803
  * const { bundles } = await client.getCartBundles('cart_123');
2587
2804
  * bundles.forEach(b => {
2588
- * 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}`);
2589
2807
  * });
2590
2808
  * ```
2591
2809
  */
@@ -2648,24 +2866,31 @@ var BrainerceClient = class {
2648
2866
  throw new BrainerceError("removeOrderBump() requires vibe-coded or storefront mode", 400);
2649
2867
  }
2650
2868
  /**
2651
- * 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.
2652
2871
  *
2653
2872
  * @param cartId - Cart ID
2654
2873
  * @param bundleOfferId - Bundle offer ID
2655
- * @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
2656
2875
  * @returns Updated cart
2657
2876
  *
2658
2877
  * @example
2659
2878
  * ```typescript
2660
2879
  * const { bundles } = await client.getCartBundles('cart_123');
2661
- * // Simple product or locked variant:
2662
- * const cart = await client.addBundleToCart('cart_123', bundles[0].id);
2663
- * // Product with variants (customer selects):
2664
- * 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
+ * });
2665
2887
  * ```
2666
2888
  */
2667
- async addBundleToCart(cartId, bundleOfferId, variantId) {
2668
- 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
+ };
2669
2894
  if (this.isVibeCodedMode()) {
2670
2895
  return this.vibeCodedRequest("POST", `/cart/${cartId}/bundle`, body);
2671
2896
  }
@@ -2781,6 +3006,9 @@ var BrainerceClient = class {
2781
3006
  couponCode: null,
2782
3007
  items: [],
2783
3008
  itemCount: 0,
3009
+ hasPriceChanges: false,
3010
+ hasUnavailableItems: false,
3011
+ unavailableItemIds: [],
2784
3012
  expiresAt: null,
2785
3013
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2786
3014
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -3017,23 +3245,21 @@ var BrainerceClient = class {
3017
3245
  * ```
3018
3246
  */
3019
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
+ };
3020
3256
  if (this.isCustomerLoggedIn()) {
3021
3257
  const cart = await this.getOrCreateCustomerCart();
3022
- const updated = await this.addToCart(cart.id, {
3023
- productId: item.productId,
3024
- variantId: item.variantId,
3025
- quantity: item.quantity,
3026
- metadata: item.metadata
3027
- });
3258
+ const updated = await this.addToCart(cart.id, payload);
3028
3259
  return updated;
3029
3260
  } else {
3030
3261
  const cart = await this.getOrCreateSessionCart();
3031
- const updated = await this.addToCart(cart.id, {
3032
- productId: item.productId,
3033
- variantId: item.variantId,
3034
- quantity: item.quantity,
3035
- metadata: item.metadata
3036
- });
3262
+ const updated = await this.addToCart(cart.id, payload);
3037
3263
  this.updateSessionCartItemCount(updated.items?.length ?? 0);
3038
3264
  return updated;
3039
3265
  }
@@ -4076,6 +4302,16 @@ var BrainerceClient = class {
4076
4302
  variantId: item.variantId || null,
4077
4303
  quantity: item.quantity,
4078
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,
4079
4315
  discountAmount: "0",
4080
4316
  promoDiscountAmount: "0",
4081
4317
  promoSource: null,
@@ -4098,6 +4334,9 @@ var BrainerceClient = class {
4098
4334
  updatedAt: item.addedAt
4099
4335
  })),
4100
4336
  itemCount: localCart.items.reduce((sum, i) => sum + i.quantity, 0),
4337
+ hasPriceChanges: false,
4338
+ hasUnavailableItems: false,
4339
+ unavailableItemIds: [],
4101
4340
  expiresAt: null,
4102
4341
  createdAt: localCart.updatedAt || (/* @__PURE__ */ new Date()).toISOString(),
4103
4342
  updatedAt: localCart.updatedAt || (/* @__PURE__ */ new Date()).toISOString()
@@ -5444,6 +5683,204 @@ var BrainerceClient = class {
5444
5683
  `/api/v1/attributes/${attributeId}/options/${optionId}`
5445
5684
  );
5446
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
+ }
5447
5884
  // -------------------- Shipping: Zones and Rates (Admin) --------------------
5448
5885
  // These methods require Admin mode (apiKey)
5449
5886
  /**
@@ -5623,7 +6060,10 @@ var BrainerceClient = class {
5623
6060
  type: d.type,
5624
6061
  required: d.required,
5625
6062
  enumValues: d.enumValues || void 0,
5626
- position: d.position
6063
+ position: d.position,
6064
+ isCustomerInput: d.isCustomerInput,
6065
+ appliesToAllProducts: d.appliesToAllProducts,
6066
+ filterable: d.filterable
5627
6067
  }))
5628
6068
  };
5629
6069
  }
@@ -5679,6 +6119,107 @@ var BrainerceClient = class {
5679
6119
  async deleteMetafieldDefinition(definitionId) {
5680
6120
  await this.adminRequest("DELETE", `/api/v1/metafield-definitions/${definitionId}`);
5681
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
+ }
5682
6223
  /**
5683
6224
  * Replace the list of products a (customer-input) metafield definition is
5684
6225
  * attached to. Diff-scoped: only rows for this definition are touched, so
@@ -6248,6 +6789,55 @@ function createWebhookHandler(handlers) {
6248
6789
  };
6249
6790
  }
6250
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
+
6251
6841
  // src/types.ts
6252
6842
  function isHtmlDescription(product) {
6253
6843
  if (product?.descriptionFormat === "html") return true;
@@ -6487,9 +7077,11 @@ export {
6487
7077
  getStockStatus,
6488
7078
  getVariantOptions,
6489
7079
  getVariantPrice,
7080
+ isAllowedPaymentUrl,
6490
7081
  isCouponApplicableToProduct,
6491
7082
  isHtmlDescription,
6492
7083
  isWebhookEventType,
6493
7084
  parseWebhookEvent,
7085
+ safePaymentRedirect,
6494
7086
  verifyWebhook
6495
7087
  };