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