@brainerce/mcp-server 3.12.0 → 3.12.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/bin/http.js CHANGED
@@ -721,17 +721,19 @@ function getPaymentProvidersSection() {
721
721
  return `## Payment Provider Detection (DO THIS FIRST on checkout page!)
722
722
 
723
723
  \`\`\`typescript
724
- const { hasPayments, providers } = await client.getPaymentProviders();
724
+ const { hasPayments, providers, defaultProvider } = await client.getPaymentProviders();
725
725
  if (!hasPayments) {
726
726
  // Show error: "Payment is not configured for this store"
727
727
  return;
728
728
  }
729
- const stripeProvider = providers.find(p => p.provider === 'stripe');
730
- const growProvider = providers.find(p => p.provider === 'grow');
731
- const paypalProvider = providers.find(p => p.provider === 'paypal');
729
+ // Primary vs additive (Shopify-parity): render wallets as express buttons ABOVE the card form.
730
+ const expressMethods = providers.filter(p => p.isAdditive); // e.g. PayPal (WALLET)
731
+ const primary = defaultProvider && !defaultProvider.isAdditive ? defaultProvider : undefined;
732
732
  \`\`\`
733
733
 
734
- 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\`.
734
+ 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\`, plus the taxonomy fields \`methodType\` (\`'CREDIT_CARD'\` = the single primary card processor that settles the order; \`'WALLET'\`/other = additive), \`isAdditive\`, and \`presentation\` (\`'card_form'\` | \`'express_button'\` | \u2026).
735
+
736
+ **Primary vs. additive:** there is one primary card processor (the \`defaultProvider\`, \`isAdditive: false\`) plus any additive methods (\`isAdditive: true\`, e.g. PayPal). Render additive methods as accelerated-checkout **express buttons above** the card form \u2014 they sit alongside the primary, never replace it. Create the intent with the tapped provider's \`id\`. (Exception: a wallet-only store has no card processor, so its wallet becomes the \`defaultProvider\` and stands alone.)
735
737
 
736
738
  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**:
737
739
 
@@ -1119,11 +1121,22 @@ client-side overlay needed. Swatch colors are language-agnostic.
1119
1121
  function ProductDescription({ product }: { product: Product }) {
1120
1122
  const content = getDescriptionContent(product);
1121
1123
  if (!content) return null;
1122
- if ('html' in content) return <div dangerouslySetInnerHTML={{ __html: content.html }} />;
1124
+ // Descriptions are merchant HTML and may contain <video> + host-locked
1125
+ // YouTube/Vimeo <iframe> embeds. NEVER render unsanitized \u2014 sanitize and
1126
+ // allowlist video/source + iframe (restricted to youtube/vimeo hosts).
1127
+ if ('html' in content) {
1128
+ return <div dangerouslySetInnerHTML={{ __html: sanitizeProductHtml(content.html) }} />;
1129
+ }
1123
1130
  return <p>{content.text}</p>;
1124
1131
  }
1125
1132
  \`\`\`
1126
1133
 
1134
+ **Sanitize descriptions.** \`sanitizeProductHtml\` is the host-locked sanitizer the
1135
+ \`create-brainerce-store\` scaffold ships at \`src/lib/sanitize-html.ts\`. If you roll
1136
+ your own, allow \`video\`/\`source\` and \`iframe\` **restricted** to \`www.youtube.com\`,
1137
+ \`www.youtube-nocookie.com\`, \`player.vimeo.com\` (never arbitrary iframes), and add
1138
+ those hosts to your CSP \`frame-src\` or embeds render blank.
1139
+
1127
1140
  ### Metafields (Custom Product Fields) \u2014 display on product detail!
1128
1141
 
1129
1142
  **IMPORTANT: Render metafield values based on \`mf.type\`!** Don't just display \`mf.value\` as text for all types.
@@ -1206,7 +1219,28 @@ const { url } = await client.uploadCustomizationFile(file);
1206
1219
  // Then use the URL in metadata: metadata: { logo: url }
1207
1220
  \`\`\`
1208
1221
 
1209
- **Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\`, \`defaultValue\`, \`position\`
1222
+ **Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\` (array of \`{label, value, swatchColor?, swatchImageUrl?}\`), \`defaultValue\`, \`position\`
1223
+
1224
+ **SELECT / MULTI_SELECT \u2014 use \`option.value\` to submit, \`option.label\` to display:**
1225
+
1226
+ \`\`\`typescript
1227
+ for (const option of field.enumValues ?? []) {
1228
+ // option.value \u2192 what to submit in metadata
1229
+ // option.label \u2192 what to show the customer
1230
+ // option.swatchColor \u2192 optional hex color for color swatches
1231
+ }
1232
+ \`\`\`
1233
+
1234
+ **Display customizations in cart / checkout \u2014 no extra API call needed:**
1235
+
1236
+ CartItem and CheckoutLineItem both include a \`customizations\` object alongside \`metadata\`. Use \`customizations\` for display \u2014 it contains resolved labels, not raw keys.
1237
+
1238
+ \`\`\`typescript
1239
+ // Works for CartItem, CheckoutLineItem, and OrderItem \u2014 same shape
1240
+ const lines = Object.values(item.customizations ?? {})
1241
+ .map(c => \`\${c.label}: \${Array.isArray(c.value) ? c.value.join(', ') : c.value}\`);
1242
+ // e.g. ["Frame color: Gold", "Add-ons: Gift wrap"]
1243
+ \`\`\`
1210
1244
 
1211
1245
  ### Downloadable / Digital Products
1212
1246
 
@@ -3238,6 +3272,44 @@ by device) and can also be asked of the AI assistant ("how many visits did I get
3238
3272
  this month and from where?").
3239
3273
  `;
3240
3274
  }
3275
+ function getShippingAppSection() {
3276
+ return `## App Store Shipping (Shippo)
3277
+
3278
+ Merchants install Shippo from the Brainerce App Store. After connecting their own Shippo account
3279
+ (OAuth), live carrier rates appear automatically at checkout alongside any manually configured
3280
+ zone rates. **Billing goes directly to the merchant's Shippo account \u2014 Brainerce never charges.**
3281
+
3282
+ ### Checkout \u2014 live carrier rates
3283
+
3284
+ No SDK changes needed. Once Shippo is installed and configured, the existing
3285
+ \`getShippingRates()\` call returns both manual zone rates and live Shippo quotes:
3286
+
3287
+ \`\`\`typescript
3288
+ const { rates } = await client.setShippingAddress(checkoutId, { ... });
3289
+ // rates may include:
3290
+ // { id: 'shippo:rate_8f123abc', name: 'USPS Priority Mail', price: '8.50', currency: 'USD', source: 'carrier' }
3291
+ \`\`\`
3292
+
3293
+ Rates prefixed \`shippo:\` are live Shippo quotes. Pass the full \`id\` as \`shippingRateId\`
3294
+ when calling \`selectShippingMethod()\`.
3295
+
3296
+ ### Admin \u2014 purchase a shipping label
3297
+
3298
+ After the order is placed, the merchant purchases a label by passing the Shippo rate object_id:
3299
+
3300
+ \`\`\`typescript
3301
+ const label = await admin.createShippingLabel(orderId, {
3302
+ shippoRateObjectId: 'rate_8f123456789abcdef',
3303
+ });
3304
+ console.log(label.labelUrl); // PDF ready to print
3305
+ console.log(label.trackingNumber); // auto-populated on the order
3306
+ \`\`\`
3307
+
3308
+ The \`trackingNumber\` is stored on the \`Order\` and returned in \`getMyOrders()\`
3309
+ so customers can track their shipment automatically.
3310
+
3311
+ **SDK mode:** \`createShippingLabel()\` requires \`apiKey\` (admin mode only).`;
3312
+ }
3241
3313
  function getSectionByTopic(topic, connectionId, currency) {
3242
3314
  const cid = connectionId || "vc_YOUR_CONNECTION_ID";
3243
3315
  const cur = currency || "USD";
@@ -3297,6 +3369,9 @@ function getSectionByTopic(topic, connectionId, currency) {
3297
3369
  case "analytics":
3298
3370
  case "traffic-analytics":
3299
3371
  return getStorefrontAnalyticsSection(cid);
3372
+ case "shipping-app":
3373
+ case "shippo":
3374
+ return getShippingAppSection();
3300
3375
  case "all":
3301
3376
  return [
3302
3377
  "# Brainerce SDK \u2014 full topic dump",
@@ -3579,11 +3654,18 @@ interface ProductCustomizationField {
3579
3654
  maxLength?: number | null;
3580
3655
  minValue?: number | null; // NUMBER bounds
3581
3656
  maxValue?: number | null;
3582
- enumValues?: string[]; // REQUIRED for SELECT / MULTI_SELECT
3657
+ enumValues?: CustomizationFieldOption[]; // REQUIRED for SELECT / MULTI_SELECT
3583
3658
  defaultValue?: string | null;
3584
3659
  position: number; // Render order
3585
3660
  }
3586
3661
 
3662
+ interface CustomizationFieldOption {
3663
+ label: string; // Display label (e.g., "Rose Gold")
3664
+ value: string; // Value stored in CartItem.metadata
3665
+ swatchColor?: string | null; // Optional hex color (e.g., "#B76E79")
3666
+ swatchImageUrl?: string | null; // Optional swatch image URL
3667
+ }
3668
+
3587
3669
  interface ProductQueryParams {
3588
3670
  page?: number;
3589
3671
  limit?: number;
@@ -3677,6 +3759,7 @@ interface CartItem {
3677
3759
  image?: ProductImage | string | null;
3678
3760
  } | null;
3679
3761
  metadata?: Record<string, unknown> | null; // Buyer-submitted customization values, keyed by ProductCustomizationField.key
3762
+ customizations?: Record<string, { label: string; value: string | string[]; type: string }>; // Resolved labels \u2014 use this to display "Color: Silver" without extra getProduct() call
3680
3763
  createdAt: string;
3681
3764
  updatedAt: string;
3682
3765
  }
@@ -3776,6 +3859,7 @@ interface CheckoutLineItem {
3776
3859
  product: { id: string; name: string; sku: string; images?: ProductImage[]; };
3777
3860
  variant?: { id: string; name?: string | null; sku?: string | null; image?: ProductImage | string | null; } | null;
3778
3861
  metadata?: Record<string, unknown> | null; // Copied from CartItem.metadata \u2014 buyer customization values
3862
+ customizations?: Record<string, { label: string; value: string | string[]; type: string }>; // Resolved labels \u2014 same shape as OrderItem.customizations
3779
3863
  }
3780
3864
 
3781
3865
  interface CheckoutAddress {
@@ -4012,7 +4096,10 @@ interface PaymentProviderConfig {
4012
4096
  stripeAccountId?: string; // Stripe Connect only
4013
4097
  supportedMethods: string[];
4014
4098
  testMode: boolean;
4015
- isDefault: boolean;
4099
+ isDefault: boolean; // only a primary (CREDIT_CARD) is ever the default
4100
+ methodType: 'CREDIT_CARD' | 'WALLET' | 'OFFSITE' | 'LOCAL' | 'REDEEMABLE';
4101
+ presentation: 'card_form' | 'express_button' | 'redirect' | 'local_option' | 'redeemable_field';
4102
+ isAdditive: boolean; // wallet/alt method shown ALONGSIDE the primary (e.g. PayPal)
4016
4103
  }
4017
4104
  type PaymentProvider = PaymentProviderConfig;
4018
4105
 
@@ -5060,7 +5147,15 @@ const checkout = await client.getCheckout(checkoutId);
5060
5147
  import { client } from './brainerce';
5061
5148
 
5062
5149
  const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
5063
- // each provider: { id, provider, name, publicKey, supportedMethods, testMode, isDefault, clientSdk? }
5150
+ // each provider: { id, provider, name, publicKey, supportedMethods, testMode,
5151
+ // isDefault, methodType, presentation, isAdditive, clientSdk? }
5152
+ //
5153
+ // methodType splits providers into ONE primary card processor (CREDIT_CARD, the
5154
+ // defaultProvider that settles the order) and additive methods (isAdditive:true,
5155
+ // e.g. PayPal as a WALLET). Render additive methods as express buttons ABOVE the
5156
+ // card form \u2014 they sit alongside the primary, never replace it. Call
5157
+ // createPaymentIntent({ providerId }) with the tapped provider's id. (Wallet-only
5158
+ // store: with no card processor, the wallet becomes defaultProvider and stands alone.)
5064
5159
  //
5065
5160
  // After createPaymentIntent, the response carries a clientSdk.renderType that
5066
5161
  // tells your UI how to render the payment step. The 5 possible values:
package/dist/bin/stdio.js CHANGED
@@ -718,17 +718,19 @@ function getPaymentProvidersSection() {
718
718
  return `## Payment Provider Detection (DO THIS FIRST on checkout page!)
719
719
 
720
720
  \`\`\`typescript
721
- const { hasPayments, providers } = await client.getPaymentProviders();
721
+ const { hasPayments, providers, defaultProvider } = await client.getPaymentProviders();
722
722
  if (!hasPayments) {
723
723
  // Show error: "Payment is not configured for this store"
724
724
  return;
725
725
  }
726
- const stripeProvider = providers.find(p => p.provider === 'stripe');
727
- const growProvider = providers.find(p => p.provider === 'grow');
728
- const paypalProvider = providers.find(p => p.provider === 'paypal');
726
+ // Primary vs additive (Shopify-parity): render wallets as express buttons ABOVE the card form.
727
+ const expressMethods = providers.filter(p => p.isAdditive); // e.g. PayPal (WALLET)
728
+ const primary = defaultProvider && !defaultProvider.isAdditive ? defaultProvider : undefined;
729
729
  \`\`\`
730
730
 
731
- 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\`.
731
+ 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\`, plus the taxonomy fields \`methodType\` (\`'CREDIT_CARD'\` = the single primary card processor that settles the order; \`'WALLET'\`/other = additive), \`isAdditive\`, and \`presentation\` (\`'card_form'\` | \`'express_button'\` | \u2026).
732
+
733
+ **Primary vs. additive:** there is one primary card processor (the \`defaultProvider\`, \`isAdditive: false\`) plus any additive methods (\`isAdditive: true\`, e.g. PayPal). Render additive methods as accelerated-checkout **express buttons above** the card form \u2014 they sit alongside the primary, never replace it. Create the intent with the tapped provider's \`id\`. (Exception: a wallet-only store has no card processor, so its wallet becomes the \`defaultProvider\` and stands alone.)
732
734
 
733
735
  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**:
734
736
 
@@ -1116,11 +1118,22 @@ client-side overlay needed. Swatch colors are language-agnostic.
1116
1118
  function ProductDescription({ product }: { product: Product }) {
1117
1119
  const content = getDescriptionContent(product);
1118
1120
  if (!content) return null;
1119
- if ('html' in content) return <div dangerouslySetInnerHTML={{ __html: content.html }} />;
1121
+ // Descriptions are merchant HTML and may contain <video> + host-locked
1122
+ // YouTube/Vimeo <iframe> embeds. NEVER render unsanitized \u2014 sanitize and
1123
+ // allowlist video/source + iframe (restricted to youtube/vimeo hosts).
1124
+ if ('html' in content) {
1125
+ return <div dangerouslySetInnerHTML={{ __html: sanitizeProductHtml(content.html) }} />;
1126
+ }
1120
1127
  return <p>{content.text}</p>;
1121
1128
  }
1122
1129
  \`\`\`
1123
1130
 
1131
+ **Sanitize descriptions.** \`sanitizeProductHtml\` is the host-locked sanitizer the
1132
+ \`create-brainerce-store\` scaffold ships at \`src/lib/sanitize-html.ts\`. If you roll
1133
+ your own, allow \`video\`/\`source\` and \`iframe\` **restricted** to \`www.youtube.com\`,
1134
+ \`www.youtube-nocookie.com\`, \`player.vimeo.com\` (never arbitrary iframes), and add
1135
+ those hosts to your CSP \`frame-src\` or embeds render blank.
1136
+
1124
1137
  ### Metafields (Custom Product Fields) \u2014 display on product detail!
1125
1138
 
1126
1139
  **IMPORTANT: Render metafield values based on \`mf.type\`!** Don't just display \`mf.value\` as text for all types.
@@ -1203,7 +1216,28 @@ const { url } = await client.uploadCustomizationFile(file);
1203
1216
  // Then use the URL in metadata: metadata: { logo: url }
1204
1217
  \`\`\`
1205
1218
 
1206
- **Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\`, \`defaultValue\`, \`position\`
1219
+ **Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\` (array of \`{label, value, swatchColor?, swatchImageUrl?}\`), \`defaultValue\`, \`position\`
1220
+
1221
+ **SELECT / MULTI_SELECT \u2014 use \`option.value\` to submit, \`option.label\` to display:**
1222
+
1223
+ \`\`\`typescript
1224
+ for (const option of field.enumValues ?? []) {
1225
+ // option.value \u2192 what to submit in metadata
1226
+ // option.label \u2192 what to show the customer
1227
+ // option.swatchColor \u2192 optional hex color for color swatches
1228
+ }
1229
+ \`\`\`
1230
+
1231
+ **Display customizations in cart / checkout \u2014 no extra API call needed:**
1232
+
1233
+ CartItem and CheckoutLineItem both include a \`customizations\` object alongside \`metadata\`. Use \`customizations\` for display \u2014 it contains resolved labels, not raw keys.
1234
+
1235
+ \`\`\`typescript
1236
+ // Works for CartItem, CheckoutLineItem, and OrderItem \u2014 same shape
1237
+ const lines = Object.values(item.customizations ?? {})
1238
+ .map(c => \`\${c.label}: \${Array.isArray(c.value) ? c.value.join(', ') : c.value}\`);
1239
+ // e.g. ["Frame color: Gold", "Add-ons: Gift wrap"]
1240
+ \`\`\`
1207
1241
 
1208
1242
  ### Downloadable / Digital Products
1209
1243
 
@@ -3235,6 +3269,44 @@ by device) and can also be asked of the AI assistant ("how many visits did I get
3235
3269
  this month and from where?").
3236
3270
  `;
3237
3271
  }
3272
+ function getShippingAppSection() {
3273
+ return `## App Store Shipping (Shippo)
3274
+
3275
+ Merchants install Shippo from the Brainerce App Store. After connecting their own Shippo account
3276
+ (OAuth), live carrier rates appear automatically at checkout alongside any manually configured
3277
+ zone rates. **Billing goes directly to the merchant's Shippo account \u2014 Brainerce never charges.**
3278
+
3279
+ ### Checkout \u2014 live carrier rates
3280
+
3281
+ No SDK changes needed. Once Shippo is installed and configured, the existing
3282
+ \`getShippingRates()\` call returns both manual zone rates and live Shippo quotes:
3283
+
3284
+ \`\`\`typescript
3285
+ const { rates } = await client.setShippingAddress(checkoutId, { ... });
3286
+ // rates may include:
3287
+ // { id: 'shippo:rate_8f123abc', name: 'USPS Priority Mail', price: '8.50', currency: 'USD', source: 'carrier' }
3288
+ \`\`\`
3289
+
3290
+ Rates prefixed \`shippo:\` are live Shippo quotes. Pass the full \`id\` as \`shippingRateId\`
3291
+ when calling \`selectShippingMethod()\`.
3292
+
3293
+ ### Admin \u2014 purchase a shipping label
3294
+
3295
+ After the order is placed, the merchant purchases a label by passing the Shippo rate object_id:
3296
+
3297
+ \`\`\`typescript
3298
+ const label = await admin.createShippingLabel(orderId, {
3299
+ shippoRateObjectId: 'rate_8f123456789abcdef',
3300
+ });
3301
+ console.log(label.labelUrl); // PDF ready to print
3302
+ console.log(label.trackingNumber); // auto-populated on the order
3303
+ \`\`\`
3304
+
3305
+ The \`trackingNumber\` is stored on the \`Order\` and returned in \`getMyOrders()\`
3306
+ so customers can track their shipment automatically.
3307
+
3308
+ **SDK mode:** \`createShippingLabel()\` requires \`apiKey\` (admin mode only).`;
3309
+ }
3238
3310
  function getSectionByTopic(topic, connectionId, currency) {
3239
3311
  const cid = connectionId || "vc_YOUR_CONNECTION_ID";
3240
3312
  const cur = currency || "USD";
@@ -3294,6 +3366,9 @@ function getSectionByTopic(topic, connectionId, currency) {
3294
3366
  case "analytics":
3295
3367
  case "traffic-analytics":
3296
3368
  return getStorefrontAnalyticsSection(cid);
3369
+ case "shipping-app":
3370
+ case "shippo":
3371
+ return getShippingAppSection();
3297
3372
  case "all":
3298
3373
  return [
3299
3374
  "# Brainerce SDK \u2014 full topic dump",
@@ -3576,11 +3651,18 @@ interface ProductCustomizationField {
3576
3651
  maxLength?: number | null;
3577
3652
  minValue?: number | null; // NUMBER bounds
3578
3653
  maxValue?: number | null;
3579
- enumValues?: string[]; // REQUIRED for SELECT / MULTI_SELECT
3654
+ enumValues?: CustomizationFieldOption[]; // REQUIRED for SELECT / MULTI_SELECT
3580
3655
  defaultValue?: string | null;
3581
3656
  position: number; // Render order
3582
3657
  }
3583
3658
 
3659
+ interface CustomizationFieldOption {
3660
+ label: string; // Display label (e.g., "Rose Gold")
3661
+ value: string; // Value stored in CartItem.metadata
3662
+ swatchColor?: string | null; // Optional hex color (e.g., "#B76E79")
3663
+ swatchImageUrl?: string | null; // Optional swatch image URL
3664
+ }
3665
+
3584
3666
  interface ProductQueryParams {
3585
3667
  page?: number;
3586
3668
  limit?: number;
@@ -3674,6 +3756,7 @@ interface CartItem {
3674
3756
  image?: ProductImage | string | null;
3675
3757
  } | null;
3676
3758
  metadata?: Record<string, unknown> | null; // Buyer-submitted customization values, keyed by ProductCustomizationField.key
3759
+ customizations?: Record<string, { label: string; value: string | string[]; type: string }>; // Resolved labels \u2014 use this to display "Color: Silver" without extra getProduct() call
3677
3760
  createdAt: string;
3678
3761
  updatedAt: string;
3679
3762
  }
@@ -3773,6 +3856,7 @@ interface CheckoutLineItem {
3773
3856
  product: { id: string; name: string; sku: string; images?: ProductImage[]; };
3774
3857
  variant?: { id: string; name?: string | null; sku?: string | null; image?: ProductImage | string | null; } | null;
3775
3858
  metadata?: Record<string, unknown> | null; // Copied from CartItem.metadata \u2014 buyer customization values
3859
+ customizations?: Record<string, { label: string; value: string | string[]; type: string }>; // Resolved labels \u2014 same shape as OrderItem.customizations
3776
3860
  }
3777
3861
 
3778
3862
  interface CheckoutAddress {
@@ -4009,7 +4093,10 @@ interface PaymentProviderConfig {
4009
4093
  stripeAccountId?: string; // Stripe Connect only
4010
4094
  supportedMethods: string[];
4011
4095
  testMode: boolean;
4012
- isDefault: boolean;
4096
+ isDefault: boolean; // only a primary (CREDIT_CARD) is ever the default
4097
+ methodType: 'CREDIT_CARD' | 'WALLET' | 'OFFSITE' | 'LOCAL' | 'REDEEMABLE';
4098
+ presentation: 'card_form' | 'express_button' | 'redirect' | 'local_option' | 'redeemable_field';
4099
+ isAdditive: boolean; // wallet/alt method shown ALONGSIDE the primary (e.g. PayPal)
4013
4100
  }
4014
4101
  type PaymentProvider = PaymentProviderConfig;
4015
4102
 
@@ -5057,7 +5144,15 @@ const checkout = await client.getCheckout(checkoutId);
5057
5144
  import { client } from './brainerce';
5058
5145
 
5059
5146
  const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
5060
- // each provider: { id, provider, name, publicKey, supportedMethods, testMode, isDefault, clientSdk? }
5147
+ // each provider: { id, provider, name, publicKey, supportedMethods, testMode,
5148
+ // isDefault, methodType, presentation, isAdditive, clientSdk? }
5149
+ //
5150
+ // methodType splits providers into ONE primary card processor (CREDIT_CARD, the
5151
+ // defaultProvider that settles the order) and additive methods (isAdditive:true,
5152
+ // e.g. PayPal as a WALLET). Render additive methods as express buttons ABOVE the
5153
+ // card form \u2014 they sit alongside the primary, never replace it. Call
5154
+ // createPaymentIntent({ providerId }) with the tapped provider's id. (Wallet-only
5155
+ // store: with no card processor, the wallet becomes defaultProvider and stands alone.)
5061
5156
  //
5062
5157
  // After createPaymentIntent, the response carries a clientSdk.renderType that
5063
5158
  // tells your UI how to render the payment step. The 5 possible values:
package/dist/index.js CHANGED
@@ -744,17 +744,19 @@ function getPaymentProvidersSection() {
744
744
  return `## Payment Provider Detection (DO THIS FIRST on checkout page!)
745
745
 
746
746
  \`\`\`typescript
747
- const { hasPayments, providers } = await client.getPaymentProviders();
747
+ const { hasPayments, providers, defaultProvider } = await client.getPaymentProviders();
748
748
  if (!hasPayments) {
749
749
  // Show error: "Payment is not configured for this store"
750
750
  return;
751
751
  }
752
- const stripeProvider = providers.find(p => p.provider === 'stripe');
753
- const growProvider = providers.find(p => p.provider === 'grow');
754
- const paypalProvider = providers.find(p => p.provider === 'paypal');
752
+ // Primary vs additive (Shopify-parity): render wallets as express buttons ABOVE the card form.
753
+ const expressMethods = providers.filter(p => p.isAdditive); // e.g. PayPal (WALLET)
754
+ const primary = defaultProvider && !defaultProvider.isAdditive ? defaultProvider : undefined;
755
755
  \`\`\`
756
756
 
757
- 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\`.
757
+ 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\`, plus the taxonomy fields \`methodType\` (\`'CREDIT_CARD'\` = the single primary card processor that settles the order; \`'WALLET'\`/other = additive), \`isAdditive\`, and \`presentation\` (\`'card_form'\` | \`'express_button'\` | \u2026).
758
+
759
+ **Primary vs. additive:** there is one primary card processor (the \`defaultProvider\`, \`isAdditive: false\`) plus any additive methods (\`isAdditive: true\`, e.g. PayPal). Render additive methods as accelerated-checkout **express buttons above** the card form \u2014 they sit alongside the primary, never replace it. Create the intent with the tapped provider's \`id\`. (Exception: a wallet-only store has no card processor, so its wallet becomes the \`defaultProvider\` and stands alone.)
758
760
 
759
761
  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**:
760
762
 
@@ -1142,11 +1144,22 @@ client-side overlay needed. Swatch colors are language-agnostic.
1142
1144
  function ProductDescription({ product }: { product: Product }) {
1143
1145
  const content = getDescriptionContent(product);
1144
1146
  if (!content) return null;
1145
- if ('html' in content) return <div dangerouslySetInnerHTML={{ __html: content.html }} />;
1147
+ // Descriptions are merchant HTML and may contain <video> + host-locked
1148
+ // YouTube/Vimeo <iframe> embeds. NEVER render unsanitized \u2014 sanitize and
1149
+ // allowlist video/source + iframe (restricted to youtube/vimeo hosts).
1150
+ if ('html' in content) {
1151
+ return <div dangerouslySetInnerHTML={{ __html: sanitizeProductHtml(content.html) }} />;
1152
+ }
1146
1153
  return <p>{content.text}</p>;
1147
1154
  }
1148
1155
  \`\`\`
1149
1156
 
1157
+ **Sanitize descriptions.** \`sanitizeProductHtml\` is the host-locked sanitizer the
1158
+ \`create-brainerce-store\` scaffold ships at \`src/lib/sanitize-html.ts\`. If you roll
1159
+ your own, allow \`video\`/\`source\` and \`iframe\` **restricted** to \`www.youtube.com\`,
1160
+ \`www.youtube-nocookie.com\`, \`player.vimeo.com\` (never arbitrary iframes), and add
1161
+ those hosts to your CSP \`frame-src\` or embeds render blank.
1162
+
1150
1163
  ### Metafields (Custom Product Fields) \u2014 display on product detail!
1151
1164
 
1152
1165
  **IMPORTANT: Render metafield values based on \`mf.type\`!** Don't just display \`mf.value\` as text for all types.
@@ -1229,7 +1242,28 @@ const { url } = await client.uploadCustomizationFile(file);
1229
1242
  // Then use the URL in metadata: metadata: { logo: url }
1230
1243
  \`\`\`
1231
1244
 
1232
- **Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\`, \`defaultValue\`, \`position\`
1245
+ **Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\` (array of \`{label, value, swatchColor?, swatchImageUrl?}\`), \`defaultValue\`, \`position\`
1246
+
1247
+ **SELECT / MULTI_SELECT \u2014 use \`option.value\` to submit, \`option.label\` to display:**
1248
+
1249
+ \`\`\`typescript
1250
+ for (const option of field.enumValues ?? []) {
1251
+ // option.value \u2192 what to submit in metadata
1252
+ // option.label \u2192 what to show the customer
1253
+ // option.swatchColor \u2192 optional hex color for color swatches
1254
+ }
1255
+ \`\`\`
1256
+
1257
+ **Display customizations in cart / checkout \u2014 no extra API call needed:**
1258
+
1259
+ CartItem and CheckoutLineItem both include a \`customizations\` object alongside \`metadata\`. Use \`customizations\` for display \u2014 it contains resolved labels, not raw keys.
1260
+
1261
+ \`\`\`typescript
1262
+ // Works for CartItem, CheckoutLineItem, and OrderItem \u2014 same shape
1263
+ const lines = Object.values(item.customizations ?? {})
1264
+ .map(c => \`\${c.label}: \${Array.isArray(c.value) ? c.value.join(', ') : c.value}\`);
1265
+ // e.g. ["Frame color: Gold", "Add-ons: Gift wrap"]
1266
+ \`\`\`
1233
1267
 
1234
1268
  ### Downloadable / Digital Products
1235
1269
 
@@ -3261,6 +3295,44 @@ by device) and can also be asked of the AI assistant ("how many visits did I get
3261
3295
  this month and from where?").
3262
3296
  `;
3263
3297
  }
3298
+ function getShippingAppSection() {
3299
+ return `## App Store Shipping (Shippo)
3300
+
3301
+ Merchants install Shippo from the Brainerce App Store. After connecting their own Shippo account
3302
+ (OAuth), live carrier rates appear automatically at checkout alongside any manually configured
3303
+ zone rates. **Billing goes directly to the merchant's Shippo account \u2014 Brainerce never charges.**
3304
+
3305
+ ### Checkout \u2014 live carrier rates
3306
+
3307
+ No SDK changes needed. Once Shippo is installed and configured, the existing
3308
+ \`getShippingRates()\` call returns both manual zone rates and live Shippo quotes:
3309
+
3310
+ \`\`\`typescript
3311
+ const { rates } = await client.setShippingAddress(checkoutId, { ... });
3312
+ // rates may include:
3313
+ // { id: 'shippo:rate_8f123abc', name: 'USPS Priority Mail', price: '8.50', currency: 'USD', source: 'carrier' }
3314
+ \`\`\`
3315
+
3316
+ Rates prefixed \`shippo:\` are live Shippo quotes. Pass the full \`id\` as \`shippingRateId\`
3317
+ when calling \`selectShippingMethod()\`.
3318
+
3319
+ ### Admin \u2014 purchase a shipping label
3320
+
3321
+ After the order is placed, the merchant purchases a label by passing the Shippo rate object_id:
3322
+
3323
+ \`\`\`typescript
3324
+ const label = await admin.createShippingLabel(orderId, {
3325
+ shippoRateObjectId: 'rate_8f123456789abcdef',
3326
+ });
3327
+ console.log(label.labelUrl); // PDF ready to print
3328
+ console.log(label.trackingNumber); // auto-populated on the order
3329
+ \`\`\`
3330
+
3331
+ The \`trackingNumber\` is stored on the \`Order\` and returned in \`getMyOrders()\`
3332
+ so customers can track their shipment automatically.
3333
+
3334
+ **SDK mode:** \`createShippingLabel()\` requires \`apiKey\` (admin mode only).`;
3335
+ }
3264
3336
  function getSectionByTopic(topic, connectionId, currency) {
3265
3337
  const cid = connectionId || "vc_YOUR_CONNECTION_ID";
3266
3338
  const cur = currency || "USD";
@@ -3320,6 +3392,9 @@ function getSectionByTopic(topic, connectionId, currency) {
3320
3392
  case "analytics":
3321
3393
  case "traffic-analytics":
3322
3394
  return getStorefrontAnalyticsSection(cid);
3395
+ case "shipping-app":
3396
+ case "shippo":
3397
+ return getShippingAppSection();
3323
3398
  case "all":
3324
3399
  return [
3325
3400
  "# Brainerce SDK \u2014 full topic dump",
@@ -3602,11 +3677,18 @@ interface ProductCustomizationField {
3602
3677
  maxLength?: number | null;
3603
3678
  minValue?: number | null; // NUMBER bounds
3604
3679
  maxValue?: number | null;
3605
- enumValues?: string[]; // REQUIRED for SELECT / MULTI_SELECT
3680
+ enumValues?: CustomizationFieldOption[]; // REQUIRED for SELECT / MULTI_SELECT
3606
3681
  defaultValue?: string | null;
3607
3682
  position: number; // Render order
3608
3683
  }
3609
3684
 
3685
+ interface CustomizationFieldOption {
3686
+ label: string; // Display label (e.g., "Rose Gold")
3687
+ value: string; // Value stored in CartItem.metadata
3688
+ swatchColor?: string | null; // Optional hex color (e.g., "#B76E79")
3689
+ swatchImageUrl?: string | null; // Optional swatch image URL
3690
+ }
3691
+
3610
3692
  interface ProductQueryParams {
3611
3693
  page?: number;
3612
3694
  limit?: number;
@@ -3700,6 +3782,7 @@ interface CartItem {
3700
3782
  image?: ProductImage | string | null;
3701
3783
  } | null;
3702
3784
  metadata?: Record<string, unknown> | null; // Buyer-submitted customization values, keyed by ProductCustomizationField.key
3785
+ customizations?: Record<string, { label: string; value: string | string[]; type: string }>; // Resolved labels \u2014 use this to display "Color: Silver" without extra getProduct() call
3703
3786
  createdAt: string;
3704
3787
  updatedAt: string;
3705
3788
  }
@@ -3799,6 +3882,7 @@ interface CheckoutLineItem {
3799
3882
  product: { id: string; name: string; sku: string; images?: ProductImage[]; };
3800
3883
  variant?: { id: string; name?: string | null; sku?: string | null; image?: ProductImage | string | null; } | null;
3801
3884
  metadata?: Record<string, unknown> | null; // Copied from CartItem.metadata \u2014 buyer customization values
3885
+ customizations?: Record<string, { label: string; value: string | string[]; type: string }>; // Resolved labels \u2014 same shape as OrderItem.customizations
3802
3886
  }
3803
3887
 
3804
3888
  interface CheckoutAddress {
@@ -4035,7 +4119,10 @@ interface PaymentProviderConfig {
4035
4119
  stripeAccountId?: string; // Stripe Connect only
4036
4120
  supportedMethods: string[];
4037
4121
  testMode: boolean;
4038
- isDefault: boolean;
4122
+ isDefault: boolean; // only a primary (CREDIT_CARD) is ever the default
4123
+ methodType: 'CREDIT_CARD' | 'WALLET' | 'OFFSITE' | 'LOCAL' | 'REDEEMABLE';
4124
+ presentation: 'card_form' | 'express_button' | 'redirect' | 'local_option' | 'redeemable_field';
4125
+ isAdditive: boolean; // wallet/alt method shown ALONGSIDE the primary (e.g. PayPal)
4039
4126
  }
4040
4127
  type PaymentProvider = PaymentProviderConfig;
4041
4128
 
@@ -5083,7 +5170,15 @@ const checkout = await client.getCheckout(checkoutId);
5083
5170
  import { client } from './brainerce';
5084
5171
 
5085
5172
  const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
5086
- // each provider: { id, provider, name, publicKey, supportedMethods, testMode, isDefault, clientSdk? }
5173
+ // each provider: { id, provider, name, publicKey, supportedMethods, testMode,
5174
+ // isDefault, methodType, presentation, isAdditive, clientSdk? }
5175
+ //
5176
+ // methodType splits providers into ONE primary card processor (CREDIT_CARD, the
5177
+ // defaultProvider that settles the order) and additive methods (isAdditive:true,
5178
+ // e.g. PayPal as a WALLET). Render additive methods as express buttons ABOVE the
5179
+ // card form \u2014 they sit alongside the primary, never replace it. Call
5180
+ // createPaymentIntent({ providerId }) with the tapped provider's id. (Wallet-only
5181
+ // store: with no card processor, the wallet becomes defaultProvider and stands alone.)
5087
5182
  //
5088
5183
  // After createPaymentIntent, the response carries a clientSdk.renderType that
5089
5184
  // tells your UI how to render the payment step. The 5 possible values:
package/dist/index.mjs CHANGED
@@ -712,17 +712,19 @@ function getPaymentProvidersSection() {
712
712
  return `## Payment Provider Detection (DO THIS FIRST on checkout page!)
713
713
 
714
714
  \`\`\`typescript
715
- const { hasPayments, providers } = await client.getPaymentProviders();
715
+ const { hasPayments, providers, defaultProvider } = await client.getPaymentProviders();
716
716
  if (!hasPayments) {
717
717
  // Show error: "Payment is not configured for this store"
718
718
  return;
719
719
  }
720
- const stripeProvider = providers.find(p => p.provider === 'stripe');
721
- const growProvider = providers.find(p => p.provider === 'grow');
722
- const paypalProvider = providers.find(p => p.provider === 'paypal');
720
+ // Primary vs additive (Shopify-parity): render wallets as express buttons ABOVE the card form.
721
+ const expressMethods = providers.filter(p => p.isAdditive); // e.g. PayPal (WALLET)
722
+ const primary = defaultProvider && !defaultProvider.isAdditive ? defaultProvider : undefined;
723
723
  \`\`\`
724
724
 
725
- 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\`.
725
+ 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\`, plus the taxonomy fields \`methodType\` (\`'CREDIT_CARD'\` = the single primary card processor that settles the order; \`'WALLET'\`/other = additive), \`isAdditive\`, and \`presentation\` (\`'card_form'\` | \`'express_button'\` | \u2026).
726
+
727
+ **Primary vs. additive:** there is one primary card processor (the \`defaultProvider\`, \`isAdditive: false\`) plus any additive methods (\`isAdditive: true\`, e.g. PayPal). Render additive methods as accelerated-checkout **express buttons above** the card form \u2014 they sit alongside the primary, never replace it. Create the intent with the tapped provider's \`id\`. (Exception: a wallet-only store has no card processor, so its wallet becomes the \`defaultProvider\` and stands alone.)
726
728
 
727
729
  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**:
728
730
 
@@ -1110,11 +1112,22 @@ client-side overlay needed. Swatch colors are language-agnostic.
1110
1112
  function ProductDescription({ product }: { product: Product }) {
1111
1113
  const content = getDescriptionContent(product);
1112
1114
  if (!content) return null;
1113
- if ('html' in content) return <div dangerouslySetInnerHTML={{ __html: content.html }} />;
1115
+ // Descriptions are merchant HTML and may contain <video> + host-locked
1116
+ // YouTube/Vimeo <iframe> embeds. NEVER render unsanitized \u2014 sanitize and
1117
+ // allowlist video/source + iframe (restricted to youtube/vimeo hosts).
1118
+ if ('html' in content) {
1119
+ return <div dangerouslySetInnerHTML={{ __html: sanitizeProductHtml(content.html) }} />;
1120
+ }
1114
1121
  return <p>{content.text}</p>;
1115
1122
  }
1116
1123
  \`\`\`
1117
1124
 
1125
+ **Sanitize descriptions.** \`sanitizeProductHtml\` is the host-locked sanitizer the
1126
+ \`create-brainerce-store\` scaffold ships at \`src/lib/sanitize-html.ts\`. If you roll
1127
+ your own, allow \`video\`/\`source\` and \`iframe\` **restricted** to \`www.youtube.com\`,
1128
+ \`www.youtube-nocookie.com\`, \`player.vimeo.com\` (never arbitrary iframes), and add
1129
+ those hosts to your CSP \`frame-src\` or embeds render blank.
1130
+
1118
1131
  ### Metafields (Custom Product Fields) \u2014 display on product detail!
1119
1132
 
1120
1133
  **IMPORTANT: Render metafield values based on \`mf.type\`!** Don't just display \`mf.value\` as text for all types.
@@ -1197,7 +1210,28 @@ const { url } = await client.uploadCustomizationFile(file);
1197
1210
  // Then use the URL in metadata: metadata: { logo: url }
1198
1211
  \`\`\`
1199
1212
 
1200
- **Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\`, \`defaultValue\`, \`position\`
1213
+ **Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\` (array of \`{label, value, swatchColor?, swatchImageUrl?}\`), \`defaultValue\`, \`position\`
1214
+
1215
+ **SELECT / MULTI_SELECT \u2014 use \`option.value\` to submit, \`option.label\` to display:**
1216
+
1217
+ \`\`\`typescript
1218
+ for (const option of field.enumValues ?? []) {
1219
+ // option.value \u2192 what to submit in metadata
1220
+ // option.label \u2192 what to show the customer
1221
+ // option.swatchColor \u2192 optional hex color for color swatches
1222
+ }
1223
+ \`\`\`
1224
+
1225
+ **Display customizations in cart / checkout \u2014 no extra API call needed:**
1226
+
1227
+ CartItem and CheckoutLineItem both include a \`customizations\` object alongside \`metadata\`. Use \`customizations\` for display \u2014 it contains resolved labels, not raw keys.
1228
+
1229
+ \`\`\`typescript
1230
+ // Works for CartItem, CheckoutLineItem, and OrderItem \u2014 same shape
1231
+ const lines = Object.values(item.customizations ?? {})
1232
+ .map(c => \`\${c.label}: \${Array.isArray(c.value) ? c.value.join(', ') : c.value}\`);
1233
+ // e.g. ["Frame color: Gold", "Add-ons: Gift wrap"]
1234
+ \`\`\`
1201
1235
 
1202
1236
  ### Downloadable / Digital Products
1203
1237
 
@@ -3229,6 +3263,44 @@ by device) and can also be asked of the AI assistant ("how many visits did I get
3229
3263
  this month and from where?").
3230
3264
  `;
3231
3265
  }
3266
+ function getShippingAppSection() {
3267
+ return `## App Store Shipping (Shippo)
3268
+
3269
+ Merchants install Shippo from the Brainerce App Store. After connecting their own Shippo account
3270
+ (OAuth), live carrier rates appear automatically at checkout alongside any manually configured
3271
+ zone rates. **Billing goes directly to the merchant's Shippo account \u2014 Brainerce never charges.**
3272
+
3273
+ ### Checkout \u2014 live carrier rates
3274
+
3275
+ No SDK changes needed. Once Shippo is installed and configured, the existing
3276
+ \`getShippingRates()\` call returns both manual zone rates and live Shippo quotes:
3277
+
3278
+ \`\`\`typescript
3279
+ const { rates } = await client.setShippingAddress(checkoutId, { ... });
3280
+ // rates may include:
3281
+ // { id: 'shippo:rate_8f123abc', name: 'USPS Priority Mail', price: '8.50', currency: 'USD', source: 'carrier' }
3282
+ \`\`\`
3283
+
3284
+ Rates prefixed \`shippo:\` are live Shippo quotes. Pass the full \`id\` as \`shippingRateId\`
3285
+ when calling \`selectShippingMethod()\`.
3286
+
3287
+ ### Admin \u2014 purchase a shipping label
3288
+
3289
+ After the order is placed, the merchant purchases a label by passing the Shippo rate object_id:
3290
+
3291
+ \`\`\`typescript
3292
+ const label = await admin.createShippingLabel(orderId, {
3293
+ shippoRateObjectId: 'rate_8f123456789abcdef',
3294
+ });
3295
+ console.log(label.labelUrl); // PDF ready to print
3296
+ console.log(label.trackingNumber); // auto-populated on the order
3297
+ \`\`\`
3298
+
3299
+ The \`trackingNumber\` is stored on the \`Order\` and returned in \`getMyOrders()\`
3300
+ so customers can track their shipment automatically.
3301
+
3302
+ **SDK mode:** \`createShippingLabel()\` requires \`apiKey\` (admin mode only).`;
3303
+ }
3232
3304
  function getSectionByTopic(topic, connectionId, currency) {
3233
3305
  const cid = connectionId || "vc_YOUR_CONNECTION_ID";
3234
3306
  const cur = currency || "USD";
@@ -3288,6 +3360,9 @@ function getSectionByTopic(topic, connectionId, currency) {
3288
3360
  case "analytics":
3289
3361
  case "traffic-analytics":
3290
3362
  return getStorefrontAnalyticsSection(cid);
3363
+ case "shipping-app":
3364
+ case "shippo":
3365
+ return getShippingAppSection();
3291
3366
  case "all":
3292
3367
  return [
3293
3368
  "# Brainerce SDK \u2014 full topic dump",
@@ -3570,11 +3645,18 @@ interface ProductCustomizationField {
3570
3645
  maxLength?: number | null;
3571
3646
  minValue?: number | null; // NUMBER bounds
3572
3647
  maxValue?: number | null;
3573
- enumValues?: string[]; // REQUIRED for SELECT / MULTI_SELECT
3648
+ enumValues?: CustomizationFieldOption[]; // REQUIRED for SELECT / MULTI_SELECT
3574
3649
  defaultValue?: string | null;
3575
3650
  position: number; // Render order
3576
3651
  }
3577
3652
 
3653
+ interface CustomizationFieldOption {
3654
+ label: string; // Display label (e.g., "Rose Gold")
3655
+ value: string; // Value stored in CartItem.metadata
3656
+ swatchColor?: string | null; // Optional hex color (e.g., "#B76E79")
3657
+ swatchImageUrl?: string | null; // Optional swatch image URL
3658
+ }
3659
+
3578
3660
  interface ProductQueryParams {
3579
3661
  page?: number;
3580
3662
  limit?: number;
@@ -3668,6 +3750,7 @@ interface CartItem {
3668
3750
  image?: ProductImage | string | null;
3669
3751
  } | null;
3670
3752
  metadata?: Record<string, unknown> | null; // Buyer-submitted customization values, keyed by ProductCustomizationField.key
3753
+ customizations?: Record<string, { label: string; value: string | string[]; type: string }>; // Resolved labels \u2014 use this to display "Color: Silver" without extra getProduct() call
3671
3754
  createdAt: string;
3672
3755
  updatedAt: string;
3673
3756
  }
@@ -3767,6 +3850,7 @@ interface CheckoutLineItem {
3767
3850
  product: { id: string; name: string; sku: string; images?: ProductImage[]; };
3768
3851
  variant?: { id: string; name?: string | null; sku?: string | null; image?: ProductImage | string | null; } | null;
3769
3852
  metadata?: Record<string, unknown> | null; // Copied from CartItem.metadata \u2014 buyer customization values
3853
+ customizations?: Record<string, { label: string; value: string | string[]; type: string }>; // Resolved labels \u2014 same shape as OrderItem.customizations
3770
3854
  }
3771
3855
 
3772
3856
  interface CheckoutAddress {
@@ -4003,7 +4087,10 @@ interface PaymentProviderConfig {
4003
4087
  stripeAccountId?: string; // Stripe Connect only
4004
4088
  supportedMethods: string[];
4005
4089
  testMode: boolean;
4006
- isDefault: boolean;
4090
+ isDefault: boolean; // only a primary (CREDIT_CARD) is ever the default
4091
+ methodType: 'CREDIT_CARD' | 'WALLET' | 'OFFSITE' | 'LOCAL' | 'REDEEMABLE';
4092
+ presentation: 'card_form' | 'express_button' | 'redirect' | 'local_option' | 'redeemable_field';
4093
+ isAdditive: boolean; // wallet/alt method shown ALONGSIDE the primary (e.g. PayPal)
4007
4094
  }
4008
4095
  type PaymentProvider = PaymentProviderConfig;
4009
4096
 
@@ -5051,7 +5138,15 @@ const checkout = await client.getCheckout(checkoutId);
5051
5138
  import { client } from './brainerce';
5052
5139
 
5053
5140
  const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
5054
- // each provider: { id, provider, name, publicKey, supportedMethods, testMode, isDefault, clientSdk? }
5141
+ // each provider: { id, provider, name, publicKey, supportedMethods, testMode,
5142
+ // isDefault, methodType, presentation, isAdditive, clientSdk? }
5143
+ //
5144
+ // methodType splits providers into ONE primary card processor (CREDIT_CARD, the
5145
+ // defaultProvider that settles the order) and additive methods (isAdditive:true,
5146
+ // e.g. PayPal as a WALLET). Render additive methods as express buttons ABOVE the
5147
+ // card form \u2014 they sit alongside the primary, never replace it. Call
5148
+ // createPaymentIntent({ providerId }) with the tapped provider's id. (Wallet-only
5149
+ // store: with no card processor, the wallet becomes defaultProvider and stands alone.)
5055
5150
  //
5056
5151
  // After createPaymentIntent, the response carries a clientSdk.renderType that
5057
5152
  // tells your UI how to render the payment step. The 5 possible values:
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@brainerce/mcp-server",
3
- "version": "3.12.0",
3
+ "version": "3.12.1",
4
+ "packageManager": "pnpm@9.0.0",
4
5
  "description": "Framework-agnostic domain knowledge API for Brainerce. Provides SDK docs, types, business flows, critical rules, and store capabilities to AI tools like Lovable, Cursor, Bolt, v0, and Claude Code. Does NOT provide framework-specific boilerplate — clients build in whatever framework fits.",
5
6
  "bin": {
6
7
  "brainerce-mcp": "dist/bin/stdio.js"