@brainerce/mcp-server 3.8.0 → 3.11.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/dist/index.js CHANGED
@@ -735,7 +735,7 @@ const growProvider = providers.find(p => p.provider === 'grow');
735
735
  const paypalProvider = providers.find(p => p.provider === 'paypal');
736
736
  \`\`\`
737
737
 
738
- Each provider has: \`id\`, \`provider\` (flexible string \u2014 \`'stripe'\`, \`'grow'\`, \`'paypal'\`, \`'cardcom'\`, \`'sandbox'\`, and future providers), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
738
+ Each provider has: \`id\`, \`provider\` (flexible string \u2014 \`'stripe'\`, \`'grow'\`, \`'paypal'\`, \`'cardcom'\`, \`'morning'\`, \`'takbull'\`, \`'sandbox'\`, and future providers), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
739
739
 
740
740
  The payment intent returned from \`createPaymentIntent()\` includes a \`clientSdk\` object whose \`renderType\` tells you exactly how to render \u2014 **branch on THAT, not on the provider name**:
741
741
 
@@ -1179,6 +1179,8 @@ const material = getProductMetafieldValue(product, 'material');
1179
1179
 
1180
1180
  **Metafield fields:** \`definitionName\` (display label), \`definitionKey\` (lookup key), \`value\`, \`type\` (IMAGE, GALLERY, URL, COLOR, BOOLEAN, DATE, DATETIME, TEXT, TEXTAREA, NUMBER, DIMENSION, WEIGHT, JSON)
1181
1181
 
1182
+ **Translations:** When the store has i18n enabled and \`client.setLocale()\` is active, both the \`definitionName\` (the merchant-authored label like "Warranty Info" / "\u05DE\u05D9\u05D3\u05E2 \u05D0\u05D7\u05E8\u05D9\u05D5\u05EA") **and** free-text \`value\` come back already translated. No per-call locale param needed.
1183
+
1182
1184
  ### Product Customization Fields (Customer Input)
1183
1185
 
1184
1186
  Some products allow customers to provide input at purchase time (e.g., text on a cake, upload a logo). Check \`product.customizationFields\` and render input fields accordingly.
@@ -1312,6 +1314,8 @@ await client.removeCoupon(cartId);
1312
1314
  const totals = getCartTotals(cart); // { subtotal, discount, shipping, total }
1313
1315
  \`\`\`
1314
1316
 
1317
+ > **Region-restricted coupons:** a coupon may carry \`regionIds\`. If the buyer's checkout region isn't in that list, \`applyCoupon\` / \`applyCheckoutCoupon\` reject the code even when everything else matches. Empty/omitted \`regionIds\` = valid in all regions.
1318
+
1315
1319
  **On the checkout page** (after checkout session exists \u2014 ALWAYS use this when checkoutId is available):
1316
1320
  \`\`\`typescript
1317
1321
  // Applies to cart AND updates checkout totals in one call
@@ -1639,6 +1643,8 @@ const groups: ModifierGroup[] = product.modifierGroups ?? [];
1639
1643
 
1640
1644
  **Money fields are strings.** \`priceDelta\` is \`"5.00"\` (or \`"-2.00"\` for downsell modifiers \u2014 see below). Use \`parseFloat()\` for display arithmetic; never compute the line total client-side \u2014 the server runs the free-allocation policy and returns the final \`unitPrice\` snapshot on the cart line.
1641
1645
 
1646
+ **Translations are automatic.** Both \`group.name\`/\`group.description\` and each \`modifier.name\`/\`modifier.description\` are overlay-resolved by the server on every read. Call \`client.setLocale('he')\` once and the modifier picker renders in Hebrew with no extra code (\`"\u05EA\u05D5\u05E1\u05E4\u05D5\u05EA"\` / \`"\u05D6\u05D9\u05EA\u05D9\u05DD"\`). See the \`i18n\` topic for the full multi-language flow.
1647
+
1642
1648
  ### Render: SINGLE \u2192 radio, MULTIPLE \u2192 checkbox
1643
1649
 
1644
1650
  Walk \`groups\` and pick the input type by \`selectionType\`. Sort modifiers by \`position\`.
@@ -2040,19 +2046,24 @@ window.location.href = authorizationUrl;
2040
2046
  \`\`\`typescript
2041
2047
  const params = new URLSearchParams(window.location.search);
2042
2048
  if (params.get('oauth_success') === 'true') {
2043
- const token = params.get('token');
2044
- if (token) {
2045
- client.setCustomerToken(token);
2046
- localStorage.setItem('customerToken', token);
2047
- // Optional: read customer_id, customer_email, is_new from params
2049
+ // Single-use auth_code is exchanged for the JWT via POST \u2014 keeps the JWT
2050
+ // out of the URL (browser history, CDN logs, Referer header).
2051
+ const code = params.get('auth_code');
2052
+ if (code) {
2053
+ const result = await client.exchangeOAuthCode(code);
2054
+ client.setCustomerToken(result.token);
2055
+ localStorage.setItem('customerToken', result.token);
2056
+ // Optional: result.customer, result.isNewCustomer, result.redirectUrl
2048
2057
  // Link guest cart: await client.linkCart(cartId);
2049
- window.location.href = '/account';
2058
+ window.location.href = result.redirectUrl || '/account';
2050
2059
  }
2051
2060
  } else if (params.get('oauth_error')) {
2052
2061
  // Show error: params.get('oauth_error')
2053
2062
  }
2054
2063
  \`\`\`
2055
2064
 
2065
+ > The legacy redirect format placed the JWT directly in the URL as \`?token=\`. That format is still emitted for backward compatibility, but it will be removed in the next major release \u2014 migrate to \`auth_code\` + \`exchangeOAuthCode()\` now.
2066
+
2056
2067
  ### Account Page (/account) \u2014 uses getMyProfile() and getMyOrders()
2057
2068
 
2058
2069
  \`\`\`typescript
@@ -2281,7 +2292,17 @@ overlay needed:
2281
2292
  - \`variants[].name\`
2282
2293
  - \`variant.attributes\` keys and values \u2014 \`getVariantOptions(variant)\` returns translated attribute names and option values automatically
2283
2294
  - \`productAttributeOptions[].attribute.name\`, \`attributeOption.name\`
2284
- - \`metafields[].value\`
2295
+ - \`metafields[].value\` (the free-text values customers submit)
2296
+ - \`metafields[].definition.name\`, \`metafields[].definition.description\` (the merchant-authored custom-field labels \u2014 "Warranty Info" / "\u05DE\u05D9\u05D3\u05E2 \u05D0\u05D7\u05E8\u05D9\u05D5\u05EA")
2297
+ - \`modifierGroups[].name\`, \`modifierGroups[].description\` (the group label shown on the PDP \u2014 e.g. "Toppings" / "\u05EA\u05D5\u05E1\u05E4\u05D5\u05EA")
2298
+ - \`modifierGroups[].modifiers[].name\`, \`modifierGroups[].modifiers[].description\` (each individual modifier \u2014 e.g. "Olives" / "\u05D6\u05D9\u05EA\u05D9\u05DD")
2299
+
2300
+ **Promotional surfaces:**
2301
+ - \`cart.bundles[].name\`, \`cart.bundles[].description\` (the bundle's own merchant-typed label, e.g. "Lunch Combo" / "\u05D0\u05E8\u05D5\u05D7\u05EA \u05E6\u05D4\u05E8\u05D9\u05D9\u05DD")
2302
+ - \`cart.bundles[].offeredProducts[].name\`, \`.slug\` (each product inside the bundle \u2014 fetched fresh from \`Product.translations\`)
2303
+ - \`checkout.bumps[].title\`, \`checkout.bumps[].description\` (the bump headline shown at checkout \u2014 falls back to the translated product name when merchant didn't override)
2304
+ - \`checkout.bumps[].bumpProduct.name\`, \`.slug\` (the underlying product)
2305
+ - Discount-rule names/descriptions (when surfaced as banner text via \`displayConfig\`)
2285
2306
 
2286
2307
  **Taxonomy (\`getCategories\`, \`getBrands\`, \`getTags\`):**
2287
2308
  - \`name\` on each item
@@ -2373,6 +2394,67 @@ Read \`storeInfo.i18n.supportedLocales\` and render a switcher in the header
2373
2394
  that navigates to \`/{otherLocale}/{currentPathWithoutLocale}\`. The middleware
2374
2395
  handles the canonical redirect when the user picks the default locale.`;
2375
2396
  }
2397
+ function getMultilingualSeoSection() {
2398
+ return `## Multilingual SEO
2399
+
2400
+ ### Slugs per locale (IMPORTANT)
2401
+
2402
+ Brainerce stores per-locale slugs in \`Product.translations[locale].slug\`.
2403
+ The API exposes them as \`product.localeSlugs\` \u2014 a flat \`Record<string, string>\`.
2404
+
2405
+ \`\`\`typescript
2406
+ // product.localeSlugs = { he: '\u05DE\u05D6\u05E8\u05DF-\u05D9\u05D5\u05D2\u05D4', en: 'yoga-mattress', ar: '\u062D\u0635\u064A\u0631\u0629-\u064A\u0648\u063A\u0627' }
2407
+ const locSlug = product.localeSlugs?.[locale] ?? product.slug;
2408
+ \`\`\`
2409
+
2410
+ Use AI translation (\`translateEntities\` / the AI Translate button in the admin) to populate
2411
+ locale-specific slugs automatically. Until translated, all locales fall back to the base slug
2412
+ (which Google still accepts \u2014 it resolves to the correct locale content via Accept-Language).
2413
+
2414
+ ### hreflang tags
2415
+
2416
+ Every product page in a multi-locale store MUST emit bidirectional hreflang tags or Google
2417
+ ignores them entirely. The scaffolded template generates them automatically when
2418
+ \`NEXT_PUBLIC_BRAINERCE_LOCALES=he,en\` is set.
2419
+
2420
+ \`\`\`html
2421
+ <!-- Example output for /en/products/yoga-mattress -->
2422
+ <link rel="alternate" hreflang="he" href="https://store.com/products/\u05DE\u05D6\u05E8\u05DF-\u05D9\u05D5\u05D2\u05D4" />
2423
+ <link rel="alternate" hreflang="en" href="https://store.com/en/products/yoga-mattress" />
2424
+ <link rel="alternate" hreflang="x-default" href="https://store.com/products/\u05DE\u05D6\u05E8\u05DF-\u05D9\u05D5\u05D2\u05D4" />
2425
+ \`\`\`
2426
+
2427
+ If building a custom storefront, add to \`generateMetadata()\`:
2428
+ \`\`\`typescript
2429
+ alternates: {
2430
+ canonical: locale === defaultLoc ? \`/products/\${slug}\` : \`/\${locale}/products/\${slug}\`,
2431
+ languages: {
2432
+ he: \`\${baseUrl}/products/\${localeSlugs.he ?? baseSlug}\`,
2433
+ en: \`\${baseUrl}/en/products/\${localeSlugs.en ?? baseSlug}\`,
2434
+ 'x-default': \`\${baseUrl}/products/\${localeSlugs[defaultLoc] ?? baseSlug}\`,
2435
+ },
2436
+ },
2437
+ \`\`\`
2438
+
2439
+ ### URL structure
2440
+
2441
+ Google-recommended for headless storefronts:
2442
+ - Default locale: \`/products/\u05DE\u05D6\u05E8\u05DF-\u05D9\u05D5\u05D2\u05D4\` (no prefix)
2443
+ - Other locales: \`/en/products/yoga-mattress\` (prefixed)
2444
+ - \u274C Never: \`?locale=he\` query params (Google rates these as "Not recommended")
2445
+
2446
+ ### Sitemap
2447
+
2448
+ The scaffolded \`sitemap.ts\` generates per-locale entries automatically:
2449
+ \`\`\`
2450
+ /products/\u05DE\u05D6\u05E8\u05DF-\u05D9\u05D5\u05D2\u05D4 (he \u2014 default, no prefix)
2451
+ /en/products/yoga-mattress (en)
2452
+ /ar/products/\u062D\u0635\u064A\u0631\u0629-\u064A\u0648\u063A\u0627 (ar)
2453
+ \`\`\`
2454
+
2455
+ Hebrew, Arabic, and other non-Latin slugs are fully supported \u2014 Google recommends
2456
+ using the audience's language in URLs and indexes non-ASCII characters correctly.`;
2457
+ }
2376
2458
  function getAdminApiSection() {
2377
2459
  return `## Admin API (v0.19.0+)
2378
2460
 
@@ -2383,7 +2465,9 @@ const admin = new BrainerceClient({ apiKey: 'brainerce_...' });
2383
2465
 
2384
2466
  // Taxonomy: listCategories(), listBrands(), listTags(), listAttributes()
2385
2467
  // Shipping: listShippingZones(), createZoneShippingRate()
2386
- // Tax: getTaxRates(), createTaxRate()
2468
+ // Tax: getTaxRates(), createTaxRate() \u2014 a rate may target a tax class via taxClassId
2469
+ // Tax classes: getTaxClasses(), createTaxClass(), assignTaxClass(), mergeTaxClasses() (scope tax-classes:*)
2470
+ // Regions: getRegions(), createRegion(), setDefaultRegion(), updateRegionPaymentProviders() (scope regions:*)
2387
2471
  // Metafields: getMetafieldDefinitions(), setProductMetafield()
2388
2472
  // Team: getTeamMembers(), inviteTeamMember()
2389
2473
  // Email: getEmailTemplates(), createEmailTemplate()
@@ -2391,6 +2475,13 @@ const admin = new BrainerceClient({ apiKey: 'brainerce_...' });
2391
2475
  // OAuth: getOAuthProviders(), configureOAuthProvider()
2392
2476
  \`\`\`
2393
2477
 
2478
+ Store-level team management (\`inviteStoreMember\`, \`updateStoreMember\`) is the
2479
+ canonical replacement for the deprecated account-level helpers above. The
2480
+ invite accepts an optional \`salesChannelIds\` (vibe-coded \`connectionId\`s,
2481
+ \`vc_*\`) to restrict a member to specific channels \u2014 omit or pass \`[]\` for all
2482
+ channels. Use \`updateStoreMemberSalesChannels(storeId, memberId, { salesChannelIds })\`
2483
+ to change that scope later.
2484
+
2394
2485
  ### Per-Channel Publishing
2395
2486
 
2396
2487
  Categories, tags, brands, and metafield definitions are gated to specific
@@ -2422,7 +2513,97 @@ calls fail with \`404 Not Found\`.
2422
2513
 
2423
2514
  The vibe-coded read endpoints (used by storefront SDK calls in \`connectionId\`
2424
2515
  mode) automatically filter by these junction tables, so storefronts never see
2425
- entities that weren't published to their site.`;
2516
+ entities that weren't published to their site.
2517
+
2518
+ ### Tax classes (differential tax rates)
2519
+
2520
+ Charge different rates for different product types. A \`TaxRate\` may target a
2521
+ class via \`taxClassId\`; a rate with \`taxClassId: null\` is the **Standard
2522
+ fallback**. Checkout resolves each line's class **variant \u2192 product \u2192 category
2523
+ \u2192 store default \u2192 null**, then picks the matching rate (Standard if none).
2524
+ \`rate\` is a whole percentage (\`7.25\` = 7.25%). Requires the
2525
+ \`tax-classes:read\` / \`tax-classes:write\` scopes.
2526
+
2527
+ \`\`\`typescript
2528
+ const food = await admin.createTaxClass({ name: 'Food', slug: 'food' });
2529
+ await admin.assignTaxClass(food.id, { productIds: ['prod_1'], categoryIds: ['cat_food'] });
2530
+
2531
+ // class-specific rate \u2014 only food lines use it; everything else uses Standard
2532
+ await admin.createTaxRate({ name: 'VAT (Food)', rate: 0, country: 'GB', taxClassId: food.id });
2533
+
2534
+ // delete is blocked (409) while dependents exist \u2014 merge moves FKs then deletes:
2535
+ await admin.mergeTaxClasses(food.id, standardClassId);
2536
+ \`\`\`
2537
+
2538
+ Storefront (public, no apiKey \u2014 \`storeId\` mode): \`getStoreTaxClasses()\` lists
2539
+ the store's classes (storefront-safe fields only) for a transparency badge.
2540
+
2541
+ For a buyer-facing tax preview on PDP / PLP / cart, \`estimateTax({ country,
2542
+ subtotal })\` returns the tax portion at the Standard rate for the buyer's
2543
+ country. **Non-binding** \u2014 the authoritative tax still runs at checkout with
2544
+ the full shipping address (state / postal / per-class). Always render with an
2545
+ "Estimate" affordance.
2546
+
2547
+ \`\`\`typescript
2548
+ const { estimatedTax, rate, currency, note } = await store.estimateTax({
2549
+ country: 'IT', subtotal: 100,
2550
+ });
2551
+ // \u2192 { appliesTax: true, rate: 22, estimatedTax: 22, currency: 'EUR',
2552
+ // note: 'Estimate \u2014 final tax calculated at checkout\u2026' }
2553
+ \`\`\`
2554
+
2555
+ ### Regions (multi-currency / per-region providers)
2556
+
2557
+ A region binds countries \u2192 currency + tax-display mode + enabled payment
2558
+ providers. Manage them via the SDK (scopes \`regions:read\` / \`regions:write\`).
2559
+ \`detectRegion\` is a pure client-side helper to map a buyer's country to a
2560
+ region; pass the resolved \`regionId\` to \`createCheckout\` to associate the
2561
+ checkout with a region (recorded for reporting + provider scoping; currency
2562
+ follows the cart until FX price conversion lands).
2563
+
2564
+ \`\`\`typescript
2565
+ const eu = await admin.createRegion({
2566
+ name: 'EU', currency: 'EUR', countries: ['DE', 'FR'],
2567
+ taxInclusive: true, paymentProviderIds: ['app_inst_stripe'],
2568
+ });
2569
+ await admin.setDefaultRegion(eu.id);
2570
+
2571
+ const { data: regions } = await admin.getRegions();
2572
+ const region = admin.detectRegion('DE', regions); // \u2192 eu, else default, else null
2573
+ \`\`\`
2574
+
2575
+ Storefront (public, no apiKey \u2014 \`storeId\` mode): \`getStoreRegions()\` lists
2576
+ active regions, \`getStoreRegion(id)\` returns one region + its payment providers.
2577
+ Pair with \`detectRegion\` to pick the buyer's region for currency display.
2578
+
2579
+ \`\`\`typescript
2580
+ const store = new BrainerceClient({ storeId: 'store_123' });
2581
+ const { data: regions } = await store.getStoreRegions();
2582
+ const region = await store.getStoreRegion(regions[0].id);
2583
+ \`\`\`
2584
+
2585
+ For a single round trip, \`getAutoRegion(country)\` resolves a buyer's country
2586
+ to a region server-side. Brainerce does NOT derive the country from the request
2587
+ IP (the storefront server is what reaches the backend, not the end-customer) \u2014
2588
+ your storefront extracts the country from its edge runtime (Cloudflare
2589
+ \`CF-IPCountry\`, Vercel \`request.geo?.country\`, Fastly \`client-geo-country\`,
2590
+ etc.) and forwards it. Returns \`matched=true\` on explicit country hit, \`false\`
2591
+ when falling back to the default region.
2592
+
2593
+ \`\`\`typescript
2594
+ const country = headers.get('cf-ipcountry') ?? 'US';
2595
+ const { region, matched } = await store.getAutoRegion(country);
2596
+ \`\`\`
2597
+
2598
+ A shipping zone can be limited to regions via \`regionIds\` \u2014 the zone is then
2599
+ only offered to checkouts that resolved to one of those regions. Empty/omitted =
2600
+ available for any region (default); each id must belong to the same store.
2601
+
2602
+ \`\`\`typescript
2603
+ await admin.createShippingZone({
2604
+ name: 'EU Express', countries: ['DE', 'FR', 'IT'], regionIds: [eu.id],
2605
+ });
2606
+ \`\`\``;
2426
2607
  }
2427
2608
  function getContactInquiriesSection() {
2428
2609
  return `## Contact Inquiries & Forms (optional)
@@ -2877,6 +3058,146 @@ entries \u2014 pick a descriptive slug (\`'shipping'\`, \`'returns'\`, \`'about'
2877
3058
  - \`get-type-definitions\` with \`domain: 'content'\` for the TypeScript
2878
3059
  interfaces.`;
2879
3060
  }
3061
+ function getBlogSection() {
3062
+ return `## Blog
3063
+
3064
+ Merchants write and schedule blog posts in **Content \u2192 Blog** in the dashboard.
3065
+ Storefronts choose their own URL scheme \u2014 render posts at \`/blog/[slug]\`,
3066
+ \`/articles/[slug]\`, or whatever fits the brand.
3067
+
3068
+ Public reads carry \`Cache-Control: public, max-age=300, stale-while-revalidate=60\`.
3069
+
3070
+ **Scheduling:** A post is visible once \`status === 'PUBLISHED'\` and
3071
+ \`publishedAt <= now()\`. Set a future \`publishedAt\` while publishing to
3072
+ schedule \u2014 no cron needed; visibility is computed at query time.
3073
+
3074
+ ### Reading posts (any SDK mode)
3075
+
3076
+ \`\`\`typescript
3077
+ // List published posts
3078
+ const { data: posts, meta } = await client.blog.getPosts({ page: 1, limit: 10 });
3079
+
3080
+ // Filter by category or tag
3081
+ const { data: news } = await client.blog.getPosts({ category: 'news' });
3082
+ const { data: tips } = await client.blog.getPosts({ tag: 'tutorial' });
3083
+
3084
+ // Fetch one by slug (returns null on 404)
3085
+ const post = await client.blog.getPost('my-first-post');
3086
+ if (post) {
3087
+ console.log(post.title); // string
3088
+ console.log(post.content); // HTML string from rich-text editor
3089
+ console.log(post.excerpt); // optional short summary
3090
+ console.log(post.tags); // string[]
3091
+ console.log(post.coverImageUrl, post.coverImageAlt);
3092
+ console.log(post.publishedAt); // ISO 8601 or undefined
3093
+ }
3094
+ \`\`\`
3095
+
3096
+ ### BlogPost fields
3097
+
3098
+ | Field | Type | Notes |
3099
+ | ----------------- | ----------- | ------------------------------------- |
3100
+ | \`id\` | string | |
3101
+ | \`title\` | string | |
3102
+ | \`slug\` | string | Unique per store, URL-safe |
3103
+ | \`category\` | string? | Free-form (e.g. \`'news'\`) |
3104
+ | \`content\` | string | HTML from rich-text editor |
3105
+ | \`excerpt\` | string? | Short summary for listing cards |
3106
+ | \`coverImageUrl\` | string? | |
3107
+ | \`coverImageAlt\` | string? | |
3108
+ | \`author\` | string? | |
3109
+ | \`tags\` | string[] | |
3110
+ | \`publishedAt\` | string? | ISO 8601; null = publish immediately |
3111
+ | \`seoTitle\` | string? | |
3112
+ | \`seoDescription\`| string? | |
3113
+ | \`ogImageUrl\` | string? | |
3114
+
3115
+ ### Rendering blog content
3116
+
3117
+ \`\`\`tsx
3118
+ // app/blog/[slug]/page.tsx
3119
+ import DOMPurify from 'isomorphic-dompurify';
3120
+
3121
+ const post = await brainerce.blog.getPost(params.slug);
3122
+ if (!post) notFound();
3123
+
3124
+ // Always sanitize before rendering HTML \u2014 treat external-origin content as untrusted
3125
+ const safeHtml = DOMPurify.sanitize(post.content);
3126
+ return <div dangerouslySetInnerHTML={{ __html: safeHtml }} className="prose" />;
3127
+ \`\`\`
3128
+
3129
+ The \`content\` field is HTML produced by the Lexical rich-text editor.
3130
+ **Always sanitize with DOMPurify (or equivalent) before rendering via
3131
+ \`dangerouslySetInnerHTML\`** \u2014 even though the content originates from your
3132
+ own editor, defense-in-depth prevents any stored-XSS path if content is ever
3133
+ migrated or imported from an external source.
3134
+ Apply a \`prose\` class (Tailwind Typography) for automatic styling.
3135
+
3136
+ ### REST endpoints
3137
+
3138
+ | Method | Path | Returns |
3139
+ |--------|------|---------|
3140
+ | GET | \`/api/stores/:storeId/blog/posts\` | \`BlogPostListResponse\` |
3141
+ | GET | \`/api/stores/:storeId/blog/posts/:slug\` | \`BlogPost\` or 404 |
3142
+ | GET | \`/api/vc/:connectionId/blog/posts\` | Same, channel-scoped |
3143
+ | GET | \`/api/vc/:connectionId/blog/posts/:slug\` | \`BlogPost\` or 404 |
3144
+ `;
3145
+ }
3146
+ function getStorefrontBotSection(connectionId) {
3147
+ return `## Storefront Bot (AI chat widget)
3148
+
3149
+ The store's AI shopping assistant. ALL configuration (name, avatar, colors,
3150
+ greeting, starter questions, capabilities, guardrails) lives in the merchant
3151
+ dashboard \u2014 the embed never decides behavior, and the widget renders nothing
3152
+ until the merchant switches the bot Live.
3153
+
3154
+ ### Zero-code embed (any site)
3155
+
3156
+ \`\`\`html
3157
+ <script src="https://cdn.brainerce.com/bot.js" data-connection-id="${connectionId}" defer></script>
3158
+ \`\`\`
3159
+
3160
+ Keep the tag exactly this bare \u2014 do NOT add \`integrity\` or \`crossorigin\`
3161
+ (the bootstrap is intentionally mutable; it is origin-pinned by CSP instead).
3162
+
3163
+ ### SDK mount (React / Next.js / any bundler)
3164
+
3165
+ \`\`\`ts
3166
+ import { BrainerceBot } from 'brainerce/bot';
3167
+
3168
+ // client-side only (e.g. in a useEffect) \u2014 mounts a floating chat bubble
3169
+ const bot = await BrainerceBot.mount({ connectionId: '${connectionId}' });
3170
+ bot?.destroy(); // optional teardown
3171
+ \`\`\`
3172
+
3173
+ \`mount\` resolves to \`null\` when the bot is disabled (FREE plan, switched
3174
+ off, or unconfigured) \u2014 safe to call unconditionally. Options: \`connectionId\`
3175
+ (required), \`baseUrl\` (API origin override), \`target\` (mount element),
3176
+ \`onAddToCart\` (\`({ productId, variantId, quantity }) => boolean | Promise<boolean>\`
3177
+ \u2014 route the widget's cart adds through the site's own cart; \`variantId\` is
3178
+ null for simple products; return false to fall back to the product page).
3179
+
3180
+ Behavior: persists an anonymous session in localStorage, restores the
3181
+ conversation on revisit, streams answers, and shows product recommendation
3182
+ cards (image, price, add-to-cart / view). Multi-variant products get an
3183
+ in-card variant picker; shoppers can also ask the assistant to add an item
3184
+ and it goes through the same cart chain. Zero-result searches feed the
3185
+ merchant's "unmet demand" analytics. Aside from shopper-initiated cart adds,
3186
+ the bot is read-only.
3187
+
3188
+ Add-to-cart resolution chain: \`onAddToCart\` option \u2192 cancelable
3189
+ \`brainerce:bot:add-to-cart\` CustomEvent on window
3190
+ (\`detail: { productId, variantId, quantity, connectionId }\`; call
3191
+ preventDefault() after handling) \u2192 navigate to the product page.
3192
+
3193
+ Rules:
3194
+ - Mount once per page; do not pass any visual config \u2014 the dashboard owns it.
3195
+ - Do not wrap or restyle the widget; it renders in its own Shadow DOM.
3196
+ - Product card links expect the conventional \`/products/<slug>\` route.
3197
+ - Wire \`onAddToCart\` to the site's cart (see the cart section) so the header
3198
+ count updates when the bot adds items.
3199
+ `;
3200
+ }
2880
3201
  function getSectionByTopic(topic, connectionId, currency) {
2881
3202
  const cid = connectionId || "vc_YOUR_CONNECTION_ID";
2882
3203
  const cur = currency || "USD";
@@ -2915,6 +3236,10 @@ function getSectionByTopic(topic, connectionId, currency) {
2915
3236
  return getTaxDisplaySection(cur);
2916
3237
  case "i18n":
2917
3238
  return getI18nSection();
3239
+ case "multilingual-seo":
3240
+ case "hreflang":
3241
+ case "seo-i18n":
3242
+ return getMultilingualSeoSection();
2918
3243
  case "critical-rules":
2919
3244
  return getCriticalRulesSection() + "\n\n" + getTypeQuickReference();
2920
3245
  case "type-reference":
@@ -2925,6 +3250,10 @@ function getSectionByTopic(topic, connectionId, currency) {
2925
3250
  return getContactInquiriesSection();
2926
3251
  case "content":
2927
3252
  return getContentSection();
3253
+ case "blog":
3254
+ return getBlogSection();
3255
+ case "storefront-bot":
3256
+ return getStorefrontBotSection(cid);
2928
3257
  case "all":
2929
3258
  return [
2930
3259
  "# Brainerce SDK \u2014 full topic dump",
@@ -3009,6 +3338,10 @@ function getSectionByTopic(topic, connectionId, currency) {
3009
3338
  "",
3010
3339
  "---",
3011
3340
  "",
3341
+ getMultilingualSeoSection(),
3342
+ "",
3343
+ "---",
3344
+ "",
3012
3345
  getAdminApiSection(),
3013
3346
  "",
3014
3347
  "---",
@@ -3017,10 +3350,18 @@ function getSectionByTopic(topic, connectionId, currency) {
3017
3350
  "",
3018
3351
  "---",
3019
3352
  "",
3020
- getContentSection()
3353
+ getContentSection(),
3354
+ "",
3355
+ "---",
3356
+ "",
3357
+ getBlogSection(),
3358
+ "",
3359
+ "---",
3360
+ "",
3361
+ getStorefrontBotSection(cid)
3021
3362
  ].join("\n");
3022
3363
  default:
3023
- return `Unknown topic: "${topic}". Available topics: setup, products, cart, checkout, checkout-custom-fields, payment, auth, order-confirmation, inventory, discounts, recommendations, product-customization-fields, tax, i18n, critical-rules, type-reference, admin, inquiries, content, all`;
3364
+ return `Unknown topic: "${topic}". Available topics: setup, products, cart, checkout, checkout-custom-fields, payment, auth, order-confirmation, inventory, discounts, recommendations, product-customization-fields, tax, i18n, critical-rules, type-reference, admin, inquiries, content, blog, all`;
3024
3365
  }
3025
3366
  }
3026
3367
 
@@ -3049,6 +3390,7 @@ var GET_SDK_DOCS_SCHEMA = {
3049
3390
  "admin",
3050
3391
  "inquiries",
3051
3392
  "content",
3393
+ "storefront-bot",
3052
3394
  "all"
3053
3395
  ]).describe("The SDK documentation topic to retrieve"),
3054
3396
  salesChannelId: import_zod.z.string().optional().describe("Sales channel ID (starts with vc_). Used to personalize setup code."),
@@ -3080,12 +3422,18 @@ interface Product {
3080
3422
  description?: string | null;
3081
3423
  descriptionFormat?: 'text' | 'html' | 'markdown' | null;
3082
3424
  sku: string;
3083
- basePrice: string; // Use parseFloat() for calculations
3084
- salePrice?: string | null;
3425
+ basePrice: string; // For VARIABLE products: MIN(variants.price). For SIMPLE: the parent price. parseFloat() for math.
3426
+ salePrice?: string | null; // For VARIABLE: MIN(variants.salePrice WHERE NOT NULL) \u2014 null iff no variant is on sale (WC is_on_sale() semantic). For SIMPLE: the parent sale price. Reliable "on sale?" check: product.salePrice !== null.
3085
3427
  costPrice?: string | null;
3086
- priceMin?: string | null; // Lowest variant price (VARIABLE products). Use for catalog/JSON-LD range display.
3087
- priceMax?: string | null; // Highest variant price (VARIABLE products).
3428
+ priceMin?: string | null; // Lowest variant price (VARIABLE products) \u2014 same as basePrice for VARIABLE, kept for back-compat and JSON-LD range display.
3429
+ priceMax?: string | null; // Highest variant price (VARIABLE products). Use with priceMin for "\u20AA49 \u2013 \u20AA199" range.
3088
3430
  priceVaries?: boolean; // true when variant prices differ \u2014 show "\u20AA49 \u2013 \u20AA199" range.
3431
+ // FX DISPLAY overlay (PRD \xA723). Set only when getProducts({ regionId }) was called AND region.currency !== store.currency. basePrice/salePrice above stay in store currency \u2014 these are additive, display-only. The buyer is still charged in store currency at checkout; payment provider handles customer-side FX.
3432
+ displayPrice?: string; // basePrice \xD7 daily FX rate, in displayCurrency.
3433
+ displaySalePrice?: string; // salePrice \xD7 rate (if salePrice present).
3434
+ displayPriceMin?: string; // priceMin \xD7 rate (VARIABLE products).
3435
+ displayPriceMax?: string; // priceMax \xD7 rate (VARIABLE products).
3436
+ displayCurrency?: string; // ISO 4217 of the region \u2014 e.g. "EUR".
3089
3437
  status: string;
3090
3438
  type: 'SIMPLE' | 'VARIABLE';
3091
3439
  isDownloadable?: boolean;
@@ -3129,6 +3477,10 @@ interface ProductVariant {
3129
3477
  name?: string | null;
3130
3478
  price?: string | null;
3131
3479
  salePrice?: string | null;
3480
+ // FX DISPLAY overlay (PRD \xA723) \u2014 same semantics as on the parent Product.
3481
+ displayPrice?: string;
3482
+ displaySalePrice?: string;
3483
+ displayCurrency?: string;
3132
3484
  attributes?: Record<string, string> | null;
3133
3485
  options?: Array<{ name: string; value: string }>;
3134
3486
  inventory?: InventoryInfo | null;
@@ -3200,6 +3552,11 @@ interface ProductQueryParams {
3200
3552
  metafields?: Record<string, string | string[]>;
3201
3553
  sortBy?: 'name' | 'price' | 'createdAt';
3202
3554
  sortOrder?: 'asc' | 'desc';
3555
+ // PRD \xA722: resolve DISPLAY prices for this region. When set + valid, each
3556
+ // product/variant gains resolvedPrice/resolvedCurrency/priceSource (additive;
3557
+ // basePrice/salePrice untouched). Absent/invalid region \u2192 nothing attached.
3558
+ // Display-only. Ignored in vibe-coded (vc_*) mode (storefront/admin paths only).
3559
+ regionId?: string;
3203
3560
  }
3204
3561
 
3205
3562
  interface SearchSuggestions {
@@ -3325,6 +3682,7 @@ interface Checkout {
3325
3682
  status: CheckoutStatus;
3326
3683
  email?: string | null;
3327
3684
  customerId?: string | null;
3685
+ regionId?: string | null; // multi-region: recorded for reporting + provider scoping (currency follows the cart until FX lands)
3328
3686
  shippingAddress?: CheckoutAddress | null;
3329
3687
  billingAddress?: CheckoutAddress | null;
3330
3688
  shippingRateId?: string | null;
@@ -3401,6 +3759,7 @@ interface CreateCheckoutDto {
3401
3759
  cartId: string;
3402
3760
  customerId?: string;
3403
3761
  selectedItemIds?: string[]; // Partial checkout
3762
+ regionId?: string; // multi-region: associate the checkout with a region (must belong to the store; 400 if unknown)
3404
3763
  }
3405
3764
 
3406
3765
  // startGuestCheckout() return type \u2014 DISCRIMINATED UNION
@@ -3419,6 +3778,8 @@ interface TaxBreakdownItem {
3419
3778
  name: string;
3420
3779
  rate: number; // decimal: 0.17 = 17%
3421
3780
  amount: number;
3781
+ taxClassId?: string | null; // rate's tax class (null = Standard)
3782
+ taxClassSlug?: string | null; // class slug for attribution (null = Standard)
3422
3783
  }`;
3423
3784
  var ORDERS_TYPES = `// ---- Orders ----
3424
3785
 
@@ -3476,6 +3837,7 @@ interface OrderItem {
3476
3837
  // Snapshot of buyer-submitted customization values captured at checkout.
3477
3838
  // Keyed by metafield slug. \`value\` is string[] for MULTI_SELECT / GALLERY, string otherwise.
3478
3839
  customizations?: Record<string, { label: string; value: string | string[]; type: string }>;
3840
+ taxClassSlug?: string; // frozen slug of the line's resolved tax class (omitted = Standard fallback)
3479
3841
  }
3480
3842
 
3481
3843
  interface OrderCustomer {
@@ -3658,7 +4020,7 @@ function formatPrice(priceString: string | number | undefined | null, options?:
3658
4020
  function getProductPrice(product: Pick<Product, 'basePrice' | 'salePrice'>): number;
3659
4021
  function getProductPriceInfo(product: Pick<Product, 'basePrice' | 'salePrice' | 'discount' | 'priceMin' | 'priceVaries'>): {
3660
4022
  price: number; originalPrice: number; isOnSale: boolean; discountAmount: number; discountPercent: number;
3661
- }; // Falls back to priceMin when basePrice=0 (VARIABLE products)
4023
+ }; // For VARIABLE products, basePrice/salePrice are pre-aggregated (MIN of variants) \u2014 the helper just reads them. Use this whenever you need an "on sale?" badge or a "X% off" label.
3662
4024
  function getVariantPrice(variant: Pick<ProductVariant, 'price' | 'salePrice'>, productBasePrice: string): number;
3663
4025
 
3664
4026
  // Cart helpers
@@ -4191,6 +4553,122 @@ export interface Content<T extends ContentType = ContentType> extends ContentSum
4191
4553
  // stale-while-revalidate=60. Storefront changes propagate within ~5 min
4192
4554
  // of the merchant publishing. Do not add extra client-side caching
4193
4555
  // beyond Next.js's default fetch cache.`;
4556
+ var REGIONS_TYPES = `// ---- Regions & Tax Classes (Admin mode, apiKey) ----
4557
+ // storeId is derived from the API key \u2014 never pass it on admin calls.
4558
+
4559
+ // ---- Regions ---- (scopes: regions:read / regions:write)
4560
+ // A region binds countries \u2192 currency + tax-display mode + payment providers.
4561
+ interface Region {
4562
+ id: string;
4563
+ accountId: string;
4564
+ storeId: string;
4565
+ name: string;
4566
+ slug: string;
4567
+ currency: string; // ISO 4217, e.g. "EUR"
4568
+ countries: string[]; // ISO 3166-1 alpha-2, e.g. ["DE","FR"]
4569
+ taxInclusive: boolean; // show prices tax-inclusive in this region?
4570
+ automaticTaxes: boolean;
4571
+ isDefault: boolean; // fallback when buyer's country maps to no region
4572
+ isActive: boolean;
4573
+ paymentProviders?: RegionPaymentProvider[];
4574
+ createdAt: string;
4575
+ updatedAt: string;
4576
+ }
4577
+
4578
+ interface RegionPaymentProvider {
4579
+ id: string;
4580
+ regionId: string;
4581
+ appInstallationId: string;
4582
+ isEnabled: boolean;
4583
+ createdAt: string;
4584
+ }
4585
+
4586
+ interface CreateRegionDto {
4587
+ name: string;
4588
+ currency: string; // ISO 4217
4589
+ countries: string[]; // ISO 3166-1 alpha-2
4590
+ taxInclusive?: boolean;
4591
+ automaticTaxes?: boolean;
4592
+ isDefault?: boolean;
4593
+ paymentProviderIds?: string[]; // AppInstallation IDs to enable here
4594
+ }
4595
+ interface UpdateRegionDto extends Partial<CreateRegionDto> { isActive?: boolean }
4596
+
4597
+ // client.detectRegion(country, regions): Region | null \u2014 pure, no network.
4598
+ // \u2192 region whose countries includes the code, else the default region, else null.
4599
+ // Pass the resolved regionId to createCheckout to associate the checkout with a
4600
+ // region (recorded for reporting + provider scoping; currency follows the cart
4601
+ // until FX price conversion lands).
4602
+ //
4603
+ // Storefront (public, no apiKey \u2014 storeId mode):
4604
+ interface PublicRegion {
4605
+ id: string; name: string; slug: string; currency: string;
4606
+ countries: string[]; taxInclusive: boolean; isDefault: boolean;
4607
+ }
4608
+ interface PublicRegionDetail extends PublicRegion {
4609
+ paymentProviders: Array<{ id: string; appId: string; name: string | null }>;
4610
+ }
4611
+ // getStoreRegions(): { data: PublicRegion[] } \u2014 active regions, default first.
4612
+ // getStoreRegion(regionId): PublicRegionDetail \u2014 one region + its providers.
4613
+ interface AutoRegionResponse {
4614
+ region: PublicRegion | null;
4615
+ matched: boolean; // true=country was in a region's list; false=fell back to default
4616
+ country: string; // upper-cased ISO-3166-1 alpha-2; '' when caller omitted it
4617
+ }
4618
+ // getAutoRegion(country): AutoRegionResponse \u2014 server-side resolution in one
4619
+ // round trip. Pair with the country your edge runtime extracts
4620
+ // (CF-IPCountry / request.geo.country / etc.) \u2014 Brainerce does NOT derive the
4621
+ // country from the request IP server-side (storefront != end-customer).
4622
+
4623
+ // ---- Tax Classes ---- (scopes: tax-classes:read / tax-classes:write)
4624
+ // Charge differential rates by product type. A TaxRate may target a class via
4625
+ // taxClassId; a rate with taxClassId=null is the Standard fallback. Per-line
4626
+ // resolution: variant \u2192 product \u2192 category \u2192 store default \u2192 null.
4627
+ interface TaxClass {
4628
+ id: string;
4629
+ accountId: string;
4630
+ storeId: string;
4631
+ name: string;
4632
+ slug: string; // kebab-case, unique per store
4633
+ description?: string | null;
4634
+ isDefault: boolean; // auto-applied to products without an explicit class
4635
+ createdAt: string;
4636
+ updatedAt: string;
4637
+ }
4638
+ interface CreateTaxClassDto { name: string; slug: string; description?: string; isDefault?: boolean }
4639
+ type UpdateTaxClassDto = Partial<CreateTaxClassDto>;
4640
+ // Bulk-assign a class to entities:
4641
+ interface AssignTaxClassDto { productIds?: string[]; variantIds?: string[]; categoryIds?: string[] }
4642
+
4643
+ // TaxRate / CreateTaxRateDto carry an optional taxClassId (null = Standard).
4644
+ // rate is a WHOLE PERCENTAGE (7.25 = 7.25%).
4645
+ //
4646
+ // SDK methods (admin):
4647
+ // Regions: getRegions(), getRegion(id), createRegion(dto), updateRegion(id, dto),
4648
+ // deleteRegion(id), setDefaultRegion(id), updateRegionPaymentProviders(id, ids),
4649
+ // addRegionCountries(id, codes), removeRegionCountry(id, code),
4650
+ // getRegionCompatibleProviders(id), detectRegion(country, regions)
4651
+ // Tax classes: getTaxClasses(), getTaxClass(id), createTaxClass(dto), updateTaxClass(id, dto),
4652
+ // deleteTaxClass(id), setDefaultTaxClass(id), assignTaxClass(id, dto),
4653
+ // mergeTaxClasses(id, targetId)
4654
+ //
4655
+ // SDK methods (storefront, public \u2014 storeId mode, no apiKey):
4656
+ // getStoreRegions(), getStoreRegion(id), getAutoRegion(country)
4657
+ // getStoreTaxClasses(), estimateTax({ country?, subtotal })
4658
+ //
4659
+ // estimateTax returns:
4660
+ interface TaxEstimateResponse {
4661
+ appliesTax: boolean;
4662
+ rate: number | null; // percent (18 = 18%) \u2014 null when no matching rule
4663
+ rateName: string | null;
4664
+ estimatedTax: number; // tax portion of subtotal in store currency
4665
+ pricesIncludeTax: boolean;
4666
+ currency: string; // store currency (cart/order currency)
4667
+ note: string; // disclaimer \u2014 preview is non-binding
4668
+ }
4669
+ // Non-binding by design: the authoritative tax runs at checkout against the
4670
+ // full shipping address. The country comes from your edge runtime
4671
+ // (CF-IPCountry / request.geo.country / etc.), NOT the request IP.`;
4194
4672
  var TYPES_BY_DOMAIN = {
4195
4673
  products: PRODUCTS_TYPES,
4196
4674
  cart: CART_TYPES,
@@ -4202,7 +4680,8 @@ var TYPES_BY_DOMAIN = {
4202
4680
  inquiries: INQUIRIES_TYPES,
4203
4681
  reviews: REVIEWS_TYPES,
4204
4682
  "modifier-groups": MODIFIER_GROUPS_TYPES,
4205
- content: CONTENT_TYPES
4683
+ content: CONTENT_TYPES,
4684
+ regions: REGIONS_TYPES
4206
4685
  };
4207
4686
  function getTypesByDomain(domain) {
4208
4687
  if (domain === "all") {
@@ -4233,10 +4712,12 @@ var GET_TYPE_DEFINITIONS_SCHEMA = {
4233
4712
  "helpers",
4234
4713
  "inquiries",
4235
4714
  "reviews",
4715
+ "modifier-groups",
4236
4716
  "content",
4717
+ "regions",
4237
4718
  "all"
4238
4719
  ]).describe(
4239
- 'The domain of types to retrieve. Use "helpers" for helper function signatures and common types like StoreInfo, PaginatedResponse. Use "content" for FAQ / Footer / Header / Announcement / RichText / Page.'
4720
+ 'The domain of types to retrieve. Use "helpers" for helper function signatures and common types like StoreInfo, PaginatedResponse. Use "content" for FAQ / Footer / Header / Announcement / RichText / Page. Use "regions" for Region / TaxClass admin types.'
4240
4721
  )
4241
4722
  };
4242
4723
  async function handleGetTypeDefinitions(args) {
@@ -6026,11 +6507,16 @@ Build both steps EVEN IF the store has no email provider configured today \u2014
6026
6507
  });
6027
6508
  window.location.href = authorizationUrl; // full-page redirect, NOT a popup
6028
6509
  \`\`\`
6029
- 3. **On the callback page** the URL contains \`token\` + \`oauth_success\` (or \`oauth_error\`) query params. Extract and apply the token:
6510
+ 3. **On the callback page** the URL contains \`auth_code\` + \`oauth_success\` (or \`oauth_error\`) query params. Exchange the single-use code for the JWT \u2014 never read the token from the URL:
6030
6511
  \`\`\`ts
6031
- const token = new URLSearchParams(location.search).get('token');
6032
- if (token) client.setCustomerToken(token); // then redirect to account
6512
+ const params = new URLSearchParams(location.search);
6513
+ const code = params.get('auth_code');
6514
+ if (code) {
6515
+ const result = await client.exchangeOAuthCode(code);
6516
+ client.setCustomerToken(result.token); // then redirect to account
6517
+ }
6033
6518
  \`\`\`
6519
+ The legacy \`?token=\` URL param is still emitted for backward compatibility but will be removed in the next major release \u2014 migrate to \`auth_code\` now.
6034
6520
  4. **On \`oauth_error\`:** redirect to login with an error message.
6035
6521
 
6036
6522
  Build the OAuth button region AND the callback handler even when no providers are configured. They auto-hide.`
@@ -6512,7 +6998,9 @@ function renderCapabilitiesSummary(caps) {
6512
6998
  );
6513
6999
  lines.push(`- Downloadable products: ${caps.features.hasDownloadableProducts ? "yes" : "no"}`);
6514
7000
  lines.push(`- Checkout custom fields: ${caps.features.hasCheckoutCustomFields ? "yes" : "no"}`);
6515
- lines.push(`- Content (FAQ/footer/header/announcements/pages): ${caps.features.hasContent ? "seeded" : "none \u2014 build chrome UI with fallbacks"}`);
7001
+ lines.push(
7002
+ `- Content (FAQ/footer/header/announcements/pages): ${caps.features.hasContent ? "seeded" : "none \u2014 build chrome UI with fallbacks"}`
7003
+ );
6516
7004
  lines.push(
6517
7005
  `- Email verification: ${caps.connection.requireEmailVerification ? "required" : "not required (still build the verify-email flow)"}`
6518
7006
  );