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/README.md +4662 -4525
- package/dist/index.d.mts +1013 -51
- package/dist/index.d.ts +1013 -51
- package/dist/index.js +562 -30
- package/dist/index.mjs +560 -30
- package/package.json +76 -76
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
|
-
|
|
201
|
-
|
|
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 (
|
|
207
|
-
console.warn('BrainerceClient:
|
|
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
|
|
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 =
|
|
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
|
|
311
|
+
* Check if client is in sales-channel mode (using salesChannelId / legacy connectionId).
|
|
304
312
|
*/
|
|
305
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
2724
|
-
*
|
|
2725
|
-
*
|
|
2726
|
-
*
|
|
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,
|
|
2730
|
-
const body = {
|
|
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
|
};
|