brainerce 1.46.2 → 1.47.1

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
@@ -1244,6 +1244,34 @@ var BrainerceClient = class {
1244
1244
  }
1245
1245
  throw new BrainerceError("getCategories is only available in vibe-coded mode", 400);
1246
1246
  }
1247
+ /**
1248
+ * Get one category by slug — the payload for a storefront category
1249
+ * (collection) landing page: name, description HTML, meta, breadcrumb and
1250
+ * product count. Fetch the products themselves with
1251
+ * `getProducts({ categories: [category.id] })`. Vibe-coded mode only, like
1252
+ * {@link getCategories}.
1253
+ *
1254
+ * @example
1255
+ * ```typescript
1256
+ * const category = await client.getCategoryBySlug('running-shoes').catch(() => null);
1257
+ * if (!category) notFound();
1258
+ * const { data: products } = await client.getProducts({ categories: [category.id] });
1259
+ * ```
1260
+ */
1261
+ async getCategoryBySlug(slug, options) {
1262
+ const headerOverrides = options?.locale ? { "Accept-Language": options.locale } : void 0;
1263
+ const encodedSlug = encodePathSegment(slug);
1264
+ if (this.isVibeCodedMode()) {
1265
+ return this.vibeCodedRequest(
1266
+ "GET",
1267
+ `/categories/slug/${encodedSlug}`,
1268
+ void 0,
1269
+ void 0,
1270
+ headerOverrides
1271
+ );
1272
+ }
1273
+ throw new BrainerceError("getCategoryBySlug is only available in vibe-coded mode", 400);
1274
+ }
1247
1275
  /**
1248
1276
  * Get available brands for filtering products
1249
1277
  * Works in vibe-coded mode
@@ -6396,15 +6424,18 @@ var BrainerceClient = class {
6396
6424
  // these do NOT branch on isVibeCodedMode() (that would 404 silently).
6397
6425
  /**
6398
6426
  * Get the logged-in customer's loyalty status: enrollment, points balance,
6399
- * lifetime earned, and the program's display config (requires customerToken).
6400
- * Only available in storefront mode. `program` is null when the store has no
6401
- * loyalty program.
6427
+ * lifetime earned, the program's display config, earned milestone `badges`,
6428
+ * and the `paidMembership` subscription state (null for free members).
6429
+ * Requires customerToken. Only available in storefront mode. `program` is
6430
+ * null when the store has no loyalty program.
6402
6431
  *
6403
6432
  * @example
6404
6433
  * ```typescript
6405
6434
  * client.setCustomerToken(auth.token);
6406
6435
  * const status = await client.getLoyaltyStatus();
6407
6436
  * if (status.enrolled) console.log(`${status.pointsBalance} ${status.program?.pointsName}`);
6437
+ * status.badges?.forEach((b) => console.log(`🏅 ${b.name}`));
6438
+ * if (status.paidMembership?.status === 'ACTIVE') showPremiumPerks(status.paidMembership.plan);
6408
6439
  * ```
6409
6440
  */
6410
6441
  async getLoyaltyStatus() {
@@ -6537,6 +6568,144 @@ var BrainerceClient = class {
6537
6568
  }
6538
6569
  throw new BrainerceError("getReferralInfo is only available in storefront mode", 400);
6539
6570
  }
6571
+ /**
6572
+ * AI-recommended reward for the logged-in customer — "recommended for you"
6573
+ * at the top of the rewards list (requires customerToken). The result is
6574
+ * ALWAYS a real reward from the store's catalog (the AI only ranks; a
6575
+ * hallucinated pick falls back to a deterministic choice). Returns
6576
+ * `{ reward: null }` when the catalog is empty. Rate-limited (5/min per
6577
+ * customer — it spends the merchant's AI credits). Only available in
6578
+ * storefront mode.
6579
+ *
6580
+ * @example
6581
+ * ```typescript
6582
+ * const { reward, reason } = await client.getRecommendedReward();
6583
+ * if (reward) showRecommendation(reward, reason);
6584
+ * ```
6585
+ */
6586
+ async getRecommendedReward() {
6587
+ if (!this.customerToken && !this.proxyMode) {
6588
+ throw new BrainerceError(
6589
+ "Customer token required. Call setCustomerToken() after login.",
6590
+ 401
6591
+ );
6592
+ }
6593
+ if (this.storeId && !this.apiKey) {
6594
+ return this.storefrontRequest(
6595
+ "GET",
6596
+ "/loyalty/rewards/recommended"
6597
+ );
6598
+ }
6599
+ throw new BrainerceError("getRecommendedReward is only available in storefront mode", 400);
6600
+ }
6601
+ /**
6602
+ * List the paid membership plans the customer can subscribe to (requires
6603
+ * customerToken). Empty when the store offers none or the program is not
6604
+ * active. Only available in storefront mode.
6605
+ *
6606
+ * @example
6607
+ * ```typescript
6608
+ * const plans = await client.getMembershipPlans();
6609
+ * ```
6610
+ */
6611
+ async getMembershipPlans() {
6612
+ if (!this.customerToken && !this.proxyMode) {
6613
+ throw new BrainerceError(
6614
+ "Customer token required. Call setCustomerToken() after login.",
6615
+ 401
6616
+ );
6617
+ }
6618
+ if (this.storeId && !this.apiKey) {
6619
+ return this.storefrontRequest("GET", "/loyalty/membership/plans");
6620
+ }
6621
+ throw new BrainerceError("getMembershipPlans is only available in storefront mode", 400);
6622
+ }
6623
+ /**
6624
+ * List the customer's saved payment methods (display fields only — brand /
6625
+ * last4 / expiry, never card data) for the membership subscribe flow.
6626
+ * Requires customerToken. Cards are vaulted by checking out with
6627
+ * `saveCard: true`. Only available in storefront mode.
6628
+ *
6629
+ * @example
6630
+ * ```typescript
6631
+ * const methods = await client.getMySavedPaymentMethods();
6632
+ * ```
6633
+ */
6634
+ async getMySavedPaymentMethods() {
6635
+ if (!this.customerToken && !this.proxyMode) {
6636
+ throw new BrainerceError(
6637
+ "Customer token required. Call setCustomerToken() after login.",
6638
+ 401
6639
+ );
6640
+ }
6641
+ if (this.storeId && !this.apiKey) {
6642
+ return this.storefrontRequest(
6643
+ "GET",
6644
+ "/loyalty/membership/payment-methods"
6645
+ );
6646
+ }
6647
+ throw new BrainerceError(
6648
+ "getMySavedPaymentMethods is only available in storefront mode",
6649
+ 400
6650
+ );
6651
+ }
6652
+ /**
6653
+ * Subscribe the customer to a paid membership plan — charges the saved card
6654
+ * IMMEDIATELY and starts the recurring cycle (requires customerToken).
6655
+ * Throws a 409 with a `code` ('card_declined' | 'requires_action' | ...)
6656
+ * when the charge fails; 3D-Secure challenges are not supported off-session
6657
+ * and surface as `requires_action`. Only available in storefront mode.
6658
+ *
6659
+ * @example
6660
+ * ```typescript
6661
+ * const membership = await client.subscribeToMembership({
6662
+ * planId: plan.id,
6663
+ * savedPaymentTokenId: method.id,
6664
+ * });
6665
+ * // membership.status === 'ACTIVE'
6666
+ * ```
6667
+ */
6668
+ async subscribeToMembership(params) {
6669
+ if (!this.customerToken && !this.proxyMode) {
6670
+ throw new BrainerceError(
6671
+ "Customer token required. Call setCustomerToken() after login.",
6672
+ 401
6673
+ );
6674
+ }
6675
+ if (this.storeId && !this.apiKey) {
6676
+ return this.storefrontRequest(
6677
+ "POST",
6678
+ "/loyalty/membership/subscribe",
6679
+ params
6680
+ );
6681
+ }
6682
+ throw new BrainerceError("subscribeToMembership is only available in storefront mode", 400);
6683
+ }
6684
+ /**
6685
+ * Cancel the customer's paid membership (requires customerToken).
6686
+ * End-of-period semantics: perks continue until `nextBillingAt`, then the
6687
+ * subscription ends without another charge (a PAST_DUE membership cancels
6688
+ * immediately). Re-subscribing to the same plan before period end simply
6689
+ * un-cancels. Only available in storefront mode.
6690
+ *
6691
+ * @example
6692
+ * ```typescript
6693
+ * const membership = await client.cancelMembership();
6694
+ * // membership.cancelAtPeriodEnd === true
6695
+ * ```
6696
+ */
6697
+ async cancelMembership() {
6698
+ if (!this.customerToken && !this.proxyMode) {
6699
+ throw new BrainerceError(
6700
+ "Customer token required. Call setCustomerToken() after login.",
6701
+ 401
6702
+ );
6703
+ }
6704
+ if (this.storeId && !this.apiKey) {
6705
+ return this.storefrontRequest("POST", "/loyalty/membership/cancel");
6706
+ }
6707
+ throw new BrainerceError("cancelMembership is only available in storefront mode", 400);
6708
+ }
6540
6709
  /**
6541
6710
  * Get the current customer's orders (requires customerToken)
6542
6711
  * Works in vibe-coded and storefront modes
@@ -8822,6 +8991,212 @@ function formatAmount(amount, currency, locale) {
8822
8991
  }
8823
8992
  }
8824
8993
 
8994
+ // src/jsonld.ts
8995
+ function absoluteUrl(siteUrl, path) {
8996
+ if (!path) return void 0;
8997
+ const base = siteUrl.replace(/\/+$/, "");
8998
+ return path.startsWith("http") ? path : `${base}${path.startsWith("/") ? path : `/${path}`}`;
8999
+ }
9000
+ function stripHtml(html) {
9001
+ return (html ?? "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
9002
+ }
9003
+ function buildArticleJsonLd(post, opts) {
9004
+ const url = absoluteUrl(opts.siteUrl, opts.path ?? `/blog/${post.slug}`);
9005
+ const result = {
9006
+ "@context": "https://schema.org",
9007
+ "@type": "Article",
9008
+ headline: post.seoTitle ?? post.title,
9009
+ name: post.title,
9010
+ ...post.excerpt || post.seoDescription ? { description: post.seoDescription ?? post.excerpt } : {},
9011
+ ...url ? { url, mainEntityOfPage: { "@type": "WebPage", "@id": url } } : {},
9012
+ ...post.coverImageUrl ? { image: [post.coverImageUrl] } : {},
9013
+ ...post.publishedAt ? { datePublished: post.publishedAt } : {},
9014
+ dateModified: post.updatedAt,
9015
+ ...post.author ? { author: { "@type": "Person", name: post.author } } : opts.organizationName ? { author: { "@type": "Organization", name: opts.organizationName } } : {},
9016
+ ...opts.organizationName ? { publisher: { "@type": "Organization", name: opts.organizationName } } : {},
9017
+ ...post.tags.length > 0 ? { keywords: post.tags.join(", ") } : {}
9018
+ };
9019
+ return result;
9020
+ }
9021
+ function buildProductJsonLd(product, opts) {
9022
+ const url = absoluteUrl(opts.siteUrl, opts.path ?? (product.slug ? `/products/${product.slug}` : void 0));
9023
+ const images = (product.images ?? []).map((img) => img.url).filter(Boolean);
9024
+ const brand = opts.brandName ?? product.brands?.[0]?.name;
9025
+ const description = stripHtml(product.description).slice(0, 5e3);
9026
+ const effectivePrice = product.salePrice ?? product.basePrice;
9027
+ const inStock = product.inventory ? (product.inventory.available ?? 0) > 0 : true;
9028
+ const isVariable = product.type === "VARIABLE" && product.priceMin && product.priceMax;
9029
+ const offer = isVariable ? {
9030
+ "@type": "AggregateOffer",
9031
+ lowPrice: product.priceMin,
9032
+ highPrice: product.priceMax,
9033
+ priceCurrency: opts.currency,
9034
+ availability: inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
9035
+ ...url ? { url } : {}
9036
+ } : {
9037
+ "@type": "Offer",
9038
+ price: effectivePrice,
9039
+ priceCurrency: opts.currency,
9040
+ availability: inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
9041
+ ...url ? { url } : {}
9042
+ };
9043
+ return {
9044
+ "@context": "https://schema.org",
9045
+ "@type": "Product",
9046
+ name: product.name,
9047
+ ...description ? { description } : {},
9048
+ ...images.length > 0 ? { image: images } : {},
9049
+ ...url ? { url } : {},
9050
+ sku: product.sku,
9051
+ // Identifiers Google uses to match a product to its catalog — key for
9052
+ // free merchant-listing eligibility without a Merchant Center feed.
9053
+ ...product.gtin ? { gtin: product.gtin } : {},
9054
+ ...product.mpn ? { mpn: product.mpn } : {},
9055
+ ...brand ? { brand: { "@type": "Brand", name: brand } } : {},
9056
+ offers: offer,
9057
+ // Google policy: never emit an empty/zero rating block.
9058
+ ...product.reviewCount && product.reviewCount > 0 && product.avgRating ? {
9059
+ aggregateRating: {
9060
+ "@type": "AggregateRating",
9061
+ ratingValue: product.avgRating,
9062
+ reviewCount: product.reviewCount
9063
+ }
9064
+ } : {}
9065
+ };
9066
+ }
9067
+ function buildOrganizationJsonLd(store, opts) {
9068
+ const sameAs = Object.values(store.socialLinks ?? {}).filter(Boolean);
9069
+ return {
9070
+ "@context": "https://schema.org",
9071
+ "@type": "Organization",
9072
+ name: store.name,
9073
+ url: opts.siteUrl,
9074
+ ...store.logo ? { logo: store.logo } : {},
9075
+ ...store.metaDescription ? { description: store.metaDescription } : {},
9076
+ ...store.contactEmail ? { email: store.contactEmail } : {},
9077
+ ...store.contactPhone ? { telephone: store.contactPhone } : {},
9078
+ ...sameAs.length > 0 ? { sameAs } : {}
9079
+ };
9080
+ }
9081
+ function buildWebsiteJsonLd(store, opts) {
9082
+ const base = opts.siteUrl.replace(/\/+$/, "");
9083
+ const result = {
9084
+ "@context": "https://schema.org",
9085
+ "@type": "WebSite",
9086
+ name: store.name,
9087
+ url: opts.siteUrl
9088
+ };
9089
+ if (opts.searchUrlTemplate?.includes("{search_term_string}")) {
9090
+ const tmpl = opts.searchUrlTemplate.startsWith("http") ? opts.searchUrlTemplate : `${base}${opts.searchUrlTemplate.startsWith("/") ? "" : "/"}${opts.searchUrlTemplate}`;
9091
+ result.potentialAction = {
9092
+ "@type": "SearchAction",
9093
+ target: { "@type": "EntryPoint", urlTemplate: tmpl },
9094
+ "query-input": "required name=search_term_string"
9095
+ };
9096
+ }
9097
+ return result;
9098
+ }
9099
+ function buildCollectionPageJsonLd(category, opts) {
9100
+ const url = absoluteUrl(opts.siteUrl, opts.path);
9101
+ const description = category.metaDescription ?? (stripHtml(category.description) || void 0);
9102
+ return {
9103
+ "@context": "https://schema.org",
9104
+ "@type": "CollectionPage",
9105
+ name: category.name,
9106
+ ...description ? { description } : {},
9107
+ ...url ? { url, mainEntityOfPage: { "@type": "WebPage", "@id": url } } : {},
9108
+ ...category.image ? { image: [category.image] } : {}
9109
+ };
9110
+ }
9111
+ function buildBreadcrumbJsonLd(items) {
9112
+ return {
9113
+ "@context": "https://schema.org",
9114
+ "@type": "BreadcrumbList",
9115
+ itemListElement: items.map((item, index) => ({
9116
+ "@type": "ListItem",
9117
+ position: index + 1,
9118
+ name: item.name,
9119
+ item: item.url
9120
+ }))
9121
+ };
9122
+ }
9123
+ function jsonLdScriptProps(data) {
9124
+ return {
9125
+ type: "application/ld+json",
9126
+ dangerouslySetInnerHTML: { __html: JSON.stringify(data).replace(/</g, "\\u003c") }
9127
+ };
9128
+ }
9129
+
9130
+ // src/sitemap.ts
9131
+ async function getBlogSitemapEntries(client, opts) {
9132
+ const base = opts.siteUrl.replace(/\/+$/, "");
9133
+ const basePath = opts.basePath ?? "/blog";
9134
+ const pageSize = Math.min(opts.pageSize ?? 100, 100);
9135
+ const maxEntries = opts.maxEntries ?? 5e3;
9136
+ const posts = [];
9137
+ let page = 1;
9138
+ for (; ; ) {
9139
+ const res = await client.blog.getPosts({ page, limit: pageSize });
9140
+ posts.push(...res.data.map((p) => ({ slug: p.slug, updatedAt: p.updatedAt })));
9141
+ if (page >= res.meta.totalPages || posts.length >= maxEntries) break;
9142
+ page += 1;
9143
+ }
9144
+ const nonDefaultLocales = opts.locales?.filter((locale) => locale !== opts.defaultLocale) ?? [];
9145
+ const entries = [];
9146
+ entries.push({ url: `${base}${basePath}`, changeFrequency: "daily", priority: 0.7 });
9147
+ for (const locale of nonDefaultLocales) {
9148
+ entries.push({
9149
+ url: `${base}/${locale}${basePath}`,
9150
+ changeFrequency: "daily",
9151
+ priority: 0.7
9152
+ });
9153
+ }
9154
+ for (const post of posts.slice(0, maxEntries)) {
9155
+ const lastModified = new Date(post.updatedAt);
9156
+ entries.push({
9157
+ url: `${base}${basePath}/${post.slug}`,
9158
+ lastModified,
9159
+ changeFrequency: "weekly",
9160
+ priority: 0.6
9161
+ });
9162
+ for (const locale of nonDefaultLocales) {
9163
+ entries.push({
9164
+ url: `${base}/${locale}${basePath}/${post.slug}`,
9165
+ lastModified,
9166
+ changeFrequency: "weekly",
9167
+ priority: 0.5
9168
+ });
9169
+ }
9170
+ }
9171
+ return entries;
9172
+ }
9173
+ async function getCategorySitemapEntries(client, opts) {
9174
+ const base = opts.siteUrl.replace(/\/+$/, "");
9175
+ const basePath = opts.basePath ?? "/category";
9176
+ const nonDefaultLocales = opts.locales?.filter((l) => l !== opts.defaultLocale) ?? [];
9177
+ const { categories } = await client.getCategories();
9178
+ const slugs = [];
9179
+ const walk = (nodes) => {
9180
+ for (const node of nodes) {
9181
+ if (node.slug) slugs.push(node.slug);
9182
+ if (node.children?.length) walk(node.children);
9183
+ }
9184
+ };
9185
+ walk(categories);
9186
+ const entries = [];
9187
+ for (const slug of slugs) {
9188
+ entries.push({ url: `${base}${basePath}/${slug}`, changeFrequency: "daily", priority: 0.8 });
9189
+ for (const locale of nonDefaultLocales) {
9190
+ entries.push({
9191
+ url: `${base}/${locale}${basePath}/${slug}`,
9192
+ changeFrequency: "daily",
9193
+ priority: 0.7
9194
+ });
9195
+ }
9196
+ }
9197
+ return entries;
9198
+ }
9199
+
8825
9200
  // src/types.ts
8826
9201
  function isHtmlDescription(product) {
8827
9202
  if (product?.descriptionFormat === "html") return true;
@@ -8837,7 +9212,7 @@ function getDescriptionContent(product) {
8837
9212
  }
8838
9213
  return { text: product.description };
8839
9214
  }
8840
- function stripHtml(html) {
9215
+ function stripHtml2(html) {
8841
9216
  if (!html) return "";
8842
9217
  return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;/gi, "'").replace(/&#x27;/gi, "'").replace(/&#x2F;/gi, "/").replace(/\s+/g, " ").trim();
8843
9218
  }
@@ -8846,7 +9221,7 @@ function deriveSeoDescription(product, options) {
8846
9221
  const maxLength = options?.maxLength ?? 160;
8847
9222
  const authored = (product.seoDescription ?? product.metaDescription ?? "").trim();
8848
9223
  if (authored) return authored;
8849
- const plain = stripHtml(product.description);
9224
+ const plain = stripHtml2(product.description);
8850
9225
  if (plain) {
8851
9226
  if (plain.length <= maxLength) return plain;
8852
9227
  const sliceLength = maxLength - 1;
@@ -9067,6 +9442,12 @@ export {
9067
9442
  BrainerceError,
9068
9443
  RTL_LOCALES,
9069
9444
  SDK_VERSION,
9445
+ buildArticleJsonLd,
9446
+ buildBreadcrumbJsonLd,
9447
+ buildCollectionPageJsonLd,
9448
+ buildOrganizationJsonLd,
9449
+ buildProductJsonLd,
9450
+ buildWebsiteJsonLd,
9070
9451
  createWebhookHandler,
9071
9452
  deriveSeoDescription,
9072
9453
  enableDevGuards,
@@ -9074,9 +9455,11 @@ export {
9074
9455
  formatPrice,
9075
9456
  formatProductPrice,
9076
9457
  formatVariantPrice,
9458
+ getBlogSitemapEntries,
9077
9459
  getCartItemImage,
9078
9460
  getCartItemName,
9079
9461
  getCartTotals,
9462
+ getCategorySitemapEntries,
9080
9463
  getDescriptionContent,
9081
9464
  getDirectionForLocale,
9082
9465
  formatPrice as getPriceDisplay,
@@ -9094,8 +9477,9 @@ export {
9094
9477
  isCouponApplicableToProduct,
9095
9478
  isHtmlDescription,
9096
9479
  isWebhookEventType,
9480
+ jsonLdScriptProps,
9097
9481
  parseWebhookEvent,
9098
9482
  safePaymentRedirect,
9099
- stripHtml,
9483
+ stripHtml2 as stripHtml,
9100
9484
  verifyWebhook
9101
9485
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainerce",
3
- "version": "1.46.2",
3
+ "version": "1.47.1",
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",