@brainerce/mcp-server 3.12.3 → 3.12.4

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
@@ -3,7 +3,6 @@
3
3
 
4
4
  // src/bin/http.ts
5
5
  var import_node_http = require("http");
6
- var import_node_crypto = require("crypto");
7
6
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
8
7
  var import_sse = require("@modelcontextprotocol/sdk/server/sse.js");
9
8
 
@@ -220,7 +219,7 @@ async function startCheckout() {
220
219
  2. Customer optionally applies coupon on cart page \u2192 \`applyCoupon(cartId, code)\`
221
220
  3. Detect payment providers \u2192 \`getPaymentProviders()\`
222
221
  4. Start checkout session (\`startGuestCheckout()\` or \`createCheckout()\`)
223
- 5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\`
222
+ 5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\` \u2014 also pass \`notes\` from the **"Order notes" textarea that every checkout page should include by default** (optional field, max 2000 chars; lands on the order for the merchant)
224
223
  6. Select shipping method \u2192 \`selectShippingMethod()\`
225
224
  6b. (Optional) Set checkout custom fields \u2192 \`setCheckoutCustomFields()\` \u2014 surcharges auto-calculated
226
225
  7. Create payment intent \u2192 \`createPaymentIntent()\` \u2192 returns \`{ clientSecret, provider }\`
@@ -242,6 +241,8 @@ function CheckoutPage() {
242
241
  const [checkout, setCheckout] = useState<Checkout | null>(null);
243
242
  const [error, setError] = useState<string | null>(null);
244
243
  const [loading, setLoading] = useState(true);
244
+ // Optional "Order notes" textarea \u2014 render it on the checkout page by default
245
+ const [orderNotes, setOrderNotes] = useState('');
245
246
 
246
247
  useEffect(() => { startCheckout(); }, []);
247
248
 
@@ -276,6 +277,7 @@ function CheckoutPage() {
276
277
  postalCode: cart.shippingAddress.postalCode,
277
278
  country: cart.shippingAddress.country,
278
279
  phone: cart.shippingAddress.phone,
280
+ notes: orderNotes || undefined, // from the "Order notes" textarea \u2014 include one by default
279
281
  });
280
282
  setCheckout(checkoutData);
281
283
 
@@ -2126,6 +2128,7 @@ A useful order card includes ALL of the following when the data is present. Each
2126
2128
  | **Tracking** | \`order.trackingNumber\`, \`order.trackingUrl\`, \`order.carrier\`, \`order.shippedAt\`, \`order.deliveredAt\` | Link out to \`trackingUrl\` when set. |
2127
2129
  | **Payment** | \`order.paymentMethod\`, \`order.financialStatus\` | Badge \`financialStatus\` (paid / pending / refunded / partially_refunded). |
2128
2130
  | Downloads | \`order.hasDownloads\` \u2192 \`client.getOrderDownloads(id)\` | Separate call; returns \`OrderDownloadLink[]\`. |
2131
+ | **Order note** | \`order.notes\` | The shopper's own checkout note, echoed back read-only. Render when present ("Your order note"). |
2129
2132
  | Financial summary | \`order.subtotal\`, \`order.appliedDiscounts\`, \`order.couponCode\` + \`couponDiscount\`, \`order.shippingAmount\`, \`order.taxAmount\`, \`order.totalAmount\` | Breakdown rows + final total. In VAT-inclusive mode \`taxAmount\` is 0 \u2014 read \`order.taxBreakdown.totalTax\` and label "Tax (incl.)". |
2130
2133
 
2131
2134
  #### Rendering \`order.items[i].customizations\` by type
@@ -2151,7 +2154,6 @@ The storefront scaffolded by \`create-brainerce-store\` already implements this.
2151
2154
  These fields are NOT returned to buyers by \`/customers/me/orders\`. If you see them in older examples, ignore them \u2014 they are for merchant/admin views only.
2152
2155
 
2153
2156
  - \`order.accountId\`, \`order.storeId\`, \`order.customerId\` (already known on the request)
2154
- - \`order.notes\` (internal merchant notes)
2155
2157
  - \`order.customFieldValues\` (order-level checkout fields \u2014 merchant concept; buyers see \`items[i].customizations\`)
2156
2158
  - \`order.appliedSurcharges\`, \`order.surchargeAmount\`, \`order.appliedRuleIds\`, \`order.downloadMeta\`, \`order.pickupLocationData\``;
2157
2159
  }
@@ -2199,7 +2201,22 @@ function OrderConfirmation() {
2199
2201
 
2200
2202
  **Common mistake:** Don't just check URL params and show success immediately!
2201
2203
  You MUST call \`waitForOrder(checkoutId)\` to verify the order was created.
2202
- Also call \`handlePaymentSuccess(checkoutId)\` to clear purchased items from the cart.`;
2204
+ Also call \`handlePaymentSuccess(checkoutId)\` to clear purchased items from the cart.
2205
+
2206
+ ### Optional: render full order details on the confirmation page
2207
+
2208
+ Want a richer thank-you page (items, shipping address, totals, the shopper's
2209
+ order note)? After \`waitForOrder\` succeeds, fetch the full buyer-safe order \u2014
2210
+ works for guests too (possession of the checkout id is the credential):
2211
+
2212
+ \`\`\`typescript
2213
+ const order = await client.getOrderByCheckout(checkoutId);
2214
+ // order.items, order.shippingAddress, order.notes, order.subtotal,
2215
+ // order.shippingAmount, order.taxAmount / order.taxBreakdown, order.totalAmount
2216
+ \`\`\`
2217
+
2218
+ Render whatever subset fits your design \u2014 same field shapes as the buyer
2219
+ order-history view (see "Buyer order view" section).`;
2203
2220
  }
2204
2221
  function getI18nSection() {
2205
2222
  return `## Internationalization (i18n)
@@ -2529,6 +2546,8 @@ dashboard or the admin SDK).
2529
2546
 
2530
2547
  \`\`\`typescript
2531
2548
  // Publish to a specific vibe-coded site (admin mode):
2549
+ await admin.publishProductToSalesChannel('prod_id', 'vc_conn_id');
2550
+ await admin.publishCouponToSalesChannel('coupon_id', 'vc_conn_id');
2532
2551
  await admin.publishCategoryToVibeCodedSite('cat_id', 'conn_id');
2533
2552
  await admin.publishTagToVibeCodedSite('tag_id', 'conn_id');
2534
2553
  await admin.publishBrandToVibeCodedSite('brand_id', 'conn_id');
@@ -3367,6 +3386,43 @@ await client.applyCoupon(cartId, couponCode); // applies via the normal coupon e
3367
3386
  \`\`\`typescript
3368
3387
  await client.reportSocialShare('instagram'); // platform optional, self-reported, once per customer
3369
3388
  \`\`\`
3389
+
3390
+ ## Referrals (when the store enables them)
3391
+
3392
+ \`getLoyaltyStatus()\` also returns \`referralCode\` (the member's share code, null when referrals are disabled or the customer isn't enrolled) and \`referralWelcomeCoupon\` (the still-unused welcome coupon this customer got for signing up via a referral link).
3393
+
3394
+ \`\`\`typescript
3395
+ // 1. Referrer: build a share link around their code.
3396
+ const { referralCode } = await client.getLoyaltyStatus();
3397
+ const shareUrl = \`https://your-store.com/?ref=\${referralCode}\`;
3398
+
3399
+ // 2. Landing page: PUBLIC lookup \u2014 no customerToken needed.
3400
+ const info = await client.getReferralInfo(refFromQuery);
3401
+ // { valid, referrerFirstName, reward: { name, type, value, minOrderAmount } | null }
3402
+ if (info.valid) {
3403
+ // "Jane sent you a gift: 10% off your first order"
3404
+ }
3405
+
3406
+ // 3. Registration: pass the code \u2014 invalid codes never fail registration
3407
+ // (validated asynchronously server-side with fraud checks).
3408
+ await client.registerCustomer({ email, password, referralCode: refFromQuery });
3409
+
3410
+ // 4. Show the referee their welcome coupon after login:
3411
+ const { referralWelcomeCoupon } = await client.getLoyaltyStatus();
3412
+ if (referralWelcomeCoupon) {
3413
+ await client.applyCoupon(cartId, referralWelcomeCoupon.code);
3414
+ }
3415
+ \`\`\`
3416
+
3417
+ The referrer earns a points bonus after the referee's first qualifying order (held through the program's pending window, like order points). Everything server-side \u2014 no storefront earn calls.
3418
+
3419
+ ## Birthday gifts (when the store enables them)
3420
+
3421
+ No dedicated calls \u2014 collect \`birthMonth\`/\`birthDay\` (1-12 / 1-31, **no year**, must be sent together) via \`updateMyProfile()\` and the platform emails a one-time gift coupon ahead of the customer's birthday.
3422
+
3423
+ \`\`\`typescript
3424
+ await client.updateMyProfile({ birthMonth: 4, birthDay: 17 });
3425
+ \`\`\`
3370
3426
  `;
3371
3427
  }
3372
3428
  function getSectionByTopic(topic, connectionId, currency) {
@@ -3891,6 +3947,7 @@ interface Checkout {
3891
3947
  appliedSurcharges?: Array<{ key: string; name: string; value: unknown; amount: string }> | null;
3892
3948
  customFieldValues?: Record<string, unknown> | null;
3893
3949
  couponCode?: string | null;
3950
+ notes?: string | null; // Order-level shopper note \u2014 copied onto the order at completion
3894
3951
  lineItems: CheckoutLineItem[]; // Use THIS for order summary, NOT cart.items!
3895
3952
  itemCount: number;
3896
3953
  availableShippingRates?: ShippingRate[];
@@ -3959,6 +4016,7 @@ interface SetShippingAddressDto {
3959
4016
  postalCode: string;
3960
4017
  country: string;
3961
4018
  phone?: string;
4019
+ notes?: string; // Order notes textarea \u2014 include one on checkout by default! Max 2000 chars, lands on the order
3962
4020
  }
3963
4021
 
3964
4022
  interface CreateCheckoutDto {
@@ -4927,6 +4985,24 @@ interface LoyaltyStatus {
4927
4985
  nextTier: LoyaltyNextTierSummary | null; // null if at the top tier already
4928
4986
  progressToNextTier: number; // 0..1
4929
4987
  pointsToNextTier: number | null;
4988
+ referralCode?: string | null; // member's share code; null when referrals disabled / not enrolled
4989
+ referralWelcomeCoupon?: { // unused welcome coupon from signing up via a referral link
4990
+ code: string;
4991
+ type: string; // 'PERCENTAGE' | 'FIXED_AMOUNT'
4992
+ value: number;
4993
+ } | null;
4994
+ }
4995
+
4996
+ // Public referral-link lookup \u2014 NO customerToken needed (safe pre-signup)
4997
+ interface ReferralInfo {
4998
+ valid: boolean; // false for unknown/disabled codes
4999
+ referrerFirstName: string | null; // for "Jane sent you a gift" copy \u2014 no other PII
5000
+ reward: {
5001
+ name: string;
5002
+ type: 'FIXED_DISCOUNT' | 'PERCENT_DISCOUNT';
5003
+ value: number;
5004
+ minOrderAmount: number | null;
5005
+ } | null;
4930
5006
  }
4931
5007
 
4932
5008
  interface LoyaltyReward {
@@ -4957,6 +5033,9 @@ interface RedeemRewardResult {
4957
5033
  // client.getAvailableRewards(): Promise<LoyaltyReward[]>
4958
5034
  // client.redeemLoyaltyReward(rewardId: string): Promise<RedeemRewardResult>
4959
5035
  // client.reportSocialShare(platform?: string): Promise<{ awarded: boolean; points: number }>
5036
+ // client.getReferralInfo(code: string): Promise<ReferralInfo> // PUBLIC \u2014 no customerToken
5037
+ // Referral signup: client.registerCustomer({ ..., referralCode })
5038
+ // Birthday gift data: client.updateMyProfile({ birthMonth, birthDay }) // 1-12 / 1-31, no year, both together
4960
5039
  `;
4961
5040
  var TYPES_BY_DOMAIN = {
4962
5041
  products: PRODUCTS_TYPES,
@@ -6455,6 +6534,14 @@ function formatCapabilities(caps) {
6455
6534
  lines.push(
6456
6535
  caps.features.hasLoyaltyProgram ? "\u2705 Loyalty program active (points & rewards \u2014 storefront-mode only)" : "\u274C No loyalty program"
6457
6536
  );
6537
+ if (caps.features.hasLoyaltyProgram) {
6538
+ lines.push(
6539
+ caps.features.hasReferralProgram ? "\u2705 Referral program enabled (share codes + welcome reward \u2014 storefront-mode only)" : "\u274C Referral program not enabled"
6540
+ );
6541
+ lines.push(
6542
+ caps.features.hasBirthdayRewards ? "\u2705 Birthday gifts enabled (set birthMonth/birthDay via updateMyProfile)" : "\u274C Birthday gifts not enabled"
6543
+ );
6544
+ }
6458
6545
  lines.push("");
6459
6546
  lines.push("## Connection Settings");
6460
6547
  lines.push(
@@ -7640,69 +7727,29 @@ var SSE_KEEPALIVE_MS = 1e4;
7640
7727
  var SESSION_MAX_AGE_MS = 4 * 60 * 60 * 1e3;
7641
7728
  var NODE_ENV = process.env.NODE_ENV || "development";
7642
7729
  var IS_DEV = NODE_ENV === "development";
7643
- var AUTH_TOKENS = (process.env.MCP_AUTH_TOKENS || "").split(",").map((t) => t.trim()).filter((t) => t.length > 0).map((t) => Buffer.from(t, "utf8"));
7644
- if (AUTH_TOKENS.length === 0 && !IS_DEV) {
7645
- console.error(
7646
- "FATAL: MCP_AUTH_TOKENS is not set. The MCP HTTP server must run with at least one Bearer token in non-development environments.\nMint a token (e.g., `mcp_<random>`) via the dashboard and add it (comma-separated) to MCP_AUTH_TOKENS."
7647
- );
7648
- process.exit(1);
7649
- }
7650
- if (AUTH_TOKENS.length === 0 && IS_DEV) {
7651
- console.warn(
7652
- "WARN: MCP_AUTH_TOKENS is not set. Running in development mode \u2014 any non-empty Bearer token will be accepted."
7730
+ if (process.env.MCP_AUTH_TOKENS) {
7731
+ console.info(
7732
+ "NOTE: MCP_AUTH_TOKENS is set but ignored \u2014 the docs MCP server is public by design. The env var can be removed."
7653
7733
  );
7654
7734
  }
7655
7735
  var ALLOWED_ORIGINS = (process.env.MCP_ALLOWED_ORIGINS || "").split(",").map((o) => o.trim()).filter((o) => o.length > 0);
7656
7736
  var LOCALHOST_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/;
7657
7737
  var RATE_LIMIT_WINDOW_MS = parseInt(process.env.MCP_RATE_LIMIT_WINDOW_MS || "60000", 10);
7658
7738
  var RATE_LIMIT_MAX = parseInt(process.env.MCP_RATE_LIMIT_MAX || "60", 10);
7739
+ var RATE_BUCKETS_MAX = parseInt(process.env.MCP_RATE_BUCKETS_MAX || "20000", 10);
7659
7740
  var rateBuckets = /* @__PURE__ */ new Map();
7741
+ var MAX_SSE_SESSIONS = parseInt(process.env.MCP_MAX_SSE_SESSIONS || "500", 10);
7742
+ var MAX_SSE_SESSIONS_PER_IP = parseInt(process.env.MCP_MAX_SSE_SESSIONS_PER_IP || "5", 10);
7660
7743
  var MAX_BODY_BYTES = parseInt(process.env.MCP_MAX_BODY_BYTES || String(100 * 1024), 10);
7661
- function safeEqual(a, b) {
7662
- if (a.length !== b.length) {
7663
- const max = Math.max(a.length, b.length);
7664
- const ax = Buffer.alloc(max);
7665
- const bx = Buffer.alloc(max);
7666
- a.copy(ax);
7667
- b.copy(bx);
7668
- (0, import_node_crypto.timingSafeEqual)(ax, bx);
7669
- return false;
7670
- }
7671
- return (0, import_node_crypto.timingSafeEqual)(a, b);
7672
- }
7673
- function authenticate(req, res) {
7674
- const header = req.headers["authorization"];
7675
- if (typeof header !== "string" || !header.toLowerCase().startsWith("bearer ")) {
7676
- writeJson(res, 401, {
7677
- error: "Missing or malformed Authorization header. Expected: Bearer <token>."
7678
- });
7679
- return false;
7680
- }
7681
- const presented = header.slice(7).trim();
7682
- if (presented.length === 0) {
7683
- writeJson(res, 401, { error: "Empty Bearer token." });
7684
- return false;
7685
- }
7686
- if (AUTH_TOKENS.length === 0 && IS_DEV) {
7687
- return true;
7688
- }
7689
- const presentedBuf = Buffer.from(presented, "utf8");
7690
- let matched = false;
7691
- for (const token of AUTH_TOKENS) {
7692
- if (safeEqual(presentedBuf, token)) {
7693
- matched = true;
7694
- }
7695
- }
7696
- if (!matched) {
7697
- writeJson(res, 401, { error: "Invalid Bearer token." });
7698
- return false;
7699
- }
7700
- return true;
7701
- }
7702
7744
  function clientIp(req) {
7745
+ const cf = req.headers["cf-connecting-ip"];
7746
+ if (typeof cf === "string" && cf.length > 0) {
7747
+ return cf.trim();
7748
+ }
7703
7749
  const xff = req.headers["x-forwarded-for"];
7704
7750
  if (typeof xff === "string" && xff.length > 0) {
7705
- return xff.split(",")[0].trim();
7751
+ const parts = xff.split(",");
7752
+ return parts[parts.length - 1].trim();
7706
7753
  }
7707
7754
  return req.socket.remoteAddress || "unknown";
7708
7755
  }
@@ -7711,6 +7758,9 @@ function rateLimit(req, res) {
7711
7758
  const now = Date.now();
7712
7759
  const bucket = rateBuckets.get(ip);
7713
7760
  if (!bucket || bucket.resetAt <= now) {
7761
+ if (rateBuckets.size >= RATE_BUCKETS_MAX) {
7762
+ rateBuckets.clear();
7763
+ }
7714
7764
  rateBuckets.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
7715
7765
  return true;
7716
7766
  }
@@ -7834,7 +7884,6 @@ async function main() {
7834
7884
  return;
7835
7885
  }
7836
7886
  if (!rateLimit(req, res)) return;
7837
- if (!authenticate(req, res)) return;
7838
7887
  if (req.url === "/mcp") {
7839
7888
  try {
7840
7889
  const server = createServer();
@@ -7853,6 +7902,19 @@ async function main() {
7853
7902
  return;
7854
7903
  }
7855
7904
  if (req.url === "/sse" && req.method === "GET") {
7905
+ if (sseSessions.size >= MAX_SSE_SESSIONS) {
7906
+ writeJson(res, 503, { error: "Server at capacity. Try again later." });
7907
+ return;
7908
+ }
7909
+ const ip = clientIp(req);
7910
+ let ownedByIp = 0;
7911
+ for (const session of sseSessions.values()) {
7912
+ if (session.ip === ip) ownedByIp += 1;
7913
+ }
7914
+ if (ownedByIp >= MAX_SSE_SESSIONS_PER_IP) {
7915
+ writeJson(res, 429, { error: "Too many concurrent sessions from this address." });
7916
+ return;
7917
+ }
7856
7918
  try {
7857
7919
  res.setHeader("X-Accel-Buffering", "no");
7858
7920
  res.setHeader("Cache-Control", "no-cache, no-transform");
@@ -7874,7 +7936,8 @@ async function main() {
7874
7936
  transport,
7875
7937
  res,
7876
7938
  keepAliveTimer,
7877
- createdAt: Date.now()
7939
+ createdAt: Date.now(),
7940
+ ip
7878
7941
  });
7879
7942
  transport.onclose = () => {
7880
7943
  cleanupSession(sessionId);
@@ -7944,20 +8007,23 @@ ${signal} received \u2014 shutting down gracefully...`);
7944
8007
  }
7945
8008
  process.on("SIGTERM", () => shutdown("SIGTERM"));
7946
8009
  process.on("SIGINT", () => shutdown("SIGINT"));
8010
+ httpServer.headersTimeout = 15e3;
8011
+ httpServer.requestTimeout = 6e4;
7947
8012
  httpServer.listen(PORT, () => {
7948
8013
  console.info(`Brainerce MCP server running on http://localhost:${PORT}`);
7949
8014
  console.info(` Streamable HTTP: http://localhost:${PORT}/mcp`);
7950
8015
  console.info(` Legacy SSE: http://localhost:${PORT}/sse`);
7951
8016
  console.info(` Health check: http://localhost:${PORT}/health`);
7952
- console.info(
7953
- ` Auth tokens: ${AUTH_TOKENS.length} configured${AUTH_TOKENS.length === 0 && IS_DEV ? " (dev: any non-empty Bearer accepted)" : ""}`
7954
- );
8017
+ console.info(" Auth: none (public docs server)");
7955
8018
  console.info(
7956
8019
  ` CORS: ${ALLOWED_ORIGINS.length} allow-listed origin(s)${IS_DEV ? " + localhost (dev)" : ""}`
7957
8020
  );
7958
8021
  console.info(
7959
8022
  ` Rate limit: ${RATE_LIMIT_MAX} req / ${Math.round(RATE_LIMIT_WINDOW_MS / 1e3)}s per IP`
7960
8023
  );
8024
+ console.info(
8025
+ ` SSE sessions: max ${MAX_SSE_SESSIONS} total, ${MAX_SSE_SESSIONS_PER_IP} per IP`
8026
+ );
7961
8027
  console.info(` Max body: ${MAX_BODY_BYTES} bytes`);
7962
8028
  });
7963
8029
  }
package/dist/bin/stdio.js CHANGED
@@ -217,7 +217,7 @@ async function startCheckout() {
217
217
  2. Customer optionally applies coupon on cart page \u2192 \`applyCoupon(cartId, code)\`
218
218
  3. Detect payment providers \u2192 \`getPaymentProviders()\`
219
219
  4. Start checkout session (\`startGuestCheckout()\` or \`createCheckout()\`)
220
- 5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\`
220
+ 5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\` \u2014 also pass \`notes\` from the **"Order notes" textarea that every checkout page should include by default** (optional field, max 2000 chars; lands on the order for the merchant)
221
221
  6. Select shipping method \u2192 \`selectShippingMethod()\`
222
222
  6b. (Optional) Set checkout custom fields \u2192 \`setCheckoutCustomFields()\` \u2014 surcharges auto-calculated
223
223
  7. Create payment intent \u2192 \`createPaymentIntent()\` \u2192 returns \`{ clientSecret, provider }\`
@@ -239,6 +239,8 @@ function CheckoutPage() {
239
239
  const [checkout, setCheckout] = useState<Checkout | null>(null);
240
240
  const [error, setError] = useState<string | null>(null);
241
241
  const [loading, setLoading] = useState(true);
242
+ // Optional "Order notes" textarea \u2014 render it on the checkout page by default
243
+ const [orderNotes, setOrderNotes] = useState('');
242
244
 
243
245
  useEffect(() => { startCheckout(); }, []);
244
246
 
@@ -273,6 +275,7 @@ function CheckoutPage() {
273
275
  postalCode: cart.shippingAddress.postalCode,
274
276
  country: cart.shippingAddress.country,
275
277
  phone: cart.shippingAddress.phone,
278
+ notes: orderNotes || undefined, // from the "Order notes" textarea \u2014 include one by default
276
279
  });
277
280
  setCheckout(checkoutData);
278
281
 
@@ -2123,6 +2126,7 @@ A useful order card includes ALL of the following when the data is present. Each
2123
2126
  | **Tracking** | \`order.trackingNumber\`, \`order.trackingUrl\`, \`order.carrier\`, \`order.shippedAt\`, \`order.deliveredAt\` | Link out to \`trackingUrl\` when set. |
2124
2127
  | **Payment** | \`order.paymentMethod\`, \`order.financialStatus\` | Badge \`financialStatus\` (paid / pending / refunded / partially_refunded). |
2125
2128
  | Downloads | \`order.hasDownloads\` \u2192 \`client.getOrderDownloads(id)\` | Separate call; returns \`OrderDownloadLink[]\`. |
2129
+ | **Order note** | \`order.notes\` | The shopper's own checkout note, echoed back read-only. Render when present ("Your order note"). |
2126
2130
  | Financial summary | \`order.subtotal\`, \`order.appliedDiscounts\`, \`order.couponCode\` + \`couponDiscount\`, \`order.shippingAmount\`, \`order.taxAmount\`, \`order.totalAmount\` | Breakdown rows + final total. In VAT-inclusive mode \`taxAmount\` is 0 \u2014 read \`order.taxBreakdown.totalTax\` and label "Tax (incl.)". |
2127
2131
 
2128
2132
  #### Rendering \`order.items[i].customizations\` by type
@@ -2148,7 +2152,6 @@ The storefront scaffolded by \`create-brainerce-store\` already implements this.
2148
2152
  These fields are NOT returned to buyers by \`/customers/me/orders\`. If you see them in older examples, ignore them \u2014 they are for merchant/admin views only.
2149
2153
 
2150
2154
  - \`order.accountId\`, \`order.storeId\`, \`order.customerId\` (already known on the request)
2151
- - \`order.notes\` (internal merchant notes)
2152
2155
  - \`order.customFieldValues\` (order-level checkout fields \u2014 merchant concept; buyers see \`items[i].customizations\`)
2153
2156
  - \`order.appliedSurcharges\`, \`order.surchargeAmount\`, \`order.appliedRuleIds\`, \`order.downloadMeta\`, \`order.pickupLocationData\``;
2154
2157
  }
@@ -2196,7 +2199,22 @@ function OrderConfirmation() {
2196
2199
 
2197
2200
  **Common mistake:** Don't just check URL params and show success immediately!
2198
2201
  You MUST call \`waitForOrder(checkoutId)\` to verify the order was created.
2199
- Also call \`handlePaymentSuccess(checkoutId)\` to clear purchased items from the cart.`;
2202
+ Also call \`handlePaymentSuccess(checkoutId)\` to clear purchased items from the cart.
2203
+
2204
+ ### Optional: render full order details on the confirmation page
2205
+
2206
+ Want a richer thank-you page (items, shipping address, totals, the shopper's
2207
+ order note)? After \`waitForOrder\` succeeds, fetch the full buyer-safe order \u2014
2208
+ works for guests too (possession of the checkout id is the credential):
2209
+
2210
+ \`\`\`typescript
2211
+ const order = await client.getOrderByCheckout(checkoutId);
2212
+ // order.items, order.shippingAddress, order.notes, order.subtotal,
2213
+ // order.shippingAmount, order.taxAmount / order.taxBreakdown, order.totalAmount
2214
+ \`\`\`
2215
+
2216
+ Render whatever subset fits your design \u2014 same field shapes as the buyer
2217
+ order-history view (see "Buyer order view" section).`;
2200
2218
  }
2201
2219
  function getI18nSection() {
2202
2220
  return `## Internationalization (i18n)
@@ -2526,6 +2544,8 @@ dashboard or the admin SDK).
2526
2544
 
2527
2545
  \`\`\`typescript
2528
2546
  // Publish to a specific vibe-coded site (admin mode):
2547
+ await admin.publishProductToSalesChannel('prod_id', 'vc_conn_id');
2548
+ await admin.publishCouponToSalesChannel('coupon_id', 'vc_conn_id');
2529
2549
  await admin.publishCategoryToVibeCodedSite('cat_id', 'conn_id');
2530
2550
  await admin.publishTagToVibeCodedSite('tag_id', 'conn_id');
2531
2551
  await admin.publishBrandToVibeCodedSite('brand_id', 'conn_id');
@@ -3364,6 +3384,43 @@ await client.applyCoupon(cartId, couponCode); // applies via the normal coupon e
3364
3384
  \`\`\`typescript
3365
3385
  await client.reportSocialShare('instagram'); // platform optional, self-reported, once per customer
3366
3386
  \`\`\`
3387
+
3388
+ ## Referrals (when the store enables them)
3389
+
3390
+ \`getLoyaltyStatus()\` also returns \`referralCode\` (the member's share code, null when referrals are disabled or the customer isn't enrolled) and \`referralWelcomeCoupon\` (the still-unused welcome coupon this customer got for signing up via a referral link).
3391
+
3392
+ \`\`\`typescript
3393
+ // 1. Referrer: build a share link around their code.
3394
+ const { referralCode } = await client.getLoyaltyStatus();
3395
+ const shareUrl = \`https://your-store.com/?ref=\${referralCode}\`;
3396
+
3397
+ // 2. Landing page: PUBLIC lookup \u2014 no customerToken needed.
3398
+ const info = await client.getReferralInfo(refFromQuery);
3399
+ // { valid, referrerFirstName, reward: { name, type, value, minOrderAmount } | null }
3400
+ if (info.valid) {
3401
+ // "Jane sent you a gift: 10% off your first order"
3402
+ }
3403
+
3404
+ // 3. Registration: pass the code \u2014 invalid codes never fail registration
3405
+ // (validated asynchronously server-side with fraud checks).
3406
+ await client.registerCustomer({ email, password, referralCode: refFromQuery });
3407
+
3408
+ // 4. Show the referee their welcome coupon after login:
3409
+ const { referralWelcomeCoupon } = await client.getLoyaltyStatus();
3410
+ if (referralWelcomeCoupon) {
3411
+ await client.applyCoupon(cartId, referralWelcomeCoupon.code);
3412
+ }
3413
+ \`\`\`
3414
+
3415
+ The referrer earns a points bonus after the referee's first qualifying order (held through the program's pending window, like order points). Everything server-side \u2014 no storefront earn calls.
3416
+
3417
+ ## Birthday gifts (when the store enables them)
3418
+
3419
+ No dedicated calls \u2014 collect \`birthMonth\`/\`birthDay\` (1-12 / 1-31, **no year**, must be sent together) via \`updateMyProfile()\` and the platform emails a one-time gift coupon ahead of the customer's birthday.
3420
+
3421
+ \`\`\`typescript
3422
+ await client.updateMyProfile({ birthMonth: 4, birthDay: 17 });
3423
+ \`\`\`
3367
3424
  `;
3368
3425
  }
3369
3426
  function getSectionByTopic(topic, connectionId, currency) {
@@ -3888,6 +3945,7 @@ interface Checkout {
3888
3945
  appliedSurcharges?: Array<{ key: string; name: string; value: unknown; amount: string }> | null;
3889
3946
  customFieldValues?: Record<string, unknown> | null;
3890
3947
  couponCode?: string | null;
3948
+ notes?: string | null; // Order-level shopper note \u2014 copied onto the order at completion
3891
3949
  lineItems: CheckoutLineItem[]; // Use THIS for order summary, NOT cart.items!
3892
3950
  itemCount: number;
3893
3951
  availableShippingRates?: ShippingRate[];
@@ -3956,6 +4014,7 @@ interface SetShippingAddressDto {
3956
4014
  postalCode: string;
3957
4015
  country: string;
3958
4016
  phone?: string;
4017
+ notes?: string; // Order notes textarea \u2014 include one on checkout by default! Max 2000 chars, lands on the order
3959
4018
  }
3960
4019
 
3961
4020
  interface CreateCheckoutDto {
@@ -4924,6 +4983,24 @@ interface LoyaltyStatus {
4924
4983
  nextTier: LoyaltyNextTierSummary | null; // null if at the top tier already
4925
4984
  progressToNextTier: number; // 0..1
4926
4985
  pointsToNextTier: number | null;
4986
+ referralCode?: string | null; // member's share code; null when referrals disabled / not enrolled
4987
+ referralWelcomeCoupon?: { // unused welcome coupon from signing up via a referral link
4988
+ code: string;
4989
+ type: string; // 'PERCENTAGE' | 'FIXED_AMOUNT'
4990
+ value: number;
4991
+ } | null;
4992
+ }
4993
+
4994
+ // Public referral-link lookup \u2014 NO customerToken needed (safe pre-signup)
4995
+ interface ReferralInfo {
4996
+ valid: boolean; // false for unknown/disabled codes
4997
+ referrerFirstName: string | null; // for "Jane sent you a gift" copy \u2014 no other PII
4998
+ reward: {
4999
+ name: string;
5000
+ type: 'FIXED_DISCOUNT' | 'PERCENT_DISCOUNT';
5001
+ value: number;
5002
+ minOrderAmount: number | null;
5003
+ } | null;
4927
5004
  }
4928
5005
 
4929
5006
  interface LoyaltyReward {
@@ -4954,6 +5031,9 @@ interface RedeemRewardResult {
4954
5031
  // client.getAvailableRewards(): Promise<LoyaltyReward[]>
4955
5032
  // client.redeemLoyaltyReward(rewardId: string): Promise<RedeemRewardResult>
4956
5033
  // client.reportSocialShare(platform?: string): Promise<{ awarded: boolean; points: number }>
5034
+ // client.getReferralInfo(code: string): Promise<ReferralInfo> // PUBLIC \u2014 no customerToken
5035
+ // Referral signup: client.registerCustomer({ ..., referralCode })
5036
+ // Birthday gift data: client.updateMyProfile({ birthMonth, birthDay }) // 1-12 / 1-31, no year, both together
4957
5037
  `;
4958
5038
  var TYPES_BY_DOMAIN = {
4959
5039
  products: PRODUCTS_TYPES,
@@ -6452,6 +6532,14 @@ function formatCapabilities(caps) {
6452
6532
  lines.push(
6453
6533
  caps.features.hasLoyaltyProgram ? "\u2705 Loyalty program active (points & rewards \u2014 storefront-mode only)" : "\u274C No loyalty program"
6454
6534
  );
6535
+ if (caps.features.hasLoyaltyProgram) {
6536
+ lines.push(
6537
+ caps.features.hasReferralProgram ? "\u2705 Referral program enabled (share codes + welcome reward \u2014 storefront-mode only)" : "\u274C Referral program not enabled"
6538
+ );
6539
+ lines.push(
6540
+ caps.features.hasBirthdayRewards ? "\u2705 Birthday gifts enabled (set birthMonth/birthDay via updateMyProfile)" : "\u274C Birthday gifts not enabled"
6541
+ );
6542
+ }
6455
6543
  lines.push("");
6456
6544
  lines.push("## Connection Settings");
6457
6545
  lines.push(
package/dist/index.d.mts CHANGED
@@ -77,6 +77,10 @@ interface StoreCapabilities {
77
77
  hasContent?: boolean;
78
78
  /** True when the store has an ACTIVE loyalty program. Storefront-mode only. */
79
79
  hasLoyaltyProgram?: boolean;
80
+ /** True when the loyalty program has the referral program enabled. Storefront-mode only. */
81
+ hasReferralProgram?: boolean;
82
+ /** True when the loyalty program has birthday gifts enabled. */
83
+ hasBirthdayRewards?: boolean;
80
84
  };
81
85
  }
82
86
  declare function fetchStoreCapabilities(connectionId: string, baseUrl?: string): Promise<StoreCapabilities>;
package/dist/index.d.ts CHANGED
@@ -77,6 +77,10 @@ interface StoreCapabilities {
77
77
  hasContent?: boolean;
78
78
  /** True when the store has an ACTIVE loyalty program. Storefront-mode only. */
79
79
  hasLoyaltyProgram?: boolean;
80
+ /** True when the loyalty program has the referral program enabled. Storefront-mode only. */
81
+ hasReferralProgram?: boolean;
82
+ /** True when the loyalty program has birthday gifts enabled. */
83
+ hasBirthdayRewards?: boolean;
80
84
  };
81
85
  }
82
86
  declare function fetchStoreCapabilities(connectionId: string, baseUrl?: string): Promise<StoreCapabilities>;
package/dist/index.js CHANGED
@@ -243,7 +243,7 @@ async function startCheckout() {
243
243
  2. Customer optionally applies coupon on cart page \u2192 \`applyCoupon(cartId, code)\`
244
244
  3. Detect payment providers \u2192 \`getPaymentProviders()\`
245
245
  4. Start checkout session (\`startGuestCheckout()\` or \`createCheckout()\`)
246
- 5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\`
246
+ 5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\` \u2014 also pass \`notes\` from the **"Order notes" textarea that every checkout page should include by default** (optional field, max 2000 chars; lands on the order for the merchant)
247
247
  6. Select shipping method \u2192 \`selectShippingMethod()\`
248
248
  6b. (Optional) Set checkout custom fields \u2192 \`setCheckoutCustomFields()\` \u2014 surcharges auto-calculated
249
249
  7. Create payment intent \u2192 \`createPaymentIntent()\` \u2192 returns \`{ clientSecret, provider }\`
@@ -265,6 +265,8 @@ function CheckoutPage() {
265
265
  const [checkout, setCheckout] = useState<Checkout | null>(null);
266
266
  const [error, setError] = useState<string | null>(null);
267
267
  const [loading, setLoading] = useState(true);
268
+ // Optional "Order notes" textarea \u2014 render it on the checkout page by default
269
+ const [orderNotes, setOrderNotes] = useState('');
268
270
 
269
271
  useEffect(() => { startCheckout(); }, []);
270
272
 
@@ -299,6 +301,7 @@ function CheckoutPage() {
299
301
  postalCode: cart.shippingAddress.postalCode,
300
302
  country: cart.shippingAddress.country,
301
303
  phone: cart.shippingAddress.phone,
304
+ notes: orderNotes || undefined, // from the "Order notes" textarea \u2014 include one by default
302
305
  });
303
306
  setCheckout(checkoutData);
304
307
 
@@ -2149,6 +2152,7 @@ A useful order card includes ALL of the following when the data is present. Each
2149
2152
  | **Tracking** | \`order.trackingNumber\`, \`order.trackingUrl\`, \`order.carrier\`, \`order.shippedAt\`, \`order.deliveredAt\` | Link out to \`trackingUrl\` when set. |
2150
2153
  | **Payment** | \`order.paymentMethod\`, \`order.financialStatus\` | Badge \`financialStatus\` (paid / pending / refunded / partially_refunded). |
2151
2154
  | Downloads | \`order.hasDownloads\` \u2192 \`client.getOrderDownloads(id)\` | Separate call; returns \`OrderDownloadLink[]\`. |
2155
+ | **Order note** | \`order.notes\` | The shopper's own checkout note, echoed back read-only. Render when present ("Your order note"). |
2152
2156
  | Financial summary | \`order.subtotal\`, \`order.appliedDiscounts\`, \`order.couponCode\` + \`couponDiscount\`, \`order.shippingAmount\`, \`order.taxAmount\`, \`order.totalAmount\` | Breakdown rows + final total. In VAT-inclusive mode \`taxAmount\` is 0 \u2014 read \`order.taxBreakdown.totalTax\` and label "Tax (incl.)". |
2153
2157
 
2154
2158
  #### Rendering \`order.items[i].customizations\` by type
@@ -2174,7 +2178,6 @@ The storefront scaffolded by \`create-brainerce-store\` already implements this.
2174
2178
  These fields are NOT returned to buyers by \`/customers/me/orders\`. If you see them in older examples, ignore them \u2014 they are for merchant/admin views only.
2175
2179
 
2176
2180
  - \`order.accountId\`, \`order.storeId\`, \`order.customerId\` (already known on the request)
2177
- - \`order.notes\` (internal merchant notes)
2178
2181
  - \`order.customFieldValues\` (order-level checkout fields \u2014 merchant concept; buyers see \`items[i].customizations\`)
2179
2182
  - \`order.appliedSurcharges\`, \`order.surchargeAmount\`, \`order.appliedRuleIds\`, \`order.downloadMeta\`, \`order.pickupLocationData\``;
2180
2183
  }
@@ -2222,7 +2225,22 @@ function OrderConfirmation() {
2222
2225
 
2223
2226
  **Common mistake:** Don't just check URL params and show success immediately!
2224
2227
  You MUST call \`waitForOrder(checkoutId)\` to verify the order was created.
2225
- Also call \`handlePaymentSuccess(checkoutId)\` to clear purchased items from the cart.`;
2228
+ Also call \`handlePaymentSuccess(checkoutId)\` to clear purchased items from the cart.
2229
+
2230
+ ### Optional: render full order details on the confirmation page
2231
+
2232
+ Want a richer thank-you page (items, shipping address, totals, the shopper's
2233
+ order note)? After \`waitForOrder\` succeeds, fetch the full buyer-safe order \u2014
2234
+ works for guests too (possession of the checkout id is the credential):
2235
+
2236
+ \`\`\`typescript
2237
+ const order = await client.getOrderByCheckout(checkoutId);
2238
+ // order.items, order.shippingAddress, order.notes, order.subtotal,
2239
+ // order.shippingAmount, order.taxAmount / order.taxBreakdown, order.totalAmount
2240
+ \`\`\`
2241
+
2242
+ Render whatever subset fits your design \u2014 same field shapes as the buyer
2243
+ order-history view (see "Buyer order view" section).`;
2226
2244
  }
2227
2245
  function getI18nSection() {
2228
2246
  return `## Internationalization (i18n)
@@ -2552,6 +2570,8 @@ dashboard or the admin SDK).
2552
2570
 
2553
2571
  \`\`\`typescript
2554
2572
  // Publish to a specific vibe-coded site (admin mode):
2573
+ await admin.publishProductToSalesChannel('prod_id', 'vc_conn_id');
2574
+ await admin.publishCouponToSalesChannel('coupon_id', 'vc_conn_id');
2555
2575
  await admin.publishCategoryToVibeCodedSite('cat_id', 'conn_id');
2556
2576
  await admin.publishTagToVibeCodedSite('tag_id', 'conn_id');
2557
2577
  await admin.publishBrandToVibeCodedSite('brand_id', 'conn_id');
@@ -3390,6 +3410,43 @@ await client.applyCoupon(cartId, couponCode); // applies via the normal coupon e
3390
3410
  \`\`\`typescript
3391
3411
  await client.reportSocialShare('instagram'); // platform optional, self-reported, once per customer
3392
3412
  \`\`\`
3413
+
3414
+ ## Referrals (when the store enables them)
3415
+
3416
+ \`getLoyaltyStatus()\` also returns \`referralCode\` (the member's share code, null when referrals are disabled or the customer isn't enrolled) and \`referralWelcomeCoupon\` (the still-unused welcome coupon this customer got for signing up via a referral link).
3417
+
3418
+ \`\`\`typescript
3419
+ // 1. Referrer: build a share link around their code.
3420
+ const { referralCode } = await client.getLoyaltyStatus();
3421
+ const shareUrl = \`https://your-store.com/?ref=\${referralCode}\`;
3422
+
3423
+ // 2. Landing page: PUBLIC lookup \u2014 no customerToken needed.
3424
+ const info = await client.getReferralInfo(refFromQuery);
3425
+ // { valid, referrerFirstName, reward: { name, type, value, minOrderAmount } | null }
3426
+ if (info.valid) {
3427
+ // "Jane sent you a gift: 10% off your first order"
3428
+ }
3429
+
3430
+ // 3. Registration: pass the code \u2014 invalid codes never fail registration
3431
+ // (validated asynchronously server-side with fraud checks).
3432
+ await client.registerCustomer({ email, password, referralCode: refFromQuery });
3433
+
3434
+ // 4. Show the referee their welcome coupon after login:
3435
+ const { referralWelcomeCoupon } = await client.getLoyaltyStatus();
3436
+ if (referralWelcomeCoupon) {
3437
+ await client.applyCoupon(cartId, referralWelcomeCoupon.code);
3438
+ }
3439
+ \`\`\`
3440
+
3441
+ The referrer earns a points bonus after the referee's first qualifying order (held through the program's pending window, like order points). Everything server-side \u2014 no storefront earn calls.
3442
+
3443
+ ## Birthday gifts (when the store enables them)
3444
+
3445
+ No dedicated calls \u2014 collect \`birthMonth\`/\`birthDay\` (1-12 / 1-31, **no year**, must be sent together) via \`updateMyProfile()\` and the platform emails a one-time gift coupon ahead of the customer's birthday.
3446
+
3447
+ \`\`\`typescript
3448
+ await client.updateMyProfile({ birthMonth: 4, birthDay: 17 });
3449
+ \`\`\`
3393
3450
  `;
3394
3451
  }
3395
3452
  function getSectionByTopic(topic, connectionId, currency) {
@@ -3914,6 +3971,7 @@ interface Checkout {
3914
3971
  appliedSurcharges?: Array<{ key: string; name: string; value: unknown; amount: string }> | null;
3915
3972
  customFieldValues?: Record<string, unknown> | null;
3916
3973
  couponCode?: string | null;
3974
+ notes?: string | null; // Order-level shopper note \u2014 copied onto the order at completion
3917
3975
  lineItems: CheckoutLineItem[]; // Use THIS for order summary, NOT cart.items!
3918
3976
  itemCount: number;
3919
3977
  availableShippingRates?: ShippingRate[];
@@ -3982,6 +4040,7 @@ interface SetShippingAddressDto {
3982
4040
  postalCode: string;
3983
4041
  country: string;
3984
4042
  phone?: string;
4043
+ notes?: string; // Order notes textarea \u2014 include one on checkout by default! Max 2000 chars, lands on the order
3985
4044
  }
3986
4045
 
3987
4046
  interface CreateCheckoutDto {
@@ -4950,6 +5009,24 @@ interface LoyaltyStatus {
4950
5009
  nextTier: LoyaltyNextTierSummary | null; // null if at the top tier already
4951
5010
  progressToNextTier: number; // 0..1
4952
5011
  pointsToNextTier: number | null;
5012
+ referralCode?: string | null; // member's share code; null when referrals disabled / not enrolled
5013
+ referralWelcomeCoupon?: { // unused welcome coupon from signing up via a referral link
5014
+ code: string;
5015
+ type: string; // 'PERCENTAGE' | 'FIXED_AMOUNT'
5016
+ value: number;
5017
+ } | null;
5018
+ }
5019
+
5020
+ // Public referral-link lookup \u2014 NO customerToken needed (safe pre-signup)
5021
+ interface ReferralInfo {
5022
+ valid: boolean; // false for unknown/disabled codes
5023
+ referrerFirstName: string | null; // for "Jane sent you a gift" copy \u2014 no other PII
5024
+ reward: {
5025
+ name: string;
5026
+ type: 'FIXED_DISCOUNT' | 'PERCENT_DISCOUNT';
5027
+ value: number;
5028
+ minOrderAmount: number | null;
5029
+ } | null;
4953
5030
  }
4954
5031
 
4955
5032
  interface LoyaltyReward {
@@ -4980,6 +5057,9 @@ interface RedeemRewardResult {
4980
5057
  // client.getAvailableRewards(): Promise<LoyaltyReward[]>
4981
5058
  // client.redeemLoyaltyReward(rewardId: string): Promise<RedeemRewardResult>
4982
5059
  // client.reportSocialShare(platform?: string): Promise<{ awarded: boolean; points: number }>
5060
+ // client.getReferralInfo(code: string): Promise<ReferralInfo> // PUBLIC \u2014 no customerToken
5061
+ // Referral signup: client.registerCustomer({ ..., referralCode })
5062
+ // Birthday gift data: client.updateMyProfile({ birthMonth, birthDay }) // 1-12 / 1-31, no year, both together
4983
5063
  `;
4984
5064
  var TYPES_BY_DOMAIN = {
4985
5065
  products: PRODUCTS_TYPES,
@@ -6478,6 +6558,14 @@ function formatCapabilities(caps) {
6478
6558
  lines.push(
6479
6559
  caps.features.hasLoyaltyProgram ? "\u2705 Loyalty program active (points & rewards \u2014 storefront-mode only)" : "\u274C No loyalty program"
6480
6560
  );
6561
+ if (caps.features.hasLoyaltyProgram) {
6562
+ lines.push(
6563
+ caps.features.hasReferralProgram ? "\u2705 Referral program enabled (share codes + welcome reward \u2014 storefront-mode only)" : "\u274C Referral program not enabled"
6564
+ );
6565
+ lines.push(
6566
+ caps.features.hasBirthdayRewards ? "\u2705 Birthday gifts enabled (set birthMonth/birthDay via updateMyProfile)" : "\u274C Birthday gifts not enabled"
6567
+ );
6568
+ }
6481
6569
  lines.push("");
6482
6570
  lines.push("## Connection Settings");
6483
6571
  lines.push(
package/dist/index.mjs CHANGED
@@ -211,7 +211,7 @@ async function startCheckout() {
211
211
  2. Customer optionally applies coupon on cart page \u2192 \`applyCoupon(cartId, code)\`
212
212
  3. Detect payment providers \u2192 \`getPaymentProviders()\`
213
213
  4. Start checkout session (\`startGuestCheckout()\` or \`createCheckout()\`)
214
- 5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\`
214
+ 5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\` \u2014 also pass \`notes\` from the **"Order notes" textarea that every checkout page should include by default** (optional field, max 2000 chars; lands on the order for the merchant)
215
215
  6. Select shipping method \u2192 \`selectShippingMethod()\`
216
216
  6b. (Optional) Set checkout custom fields \u2192 \`setCheckoutCustomFields()\` \u2014 surcharges auto-calculated
217
217
  7. Create payment intent \u2192 \`createPaymentIntent()\` \u2192 returns \`{ clientSecret, provider }\`
@@ -233,6 +233,8 @@ function CheckoutPage() {
233
233
  const [checkout, setCheckout] = useState<Checkout | null>(null);
234
234
  const [error, setError] = useState<string | null>(null);
235
235
  const [loading, setLoading] = useState(true);
236
+ // Optional "Order notes" textarea \u2014 render it on the checkout page by default
237
+ const [orderNotes, setOrderNotes] = useState('');
236
238
 
237
239
  useEffect(() => { startCheckout(); }, []);
238
240
 
@@ -267,6 +269,7 @@ function CheckoutPage() {
267
269
  postalCode: cart.shippingAddress.postalCode,
268
270
  country: cart.shippingAddress.country,
269
271
  phone: cart.shippingAddress.phone,
272
+ notes: orderNotes || undefined, // from the "Order notes" textarea \u2014 include one by default
270
273
  });
271
274
  setCheckout(checkoutData);
272
275
 
@@ -2117,6 +2120,7 @@ A useful order card includes ALL of the following when the data is present. Each
2117
2120
  | **Tracking** | \`order.trackingNumber\`, \`order.trackingUrl\`, \`order.carrier\`, \`order.shippedAt\`, \`order.deliveredAt\` | Link out to \`trackingUrl\` when set. |
2118
2121
  | **Payment** | \`order.paymentMethod\`, \`order.financialStatus\` | Badge \`financialStatus\` (paid / pending / refunded / partially_refunded). |
2119
2122
  | Downloads | \`order.hasDownloads\` \u2192 \`client.getOrderDownloads(id)\` | Separate call; returns \`OrderDownloadLink[]\`. |
2123
+ | **Order note** | \`order.notes\` | The shopper's own checkout note, echoed back read-only. Render when present ("Your order note"). |
2120
2124
  | Financial summary | \`order.subtotal\`, \`order.appliedDiscounts\`, \`order.couponCode\` + \`couponDiscount\`, \`order.shippingAmount\`, \`order.taxAmount\`, \`order.totalAmount\` | Breakdown rows + final total. In VAT-inclusive mode \`taxAmount\` is 0 \u2014 read \`order.taxBreakdown.totalTax\` and label "Tax (incl.)". |
2121
2125
 
2122
2126
  #### Rendering \`order.items[i].customizations\` by type
@@ -2142,7 +2146,6 @@ The storefront scaffolded by \`create-brainerce-store\` already implements this.
2142
2146
  These fields are NOT returned to buyers by \`/customers/me/orders\`. If you see them in older examples, ignore them \u2014 they are for merchant/admin views only.
2143
2147
 
2144
2148
  - \`order.accountId\`, \`order.storeId\`, \`order.customerId\` (already known on the request)
2145
- - \`order.notes\` (internal merchant notes)
2146
2149
  - \`order.customFieldValues\` (order-level checkout fields \u2014 merchant concept; buyers see \`items[i].customizations\`)
2147
2150
  - \`order.appliedSurcharges\`, \`order.surchargeAmount\`, \`order.appliedRuleIds\`, \`order.downloadMeta\`, \`order.pickupLocationData\``;
2148
2151
  }
@@ -2190,7 +2193,22 @@ function OrderConfirmation() {
2190
2193
 
2191
2194
  **Common mistake:** Don't just check URL params and show success immediately!
2192
2195
  You MUST call \`waitForOrder(checkoutId)\` to verify the order was created.
2193
- Also call \`handlePaymentSuccess(checkoutId)\` to clear purchased items from the cart.`;
2196
+ Also call \`handlePaymentSuccess(checkoutId)\` to clear purchased items from the cart.
2197
+
2198
+ ### Optional: render full order details on the confirmation page
2199
+
2200
+ Want a richer thank-you page (items, shipping address, totals, the shopper's
2201
+ order note)? After \`waitForOrder\` succeeds, fetch the full buyer-safe order \u2014
2202
+ works for guests too (possession of the checkout id is the credential):
2203
+
2204
+ \`\`\`typescript
2205
+ const order = await client.getOrderByCheckout(checkoutId);
2206
+ // order.items, order.shippingAddress, order.notes, order.subtotal,
2207
+ // order.shippingAmount, order.taxAmount / order.taxBreakdown, order.totalAmount
2208
+ \`\`\`
2209
+
2210
+ Render whatever subset fits your design \u2014 same field shapes as the buyer
2211
+ order-history view (see "Buyer order view" section).`;
2194
2212
  }
2195
2213
  function getI18nSection() {
2196
2214
  return `## Internationalization (i18n)
@@ -2520,6 +2538,8 @@ dashboard or the admin SDK).
2520
2538
 
2521
2539
  \`\`\`typescript
2522
2540
  // Publish to a specific vibe-coded site (admin mode):
2541
+ await admin.publishProductToSalesChannel('prod_id', 'vc_conn_id');
2542
+ await admin.publishCouponToSalesChannel('coupon_id', 'vc_conn_id');
2523
2543
  await admin.publishCategoryToVibeCodedSite('cat_id', 'conn_id');
2524
2544
  await admin.publishTagToVibeCodedSite('tag_id', 'conn_id');
2525
2545
  await admin.publishBrandToVibeCodedSite('brand_id', 'conn_id');
@@ -3358,6 +3378,43 @@ await client.applyCoupon(cartId, couponCode); // applies via the normal coupon e
3358
3378
  \`\`\`typescript
3359
3379
  await client.reportSocialShare('instagram'); // platform optional, self-reported, once per customer
3360
3380
  \`\`\`
3381
+
3382
+ ## Referrals (when the store enables them)
3383
+
3384
+ \`getLoyaltyStatus()\` also returns \`referralCode\` (the member's share code, null when referrals are disabled or the customer isn't enrolled) and \`referralWelcomeCoupon\` (the still-unused welcome coupon this customer got for signing up via a referral link).
3385
+
3386
+ \`\`\`typescript
3387
+ // 1. Referrer: build a share link around their code.
3388
+ const { referralCode } = await client.getLoyaltyStatus();
3389
+ const shareUrl = \`https://your-store.com/?ref=\${referralCode}\`;
3390
+
3391
+ // 2. Landing page: PUBLIC lookup \u2014 no customerToken needed.
3392
+ const info = await client.getReferralInfo(refFromQuery);
3393
+ // { valid, referrerFirstName, reward: { name, type, value, minOrderAmount } | null }
3394
+ if (info.valid) {
3395
+ // "Jane sent you a gift: 10% off your first order"
3396
+ }
3397
+
3398
+ // 3. Registration: pass the code \u2014 invalid codes never fail registration
3399
+ // (validated asynchronously server-side with fraud checks).
3400
+ await client.registerCustomer({ email, password, referralCode: refFromQuery });
3401
+
3402
+ // 4. Show the referee their welcome coupon after login:
3403
+ const { referralWelcomeCoupon } = await client.getLoyaltyStatus();
3404
+ if (referralWelcomeCoupon) {
3405
+ await client.applyCoupon(cartId, referralWelcomeCoupon.code);
3406
+ }
3407
+ \`\`\`
3408
+
3409
+ The referrer earns a points bonus after the referee's first qualifying order (held through the program's pending window, like order points). Everything server-side \u2014 no storefront earn calls.
3410
+
3411
+ ## Birthday gifts (when the store enables them)
3412
+
3413
+ No dedicated calls \u2014 collect \`birthMonth\`/\`birthDay\` (1-12 / 1-31, **no year**, must be sent together) via \`updateMyProfile()\` and the platform emails a one-time gift coupon ahead of the customer's birthday.
3414
+
3415
+ \`\`\`typescript
3416
+ await client.updateMyProfile({ birthMonth: 4, birthDay: 17 });
3417
+ \`\`\`
3361
3418
  `;
3362
3419
  }
3363
3420
  function getSectionByTopic(topic, connectionId, currency) {
@@ -3882,6 +3939,7 @@ interface Checkout {
3882
3939
  appliedSurcharges?: Array<{ key: string; name: string; value: unknown; amount: string }> | null;
3883
3940
  customFieldValues?: Record<string, unknown> | null;
3884
3941
  couponCode?: string | null;
3942
+ notes?: string | null; // Order-level shopper note \u2014 copied onto the order at completion
3885
3943
  lineItems: CheckoutLineItem[]; // Use THIS for order summary, NOT cart.items!
3886
3944
  itemCount: number;
3887
3945
  availableShippingRates?: ShippingRate[];
@@ -3950,6 +4008,7 @@ interface SetShippingAddressDto {
3950
4008
  postalCode: string;
3951
4009
  country: string;
3952
4010
  phone?: string;
4011
+ notes?: string; // Order notes textarea \u2014 include one on checkout by default! Max 2000 chars, lands on the order
3953
4012
  }
3954
4013
 
3955
4014
  interface CreateCheckoutDto {
@@ -4918,6 +4977,24 @@ interface LoyaltyStatus {
4918
4977
  nextTier: LoyaltyNextTierSummary | null; // null if at the top tier already
4919
4978
  progressToNextTier: number; // 0..1
4920
4979
  pointsToNextTier: number | null;
4980
+ referralCode?: string | null; // member's share code; null when referrals disabled / not enrolled
4981
+ referralWelcomeCoupon?: { // unused welcome coupon from signing up via a referral link
4982
+ code: string;
4983
+ type: string; // 'PERCENTAGE' | 'FIXED_AMOUNT'
4984
+ value: number;
4985
+ } | null;
4986
+ }
4987
+
4988
+ // Public referral-link lookup \u2014 NO customerToken needed (safe pre-signup)
4989
+ interface ReferralInfo {
4990
+ valid: boolean; // false for unknown/disabled codes
4991
+ referrerFirstName: string | null; // for "Jane sent you a gift" copy \u2014 no other PII
4992
+ reward: {
4993
+ name: string;
4994
+ type: 'FIXED_DISCOUNT' | 'PERCENT_DISCOUNT';
4995
+ value: number;
4996
+ minOrderAmount: number | null;
4997
+ } | null;
4921
4998
  }
4922
4999
 
4923
5000
  interface LoyaltyReward {
@@ -4948,6 +5025,9 @@ interface RedeemRewardResult {
4948
5025
  // client.getAvailableRewards(): Promise<LoyaltyReward[]>
4949
5026
  // client.redeemLoyaltyReward(rewardId: string): Promise<RedeemRewardResult>
4950
5027
  // client.reportSocialShare(platform?: string): Promise<{ awarded: boolean; points: number }>
5028
+ // client.getReferralInfo(code: string): Promise<ReferralInfo> // PUBLIC \u2014 no customerToken
5029
+ // Referral signup: client.registerCustomer({ ..., referralCode })
5030
+ // Birthday gift data: client.updateMyProfile({ birthMonth, birthDay }) // 1-12 / 1-31, no year, both together
4951
5031
  `;
4952
5032
  var TYPES_BY_DOMAIN = {
4953
5033
  products: PRODUCTS_TYPES,
@@ -6446,6 +6526,14 @@ function formatCapabilities(caps) {
6446
6526
  lines.push(
6447
6527
  caps.features.hasLoyaltyProgram ? "\u2705 Loyalty program active (points & rewards \u2014 storefront-mode only)" : "\u274C No loyalty program"
6448
6528
  );
6529
+ if (caps.features.hasLoyaltyProgram) {
6530
+ lines.push(
6531
+ caps.features.hasReferralProgram ? "\u2705 Referral program enabled (share codes + welcome reward \u2014 storefront-mode only)" : "\u274C Referral program not enabled"
6532
+ );
6533
+ lines.push(
6534
+ caps.features.hasBirthdayRewards ? "\u2705 Birthday gifts enabled (set birthMonth/birthDay via updateMyProfile)" : "\u274C Birthday gifts not enabled"
6535
+ );
6536
+ }
6449
6537
  lines.push("");
6450
6538
  lines.push("## Connection Settings");
6451
6539
  lines.push(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainerce/mcp-server",
3
- "version": "3.12.3",
3
+ "version": "3.12.4",
4
4
  "packageManager": "pnpm@9.0.0",
5
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.",
6
6
  "bin": {