brainerce 1.55.0 → 1.57.0
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 +83 -7
- package/dist/index.d.mts +201 -2
- package/dist/index.d.ts +201 -2
- package/dist/index.js +233 -12
- package/dist/index.mjs +232 -12
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -212,6 +212,14 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
212
212
|
* This is needed because Stripe redirects lose in-memory state.
|
|
213
213
|
*/
|
|
214
214
|
this.ACTIVE_CHECKOUT_KEY = "brainerce_active_checkout";
|
|
215
|
+
/**
|
|
216
|
+
* Merge the resolved GA4 stitch ids onto a request body — only for fields
|
|
217
|
+
* the caller didn't already set explicitly (explicit values always win).
|
|
218
|
+
* No-op (returns `dto` unchanged) if `loadGoogleAnalytics()` was never
|
|
219
|
+
* called, or if it hasn't resolved any ids by the time this is awaited.
|
|
220
|
+
*/
|
|
221
|
+
/** localStorage key holding the last non-direct-touch attribution blob. */
|
|
222
|
+
this.TRAFFIC_ATTR_KEY = "brainerce_attr";
|
|
215
223
|
// -------------------- Contact Forms (schema) --------------------
|
|
216
224
|
/**
|
|
217
225
|
* List active contact forms configured for the store.
|
|
@@ -617,6 +625,7 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
617
625
|
this.onCartReset = options.onCartReset;
|
|
618
626
|
this.hydrateSessionCart();
|
|
619
627
|
this.detectRecoverCartFromUrl();
|
|
628
|
+
this.captureTrafficAttribution();
|
|
620
629
|
}
|
|
621
630
|
// -------------------- Locale --------------------
|
|
622
631
|
/**
|
|
@@ -1439,11 +1448,82 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
1439
1448
|
}
|
|
1440
1449
|
}
|
|
1441
1450
|
/**
|
|
1442
|
-
*
|
|
1443
|
-
*
|
|
1444
|
-
*
|
|
1445
|
-
*
|
|
1451
|
+
* Record the visit's traffic origin — LAST NON-DIRECT TOUCH semantics: an
|
|
1452
|
+
* external referrer or any utm_source overwrites the stored blob; a direct
|
|
1453
|
+
* or internal navigation keeps the previous touch. Runs once per client
|
|
1454
|
+
* construction (i.e. per page load in browser storefronts) and must never
|
|
1455
|
+
* throw — attribution is telemetry, the storefront always wins.
|
|
1456
|
+
*/
|
|
1457
|
+
captureTrafficAttribution() {
|
|
1458
|
+
try {
|
|
1459
|
+
if (typeof window === "undefined" || !window.localStorage) return;
|
|
1460
|
+
const params = new URLSearchParams(window.location.search);
|
|
1461
|
+
const utmSource = params.get("utm_source") || void 0;
|
|
1462
|
+
const utmMedium = params.get("utm_medium") || void 0;
|
|
1463
|
+
const utmCampaign = params.get("utm_campaign") || void 0;
|
|
1464
|
+
let referrerHost;
|
|
1465
|
+
if (document.referrer) {
|
|
1466
|
+
try {
|
|
1467
|
+
const ref = new URL(document.referrer);
|
|
1468
|
+
if (ref.hostname && ref.hostname !== window.location.hostname) {
|
|
1469
|
+
referrerHost = ref.hostname.toLowerCase().replace(/^www\./, "");
|
|
1470
|
+
}
|
|
1471
|
+
} catch {
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
if (!referrerHost && !utmSource) return;
|
|
1475
|
+
window.localStorage.setItem(
|
|
1476
|
+
this.TRAFFIC_ATTR_KEY,
|
|
1477
|
+
JSON.stringify({ referrerHost, utmSource, utmMedium, utmCampaign, at: Date.now() })
|
|
1478
|
+
);
|
|
1479
|
+
} catch {
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
/** The stored attribution as request-body fields, or null when absent/stale. */
|
|
1483
|
+
getTrafficAttribution() {
|
|
1484
|
+
try {
|
|
1485
|
+
if (typeof window === "undefined" || !window.localStorage) return null;
|
|
1486
|
+
const raw = window.localStorage.getItem(this.TRAFFIC_ATTR_KEY);
|
|
1487
|
+
if (!raw) return null;
|
|
1488
|
+
const blob = JSON.parse(raw);
|
|
1489
|
+
if (!blob || typeof blob !== "object") return null;
|
|
1490
|
+
if (typeof blob.at !== "number" || Date.now() - blob.at > _BrainerceClient.TRAFFIC_ATTR_MAX_AGE_MS) {
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
const out = {};
|
|
1494
|
+
if (typeof blob.referrerHost === "string" && blob.referrerHost) {
|
|
1495
|
+
out.trafficReferrerHost = blob.referrerHost.slice(0, 253);
|
|
1496
|
+
}
|
|
1497
|
+
if (typeof blob.utmSource === "string" && blob.utmSource) {
|
|
1498
|
+
out.trafficUtmSource = blob.utmSource.slice(0, 150);
|
|
1499
|
+
}
|
|
1500
|
+
if (typeof blob.utmMedium === "string" && blob.utmMedium) {
|
|
1501
|
+
out.trafficUtmMedium = blob.utmMedium.slice(0, 150);
|
|
1502
|
+
}
|
|
1503
|
+
if (typeof blob.utmCampaign === "string" && blob.utmCampaign) {
|
|
1504
|
+
out.trafficUtmCampaign = blob.utmCampaign.slice(0, 150);
|
|
1505
|
+
}
|
|
1506
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
1507
|
+
} catch {
|
|
1508
|
+
return null;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Merge the stored traffic attribution onto a request body — only for
|
|
1513
|
+
* fields the caller didn't set explicitly (explicit values always win),
|
|
1514
|
+
* mirroring `withAnalyticsStitchIds`. No-op outside the browser.
|
|
1446
1515
|
*/
|
|
1516
|
+
withTrafficAttribution(dto) {
|
|
1517
|
+
const attr = this.getTrafficAttribution();
|
|
1518
|
+
if (!attr) return dto;
|
|
1519
|
+
return {
|
|
1520
|
+
...dto ?? {},
|
|
1521
|
+
trafficReferrerHost: dto?.trafficReferrerHost ?? attr.trafficReferrerHost,
|
|
1522
|
+
trafficUtmSource: dto?.trafficUtmSource ?? attr.trafficUtmSource,
|
|
1523
|
+
trafficUtmMedium: dto?.trafficUtmMedium ?? attr.trafficUtmMedium,
|
|
1524
|
+
trafficUtmCampaign: dto?.trafficUtmCampaign ?? attr.trafficUtmCampaign
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1447
1527
|
async withAnalyticsStitchIds(dto) {
|
|
1448
1528
|
if (!this._ga4StitchPromise) return dto;
|
|
1449
1529
|
try {
|
|
@@ -1567,6 +1647,50 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
1567
1647
|
queryParamsWithRegion
|
|
1568
1648
|
);
|
|
1569
1649
|
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Lightweight product rows for sitemap generation: `slug`, `updatedAt`, and
|
|
1652
|
+
* per-locale `localeSlugs` only — up to `limit` (max 5000) in ONE request,
|
|
1653
|
+
* with none of the 100-per-page clamp the full listing applies.
|
|
1654
|
+
*
|
|
1655
|
+
* Sales-channel (`salesChannelId`) mode only; other modes throw so
|
|
1656
|
+
* {@link getProductSitemapEntries} (sitemap.ts) can catch and fall back to
|
|
1657
|
+
* paginating `getProducts`. Prefer that helper over calling this directly.
|
|
1658
|
+
*/
|
|
1659
|
+
async getSitemapProducts(limit = 5e3) {
|
|
1660
|
+
if (!this.isVibeCodedMode()) {
|
|
1661
|
+
throw new Error("getSitemapProducts is available in salesChannelId mode only");
|
|
1662
|
+
}
|
|
1663
|
+
const res = await this.vibeCodedRequest("GET", "/sitemap-products", void 0, { limit: Math.min(limit, 5e3) });
|
|
1664
|
+
return res.data;
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Resolve an old (renamed) slug to the entity's CURRENT slug, so the
|
|
1668
|
+
* storefront can issue a permanent (301/308) redirect instead of a 404.
|
|
1669
|
+
*
|
|
1670
|
+
* Call this in the catch/not-found path of a product or blog page:
|
|
1671
|
+
* the platform records every slug rename, so a URL that stopped matching
|
|
1672
|
+
* usually has a redirect. Returns `null` when there is no redirect (real
|
|
1673
|
+
* 404) or outside sales-channel mode — always fall through to notFound().
|
|
1674
|
+
*
|
|
1675
|
+
* @example
|
|
1676
|
+
* ```typescript
|
|
1677
|
+
* // app/products/[slug]/page.tsx — in the catch path:
|
|
1678
|
+
* const redirect = await client.resolveSlugRedirect('product', slug);
|
|
1679
|
+
* if (redirect) permanentRedirect(`/products/${redirect.currentSlug}`);
|
|
1680
|
+
* notFound();
|
|
1681
|
+
* ```
|
|
1682
|
+
*/
|
|
1683
|
+
async resolveSlugRedirect(entityType, slug) {
|
|
1684
|
+
if (!this.isVibeCodedMode()) return null;
|
|
1685
|
+
try {
|
|
1686
|
+
return await this.vibeCodedRequest(
|
|
1687
|
+
"GET",
|
|
1688
|
+
`/slug-redirects/${entityType}/${encodePathSegment(slug)}`
|
|
1689
|
+
);
|
|
1690
|
+
} catch {
|
|
1691
|
+
return null;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1570
1694
|
/**
|
|
1571
1695
|
* Get a single product by ID
|
|
1572
1696
|
* Works in vibe-coded, storefront (public), and admin mode
|
|
@@ -5256,7 +5380,7 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
5256
5380
|
* ```
|
|
5257
5381
|
*/
|
|
5258
5382
|
async setCheckoutCustomer(checkoutId, data) {
|
|
5259
|
-
const body = await this.withAnalyticsStitchIds(data);
|
|
5383
|
+
const body = this.withTrafficAttribution(await this.withAnalyticsStitchIds(data));
|
|
5260
5384
|
if (this.isVibeCodedMode()) {
|
|
5261
5385
|
return this.vibeCodedRequest(
|
|
5262
5386
|
"PATCH",
|
|
@@ -5358,7 +5482,9 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
5358
5482
|
* ```
|
|
5359
5483
|
*/
|
|
5360
5484
|
async setShippingAddress(checkoutId, address) {
|
|
5361
|
-
const body =
|
|
5485
|
+
const body = this.withTrafficAttribution(
|
|
5486
|
+
await this.withAnalyticsStitchIds(this.stripResolvedOnlyAddressFields(address))
|
|
5487
|
+
);
|
|
5362
5488
|
if (this.isVibeCodedMode()) {
|
|
5363
5489
|
return this.vibeCodedRequest(
|
|
5364
5490
|
"PATCH",
|
|
@@ -8859,6 +8985,41 @@ var _BrainerceClient = class _BrainerceClient {
|
|
|
8859
8985
|
{ salesChannelId }
|
|
8860
8986
|
);
|
|
8861
8987
|
}
|
|
8988
|
+
/**
|
|
8989
|
+
* Attach a customer to a sales channel (admin mode) — marks them as active in
|
|
8990
|
+
* that storefront. Accepts the sales-channel record ID or its public `vc_*`
|
|
8991
|
+
* connection ID.
|
|
8992
|
+
*
|
|
8993
|
+
* Rarely needed: the platform records a channel by itself whenever the
|
|
8994
|
+
* customer registers, signs in or checks out on it. Use this for migrations
|
|
8995
|
+
* from another system and for fixing up records you created yourself. A
|
|
8996
|
+
* customer belongs to one store but can be active in any number of its
|
|
8997
|
+
* channels, so calling this for several channels is normal and expected.
|
|
8998
|
+
*
|
|
8999
|
+
* This does not change where the customer CAME FROM — for that, pass
|
|
9000
|
+
* `acquisitionSalesChannelId` to {@link updateCustomer}.
|
|
9001
|
+
*/
|
|
9002
|
+
async publishCustomerToSalesChannel(customerId, salesChannelId) {
|
|
9003
|
+
return this.adminRequest(
|
|
9004
|
+
"POST",
|
|
9005
|
+
`/api/v1/customers/${encodePathSegment(customerId)}/publish-sales-channel`,
|
|
9006
|
+
{ salesChannelId }
|
|
9007
|
+
);
|
|
9008
|
+
}
|
|
9009
|
+
/**
|
|
9010
|
+
* Detach a customer from a sales channel (admin mode).
|
|
9011
|
+
*
|
|
9012
|
+
* A correction, NOT a block — it does not stop that person from buying on
|
|
9013
|
+
* that storefront, and the channel is recorded again the next time they sign
|
|
9014
|
+
* in or order there. There is no way to bar a customer from a channel.
|
|
9015
|
+
*/
|
|
9016
|
+
async unpublishCustomerFromSalesChannel(customerId, salesChannelId) {
|
|
9017
|
+
return this.adminRequest(
|
|
9018
|
+
"POST",
|
|
9019
|
+
`/api/v1/customers/${encodePathSegment(customerId)}/unpublish-sales-channel`,
|
|
9020
|
+
{ salesChannelId }
|
|
9021
|
+
);
|
|
9022
|
+
}
|
|
8862
9023
|
/**
|
|
8863
9024
|
* Publish a coupon to a sales channel (admin mode) — makes it redeemable on
|
|
8864
9025
|
* that vibe-coded storefront. Accepts the sales-channel record ID or its
|
|
@@ -9550,6 +9711,8 @@ _BrainerceClient.RESOLVED_ONLY_ADDRESS_FIELDS = [
|
|
|
9550
9711
|
"lng",
|
|
9551
9712
|
"formattedAddress"
|
|
9552
9713
|
];
|
|
9714
|
+
/** Attribution older than this is stale and never forwarded (classic 30-day window). */
|
|
9715
|
+
_BrainerceClient.TRAFFIC_ATTR_MAX_AGE_MS = 30 * 24 * 3600 * 1e3;
|
|
9553
9716
|
var BrainerceClient = _BrainerceClient;
|
|
9554
9717
|
var BrainerceError = class extends Error {
|
|
9555
9718
|
constructor(message, statusCode, details) {
|
|
@@ -10050,7 +10213,8 @@ function buildProductJsonLd(product, opts) {
|
|
|
10050
10213
|
const brand = opts.brandName ?? product.brands?.[0]?.name;
|
|
10051
10214
|
const description = stripHtml(product.description).slice(0, 5e3);
|
|
10052
10215
|
const effectivePrice = product.salePrice ?? product.basePrice;
|
|
10053
|
-
const
|
|
10216
|
+
const inv = product.inventory;
|
|
10217
|
+
const availability = !inv ? "https://schema.org/InStock" : inv.inStock ?? (inv.available ?? 0) > 0 ? "https://schema.org/InStock" : inv.canPurchase ? "https://schema.org/BackOrder" : "https://schema.org/OutOfStock";
|
|
10054
10218
|
const isVariable = product.type === "VARIABLE" && product.priceMin && product.priceMax;
|
|
10055
10219
|
const itemCondition = "https://schema.org/NewCondition";
|
|
10056
10220
|
const shippingDetails = (opts.shipping ?? []).filter((z) => z.amount !== null).map((z) => ({
|
|
@@ -10081,8 +10245,9 @@ function buildProductJsonLd(product, opts) {
|
|
|
10081
10245
|
"@type": "AggregateOffer",
|
|
10082
10246
|
lowPrice: product.priceMin,
|
|
10083
10247
|
highPrice: product.priceMax,
|
|
10248
|
+
...product.variants?.length ? { offerCount: product.variants.length } : {},
|
|
10084
10249
|
priceCurrency: opts.currency,
|
|
10085
|
-
availability
|
|
10250
|
+
availability,
|
|
10086
10251
|
itemCondition,
|
|
10087
10252
|
...shippingDetails.length > 0 ? { shippingDetails } : {},
|
|
10088
10253
|
...url ? { url } : {}
|
|
@@ -10090,7 +10255,7 @@ function buildProductJsonLd(product, opts) {
|
|
|
10090
10255
|
"@type": "Offer",
|
|
10091
10256
|
price: effectivePrice,
|
|
10092
10257
|
priceCurrency: opts.currency,
|
|
10093
|
-
availability
|
|
10258
|
+
availability,
|
|
10094
10259
|
itemCondition,
|
|
10095
10260
|
// Only meaningful for an active sale price with a known end date —
|
|
10096
10261
|
// a regular (non-sale) price has no expiry to declare.
|
|
@@ -10105,19 +10270,25 @@ function buildProductJsonLd(product, opts) {
|
|
|
10105
10270
|
...description ? { description } : {},
|
|
10106
10271
|
...images.length > 0 ? { image: images } : {},
|
|
10107
10272
|
...url ? { url } : {},
|
|
10108
|
-
|
|
10273
|
+
// Fall back to the product id so the offer always carries a stable SKU —
|
|
10274
|
+
// Merchant Center matching and review aggregation both key on it.
|
|
10275
|
+
sku: product.sku || product.id,
|
|
10109
10276
|
// Identifiers Google uses to match a product to its catalog — key for
|
|
10110
10277
|
// free merchant-listing eligibility without a Merchant Center feed.
|
|
10111
10278
|
...product.gtin ? { gtin: product.gtin } : {},
|
|
10112
10279
|
...product.mpn ? { mpn: product.mpn } : {},
|
|
10113
10280
|
...brand ? { brand: { "@type": "Brand", name: brand } } : {},
|
|
10114
10281
|
offers: offer,
|
|
10115
|
-
// Google policy: never emit an empty/zero rating block.
|
|
10282
|
+
// Google policy: never emit an empty/zero rating block. bestRating /
|
|
10283
|
+
// worstRating make the 1-5 scale explicit so aggregators can't misread
|
|
10284
|
+
// a 4.8 on an assumed 0-10 scale.
|
|
10116
10285
|
...product.reviewCount && product.reviewCount > 0 && product.avgRating ? {
|
|
10117
10286
|
aggregateRating: {
|
|
10118
10287
|
"@type": "AggregateRating",
|
|
10119
10288
|
ratingValue: product.avgRating,
|
|
10120
|
-
reviewCount: product.reviewCount
|
|
10289
|
+
reviewCount: product.reviewCount,
|
|
10290
|
+
bestRating: 5,
|
|
10291
|
+
worstRating: 1
|
|
10121
10292
|
}
|
|
10122
10293
|
} : {}
|
|
10123
10294
|
};
|
|
@@ -10228,6 +10399,54 @@ async function getBlogSitemapEntries(client, opts) {
|
|
|
10228
10399
|
}
|
|
10229
10400
|
return entries;
|
|
10230
10401
|
}
|
|
10402
|
+
async function getProductSitemapEntries(client, opts) {
|
|
10403
|
+
const base = opts.siteUrl.replace(/\/+$/, "");
|
|
10404
|
+
const basePath = opts.basePath ?? "/products";
|
|
10405
|
+
const pageSize = Math.min(opts.pageSize ?? 100, 100);
|
|
10406
|
+
const maxEntries = opts.maxEntries ?? 5e3;
|
|
10407
|
+
let rows = [];
|
|
10408
|
+
try {
|
|
10409
|
+
rows = await client.getSitemapProducts(maxEntries);
|
|
10410
|
+
} catch {
|
|
10411
|
+
let page = 1;
|
|
10412
|
+
for (; ; ) {
|
|
10413
|
+
const res = await client.getProducts({ page, limit: pageSize });
|
|
10414
|
+
rows.push(
|
|
10415
|
+
...res.data.map((p) => ({
|
|
10416
|
+
slug: p.slug ?? null,
|
|
10417
|
+
id: p.id,
|
|
10418
|
+
updatedAt: p.updatedAt,
|
|
10419
|
+
localeSlugs: p.localeSlugs ?? null
|
|
10420
|
+
}))
|
|
10421
|
+
);
|
|
10422
|
+
if (page >= res.meta.totalPages || rows.length >= maxEntries) break;
|
|
10423
|
+
page += 1;
|
|
10424
|
+
}
|
|
10425
|
+
}
|
|
10426
|
+
const nonDefaultLocales = opts.locales?.filter((locale) => locale !== opts.defaultLocale) ?? [];
|
|
10427
|
+
const entries = [];
|
|
10428
|
+
for (const row of rows.slice(0, maxEntries)) {
|
|
10429
|
+
const baseSlug = row.slug || row.id;
|
|
10430
|
+
if (!baseSlug) continue;
|
|
10431
|
+
const lastModified = row.updatedAt ? new Date(row.updatedAt) : void 0;
|
|
10432
|
+
const localeSlugs = row.localeSlugs ?? {};
|
|
10433
|
+
entries.push({
|
|
10434
|
+
url: `${base}${basePath}/${baseSlug}`,
|
|
10435
|
+
...lastModified ? { lastModified } : {},
|
|
10436
|
+
changeFrequency: "daily",
|
|
10437
|
+
priority: 0.8
|
|
10438
|
+
});
|
|
10439
|
+
for (const locale of nonDefaultLocales) {
|
|
10440
|
+
entries.push({
|
|
10441
|
+
url: `${base}/${locale}${basePath}/${localeSlugs[locale] || baseSlug}`,
|
|
10442
|
+
...lastModified ? { lastModified } : {},
|
|
10443
|
+
changeFrequency: "daily",
|
|
10444
|
+
priority: 0.7
|
|
10445
|
+
});
|
|
10446
|
+
}
|
|
10447
|
+
}
|
|
10448
|
+
return entries;
|
|
10449
|
+
}
|
|
10231
10450
|
async function getCategorySitemapEntries(client, opts) {
|
|
10232
10451
|
const base = opts.siteUrl.replace(/\/+$/, "");
|
|
10233
10452
|
const basePath = opts.basePath ?? "/category";
|
|
@@ -10532,6 +10751,7 @@ export {
|
|
|
10532
10751
|
getProductMetafieldsByType,
|
|
10533
10752
|
getProductPrice,
|
|
10534
10753
|
getProductPriceInfo,
|
|
10754
|
+
getProductSitemapEntries,
|
|
10535
10755
|
getProductSwatches,
|
|
10536
10756
|
getStockStatus,
|
|
10537
10757
|
getVariantOptions,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainerce",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.57.0",
|
|
4
4
|
"description": "Official SDK for building e-commerce storefronts with Brainerce Platform. Perfect for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|