@binaryandthread/mcp 0.1.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,7 +56,7 @@ Override any of it via `env` for local dev / staging, or to attach your customer
56
56
  "env": {
57
57
  "BT_STORE_URL": "http://localhost:9000", // override the baked-in prod URL (dev/staging)
58
58
  "BT_PUBLISHABLE_KEY": "pk_...", // PUBLIC by design, never a secret key
59
- "BT_REGION": "se", // optional ISO-2; pricing is region-resolved, never hardcoded
59
+ "BT_REGION": "se", // optional ISO-2 (se · us · any EU country e.g. de/fr/ie); region-resolved, never hardcoded
60
60
  "BT_TOKEN": "..." // optional customer JWT (own scope), from your keychain, NEVER the chat
61
61
  }
62
62
  }
@@ -71,7 +71,14 @@ Override any of it via `env` for local dev / staging, or to attach your customer
71
71
  | `BT_STORE_URL` | no | the Medusa Store API base URL (default: production) |
72
72
  | `BT_PUBLISHABLE_KEY` | no | the **publishable** key, public by design (default: production; a secret `sk_` key is rejected) |
73
73
  | `BT_TOKEN` | no | the customer JWT (own data only); read from the OS keychain / a configured env, **never from the chat** |
74
- | `BT_REGION` | no | ISO-2 country code to resolve pricing (e.g. `se`); never a hardcoded currency literal |
74
+ | `BT_REGION` | no | ISO-2 country code to resolve pricing — `se` (SEK), `us` (USD), or any EU country e.g. `de`/`fr`/`ie` (EUR); never a hardcoded currency literal. Sets the **default**; `list_products` / `get_product` / `add_to_cart` also take a per-call `region` arg that overrides it (a country we don't ship to is refused, not priced in the wrong currency). |
75
+
76
+ ### Onboarding for the agent
77
+
78
+ A cold agent doesn't have to infer the flow: the server exposes a **`getting-started` resource**
79
+ (`bt://getting-started`) describing the `list_products → get_product → add_to_cart → checkout →
80
+ check_payment` flow and the money boundary, and a **`buy` prompt** that walks the purchase (respecting
81
+ that the card + address are entered by the human in the browser, never in the chat).
75
82
 
76
83
  ## Security posture (the MCP-specific red-team gates)
77
84
 
package/dist/bin.js CHANGED
@@ -286,25 +286,45 @@ var StoreClient = class {
286
286
  return this.regionsCache;
287
287
  }
288
288
  /**
289
- * Resolve a region by country code, falling back to region #1.
290
- * SEK is region #1 by configuration — NEVER a hardcoded literal.
289
+ * Resolve a region by country code, WITH provenance (matched / home / requested), so callers on
290
+ * the money path can fail closed on an unmatched explicit country instead of silently pricing in
291
+ * the wrong currency (F-A2). Region #1 is the launch/home default — SEK today, derived, never a
292
+ * hardcoded literal.
293
+ *
294
+ * Semantics:
295
+ * - country given AND a region ships there → { matched: true, home: false }.
296
+ * - country given, none ships there → { matched: false, home: false } (money surfaces MUST
297
+ * refuse; discovery may show the home region + a warning). We still return a region so a
298
+ * discovery command can render *something*, but `matched:false` is the signal to fail closed.
299
+ * - no country given → { matched: false, home: true } (the legitimate
300
+ * launch default — not a "wrong" currency).
291
301
  */
292
- async resolveRegion(countryCode) {
302
+ async resolveRegionResult(countryCode) {
293
303
  const regions = this.regionsCache ?? await this.getRegions();
294
304
  if (regions.length === 0) {
295
305
  throw new Error("no regions configured \u2014 cannot resolve a region for pricing");
296
306
  }
307
+ const home = regions[0];
297
308
  if (countryCode) {
298
309
  const cc = countryCode.toLowerCase();
299
310
  const match = regions.find(
300
311
  (r) => (r.countries ?? []).some((c) => (c.iso_2 ?? "").toLowerCase() === cc)
301
312
  );
302
313
  if (match) {
303
- return match;
314
+ return { region: match, matched: true, requested: cc, home: false };
304
315
  }
305
- this.logWarn("store.region.unmatched", { countryCode: cc, fallback: regions[0].id });
316
+ this.logWarn("store.region.unmatched", { countryCode: cc, fallback: home.id });
317
+ return { region: home, matched: false, requested: cc, home: false };
306
318
  }
307
- return regions[0];
319
+ return { region: home, matched: false, requested: void 0, home: true };
320
+ }
321
+ /**
322
+ * Resolve a region by country code, falling back to region #1. A thin wrapper over
323
+ * resolveRegionResult for callers that only need the region (back-compat).
324
+ * SEK is region #1 by configuration — NEVER a hardcoded literal.
325
+ */
326
+ async resolveRegion(countryCode) {
327
+ return (await this.resolveRegionResult(countryCode)).region;
308
328
  }
309
329
  // ----- products (region resolved BEFORE pricing) ---------------------------
310
330
  async listProducts(opts) {
@@ -623,13 +643,57 @@ function productLore(p) {
623
643
  ).map((s) => ({ url: s.url, label: s.label }));
624
644
  return { text, sources };
625
645
  }
646
+ function isCartTaxInclusive(cart) {
647
+ return (cart?.items ?? []).some((i) => i.is_tax_inclusive === true);
648
+ }
626
649
 
627
650
  // src/server.ts
628
651
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
629
652
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
653
+
654
+ // src/version.ts
655
+ var VERSION = "0.3.0";
656
+
657
+ // src/onboarding.ts
658
+ var GETTING_STARTED_URI = "bt://getting-started";
659
+ var GETTING_STARTED = `binary & thread \u2014 buying from your agent.
660
+
661
+ the flow (each tool returns machine-readable structuredContent, so you can chain without regexing prose):
662
+ 1. list_products [region] what's in stock, priced in the ship-to market's currency
663
+ 2. get_product <handle> full detail + an authoritative variant_id + sizes + colours
664
+ 3. add_to_cart \u2026 start or reuse a cart (returns a durable cart_id handle)
665
+ 4. get_cart <cart_id> the authoritative cart \u2014 note: the total is PRE-shipping
666
+ 5. checkout <cart_id> mint a pay link + the SHIPPING-INCLUSIVE total the buyer pays
667
+ 6. check_payment <session_id> poll once for the result (call again if still pending)
668
+
669
+ the dev-channel discount: every purchase through this MCP (or the bt CLI) is 10% off the web \u2014
670
+ applied automatically at checkout, no code. checkout returns the discounted total; quote that.
671
+
672
+ the boundary (by design): payment + ship-to details are entered by the human on the storefront.
673
+ they never enter this chat, and you cannot complete a purchase yourself \u2014 you walk them to the door.
674
+
675
+ region + currency: prices and shipping follow the ship-to country. pass region (ISO-2, e.g. se / us /
676
+ de) to list_products / get_product / add_to_cart, or the host can set BT_REGION. a country we don't
677
+ ship to is refused rather than priced in the wrong currency.`;
678
+ function buyPromptText(handle, region) {
679
+ const target = handle ? `"${handle}"` : "the piece the user wants";
680
+ const reg = region ? ` Price it for region "${region}".` : "";
681
+ return `Help me buy ${target} from binary & thread through the MCP tools.${reg}
682
+
683
+ Steps:
684
+ 1. If you don't have an exact variant_id, call get_product to get valid sizes/colours + the variant_id.
685
+ 2. add_to_cart with the variant_id (or handle + size + colour). Keep the returned cart_id.
686
+ 3. Show me the cart total, and remind me it excludes shipping until checkout (where a 10%
687
+ dev-channel discount also applies \u2014 the terminal's cut).
688
+ 4. When I confirm, call checkout and give me the pay link + the shipping-inclusive, discounted total.
689
+ 5. Do NOT ask me for card or address details \u2014 I enter those in the browser. After I pay, use
690
+ check_payment with the session_id to confirm the order.`;
691
+ }
692
+
693
+ // src/server.ts
630
694
  var SERVER_INFO = {
631
695
  name: "binary-and-thread",
632
- version: "0.1.0"
696
+ version: VERSION
633
697
  };
634
698
  function buildServer(tools) {
635
699
  const server = new McpServer(SERVER_INFO);
@@ -652,7 +716,35 @@ function buildServer(tools) {
652
716
  );
653
717
  toolNames.push(t.name);
654
718
  }
655
- return { server, toolNames };
719
+ const registerResource = server.registerResource.bind(server);
720
+ registerResource(
721
+ "getting-started",
722
+ GETTING_STARTED_URI,
723
+ { title: "binary & thread \u2014 getting started", description: "the buy flow + the money boundary, for a cold agent", mimeType: "text/plain" },
724
+ () => ({ contents: [{ uri: GETTING_STARTED_URI, text: GETTING_STARTED, mimeType: "text/plain" }] })
725
+ );
726
+ const resourceNames = ["getting-started"];
727
+ const registerPrompt = server.registerPrompt.bind(server);
728
+ registerPrompt(
729
+ "buy",
730
+ { title: "buy a piece", description: "walk through buying a piece (respecting the card-stays-with-the-human boundary)" },
731
+ (args) => ({
732
+ messages: [
733
+ {
734
+ role: "user",
735
+ content: {
736
+ type: "text",
737
+ text: buyPromptText(
738
+ typeof args?.handle === "string" ? args.handle : void 0,
739
+ typeof args?.region === "string" ? args.region : void 0
740
+ )
741
+ }
742
+ }
743
+ ]
744
+ })
745
+ );
746
+ const promptNames = ["buy"];
747
+ return { server, toolNames, resourceNames, promptNames };
656
748
  }
657
749
  async function startServer(tools) {
658
750
  const { server } = buildServer(tools);
@@ -774,13 +866,25 @@ function fmtPrice(amount, currency) {
774
866
  const cur = (currency ?? "").toUpperCase();
775
867
  return cur ? `${amount} ${cur}` : `${amount}`;
776
868
  }
869
+ function taxSuffix(isTaxInclusive) {
870
+ return isTaxInclusive ? " incl. VAT" : "";
871
+ }
777
872
  function firstPrice(p) {
778
873
  const v = (p.variants ?? [])[0];
779
874
  return {
780
875
  amount: v?.calculated_price?.calculated_amount ?? null,
781
- currency: v?.calculated_price?.currency_code ?? null
876
+ currency: v?.calculated_price?.currency_code ?? null,
877
+ taxInclusive: v?.calculated_price?.is_calculated_price_tax_inclusive ?? null
782
878
  };
783
879
  }
880
+ function cartItems(cart) {
881
+ return (cart.items ?? []).map((li) => ({
882
+ variant_id: li.variant_id ?? null,
883
+ title: sanitize(li.product_title ?? li.title ?? "item", 80),
884
+ qty: li.quantity ?? 1,
885
+ total: li.total ?? null
886
+ }));
887
+ }
784
888
  function cartSummary(cart, region) {
785
889
  const lines = [];
786
890
  lines.push(`cart ${cart.id}`);
@@ -793,45 +897,56 @@ function cartSummary(cart, region) {
793
897
  lines.push(` ${li.quantity}x ${title} \u2014 ${fmtPrice(li.total, cart.currency_code ?? region?.currency_code)}`);
794
898
  }
795
899
  }
796
- lines.push(` total ${fmtPrice(cart.total, cart.currency_code ?? region?.currency_code)} incl. VAT`);
900
+ const shipNote = (cart.shipping_methods ?? []).length > 0 ? "" : " (excl. shipping \u2014 calculated at checkout)";
901
+ lines.push(` total ${fmtPrice(cart.total, cart.currency_code ?? region?.currency_code)}${taxSuffix(isCartTaxInclusive(cart))}${shipNote}`);
797
902
  return lines.join("\n");
798
903
  }
799
904
  function makeTools(client, opts = {}) {
800
905
  const regionCC = opts.region;
801
906
  const list_products = {
802
907
  name: "list_products",
803
- description: "List the pieces in stock (text-only \u2014 no images, to keep the agent's context lean). Returns each product's handle, price incl. VAT, and stage. Use get_product for one item's image.",
804
- inputSchema: { stage: z.string().optional().describe("filter by channel stage, e.g. 'main' or 'next'") },
908
+ description: "List the pieces in stock (text-only \u2014 no images, to keep the agent's context lean). Returns each product's handle, price (VAT included where applicable), and stage. Use get_product for one item's image. Prices are in the region's currency \u2014 pass `region` (an ISO-2 country) to price for a specific market, else the server default.",
909
+ inputSchema: {
910
+ stage: z.string().optional().describe("filter by channel stage: 'main' or 'next'"),
911
+ region: z.string().optional().describe("ISO-2 ship-to country to price for (e.g. se / us / de); overrides the server default")
912
+ },
805
913
  handler: async (args) => {
806
- const region = await client.resolveRegion(regionCC);
914
+ const cc = typeof args.region === "string" && args.region.trim() || regionCC;
915
+ const rr = await client.resolveRegionResult(cc);
916
+ const region = rr.region;
807
917
  const products = await client.listProducts({ region });
808
918
  const stage = typeof args.stage === "string" ? args.stage.toLowerCase() : void 0;
809
919
  const rows = products.map((p) => ({ p, stage: String(p.metadata?.["channel_stage"] ?? "main") })).filter((r) => stage ? r.stage.toLowerCase() === stage : true);
810
920
  if (rows.length === 0) {
811
- return result("nothing in stock on this branch yet.", { products: [] });
921
+ return result("nothing in stock on this branch yet.", { products: [], currency: region.currency_code, region: rr.requested ?? null });
812
922
  }
813
923
  const lines = rows.map(({ p, stage: stage2 }) => {
814
- const { amount, currency } = firstPrice(p);
924
+ const { amount, currency, taxInclusive } = firstPrice(p);
815
925
  const handle = sanitize(p.handle ?? p.id, 60);
816
926
  const title = sanitize(p.title ?? "", 80);
817
- return `${handle} ${fmtPrice(amount, currency)} incl. VAT [${stage2}] ${title}`.trimEnd();
927
+ return `${handle} ${fmtPrice(amount, currency)}${taxSuffix(taxInclusive)} [${stage2}] ${title}`.trimEnd();
818
928
  });
819
929
  const productList = rows.map(({ p, stage: stage2 }) => {
820
- const { amount, currency } = firstPrice(p);
821
- return { handle: p.handle ?? p.id, title: p.title ?? "", price: amount, currency, stage: stage2 };
930
+ const { amount, currency, taxInclusive } = firstPrice(p);
931
+ return { handle: p.handle ?? p.id, title: sanitize(p.title ?? "", 80), price: amount, currency, tax_inclusive: taxInclusive, stage: stage2 };
822
932
  });
823
- return result(lines.join("\n"), { products: productList });
933
+ return result(lines.join("\n"), { products: productList, currency: region.currency_code, region: rr.requested ?? null });
824
934
  }
825
935
  };
826
936
  const get_product = {
827
937
  name: "get_product",
828
938
  // M3 ★ — a hidden line for the schema-readers (devs read tool descriptions). Rewards the
829
939
  // contributor instinct + hints at a real feature (the next branch / drops). Never alters behaviour.
830
- description: "Get one piece in full: its readme, the spec sheet (material/weight/fit/print/delivery/care), sizes + price incl. VAT, the lore (the story behind it, with sources), and its image (rendered + the Blob URL as a text floor). // you read the schema. that's the contributor instinct. ask me about the next branch.",
831
- inputSchema: { handle: z.string().describe("the product handle/slug") },
940
+ description: "Get one piece in full: its readme, the spec sheet (material/weight/fit/print/delivery/care), sizes + price (VAT included where applicable), the lore (the story behind it, with sources), and its image (rendered + the Blob URL as a text floor). Prices are in the region's currency \u2014 pass `region` (ISO-2) to price for a market. // you read the schema. that's the contributor instinct. ask me about the next branch.",
941
+ inputSchema: {
942
+ handle: z.string().describe("the product handle/slug (from list_products)"),
943
+ region: z.string().optional().describe("ISO-2 ship-to country to price for (e.g. se / us / de); overrides the server default")
944
+ },
832
945
  handler: async (args) => {
833
946
  const handle = String(args.handle ?? "");
834
- const region = await client.resolveRegion(regionCC);
947
+ const cc = typeof args.region === "string" && args.region.trim() || regionCC;
948
+ const rr = await client.resolveRegionResult(cc);
949
+ const region = rr.region;
835
950
  const product = await client.getProduct(handle, { region });
836
951
  if (!product) {
837
952
  return errorText(`no product with handle "${sanitize(handle, 60)}". try list_products.`);
@@ -858,7 +973,7 @@ function makeTools(client, opts = {}) {
858
973
  }
859
974
  const v0 = (product.variants ?? [])[0];
860
975
  if (v0?.calculated_price) {
861
- lines.push(`price: ${fmtPrice(v0.calculated_price.calculated_amount, v0.calculated_price.currency_code)} incl. VAT`);
976
+ lines.push(`price: ${fmtPrice(v0.calculated_price.calculated_amount, v0.calculated_price.currency_code)}${taxSuffix(v0.calculated_price.is_calculated_price_tax_inclusive)}`);
862
977
  }
863
978
  const specs = productSpecs(product);
864
979
  lines.push("specs:");
@@ -877,36 +992,59 @@ function makeTools(client, opts = {}) {
877
992
  const img = await buildImageContent(product.thumbnail);
878
993
  content.push(...img);
879
994
  }
995
+ const variantMap = (product.variants ?? []).map((v) => ({
996
+ variant_id: v.id,
997
+ size: v.title ?? null,
998
+ colour: variantColour(v) ?? null,
999
+ price: v.calculated_price?.calculated_amount ?? null
1000
+ }));
1001
+ const sizeList = colours.length > 0 ? Object.fromEntries(colours.map((c) => [c, sizesForColour(product, c)])) : (product.variants ?? []).map((v) => v.title ?? "?");
880
1002
  return {
881
1003
  content,
1004
+ // G8/D3 — the untrusted backend TEXT (title/readme/specs/lore) is sanitize()'d before it
1005
+ // enters the machine-readable path too (the header's promise), not just the text block;
1006
+ // ids/urls/numbers pass through redactForOutput unchanged.
882
1007
  structuredContent: redactForOutput({
883
1008
  handle: product.handle ?? handle,
884
- title: product.title ?? "",
1009
+ title: sanitize(product.title ?? "", 120),
885
1010
  price: v0?.calculated_price?.calculated_amount ?? null,
886
1011
  currency: v0?.calculated_price?.currency_code ?? null,
1012
+ tax_inclusive: v0?.calculated_price?.is_calculated_price_tax_inclusive ?? null,
1013
+ region: rr.requested ?? null,
887
1014
  colours,
1015
+ sizes: sizeList,
1016
+ variants: variantMap,
888
1017
  image_url: product.thumbnail ?? null,
889
1018
  // the PDP content, machine-readable: readme + the spec sheet + the lore (+ sources).
890
- readme: productReadme(product),
891
- specs,
892
- lore
1019
+ readme: sanitize(productReadme(product), 2e3),
1020
+ specs: specs.map((s) => ({ key: sanitize(s.key, 40), value: sanitize(s.value, 200) })),
1021
+ lore: {
1022
+ text: sanitize(lore.text ?? "", 1200),
1023
+ sources: lore.sources.map((s) => ({ label: sanitize(s.label, 120), url: s.url }))
1024
+ }
893
1025
  })
894
1026
  };
895
1027
  }
896
1028
  };
897
1029
  const add_to_cart = {
898
1030
  name: "add_to_cart",
899
- description: "Add a piece to a cart. Pass cart_id to reuse an existing cart (the durable handle), or omit it to start a fresh one. Returns the cart_id and the server-side cart summary (price incl. VAT).",
1031
+ description: "Add a piece to a cart. Pass cart_id to reuse an existing cart (the durable handle), or omit it to start a fresh one. Call get_product first to get valid sizes/colours (or an exact variant_id). qty is capped at 20. Returns the cart_id and the server-side cart summary (VAT included where applicable). `region` (ISO-2) sets the currency for a NEW cart only \u2014 a reused cart keeps its original region.",
900
1032
  inputSchema: {
901
- handle: z.string().optional().describe("product handle (resolves a variant by size + colour)"),
902
- variant_id: z.string().optional().describe("a specific variant id (overrides handle/size/colour)"),
903
- size: z.string().optional().describe("size label, e.g. M / L / XL"),
904
- colour: z.string().optional().describe("piece colour, e.g. white / black (default: the product's first colour)"),
905
- qty: z.number().int().positive().max(20).optional().describe("quantity (default 1)"),
906
- cart_id: z.string().optional().describe("reuse this cart; omit to create a new one")
1033
+ handle: z.string().optional().describe("product handle (resolves a variant by size + colour; call get_product for valid values)"),
1034
+ variant_id: z.string().optional().describe("a specific variant id from get_product (overrides handle/size/colour)"),
1035
+ size: z.string().optional().describe("size label from get_product, e.g. S / M / L / XL / 2XL"),
1036
+ colour: z.string().optional().describe("piece colour from get_product, e.g. white / black (default: the product's first colour)"),
1037
+ qty: z.number().int().positive().max(20).optional().describe("quantity, 1\u201320 (default 1)"),
1038
+ cart_id: z.string().optional().describe("reuse this cart; omit to create a new one"),
1039
+ region: z.string().optional().describe("ISO-2 ship-to country for a NEW cart (e.g. se / us / de); ignored when reusing a cart_id")
907
1040
  },
908
1041
  handler: async (args) => {
909
- const region = await client.resolveRegion(regionCC);
1042
+ const cc = typeof args.region === "string" && args.region.trim() || regionCC;
1043
+ const rr = await client.resolveRegionResult(cc);
1044
+ if (rr.requested && !rr.matched) {
1045
+ return errorText(`we don't ship to ${sanitize(rr.requested, 8)} yet \u2014 nothing was added. (list_products with a supported region to see prices.)`);
1046
+ }
1047
+ const region = rr.region;
910
1048
  const qty = typeof args.qty === "number" ? args.qty : 1;
911
1049
  let variantId = typeof args.variant_id === "string" ? args.variant_id : void 0;
912
1050
  if (!variantId) {
@@ -953,14 +1091,17 @@ ${cartSummary(cart, region)}`,
953
1091
  cart_id: cart.id,
954
1092
  total: cart.total ?? null,
955
1093
  currency: cart.currency_code ?? region.currency_code ?? null,
956
- item_count: itemCount
1094
+ item_count: itemCount,
1095
+ items: cartItems(cart),
1096
+ // A2 — `total` is pre-shipping until checkout attaches a method; flag it for the agent.
1097
+ total_excludes_shipping: (cart.shipping_methods ?? []).length === 0
957
1098
  }
958
1099
  );
959
1100
  }
960
1101
  };
961
1102
  const get_cart = {
962
1103
  name: "get_cart",
963
- description: "Show a cart's current contents and total (price incl. VAT), straight from the server.",
1104
+ description: "Show a cart's current contents and total (VAT included where applicable), straight from the server.",
964
1105
  inputSchema: { cart_id: z.string().describe("the cart handle from add_to_cart") },
965
1106
  handler: async (args) => {
966
1107
  const cartId = String(args.cart_id ?? "");
@@ -972,7 +1113,10 @@ ${cartSummary(cart, region)}`,
972
1113
  cart_id: cart.id,
973
1114
  total: cart.total ?? null,
974
1115
  currency: cart.currency_code ?? region.currency_code ?? null,
975
- item_count: itemCount
1116
+ item_count: itemCount,
1117
+ items: cartItems(cart),
1118
+ // A2 — signal that `total` is pre-shipping so a chaining agent doesn't quote it as final.
1119
+ total_excludes_shipping: (cart.shipping_methods ?? []).length === 0
976
1120
  });
977
1121
  } catch (e) {
978
1122
  if (e instanceof RecoverableCartError) {
@@ -992,18 +1136,26 @@ ${cartSummary(cart, region)}`,
992
1136
  handler: async (args) => {
993
1137
  const cartId = String(args.cart_id ?? "");
994
1138
  const email = typeof args.email === "string" ? args.email : void 0;
1139
+ let shipped;
995
1140
  try {
996
- await client.ensureCartShippingMethod(cartId);
1141
+ shipped = await client.ensureCartShippingMethod(cartId);
997
1142
  } catch (e) {
998
1143
  if (e instanceof RecoverableCartError) {
999
1144
  return errorText("that cart is no longer valid (expired or already committed). add an item to start a fresh one.");
1000
1145
  }
1001
1146
  return errorText("couldn't attach a shipping method to this cart \u2014 checkout isn't available for it right now.");
1002
1147
  }
1003
- const { checkout_url, session_id } = await client.mintCheckout(cartId, email, "mcp");
1148
+ const mint = await client.mintCheckout(cartId, email, "mcp");
1149
+ const { checkout_url, session_id } = mint;
1150
+ const finalTotal = mint.total ?? shipped.total ?? null;
1151
+ const finalCurrency = mint.currency ?? shipped.currency_code ?? null;
1152
+ const totalStr = `${fmtPrice(finalTotal, finalCurrency)}${taxSuffix(isCartTaxInclusive(shipped))}`;
1153
+ const discountLine = mint.discount && mint.discount.amount > 0 ? [` dev channel: \u2212${fmtPrice(mint.discount.amount, finalCurrency)} (${mint.discount.pct}% off \u2014 for buying from the terminal/agent)`] : [];
1004
1154
  const body = [
1005
1155
  "checkout ready. open this link in your browser to pay:",
1006
1156
  ` ${checkout_url}`,
1157
+ ...discountLine,
1158
+ ` total (incl. shipping): ${totalStr}`,
1007
1159
  "",
1008
1160
  "payment details are entered on the storefront (Stripe) \u2014 they never enter this chat, and",
1009
1161
  "I can't complete this for you. once you've paid, call check_payment with:",
@@ -1013,7 +1165,13 @@ ${cartSummary(cart, region)}`,
1013
1165
  // one dry true line: an agent can do everything up to the card. The card is the human's.
1014
1166
  "I staged it, priced it, and walked you to the door. the card swipe is yours \u2014 by design."
1015
1167
  ].join("\n");
1016
- return result(redactForOutput(body), { checkout_url, session_id });
1168
+ return result(redactForOutput(body), {
1169
+ checkout_url,
1170
+ session_id,
1171
+ total: finalTotal,
1172
+ currency: finalCurrency,
1173
+ discount: mint.discount ?? null
1174
+ });
1017
1175
  }
1018
1176
  };
1019
1177
  const check_payment = {
@@ -1022,7 +1180,15 @@ ${cartSummary(cart, region)}`,
1022
1180
  inputSchema: { session_id: z.string().describe("the session_id from checkout") },
1023
1181
  handler: async (args) => {
1024
1182
  const sessionId = String(args.session_id ?? "");
1025
- const res = await client.poll(sessionId);
1183
+ let res;
1184
+ try {
1185
+ res = await client.poll(sessionId);
1186
+ } catch (e) {
1187
+ if (e instanceof StoreApiError && e.status === 404) {
1188
+ return errorText("that checkout session is unknown or expired \u2014 mint a fresh one with checkout.");
1189
+ }
1190
+ throw e;
1191
+ }
1026
1192
  const out = redactForOutput({ status: res.status, order_id: res.order_id });
1027
1193
  const human = res.status === "placed" ? `paid \u2014 order placed. order_id: ${out.order_id ?? "\u2014"}` : `status: ${out.status} (call check_payment again in a moment if still pending)`;
1028
1194
  return { content: [{ type: "text", text: human }], structuredContent: out };
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../../store-client/src/redact.ts","../../store-client/src/client.ts","../../store-client/src/defaults.ts","../../store-client/src/host-allowlist.ts","../../store-client/src/colour.ts","../../store-client/src/product.ts","../src/server.ts","../src/tools.ts","../src/sanitize.ts","../src/images.ts","../src/eggs.ts","../src/config.ts","../src/bin.ts"],"sourcesContent":["// Secret/PII redaction for logs and error envelopes (SECURITY G10).\n//\n// The single rule for the whole channel: Authorization headers, tokens, and full\n// emails NEVER reach a log line, an error message, or an MCP/CLI transcript. This\n// function is applied by the StoreClient's logger and error mapper before anything\n// is emitted. It is consumed by BOTH the CLI and (Wave 4) the MCP — one redactor.\n\nconst SENSITIVE_KEYS = new Set([\n \"authorization\",\n \"token\",\n \"access_token\",\n \"refresh_token\",\n \"jwt\",\n \"idempotency-key\",\n \"x-publishable-api-key\",\n \"password\",\n \"secret\",\n \"cookie\",\n \"set-cookie\",\n])\n\nconst MASK = \"[REDACTED]\"\n\n// Bearer-token-in-a-string (e.g. \"Authorization: Bearer abc.def.ghi\" or a stray \"Bearer xyz\").\nconst BEARER_RE = /Bearer\\s+[A-Za-z0-9._\\-]+/gi\n// A plausible email anywhere in a string.\nconst EMAIL_RE = /([^\\s@]+)@([^\\s@]+\\.[^\\s@]+)/g\n\n/** Mask an email to `d***@x` form (first char + stars, host first char). */\nexport function maskEmail(email: string): string {\n const at = email.indexOf(\"@\")\n if (at <= 0) {\n return email\n }\n const local = email.slice(0, at)\n const host = email.slice(at + 1)\n const localMasked = local[0] + \"*\".repeat(Math.max(2, local.length - 1))\n const hostMasked = host.length > 0 ? host[0] : \"x\"\n return `${localMasked}@${hostMasked}`\n}\n\nfunction redactString(s: string): string {\n return s.replace(BEARER_RE, \"Bearer [REDACTED]\").replace(EMAIL_RE, (_m, _l, _h) =>\n maskEmail(`${_l}@${_h}`)\n )\n}\n\n/**\n * Deep-redact an object/array/string. Returns a structurally-similar value with\n * sensitive keys masked, Bearer tokens stripped, and emails masked. Never throws.\n *\n * `_seen` memoises input-object → its REDACTED output. This does double duty: it makes the\n * function cycle-safe (a revisited object resolves to its own — possibly still-filling — output\n * container) AND ensures a REPEATED reference is redacted every time. The previous WeakSet guard\n * returned the *original* object on revisit, leaking any token/email reachable via a 2nd path (G10).\n * The output container is registered BEFORE recursing so a cycle points at the redacted copy.\n */\nexport function redact<T>(input: T, _seen?: Map<object, unknown>): T {\n if (input == null) {\n return input\n }\n if (typeof input === \"string\") {\n return redactString(input) as unknown as T\n }\n if (typeof input !== \"object\") {\n return input\n }\n\n const seen = _seen ?? new Map<object, unknown>()\n const existing = seen.get(input as object)\n if (existing !== undefined) {\n return existing as T\n }\n\n if (Array.isArray(input)) {\n const out: unknown[] = []\n seen.set(input as object, out) // register before recursing (cycle-safe)\n for (const v of input) {\n out.push(redact(v, seen))\n }\n return out as unknown as T\n }\n\n const out: Record<string, unknown> = {}\n seen.set(input as object, out) // register before recursing (cycle-safe)\n for (const [key, value] of Object.entries(input as Record<string, unknown>)) {\n if (SENSITIVE_KEYS.has(key.toLowerCase())) {\n out[key] = MASK\n } else if (key.toLowerCase() === \"email\" && typeof value === \"string\") {\n out[key] = maskEmail(value)\n } else {\n out[key] = redact(value, seen)\n }\n }\n return out as unknown as T\n}\n","// StoreClient — the ONE shared HTTP client over the B&T Medusa Store API.\n//\n// Design (ROBUSTNESS §0): the clients are DUMB. Every dangerous property lives in the\n// backend. This module is the single place the channel's HTTP-client invariants live so\n// they are written once and contract-tested once (the CLI and the Wave-4 MCP both use it):\n//\n// - publishable key on EVERY request (public by design, T6); customer JWT when present\n// - Idempotency-Key (crypto.randomUUID) on EVERY mutation, STABLE across a retry (§2)\n// - retry 5xx + network errors with jittered backoff; NEVER retry a 4xx (F-A1/F-I2)\n// - region resolved before pricing — never a bare currency literal (F-A2/INV-11)\n// - 404/409/cart.completed on a cart mutation → RecoverableCartError (CLI makes a fresh cart) (F-B2)\n// - all logs + error envelopes pass through redact() — no token/email ever leaks (G10)\n// - NO secret key is ever stored or sent — publishable key only (T6)\n\nimport { redact } from \"./redact\"\nimport type {\n CartLike,\n ClientLogger,\n DropRow,\n HeadlessCheckout,\n OrderSummary,\n PollResult,\n ProductLike,\n RegionLike,\n ReportResult,\n RequestLinkResult,\n RetryPolicy,\n ShippingOptionLike,\n StoreClientOptions,\n VerifyLinkResult,\n WhoAmI,\n} from \"./types\"\n\nconst DEFAULT_RETRY: RetryPolicy = {\n attempts: 3,\n baseDelayMs: 200,\n maxDelayMs: 2000,\n}\n\n/** Cap on how long we'll honor a server's Retry-After, so a hostile/huge value can't hang us. */\nconst RETRY_AFTER_MAX_MS = 10_000\n\n/**\n * Per-attempt request timeout. A black-holed connection (socket accepted, no response) would\n * otherwise never settle, hanging every command/tool AND defeating the checkout poll's 20-min\n * deadline (F-I3) — the deadline can't be re-checked while stuck awaiting a dead fetch. 15s is\n * generous for the Store API while still failing closed on a hang.\n */\nconst DEFAULT_TIMEOUT_MS = 15_000\n\n/**\n * Extra fields to request on product reads. `+metadata` ADDS metadata to Medusa's default field set\n * (it doesn't replace it — variants + calculated_price still come through). metadata is where lore /\n * lore_sources / per-product spec fields live, and the Store API omits it unless asked. The `+` is\n * URL-encoded to %2B by URLSearchParams, which Medusa decodes back to `+metadata`.\n */\nconst PRODUCT_FIELDS = \"+metadata\"\n\n/**\n * Parse an HTTP Retry-After header → ms, or null if absent/unparseable. Accepts both forms:\n * delta-seconds (\"2\") and an HTTP-date. Capped at RETRY_AFTER_MAX_MS (d12).\n */\nfunction parseRetryAfterMs(res: Response): number | null {\n let raw: string | null = null\n try {\n raw = res.headers?.get?.(\"retry-after\") ?? null\n } catch {\n return null\n }\n if (!raw) {\n return null\n }\n const secs = Number(raw)\n let ms: number\n if (Number.isFinite(secs)) {\n ms = secs * 1000\n } else {\n const dateMs = Date.parse(raw)\n if (!Number.isFinite(dateMs)) {\n return null\n }\n ms = dateMs - Date.now()\n }\n return Math.min(Math.max(0, ms), RETRY_AFTER_MAX_MS)\n}\n\n/** A cart error the CLI maps to \"discard local cart_id, create a fresh cart, re-add\" (F-B2). */\nexport class RecoverableCartError extends Error {\n readonly recoverable = true as const\n readonly status: number\n constructor(message: string, status: number) {\n super(message)\n this.name = \"RecoverableCartError\"\n this.status = status\n }\n}\n\n/** A non-recoverable API error. Its message/envelope is already redacted (G10). */\nexport class StoreApiError extends Error {\n readonly status: number\n readonly path: string\n constructor(message: string, status: number, path: string) {\n super(message)\n this.name = \"StoreApiError\"\n this.status = status\n this.path = path\n }\n}\n\ntype HttpMethod = \"GET\" | \"POST\" | \"DELETE\"\n\ninterface RequestOpts {\n body?: unknown\n /** Mutations carry an Idempotency-Key. */\n idempotent?: boolean\n /** Treat a 404/409/cart.completed as a RecoverableCartError (cart mutations). */\n cartRecovery?: boolean\n query?: Record<string, string | number | undefined>\n /**\n * Statuses for which a 4xx is EXPECTED, not an error — the client_error envelope is NOT\n * logged (BUG 3: a guest whoami 401 is the normal unauthenticated path, not noise to dump).\n * The error is still thrown so the caller can branch on it.\n */\n expected4xx?: number[]\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/** crypto.randomUUID with a defensive fallback (older runtimes). */\nfunction newUuid(): string {\n const g = globalThis as unknown as { crypto?: { randomUUID?: () => string } }\n if (g.crypto?.randomUUID) {\n return g.crypto.randomUUID()\n }\n // RFC4122-ish fallback — never the happy path, just defensive.\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n const v = c === \"x\" ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n}\n\nexport class StoreClient {\n private readonly baseUrl: string\n private readonly publishableKey: string\n private readonly jwt?: string\n private readonly fetchImpl: typeof fetch\n private readonly logger: ClientLogger\n private readonly retry: RetryPolicy\n private readonly sleepImpl: (ms: number) => Promise<void>\n private readonly timeoutMs: number\n\n /** Cached resolved regions (read-only catalog optimisation). */\n private regionsCache: RegionLike[] | null = null\n\n constructor(opts: StoreClientOptions) {\n if (!opts.baseUrl) {\n throw new Error(\"StoreClient: baseUrl is required\")\n }\n if (!opts.publishableKey) {\n throw new Error(\"StoreClient: publishableKey is required\")\n }\n // Guard against accidentally shipping a secret key (T6 — defensive).\n if (/^sk_/.test(opts.publishableKey)) {\n throw new Error(\"StoreClient: refusing a secret key — the client carries only the publishable key (T6)\")\n }\n this.baseUrl = opts.baseUrl.replace(/\\/+$/, \"\")\n this.publishableKey = opts.publishableKey\n this.jwt = opts.jwt\n this.fetchImpl = opts.fetchImpl ?? (globalThis.fetch as typeof fetch)\n this.logger = opts.logger ?? console\n this.retry = { ...DEFAULT_RETRY, ...(opts.retry ?? {}) }\n this.sleepImpl = opts.sleepImpl ?? sleep\n this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS\n }\n\n // ----- the request engine --------------------------------------------------\n\n private buildUrl(path: string, query?: RequestOpts[\"query\"]): string {\n const url = new URL(this.baseUrl + path)\n if (query) {\n for (const [k, v] of Object.entries(query)) {\n if (v !== undefined && v !== null && v !== \"\") {\n url.searchParams.set(k, String(v))\n }\n }\n }\n return url.toString()\n }\n\n private baseHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"x-publishable-api-key\": this.publishableKey,\n }\n if (this.jwt) {\n headers[\"Authorization\"] = `Bearer ${this.jwt}`\n }\n return headers\n }\n\n private backoffMs(attemptIndex: number): number {\n // exponential base with full jitter, capped.\n const exp = this.retry.baseDelayMs * 2 ** attemptIndex\n const capped = Math.min(exp, this.retry.maxDelayMs)\n return Math.floor(Math.random() * (capped + 1))\n }\n\n private async request<T>(method: HttpMethod, path: string, opts: RequestOpts = {}): Promise<T> {\n const headers = this.baseHeaders()\n // Generate the idempotency key ONCE — it is reused across retries so a retried\n // mutation can never spawn a duplicate (ROBUSTNESS §2).\n if (opts.idempotent) {\n headers[\"Idempotency-Key\"] = newUuid()\n }\n const url = this.buildUrl(path, opts.query)\n const init: RequestInit = {\n method,\n headers,\n ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),\n }\n\n let lastErr: unknown\n for (let attempt = 0; attempt < this.retry.attempts; attempt++) {\n // A server-dictated delay for THIS attempt's backoff (429 Retry-After); null → jittered backoff.\n let retryAfterMs: number | null = null\n // Bound THIS attempt: a hung connection is aborted after timeoutMs. The AbortError lands in\n // the catch below and is treated as a retryable network error (so the backoff + retry, and\n // ultimately the exhausted-retries StoreApiError, all apply). Fresh controller per attempt.\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), this.timeoutMs)\n try {\n const res = await this.fetchImpl(url as unknown as string, { ...init, signal: controller.signal })\n if (res.ok) {\n // 204 / empty body tolerance\n const text = await res.text()\n return (text ? JSON.parse(text) : {}) as T\n }\n\n // Read the error body defensively (may not be JSON).\n let parsed: Record<string, unknown> = {}\n try {\n const t = await res.text()\n parsed = t ? (JSON.parse(t) as Record<string, unknown>) : {}\n } catch {\n parsed = {}\n }\n\n // Cart recovery: a stale/completed cart on a cart mutation (F-B2).\n if (\n opts.cartRecovery &&\n (res.status === 404 ||\n res.status === 409 ||\n String(parsed.type ?? \"\").includes(\"cart.completed\") ||\n /cart.*complete/i.test(String(parsed.message ?? \"\")))\n ) {\n throw new RecoverableCartError(\n `cart is stale or completed (HTTP ${res.status})`,\n res.status\n )\n }\n\n // 429 Too Many Requests — RETRYABLE, honoring Retry-After (d12). Must be handled BEFORE\n // the generic 4xx-no-retry rule below, or a rate limit becomes a hard failure.\n if (res.status === 429) {\n retryAfterMs = parseRetryAfterMs(res)\n lastErr = new StoreApiError(`rate limited (HTTP 429)`, 429, path)\n this.logWarn(\"store.request.rate_limited\", {\n path,\n attempt: attempt + 1,\n retryAfterMs,\n })\n } else if (res.status >= 400 && res.status < 500) {\n // other 4xx is a client error — do NOT retry.\n // Suppress the envelope for EXPECTED 4xx (e.g. a guest whoami 401 — BUG 3): it's a\n // normal branch the caller handles, not noise to dump to the user's terminal.\n if (!(opts.expected4xx ?? []).includes(res.status)) {\n this.logError(\"store.request.client_error\", { path, status: res.status, body: parsed })\n }\n throw new StoreApiError(\n `request failed (HTTP ${res.status})`,\n res.status,\n path\n )\n } else {\n // 5xx — retryable. Fall through to the retry loop.\n lastErr = new StoreApiError(`upstream error (HTTP ${res.status})`, res.status, path)\n this.logWarn(\"store.request.retryable\", { path, status: res.status, attempt: attempt + 1 })\n }\n } catch (e) {\n // RecoverableCartError / StoreApiError(4xx) must propagate immediately.\n if (e instanceof RecoverableCartError) {\n throw e\n }\n if (e instanceof StoreApiError && e.status >= 400 && e.status < 500) {\n throw e\n }\n // Network error, an aborted (timed-out) request, or a 5xx StoreApiError → retryable.\n lastErr = e\n this.logWarn(\"store.request.network_retry\", { path, attempt: attempt + 1 })\n } finally {\n // Always clear the per-attempt timeout — on success-return, on a rethrow, or before a retry.\n clearTimeout(timer)\n }\n\n // Backoff before the next attempt (skip after the final attempt). A 429 Retry-After, when\n // present, dictates the delay; otherwise jittered exponential backoff (d12).\n if (attempt < this.retry.attempts - 1) {\n await this.sleepImpl(retryAfterMs ?? this.backoffMs(attempt))\n }\n }\n\n // Exhausted retries. The thrown envelope is already free of secrets (we only ever\n // log/throw redacted, derived data — never the request headers/body).\n this.logError(\"store.request.exhausted\", { path, attempts: this.retry.attempts })\n if (lastErr instanceof StoreApiError) {\n throw lastErr\n }\n throw new StoreApiError(\n `request failed after ${this.retry.attempts} attempts: ${path}`,\n 0,\n path\n )\n }\n\n private logWarn(event: string, ctx: Record<string, unknown>): void {\n this.logger.warn(event, redact(ctx))\n }\n\n private logError(event: string, ctx: Record<string, unknown>): void {\n this.logger.error(event, redact(ctx))\n }\n\n // ----- regions (F-A2 / INV-11) ---------------------------------------------\n\n async getRegions(): Promise<RegionLike[]> {\n const data = await this.request<{ regions: RegionLike[] }>(\"GET\", \"/store/regions\")\n this.regionsCache = data.regions ?? []\n return this.regionsCache\n }\n\n /**\n * Resolve a region by country code, falling back to region #1.\n * SEK is region #1 by configuration — NEVER a hardcoded literal.\n */\n async resolveRegion(countryCode?: string): Promise<RegionLike> {\n const regions = this.regionsCache ?? (await this.getRegions())\n if (regions.length === 0) {\n // Fail closed: refuse to price without a resolved region (F-A2).\n throw new Error(\"no regions configured — cannot resolve a region for pricing\")\n }\n if (countryCode) {\n const cc = countryCode.toLowerCase()\n const match = regions.find((r) =>\n (r.countries ?? []).some((c) => (c.iso_2 ?? \"\").toLowerCase() === cc)\n )\n if (match) {\n return match\n }\n // A country code was given but no region is configured for it → we're about to fall back to\n // region #1 and price in ITS currency. Warn (stderr, redacted) so the mis-resolution is\n // observable instead of a silent wrong-currency cart. Matters as we go intl (F-A2 / Phase 6).\n this.logWarn(\"store.region.unmatched\", { countryCode: cc, fallback: regions[0]!.id })\n }\n // Region #1 — the launch region (SEK today, derived not hardcoded).\n return regions[0]!\n }\n\n // ----- products (region resolved BEFORE pricing) ---------------------------\n\n async listProducts(opts?: { region?: RegionLike; countryCode?: string }): Promise<ProductLike[]> {\n const region = opts?.region ?? (await this.resolveRegion(opts?.countryCode))\n // Paginate (L12): a single limit:100 call silently drops the 101st product. Page through until a\n // short page, with a hard cap so a misbehaving API can never spin this unbounded.\n const PAGE = 100\n const HARD_CAP = 1000\n const all: ProductLike[] = []\n for (let offset = 0; offset < HARD_CAP; offset += PAGE) {\n const data = await this.request<{ products: ProductLike[] }>(\"GET\", \"/store/products\", {\n // +metadata: Medusa omits product.metadata by default, and that's where lore/lore_sources\n // (and any per-product spec fields) live — without this the CLI/MCP never see them.\n query: { region_id: region.id, limit: PAGE, offset, fields: PRODUCT_FIELDS },\n })\n const page = data.products ?? []\n all.push(...page)\n if (page.length < PAGE) {\n break\n }\n }\n return all\n }\n\n async getProduct(handle: string, opts?: { region?: RegionLike; countryCode?: string }): Promise<ProductLike | null> {\n const region = opts?.region ?? (await this.resolveRegion(opts?.countryCode))\n const data = await this.request<{ products: ProductLike[] }>(\"GET\", \"/store/products\", {\n query: { handle, region_id: region.id, limit: 1, fields: PRODUCT_FIELDS },\n })\n return (data.products ?? [])[0] ?? null\n }\n\n // ----- cart (mutations carry Idempotency-Key) ------------------------------\n\n async createCart(region: RegionLike): Promise<CartLike> {\n const data = await this.request<{ cart: CartLike }>(\"POST\", \"/store/carts\", {\n idempotent: true,\n body: { region_id: region.id },\n })\n return data.cart\n }\n\n async addLineItem(cartId: string, variantId: string, quantity: number): Promise<CartLike> {\n const data = await this.request<{ cart: CartLike }>(\n \"POST\",\n `/store/carts/${encodeURIComponent(cartId)}/line-items`,\n {\n idempotent: true,\n cartRecovery: true,\n body: { variant_id: variantId, quantity },\n }\n )\n return data.cart\n }\n\n async getCart(cartId: string): Promise<CartLike> {\n const data = await this.request<{ cart: CartLike }>(\n \"GET\",\n `/store/carts/${encodeURIComponent(cartId)}`,\n { cartRecovery: true }\n )\n return data.cart\n }\n\n /** Remove a staged line item (bt unstage). Returns the updated cart (Medusa's `parent`). */\n async removeLineItem(cartId: string, itemId: string): Promise<CartLike> {\n const data = await this.request<{ parent?: CartLike; cart?: CartLike }>(\n \"DELETE\",\n `/store/carts/${encodeURIComponent(cartId)}/line-items/${encodeURIComponent(itemId)}`,\n { idempotent: true, cartRecovery: true }\n )\n return (data.parent ?? data.cart) as CartLike\n }\n\n /**\n * Ensure the cart has a shipping method before checkout (BUG 1 — the headless\n * /store/headless/checkout route guards on it, and neither client ever set one).\n *\n * - Idempotent: if the cart already carries a shipping method, no-op (no POST).\n * - Lists options via the STANDARD store route GET /store/shipping-options?cart_id=…\n * (NOT a /headless route — standard routes are always available, no kill-switch).\n * - Picks deterministically: exactly one → use it; several → the cheapest by amount.\n * - The SE launch option is region-scoped (price_type:\"flat\"), so listing by cart_id\n * returns it without a shipping address being set first (confirmed: seed-shipping.ts).\n *\n * TODO(04.5): buyer-choice / intl multiple shipping options — today we auto-pick the\n * cheapest; an explicit buyer chooser belongs to the Phase-6 international pass.\n */\n async ensureCartShippingMethod(cartId: string): Promise<CartLike> {\n // 1. read the authoritative cart; a stale/completed cart → RecoverableCartError (F-B2).\n const cart = await this.getCart(cartId)\n if (cart.shipping_methods && cart.shipping_methods.length > 0) {\n // idempotent: already satisfied.\n return cart\n }\n\n // 2. list options via the STANDARD store route (always available, no kill-switch).\n const data = await this.request<{ shipping_options: ShippingOptionLike[] }>(\n \"GET\",\n \"/store/shipping-options\",\n { query: { cart_id: cartId } }\n )\n const options = data.shipping_options ?? []\n if (options.length === 0) {\n // fail closed: never mint a checkout we know the backend will reject (BUG 1).\n throw new Error(\n \"no shipping option is available for this cart — cannot attach a shipping method\"\n )\n }\n\n // 3. pick deterministically: one → it; several → the cheapest by amount.\n const chosen =\n options.length === 1\n ? options[0]!\n : [...options].sort(\n (a, b) => (a.amount ?? Number.POSITIVE_INFINITY) - (b.amount ?? Number.POSITIVE_INFINITY)\n )[0]!\n\n // 4. attach it — a mutation, so it carries an Idempotency-Key + cart-recovery.\n const res = await this.request<{ cart: CartLike }>(\n \"POST\",\n `/store/carts/${encodeURIComponent(cartId)}/shipping-methods`,\n {\n idempotent: true,\n cartRecovery: true,\n body: { option_id: chosen.id },\n }\n )\n return res.cart\n }\n\n // ----- headless contract (04.5-01) -----------------------------------------\n\n // `channel` is REQUIRED (L11): a default silently mis-attributes analytics for a future caller that\n // forgets it. Both current callers (cli, mcp) pass it explicitly.\n async mintCheckout(cartId: string, email: string | undefined, channel: string): Promise<HeadlessCheckout> {\n return this.request<HeadlessCheckout>(\"POST\", \"/store/headless/checkout\", {\n idempotent: true,\n body: { cart_id: cartId, email, channel },\n })\n }\n\n async poll(sessionId: string): Promise<PollResult> {\n return this.request<PollResult>(\n \"GET\",\n `/store/headless/poll/${encodeURIComponent(sessionId)}`\n )\n }\n\n /** JWT-bound: returns the authenticated customer's own profile (G2). */\n async whoami(): Promise<WhoAmI> {\n // A guest (no/expired JWT) gets a 401/403 — that's the EXPECTED unauthenticated branch the\n // CLI turns into the friendly \"browsing as a guest\" line, NOT an error to dump (BUG 3).\n return this.request<WhoAmI>(\"GET\", \"/store/headless/whoami\", { expected4xx: [401, 403] })\n }\n\n // ----- magic-link auth (04.5-05) -------------------------------------------\n // Passwordless login — magic-link is the only grant (Medusa has no OAuth pairing-grant, G1).\n // request-link emails a single-use link; verify exchanges the link's token for a customer-scoped\n // JWT. The JWT is stored in the OS keychain by the CLI — never here, never in plaintext config (G7/G8).\n\n /** Start a magic-link login. Returns a UNIFORM ack regardless of whether the email exists (G12). */\n async requestMagicLink(email: string): Promise<RequestLinkResult> {\n return this.request<RequestLinkResult>(\"POST\", \"/store/headless/auth/request-link\", {\n idempotent: true,\n body: { email },\n })\n }\n\n /** Exchange a magic-link token for a customer-scoped JWT (the customer is materialized server-side). */\n async verifyMagicLink(token: string): Promise<VerifyLinkResult> {\n return this.request<VerifyLinkResult>(\"POST\", \"/store/headless/auth/verify\", {\n idempotent: true,\n body: { token },\n })\n }\n\n async listDrops(): Promise<DropRow[]> {\n const data = await this.request<{ drops: DropRow[]; count: number }>(\n \"GET\",\n \"/store/headless/drops\"\n )\n return data.drops ?? []\n }\n\n /**\n * File a developer bug report (d14 — `bt report` / MCP report_issue). Returns a correlation\n * reference. Additive: no money/cart path. The backend redacts authoritatively; callers SHOULD\n * pass an already-redacted context (the doctor snapshot). A mutation → carries an Idempotency-Key\n * so a retry can't email the report twice.\n */\n async report(message: string, context?: Record<string, unknown>): Promise<ReportResult> {\n return this.request<ReportResult>(\"POST\", \"/store/headless/report\", {\n idempotent: true,\n body: { message, ...(context !== undefined ? { context } : {}) },\n })\n }\n\n /**\n * The authenticated customer's order history (d13 — `bt log`). JWT-bound; the backend derives the\n * customer from the bearer token and returns MINIMAL fields (status/timeline only — never\n * email/address, F-H3). A guest (no/expired JWT) gets a 401 — the EXPECTED unauthenticated branch\n * we turn into the honest empty history (`[]`), NOT an error to dump (BUG 3).\n */\n async listOrders(): Promise<OrderSummary[]> {\n try {\n const data = await this.request<{ orders: OrderSummary[] }>(\"GET\", \"/store/headless/orders\", {\n expected4xx: [401, 403],\n })\n return data.orders ?? []\n } catch (e) {\n if (e instanceof StoreApiError && (e.status === 401 || e.status === 403)) {\n return []\n }\n throw e\n }\n }\n\n /**\n * Guest order lookup (AUD-8) — POST /store/order-lookup with the order id (display id OR internal\n * id) + the EXACT email in the BODY (never the query string, so the email is not access-logged).\n * The backend returns 404 on any mismatch (no enumeration) → the client surfaces null. Rate-limited\n * server-side; returns minimal status only, never PII (F-H1 / F-H3).\n */\n async getOrder(idOrDisplayId: string | number, email: string): Promise<unknown | null> {\n try {\n return await this.request<unknown>(\"POST\", \"/store/order-lookup\", {\n body: { id: String(idOrDisplayId), email },\n })\n } catch (e) {\n if (e instanceof StoreApiError && e.status === 404) {\n return null\n }\n throw e\n }\n }\n}\n","// Baked-in PUBLIC defaults for the Binary & Thread production channel.\n//\n// These let `npx @binaryandthread/cli` and the MCP server reach the live store with ZERO\n// configuration — the point of the soft-launch tarball (P4.5-7b) and decision #79 (d2).\n// store-client is bundled into both the CLI and the MCP server (tsup `noExternal`), so this\n// is the single source of truth for \"which store\" both clients talk to by default.\n//\n// SAFETY (CLAUDE.md rule 1 / SECURITY T6): the publishable key is PUBLIC BY DESIGN. It is the\n// SAME key embedded in the public storefront's client bundle (binaryandthread.com) and carries\n// no privilege beyond read + cart over the Store API — every dangerous operation lives behind\n// the backend. It is therefore NOT a secret; rule 1 governs secrets, and this is deliberately\n// published. Any env var (BT_API_URL / BT_STORE_URL / BT_PUBLISHABLE_KEY / MEDUSA_*) OVERRIDES\n// these for local dev or staging.\n\n/** The Binary & Thread production Medusa Store API base URL. */\nexport const BT_DEFAULT_STORE_URL = \"https://backend-production-511f5.up.railway.app\"\n\n// The PUBLIC Medusa publishable key (read + cart only; safe to ship — T6). Identical to the key\n// served in the public storefront bundle. NOT a secret (CLAUDE.md rule 1); the inline allow keeps\n// the gitleaks pre-commit hook from flagging a deliberately-published value.\nexport const BT_DEFAULT_PUBLISHABLE_KEY = \"pk_0d7d706860568a0414dff0436aadb9713a2609de3a1b153ef2a6f9694d23f907\" // gitleaks:allow\n","// Image-host allowlist — the canonical source for the WHOLE channel (CLAUDE.md rule 6 / SECURITY G9).\n//\n// We fetch product images ONLY from our own Vercel Blob store. Both the bt CLI (its terminal\n// image ladder) and the Wave-4 MCP (its server-side ImageContent fetch) gate every outbound\n// image fetch on this allowlist. It lives HERE in the shared client — not in the CLI — so the\n// MCP does not take a runtime dependency on the CLI package; the CLI re-exports it from here.\n//\n// HISTORY: a bare `endsWith(allowed)` suffix match was a CRITICAL allowlist-bypass bug fixed in\n// Wave 2 (it would admit `evilblob.vercel-storage.com`). The dot-delimited-subdomain check below\n// is load-bearing security — `host === allowed || host.endsWith(\".\" + allowed)`, never a bare\n// suffix. Do NOT \"simplify\" it.\n\n/**\n * Allowlisted image hosts. We fetch product images ONLY from our own Vercel Blob store\n * (public). NEVER a public temp host (uguu/0x0/catbox/transfer.sh) and NEVER an arbitrary\n * URL — that would be SSRF + a brand-IP leak (CLAUDE.md rule 6).\n */\nexport const BLOB_HOST_ALLOWLIST: readonly string[] = [\n \"blob.vercel-storage.com\", // matches the exact host + *.blob.vercel-storage.com (dot-delimited subdomains only)\n]\n\n/**\n * True if the URL's host is on the Blob allowlist.\n *\n * Exact host OR a true dot-delimited subdomain. NOT a bare suffix: a bare\n * `endsWith(allowed)` would admit `evilblob.vercel-storage.com` (allowlist bypass →\n * SSRF / brand-IP leak, CLAUDE.md rule 6). The dot guard is load-bearing.\n */\nexport function isAllowedImageHost(url: string): boolean {\n let host: string\n try {\n host = new URL(url).hostname.toLowerCase()\n } catch {\n return false\n }\n return BLOB_HOST_ALLOWLIST.some(\n (allowed) => host === allowed || host.endsWith(\".\" + allowed)\n )\n}\n","// Colour resolution — DERIVED from the variant matrix, never hardcoded (CAT-3 forward-compat).\n//\n// The dual-colour catalog (memory: dual-colour-catalog) gives each design ONE product with a\n// Colour × Size variant matrix. A variant's colour comes from, in order of trust:\n// 1. the variant option titled \"Colour\" (Model A: options {Size, Colour}, value title-cased)\n// 2. the variant title, format \"<size> / <Colour>\" (e.g. \"S / White\", \"M / Black\")\n// We NEVER hardcode white/black — future colours (CAT-3) fall out of the data for free.\n//\n// This lives in @binaryandthread/store-client (the canonical types home) so the CLI and the MCP\n// resolve colour the SAME way — written once, tested once.\n\nimport type { ProductLike, VariantLike, ImageLike } from \"./types\"\n\n/** Title-case a colour token for display (\"white\" → \"White\"); leaves multi-word reasonable. */\nexport function displayColour(colour: string): string {\n const c = colour.trim()\n if (!c) return c\n return c.charAt(0).toUpperCase() + c.slice(1).toLowerCase()\n}\n\n/**\n * The colour of a single variant, lowercased — or null if it carries no colour axis (a\n * legacy Size-only product). Reads the \"Colour\" option first, then parses the \"<size> / <Colour>\"\n * title. NEVER assumes a fixed palette.\n */\nexport function variantColour(v: VariantLike): string | null {\n // 1) the explicit Colour option (the authoritative source — memory: never parse the title).\n const opt = (v.options ?? []).find(\n (o) => (o.option?.title ?? \"\").toLowerCase() === \"colour\" || (o.option?.title ?? \"\").toLowerCase() === \"color\"\n )\n if (opt && typeof opt.value === \"string\" && opt.value.trim()) {\n return opt.value.trim().toLowerCase()\n }\n // 2) fall back to the title format \"<size> / <Colour>\".\n const title = (v.title ?? \"\").trim()\n if (title.includes(\"/\")) {\n const parts = title.split(\"/\").map((p) => p.trim())\n const tail = parts[parts.length - 1]\n if (tail) return tail.toLowerCase()\n }\n return null\n}\n\n/**\n * The DISTINCT colours offered by a product, in first-seen order, lowercased. Empty for a\n * legacy Size-only product (no Colour axis) — callers treat that as \"no colour to show\".\n */\nexport function productColours(p: ProductLike): string[] {\n const seen: string[] = []\n for (const v of p.variants ?? []) {\n const c = variantColour(v)\n if (c && !seen.includes(c)) {\n seen.push(c)\n }\n }\n return seen\n}\n\n/** The distinct sizes offered FOR a given colour (lowercased colour match), preserving order. */\nexport function sizesForColour(p: ProductLike, colour: string): string[] {\n const target = colour.trim().toLowerCase()\n const sizes: string[] = []\n for (const v of p.variants ?? []) {\n if (variantColour(v) !== target) continue\n const size = sizeOfVariant(v)\n if (size && !sizes.includes(size)) {\n sizes.push(size)\n }\n }\n return sizes\n}\n\n/** The size label of a variant — the Size option, else the title's leading \"<size>\" segment. */\nexport function sizeOfVariant(v: VariantLike): string | null {\n const opt = (v.options ?? []).find((o) => (o.option?.title ?? \"\").toLowerCase() === \"size\")\n if (opt && typeof opt.value === \"string\" && opt.value.trim()) {\n return opt.value.trim()\n }\n const title = (v.title ?? \"\").trim()\n if (title.includes(\"/\")) {\n const head = title.split(\"/\")[0]?.trim()\n if (head) return head\n }\n // No colour axis → the whole title IS the size (legacy Size-only product).\n return title || null\n}\n\n/**\n * Resolve a variant by size AND colour (the real buyability gap — bt stage / MCP add_to_cart).\n * - size + colour given → exact match on both (case-insensitive).\n * - size only → first variant of that size.\n * - colour only → first variant of that colour.\n * - neither → first variant.\n * Returns undefined when the requested size+colour combination does not exist (the caller MUST\n * surface a clear error — never silently substitute another colour the buyer didn't ask for).\n */\nexport function findVariant(\n p: ProductLike,\n opts: { size?: string; colour?: string } = {}\n): VariantLike | undefined {\n const variants = p.variants ?? []\n const size = opts.size?.trim().toLowerCase()\n const colour = opts.colour?.trim().toLowerCase()\n\n const sizeMatches = (v: VariantLike): boolean => {\n if (!size) return true\n return (sizeOfVariant(v) ?? \"\").toLowerCase() === size || (v.title ?? \"\").toLowerCase() === size\n }\n const colourMatches = (v: VariantLike): boolean => {\n if (!colour) return true\n return variantColour(v) === colour\n }\n\n if (size && colour) {\n // both specified → require BOTH; no fallback to a different colour (a wrong colour is worse\n // than a clear \"that combo isn't available\").\n return variants.find((v) => sizeMatches(v) && colourMatches(v))\n }\n if (colour && !size) {\n return variants.find((v) => colourMatches(v))\n }\n if (size && !colour) {\n // size specified → require it; NO fallback to variants[0] (L10) — a silently-wrong size is worse\n // than undefined, which lets the caller (stage.ts) surface a clear \"no matching variant\" error.\n return variants.find((v) => sizeMatches(v))\n }\n return variants[0]\n}\n\n/**\n * The image URL to render for a given colour. Resolves, in order:\n * 1. the matching variant's own images[] (most precise)\n * 2. a product image whose url/metadata names the colour\n * 3. null → the caller falls back to product.thumbnail + a \"black available\" note.\n * Never throws; a missing colour just yields null.\n */\nexport function imageForColour(p: ProductLike, colour: string): string | null {\n const target = colour.trim().toLowerCase()\n if (!target) return p.thumbnail ?? firstImageUrl(p.images) ?? null\n\n // 1) the colour's variant images.\n for (const v of p.variants ?? []) {\n if (variantColour(v) !== target) continue\n const url = firstImageUrl(v.images)\n if (url) return url\n }\n\n // 2) a product image that names the colour (url substring or metadata.colour tag).\n for (const img of p.images ?? []) {\n const tag = (img.metadata?.[\"colour\"] ?? img.metadata?.[\"color\"] ?? \"\")\n .toString()\n .toLowerCase()\n if (tag === target) {\n const u = (img.url ?? \"\").trim()\n if (u) return u\n }\n }\n for (const img of p.images ?? []) {\n const u = (img.url ?? \"\").trim()\n if (u && new RegExp(`(^|[^a-z])${escapeRegex(target)}([^a-z]|$)`, \"i\").test(u)) {\n return u\n }\n }\n return null\n}\n\nfunction firstImageUrl(images: Array<ImageLike> | null | undefined): string | null {\n for (const img of images ?? []) {\n const u = (img?.url ?? \"\").trim()\n if (u) return u\n }\n return null\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n}\n","// Canonical product-content accessors — the ONE source for a piece's README, spec sheet, and lore,\n// shared by the CLI (`bt view` / `bt blame`), the MCP (`get_product`), and (aspirationally) the\n// storefront PDP. Keeping these here means the three channels can't drift on what a piece \"says\".\n//\n// The spec fields + fallbacks mirror the storefront's buildSpecs (apps/storefront .../templates).\n\nimport type { ProductLike } from \"./types\"\n\n/** One row of the spec sheet. */\nexport interface SpecEntry {\n key: string\n value: string\n}\n\n/** A lore link (a real source behind the pun/idea). */\nexport interface LoreSource {\n url: string\n label: string\n}\n\n/** A piece's lore: the story text + any sources. Empty text = no lore on record. */\nexport interface ProductLore {\n text: string\n sources: LoreSource[]\n}\n\n/** A non-empty string, or the fallback. */\nfunction str(v: unknown, fallback: string): string {\n return typeof v === \"string\" && v.length > 0 ? v : fallback\n}\n\n/** The piece's README — the human description shown on the PDP's README toggle. */\nexport function productReadme(p: ProductLike): string {\n return typeof p.description === \"string\" ? p.description : \"\"\n}\n\n/**\n * The spec sheet — the 6 canonical rows, in PDP order, with the same fallbacks as the storefront's\n * buildSpecs (the single source of truth for what a piece is made of / how it ships).\n */\nexport function productSpecs(p: ProductLike): SpecEntry[] {\n const md = (p.metadata ?? {}) as Record<string, unknown>\n return [\n { key: \"material\", value: str(md.material, \"100% cotton\") },\n { key: \"weight\", value: str(md.weight, \"180 g/m²\") },\n { key: \"fit\", value: str(md.fit, \"Unisex heavyweight crewneck\") },\n { key: \"print\", value: str(md.print, \"Front print\") },\n { key: \"delivery\", value: str(md.delivery, \"Ships in 2–5 business days + transit\") },\n { key: \"care\", value: str(md.care, \"Cold wash, line dry\") },\n ]\n}\n\n/** The piece's lore — the story behind the pun + any real sources. Empty text = none on record. */\nexport function productLore(p: ProductLike): ProductLore {\n const md = (p.metadata ?? {}) as Record<string, unknown>\n const text = typeof md.lore === \"string\" ? md.lore : \"\"\n const raw = Array.isArray(md.lore_sources) ? md.lore_sources : []\n const sources: LoreSource[] = raw\n .filter(\n (s): s is { url: string; label: string } =>\n !!s &&\n typeof s === \"object\" &&\n typeof (s as { url?: unknown }).url === \"string\" &&\n typeof (s as { label?: unknown }).label === \"string\"\n )\n .map((s) => ({ url: s.url, label: s.label }))\n return { text, sources }\n}\n","// The B&T MCP server — stdio transport, registering exactly the channel's deterministic tools.\n//\n// The server is a THIN registrar: each tool is a deterministic wrapper over the shared\n// StoreClient (no free-form execution, no admin op — SECURITY T10/G8). The tool DEFINITIONS\n// (names, input schemas, handlers) live in tools.ts; this module just builds the McpServer and\n// wires the stdio transport so a developer's AI agent can call them over stdin/stdout.\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\"\nimport type { ZodRawShape } from \"zod\"\nimport type { ImageBlock } from \"./images\"\n\n/** What a tool handler returns — MCP content blocks (image and/or text). */\nexport interface ToolResult {\n content: ImageBlock[]\n /** Set when the tool reports a user-facing error (e.g. a 404 lookup) — not a transport error. */\n isError?: boolean\n /**\n * Machine-readable result (ids/handles like cart_id/session_id/order_id) so an agent can chain\n * tools WITHOUT regexing the prose (d9). Already redacted (G10). Mirrors the human text block.\n */\n structuredContent?: Record<string, unknown>\n}\n\n/**\n * A deterministic tool definition. `inputSchema` is a Zod raw shape (the SDK turns it into a JSON\n * schema for the agent). The handler receives the parsed args and returns content blocks. Eggs\n * (VOICE §3.2 M2/M3) may live in the `description` text but NEVER alter behavior.\n */\nexport interface ToolDef<S extends ZodRawShape = ZodRawShape> {\n name: string\n description: string\n inputSchema: S\n handler: (args: Record<string, unknown>) => Promise<ToolResult>\n}\n\nexport const SERVER_INFO = {\n name: \"binary-and-thread\",\n version: \"0.1.0\",\n} as const\n\n/**\n * Build an McpServer and register every tool. Returns the server plus the registered tool names\n * (so callers/tests can assert exactly the 10 tools are present). Pure — no transport, no I/O.\n */\nexport function buildServer(tools: ToolDef[]): { server: McpServer; toolNames: string[] } {\n const server = new McpServer(SERVER_INFO)\n const toolNames: string[] = []\n // The SDK's registerTool generic over a ZodRawShape input schema infers excessively deeply\n // (TS2589); we register through a structurally-typed view of the method to keep the call site\n // simple. The runtime contract is unchanged — name, { description, inputSchema }, handler.\n const register = server.registerTool.bind(server) as unknown as (\n name: string,\n config: { description: string; inputSchema: ZodRawShape },\n cb: (args: Record<string, unknown>) => Promise<{\n content: ImageBlock[]\n isError?: boolean\n structuredContent?: Record<string, unknown>\n }>\n ) => void\n for (const t of tools) {\n register(\n t.name,\n { description: t.description, inputSchema: t.inputSchema },\n // The SDK validates args against inputSchema before calling us. We hand the parsed args to\n // the deterministic handler and return its content blocks (+ optional structuredContent, d9).\n async (args: Record<string, unknown>) => {\n const res = await t.handler(args ?? {})\n return {\n content: res.content,\n ...(res.isError ? { isError: true } : {}),\n ...(res.structuredContent ? { structuredContent: res.structuredContent } : {}),\n }\n }\n )\n toolNames.push(t.name)\n }\n return { server, toolNames }\n}\n\n/** Build the server with the real tools and start it on stdio. Used by bin.ts. */\nexport async function startServer(tools: ToolDef[]): Promise<McpServer> {\n const { server } = buildServer(tools)\n const transport = new StdioServerTransport()\n await server.connect(transport)\n return server\n}\n","// The 10 deterministic MCP tools over the SHARED StoreClient (SECURITY G8/T10; ROBUSTNESS §4.2/4.3).\n//\n// These are thin wrappers — no free-form execution, no admin op, no money self-completion. They\n// reuse the same idempotency / region-resolution / retry / redaction that the CLI uses (it all\n// lives inside the StoreClient — written once, contract-tested once).\n//\n// The hard invariants this file enforces (the MCP red-team gates):\n// - checkout (G8 confused-deputy / F-I4): returns {checkout_url, session_id} INSTANTLY. It NEVER\n// polls, NEVER opens a browser, NEVER sets a shipping address. Its result text tells the human\n// to open the link and explicitly says \"I can't complete this for you\". The human pays on the\n// storefront (Stripe Elements) — payment details NEVER enter the chat. check_payment polls; the\n// agent re-calls at its own cadence (async handle-ID).\n// (NB: there is deliberately NO payment-detail field anywhere in this package; the security\n// verify greps for those field tokens and must come back clean.)\n// - list views are TEXT-ONLY (no per-item images — token-bloat guard, ROBUSTNESS §4.3).\n// - every backend string is sanitize()'d (G8); every output object is redactForOutput()'d (G10).\n// - get_order_status returns status/timeline only — NO address (F-H3); id + exact-email, 404 on\n// miss (F-H1).\n\nimport { z } from \"zod\"\nimport {\n RecoverableCartError,\n StoreApiError,\n productColours,\n sizesForColour,\n findVariant,\n productSpecs,\n productLore,\n productReadme,\n} from \"@binaryandthread/store-client\"\nimport type {\n StoreClient,\n ProductLike,\n CartLike,\n RegionLike,\n} from \"@binaryandthread/store-client\"\nimport { sanitize, redactForOutput } from \"./sanitize\"\nimport { buildImageContent } from \"./images\"\nimport type { ImageBlock } from \"./images\"\nimport type { ToolDef, ToolResult } from \"./server\"\nimport { TEAPOT_TEXT, FORTUNES, fortune } from \"./eggs\"\n\nexport interface ToolsOpts {\n /** ISO-2 country code to resolve pricing (never a hardcoded currency literal). */\n region?: string\n}\n\n// ----- result helpers ---------------------------------------------------------\n\nfunction text(s: string): ToolResult {\n return { content: [{ type: \"text\", text: s }] }\n}\n\nfunction errorText(s: string): ToolResult {\n return { content: [{ type: \"text\", text: s }], isError: true }\n}\n\n/** A text block PLUS machine-readable structuredContent (ids/handles) for agent chaining (d9). */\nfunction result(textBody: string, structured: Record<string, unknown>): ToolResult {\n return {\n content: [{ type: \"text\", text: textBody }],\n structuredContent: redactForOutput(structured) as Record<string, unknown>,\n }\n}\n\n/** Price line — currency comes from the variant/region, NEVER a literal (F-A2/INV-11). */\nfunction fmtPrice(amount: number | null | undefined, currency: string | null | undefined): string {\n if (amount == null) {\n return \"—\"\n }\n const cur = (currency ?? \"\").toUpperCase()\n return cur ? `${amount} ${cur}` : `${amount}`\n}\n\nfunction firstPrice(p: ProductLike): { amount: number | null; currency: string | null } {\n const v = (p.variants ?? [])[0]\n return {\n amount: v?.calculated_price?.calculated_amount ?? null,\n currency: v?.calculated_price?.currency_code ?? null,\n }\n}\n\nfunction cartSummary(cart: CartLike, region?: RegionLike): string {\n const lines: string[] = []\n lines.push(`cart ${cart.id}`)\n const items = cart.items ?? []\n if (items.length === 0) {\n lines.push(\" (empty)\")\n } else {\n for (const li of items) {\n const title = sanitize(li.product_title ?? li.title ?? \"item\", 80)\n lines.push(` ${li.quantity}x ${title} — ${fmtPrice(li.total, cart.currency_code ?? region?.currency_code)}`)\n }\n }\n lines.push(` total ${fmtPrice(cart.total, cart.currency_code ?? region?.currency_code)} incl. VAT`)\n return lines.join(\"\\n\")\n}\n\n// ----- the 10 tools ------------------------------------------------------------\n\n/** Build the 10 tool definitions bound to a StoreClient. */\nexport function makeTools(client: StoreClient, opts: ToolsOpts = {}): ToolDef[] {\n const regionCC = opts.region\n\n // list_products — TEXT-ONLY catalog (no per-item images — token-bloat guard, §4.3).\n const list_products: ToolDef = {\n name: \"list_products\",\n description:\n \"List the pieces in stock (text-only — no images, to keep the agent's context lean). \" +\n \"Returns each product's handle, price incl. VAT, and stage. Use get_product for one item's image.\",\n inputSchema: { stage: z.string().optional().describe(\"filter by channel stage, e.g. 'main' or 'next'\") },\n handler: async (args): Promise<ToolResult> => {\n const region = await client.resolveRegion(regionCC)\n const products = await client.listProducts({ region })\n const stage = typeof args.stage === \"string\" ? args.stage.toLowerCase() : undefined\n const rows = products\n .map((p) => ({ p, stage: String(p.metadata?.[\"channel_stage\"] ?? \"main\") }))\n .filter((r) => (stage ? r.stage.toLowerCase() === stage : true))\n if (rows.length === 0) {\n return result(\"nothing in stock on this branch yet.\", { products: [] })\n }\n const lines = rows.map(({ p, stage }) => {\n const { amount, currency } = firstPrice(p)\n const handle = sanitize(p.handle ?? p.id, 60)\n const title = sanitize(p.title ?? \"\", 80)\n return `${handle} ${fmtPrice(amount, currency)} incl. VAT [${stage}] ${title}`.trimEnd()\n })\n const productList = rows.map(({ p, stage }) => {\n const { amount, currency } = firstPrice(p)\n return { handle: p.handle ?? p.id, title: p.title ?? \"\", price: amount, currency, stage }\n })\n return result(lines.join(\"\\n\"), { products: productList })\n },\n }\n\n // get_product — details + the ONE product image (ImageContent + Blob URL floor).\n const get_product: ToolDef = {\n name: \"get_product\",\n // M3 ★ — a hidden line for the schema-readers (devs read tool descriptions). Rewards the\n // contributor instinct + hints at a real feature (the next branch / drops). Never alters behaviour.\n description:\n \"Get one piece in full: its readme, the spec sheet (material/weight/fit/print/delivery/care), \" +\n \"sizes + price incl. VAT, the lore (the story behind it, with sources), and its image (rendered + \" +\n \"the Blob URL as a text floor). // you read the schema. that's the contributor instinct. ask me about the next branch.\",\n inputSchema: { handle: z.string().describe(\"the product handle/slug\") },\n handler: async (args): Promise<ToolResult> => {\n const handle = String(args.handle ?? \"\")\n const region = await client.resolveRegion(regionCC)\n const product = await client.getProduct(handle, { region })\n if (!product) {\n return errorText(`no product with handle \"${sanitize(handle, 60)}\". try list_products.`)\n }\n const lines: string[] = []\n lines.push(sanitize(product.title ?? handle, 120))\n if (product.description) {\n lines.push(sanitize(product.description, 400))\n }\n const fabric = product.metadata?.[\"fabric\"]\n if (typeof fabric === \"string\") {\n lines.push(`fabric: ${sanitize(fabric, 80)}`)\n }\n // A3 — available colours, DERIVED from the variants (never hardcoded). Sizes are listed per\n // colour when there's a colour axis, else a flat list (legacy Size-only product).\n const colours = productColours(product)\n if (colours.length > 0) {\n lines.push(`available colours: ${colours.map((c) => sanitize(c, 20)).join(\", \")}`)\n for (const c of colours) {\n const cs = sizesForColour(product, c).map((s) => sanitize(s, 20))\n lines.push(` ${sanitize(c, 20)}: ${cs.join(\" \")}`)\n }\n } else {\n const sizes = (product.variants ?? []).map((v) => sanitize(v.title ?? \"?\", 20))\n lines.push(`sizes: ${sizes.join(\" \")}`)\n }\n const v0 = (product.variants ?? [])[0]\n if (v0?.calculated_price) {\n lines.push(`price: ${fmtPrice(v0.calculated_price.calculated_amount, v0.calculated_price.currency_code)} incl. VAT`)\n }\n // The full spec sheet (the PDP's technical block) + the lore — so the agent knows the piece,\n // not just its price. Single-sourced from store-client (matches the storefront).\n const specs = productSpecs(product)\n lines.push(\"specs:\")\n for (const s of specs) {\n lines.push(` ${sanitize(s.key, 20)}: ${sanitize(s.value, 120)}`)\n }\n const lore = productLore(product)\n if (lore.text) {\n lines.push(`lore: ${sanitize(lore.text, 600)}`)\n for (const src of lore.sources) {\n lines.push(` source: ${sanitize(src.label, 80)} (${sanitize(src.url, 300)})`)\n }\n }\n const content: ImageBlock[] = [{ type: \"text\", text: lines.join(\"\\n\") }]\n if (product.thumbnail) {\n const img = await buildImageContent(product.thumbnail)\n content.push(...img)\n }\n return {\n content,\n structuredContent: redactForOutput({\n handle: product.handle ?? handle,\n title: product.title ?? \"\",\n price: v0?.calculated_price?.calculated_amount ?? null,\n currency: v0?.calculated_price?.currency_code ?? null,\n colours,\n image_url: product.thumbnail ?? null,\n // the PDP content, machine-readable: readme + the spec sheet + the lore (+ sources).\n readme: productReadme(product),\n specs,\n lore,\n }) as Record<string, unknown>,\n }\n },\n }\n\n // add_to_cart — create-or-reuse a cart; return the cart_id handle + the server cart.\n const add_to_cart: ToolDef = {\n name: \"add_to_cart\",\n description:\n \"Add a piece to a cart. Pass cart_id to reuse an existing cart (the durable handle), or omit it \" +\n \"to start a fresh one. Returns the cart_id and the server-side cart summary (price incl. VAT).\",\n inputSchema: {\n handle: z.string().optional().describe(\"product handle (resolves a variant by size + colour)\"),\n variant_id: z.string().optional().describe(\"a specific variant id (overrides handle/size/colour)\"),\n size: z.string().optional().describe(\"size label, e.g. M / L / XL\"),\n colour: z.string().optional().describe(\"piece colour, e.g. white / black (default: the product's first colour)\"),\n qty: z.number().int().positive().max(20).optional().describe(\"quantity (default 1)\"),\n cart_id: z.string().optional().describe(\"reuse this cart; omit to create a new one\"),\n },\n handler: async (args): Promise<ToolResult> => {\n const region = await client.resolveRegion(regionCC)\n const qty = typeof args.qty === \"number\" ? args.qty : 1\n\n // resolve the variant id\n let variantId = typeof args.variant_id === \"string\" ? args.variant_id : undefined\n if (!variantId) {\n const handle = typeof args.handle === \"string\" ? args.handle : \"\"\n if (!handle) {\n return errorText(\"add_to_cart needs a variant_id or a handle (+ optional size/colour).\")\n }\n const product = await client.getProduct(handle, { region })\n if (!product) {\n return errorText(`no product with handle \"${sanitize(handle, 60)}\".`)\n }\n // A3 — resolve by size AND colour (DERIVED, never hardcoded). colour defaults to the\n // product's first colour; a missing size+colour combo is a CLEAR error (no substitution).\n const size = typeof args.size === \"string\" ? args.size : undefined\n const colours = productColours(product)\n const colour =\n typeof args.colour === \"string\" ? args.colour : colours.length > 0 ? colours[0] : undefined\n const variant = findVariant(product, { size, colour })\n if (!variant) {\n const have = colours.length ? ` available colours: ${colours.map((c) => sanitize(c, 20)).join(\", \")}.` : \"\"\n return errorText(\n `no variant for \"${sanitize(handle, 60)}\"${size ? ` size ${sanitize(size, 20)}` : \"\"}` +\n `${colour ? ` (${sanitize(colour, 20)})` : \"\"}.${have}`\n )\n }\n variantId = variant.id\n }\n\n // create-or-reuse the cart; a stale/completed cart → make a fresh one (F-B2).\n const reuse = typeof args.cart_id === \"string\" ? args.cart_id : undefined\n const addTo = async (cartId: string): Promise<CartLike> => client.addLineItem(cartId, variantId!, qty)\n let cart: CartLike\n try {\n const targetId = reuse ?? (await client.createCart(region)).id\n cart = await addTo(targetId)\n } catch (e) {\n if (e instanceof RecoverableCartError && reuse) {\n // the reused cart was stale/completed → silently make a fresh one (the handle rotates).\n const fresh = await client.createCart(region)\n cart = await addTo(fresh.id)\n } else {\n throw e\n }\n }\n const itemCount = (cart.items ?? []).reduce((n, li) => n + (li.quantity ?? 1), 0)\n return result(\n `added.\\ncart_id: ${cart.id} (reuse this handle on the next add / checkout)\\n${cartSummary(cart, region)}`,\n {\n cart_id: cart.id,\n total: cart.total ?? null,\n currency: cart.currency_code ?? region.currency_code ?? null,\n item_count: itemCount,\n }\n )\n },\n }\n\n // get_cart — the authoritative server cart summary.\n const get_cart: ToolDef = {\n name: \"get_cart\",\n description: \"Show a cart's current contents and total (price incl. VAT), straight from the server.\",\n inputSchema: { cart_id: z.string().describe(\"the cart handle from add_to_cart\") },\n handler: async (args): Promise<ToolResult> => {\n const cartId = String(args.cart_id ?? \"\")\n const region = await client.resolveRegion(regionCC)\n try {\n const cart = await client.getCart(cartId)\n const itemCount = (cart.items ?? []).reduce((n, li) => n + (li.quantity ?? 1), 0)\n return result(cartSummary(cart, region), {\n cart_id: cart.id,\n total: cart.total ?? null,\n currency: cart.currency_code ?? region.currency_code ?? null,\n item_count: itemCount,\n })\n } catch (e) {\n if (e instanceof RecoverableCartError) {\n return errorText(\"that cart is no longer valid (expired or already committed). add an item to start a fresh one.\")\n }\n throw e\n }\n },\n }\n\n // checkout — async handle-ID hand-off. Returns INSTANTLY. NEVER polls / opens a browser /\n // sets an address. Payment details NEVER enter the chat (G8 confused-deputy / F-I4).\n const checkout: ToolDef = {\n name: \"checkout\",\n description:\n \"Mint a checkout hand-off for a cart. Returns a checkout_url + a session_id INSTANTLY — it does \" +\n \"NOT poll and does NOT complete the purchase. The human must open the link in their own browser \" +\n \"to enter their payment + ship-to details on the storefront (those details never enter this chat). \" +\n \"After they pay, call check_payment with the session_id to see the status.\",\n inputSchema: {\n cart_id: z.string().describe(\"the cart handle to check out\"),\n email: z.string().optional().describe(\"the buyer's email for the receipt (optional)\"),\n },\n handler: async (args): Promise<ToolResult> => {\n const cartId = String(args.cart_id ?? \"\")\n const email = typeof args.email === \"string\" ? args.email : undefined\n // BUG 1: the headless checkout route requires the cart to carry a shipping method. Attach\n // one first (idempotent in the client; auto-picks the only/cheapest region option — it\n // sets a SHIPPING method, never a payment/address, so the G8 boundary is untouched). Fail\n // CLOSED: if no option is available we return an error and NEVER mint a checkout link.\n try {\n await client.ensureCartShippingMethod(cartId)\n } catch (e) {\n if (e instanceof RecoverableCartError) {\n return errorText(\"that cart is no longer valid (expired or already committed). add an item to start a fresh one.\")\n }\n return errorText(\"couldn't attach a shipping method to this cart — checkout isn't available for it right now.\")\n }\n // mintCheckout is idempotent in the client. We return its result DIRECTLY — no poll, no\n // browser, no address. This is the entire G8 boundary: the agent cannot self-complete.\n const { checkout_url, session_id } = await client.mintCheckout(cartId, email, \"mcp\")\n const body = [\n \"checkout ready. open this link in your browser to pay:\",\n ` ${checkout_url}`,\n \"\",\n \"payment details are entered on the storefront (Stripe) — they never enter this chat, and\",\n \"I can't complete this for you. once you've paid, call check_payment with:\",\n ` session_id: ${session_id}`,\n \"\",\n // brand-delight #9 — the deadpan flourish. The boundary stays honest (the line above), then\n // one dry true line: an agent can do everything up to the card. The card is the human's.\n \"I staged it, priced it, and walked you to the door. the card swipe is yours — by design.\",\n ].join(\"\\n\")\n // redact defensively (the url/session id carry no secret, but the redactor is the rule).\n return result(redactForOutput(body) as string, { checkout_url, session_id })\n },\n }\n\n // check_payment — poll ONCE; the agent re-calls at its own cadence (async handle-ID, F-I4).\n const check_payment: ToolDef = {\n name: \"check_payment\",\n description:\n \"Check a checkout's payment status ONCE (call again later to re-check — this never blocks/loops). \" +\n \"Returns the status and, once placed, the order_id.\",\n inputSchema: { session_id: z.string().describe(\"the session_id from checkout\") },\n handler: async (args): Promise<ToolResult> => {\n const sessionId = String(args.session_id ?? \"\")\n const res = await client.poll(sessionId)\n const out = redactForOutput({ status: res.status, order_id: res.order_id })\n const human =\n res.status === \"placed\"\n ? `paid — order placed. order_id: ${out.order_id ?? \"—\"}`\n : `status: ${out.status} (call check_payment again in a moment if still pending)`\n return { content: [{ type: \"text\", text: human }], structuredContent: out as Record<string, unknown> }\n },\n }\n\n // get_order_status — status/timeline TEXT only; NO address (F-H3); id + exact-email, 404 on miss (F-H1).\n const get_order_status: ToolDef = {\n name: \"get_order_status\",\n description:\n \"Look up an order's status by its display id + the EXACT email on the order. Returns status only \" +\n \"(no shipping address). A wrong id/email returns not-found (no enumeration).\",\n inputSchema: {\n display_id: z.string().describe(\"the order's display id (e.g. 1042)\"),\n email: z.string().describe(\"the exact email on the order\"),\n },\n handler: async (args): Promise<ToolResult> => {\n const displayId = String(args.display_id ?? \"\")\n const email = String(args.email ?? \"\")\n // AUD-8: the shared client now POSTs to /store/order-lookup with the email in the BODY\n // (no longer a query string); getOrder still surfaces the 404→null (no-enumeration) contract.\n let looked: unknown | null\n try {\n looked = await client.getOrder(displayId, email)\n } catch (e) {\n if (e instanceof StoreApiError && e.status === 404) {\n looked = null\n } else {\n throw e\n }\n }\n if (!looked) {\n return errorText(`no order matches that id + email.`)\n }\n // Extract ONLY status/timeline fields — NEVER the shipping address (F-H3).\n const order = (looked as { order?: Record<string, unknown> }).order ?? (looked as Record<string, unknown>)\n const status = sanitize(String(order[\"status\"] ?? order[\"fulfillment_status\"] ?? \"unknown\"), 60)\n const display = sanitize(String(order[\"display_id\"] ?? displayId), 30)\n const lines = [`order #${display}`, `status: ${status}`]\n const fulfillment = order[\"fulfillment_status\"]\n if (typeof fulfillment === \"string\" && fulfillment) {\n lines.push(`fulfillment: ${sanitize(fulfillment, 60)}`)\n }\n // The output is redacted — and we never selected the address, so no PII can leak (G10/F-H3).\n return result(redactForOutput(lines.join(\"\\n\")) as string, {\n display_id: order[\"display_id\"] ?? display,\n status,\n fulfillment_status: typeof fulfillment === \"string\" ? fulfillment : null,\n })\n },\n }\n\n // report_issue — file a bug report / feedback to the maintainers (d14). ADDITIVE: it sends NO\n // payment/PII and cannot touch any order — the one way an agent can flag a problem. The message +\n // optional context are redacted SERVER-SIDE before they're emailed (G10); we return a reference.\n const report_issue: ToolDef = {\n name: \"report_issue\",\n description:\n \"File a bug report or feedback to the maintainers. Pass a short message (and optional \" +\n \"structured context like versions/ids). Returns a reference id. It sends no payment details \" +\n \"and cannot change any order — it just files the report. Tokens/emails are redacted server-side.\",\n inputSchema: {\n message: z.string().describe(\"what went wrong / the feedback (one or two sentences)\"),\n context: z\n // zod v4 requires an explicit key type: record(keyType, valueType). (Was record(value) —\n // a pre-existing TS2554 that tsup transpiled past; fixed so `npm run typecheck` is clean.)\n .record(z.string(), z.unknown())\n .optional()\n .describe(\"optional structured context (versions, ids) — tokens/emails are redacted server-side\"),\n },\n handler: async (args): Promise<ToolResult> => {\n const message = String(args.message ?? \"\").trim()\n if (!message) {\n return errorText(\"report_issue needs a non-empty message describing the problem.\")\n }\n const context =\n args.context && typeof args.context === \"object\" && !Array.isArray(args.context)\n ? (args.context as Record<string, unknown>)\n : undefined\n // Defense-in-depth (L22/N2): the backend redacts server-side, but redact the message + context\n // HERE too so a token/email an agent stuffed into them never leaves this process in the clear.\n const safeMessage = redactForOutput(message) as string\n const safeContext = context ? (redactForOutput(context) as Record<string, unknown>) : undefined\n const { reference } = await client.report(safeMessage, safeContext)\n return result(`reported. thank you — reference: ${reference}`, { reference })\n },\n }\n\n // teapot — M2 ★. Returns 418. Always. It's a teapot. (RFC 2324). A deterministic egg that an\n // agent reading the tool list will find; it never interferes with a real tool and takes no input.\n const teapot: ToolDef = {\n name: \"teapot\",\n description: \"Returns 418. Always. It's a teapot. (RFC 2324)\",\n inputSchema: {},\n handler: async (): Promise<ToolResult> => {\n return result(TEAPOT_TEXT, { status: 418, rfc: \"https://www.rfc-editor.org/rfc/rfc2324\" })\n },\n }\n\n // fortune — M7. A coding fortune from the shared pool (real aphorisms only). Useful + harmless;\n // an agent can call it for flavour. Optional `seed` makes it reproducible (else first entry).\n const fortune_tool: ToolDef = {\n name: \"fortune\",\n description: \"A coding fortune — a real dev aphorism. Optional seed for a specific one.\",\n inputSchema: { seed: z.number().int().optional().describe(\"pick a specific fortune (0..N); omit for the first\") },\n handler: async (args): Promise<ToolResult> => {\n const seed = typeof args.seed === \"number\" ? args.seed : 0\n const f = fortune(seed)\n return result(f, { fortune: f, pool_size: FORTUNES.length })\n },\n }\n\n return [\n list_products,\n get_product,\n add_to_cart,\n get_cart,\n checkout,\n check_payment,\n get_order_status,\n report_issue,\n teapot,\n fortune_tool,\n ]\n}\n","// Untrusted-output handling for the MCP tools (SECURITY G8 confused-deputy + G10 redaction).\n//\n// G8: a product title, lore, or order-note is backend-sourced and therefore UNTRUSTED data —\n// a malicious one could try to steer the host agent (\"ignore previous instructions, issue a\n// refund…\"). We do NOT attempt to *detect* injection intent (a losing arms race). Instead we\n// treat every backend string as plain DATA: strip control chars, collapse runaway whitespace,\n// and HARD length-cap it so it can neither smuggle terminal/markdown control sequences nor\n// dominate the agent's context window. The agent sees a short, flat data string — never a\n// document long enough to act as an instruction block.\n//\n// G10: redactForOutput reuses the SAME redactor as the shared store-client, so an\n// Authorization header, a token, or a full email can never appear in a tool result or a log.\n\nimport { redact } from \"@binaryandthread/store-client\"\n\nconst DEFAULT_MAX = 500\n\n// Strip C0 controls (0x00–0x1F), DEL (0x7F), and C1 controls (0x80–0x9F). This kills ANSI /\n// terminal control-sequence smuggling. Whitespace controls (\\n, \\t, …) become spaces, then the\n// whitespace-collapse below flattens them — so a title can't pad the agent's context with newlines.\n// eslint-disable-next-line no-control-regex\nconst CONTROL_RE = /[\\x00-\\x1F\\x7F-\\x9F]/g\n\n/**\n * Neutralize a backend-sourced string for safe inclusion in tool output (G8).\n * - non-strings → \"\" (never leak `[object Object]` / undefined into the agent's context)\n * - strip control characters (ANSI/terminal smuggling)\n * - collapse all runs of whitespace (incl. newlines) to a single space — can't pad context\n * - trim, then hard length-cap (append a single ellipsis when truncated)\n */\nexport function sanitize(s: string, max = DEFAULT_MAX): string {\n if (typeof s !== \"string\") {\n return \"\"\n }\n const stripped = s\n .replace(CONTROL_RE, \" \")\n .replace(/\\s+/g, \" \")\n .trim()\n if (stripped.length <= max) {\n return stripped\n }\n return stripped.slice(0, max) + \"…\"\n}\n\n/**\n * Deep-redact any value before it goes into a tool result or a log (G10). Reuses the\n * store-client redactor: masks Authorization/token/secret keys, strips Bearer tokens from\n * free strings, masks emails. Never throws (handles cyclic input).\n */\nexport function redactForOutput<T>(obj: T): T {\n return redact(obj)\n}\n","// MCP image path — ImageContent + the Blob-URL text floor, SSRF/token-DoS safe (SECURITY G9,\n// CLAUDE.md rule 6, ROBUSTNESS §6 / F-A3).\n//\n// Why BOTH an image block AND a URL text block: Claude renders ImageContent natively on\n// claude.ai / the API, but Claude Code has a base64-bloat bug (#31208, 15–25k tokens/image) and\n// can fail to render it. So the Blob URL is ALWAYS emitted as a text block too — the reliable\n// floor (the buy is never blocked on rendering).\n//\n// SECURITY G9 — the fetch is the dangerous part, so it is gated HARD:\n// 1. host allowlist (reused verbatim from @binaryandthread/store-client — the SAME constant the\n// CLI uses) BEFORE any network call. A non-allowlisted host / look-alike subdomain → no fetch.\n// 2. scheme MUST be https — a `data:` or `http:` URL → no fetch (no inline payload, no plaintext).\n// 3. a byte cap (≤2MB) + a fetch timeout — over-cap or slow → URL-text-only (token-DoS guard).\n// 4. content-type must be an image — refuse to base64 a non-image payload.\n// On ANY of those (or any failure) we degrade to URL-text-only and fetch NOTHING. It NEVER throws.\n\nimport { isAllowedImageHost } from \"@binaryandthread/store-client\"\n\n/** ≤2MB — over this we return the URL only (token-DoS guard, G9). */\nexport const IMAGE_BYTE_CAP = 2 * 1024 * 1024\n/** Fetch timeout in ms — a slow/hung host must not stall the tool (a list/view is read-mostly). */\nexport const IMAGE_FETCH_TIMEOUT_MS = 5000\n\n/** An MCP content block — either an inline image or a text line. */\nexport type ImageBlock =\n | { type: \"image\"; data: string; mimeType: string }\n | { type: \"text\"; text: string }\n\nexport interface BuildImageOpts {\n /** Inject a fetch impl (tests). Defaults to global fetch. */\n fetchImpl?: typeof fetch\n /** Override the byte cap (tests). */\n byteCap?: number\n /** Override the timeout (tests). */\n timeoutMs?: number\n}\n\n/** The URL-text-only floor — always carries the Blob URL so the agent can hand it to the human. */\nfunction urlOnly(blobUrl: string): ImageBlock[] {\n return [{ type: \"text\", text: blobUrl }]\n}\n\n/** The \"we couldn't render it\" floor — still carries the URL (never blocks the buy). */\nfunction unavailable(blobUrl: string): ImageBlock[] {\n return [{ type: \"text\", text: `(image unavailable) ${blobUrl}` }]\n}\n\n/**\n * Build the content blocks for a single product image. Returns the Blob URL text floor in EVERY\n * case; adds an ImageContent block ONLY when the URL is an allowlisted-host https Blob URL that\n * fetches successfully within the byte cap + timeout as a real image. NEVER throws, NEVER fetches\n * a non-allowlisted / data: / non-https URL.\n */\nexport async function buildImageContent(\n blobUrl: string,\n opts: BuildImageOpts = {}\n): Promise<ImageBlock[]> {\n if (typeof blobUrl !== \"string\" || blobUrl.trim().length === 0) {\n return urlOnly(String(blobUrl ?? \"\"))\n }\n\n // (1) + (2): host allowlist + https-only, BEFORE any network call (G9 / rule 6).\n // isAllowedImageHost already returns false for an unparseable URL; we also require https.\n let scheme: string\n try {\n scheme = new URL(blobUrl).protocol\n } catch {\n return urlOnly(blobUrl)\n }\n if (scheme !== \"https:\") {\n return urlOnly(blobUrl) // data:, http:, etc. — never fetch (no SSRF, no plaintext, no inline payload)\n }\n if (!isAllowedImageHost(blobUrl)) {\n return urlOnly(blobUrl) // non-allowlisted host or look-alike subdomain — never fetch\n }\n\n const fetchImpl = opts.fetchImpl ?? (globalThis.fetch as typeof fetch)\n const byteCap = opts.byteCap ?? IMAGE_BYTE_CAP\n const timeoutMs = opts.timeoutMs ?? IMAGE_FETCH_TIMEOUT_MS\n\n // (3) + (4): the gated fetch. Any failure → URL-text floor (never throws — F-A3).\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const res = await fetchImpl(blobUrl as unknown as string, { signal: controller.signal })\n if (!res.ok) {\n return urlOnly(blobUrl)\n }\n const mimeType = (res.headers.get(\"content-type\") ?? \"\").split(\";\")[0]!.trim().toLowerCase()\n if (!mimeType.startsWith(\"image/\")) {\n return urlOnly(blobUrl) // refuse to base64 a non-image payload\n }\n // (M4) Reject an over-cap body BEFORE buffering it into memory, when the server declares its size.\n // The post-buffer check below still applies (Content-Length can be absent or wrong) — this just\n // avoids reading a known-too-large body at all.\n const declaredLen = Number.parseInt(res.headers.get(\"content-length\") ?? \"\", 10)\n if (Number.isFinite(declaredLen) && declaredLen > byteCap) {\n return urlOnly(blobUrl)\n }\n const buf = Buffer.from(await res.arrayBuffer())\n if (buf.byteLength === 0 || buf.byteLength > byteCap) {\n return urlOnly(blobUrl) // empty or over-cap → token-DoS guard\n }\n return [\n { type: \"image\", data: buf.toString(\"base64\"), mimeType },\n { type: \"text\", text: blobUrl }, // the reliable floor (bug #31208)\n ]\n } catch {\n return unavailable(blobUrl)\n } finally {\n clearTimeout(timer)\n }\n}\n","// MCP egg copy (VOICE §3.2). The MCP surface is read by AGENTS and the humans driving them, so its\n// eggs live in tool descriptions (devs read schemas) and in deterministic tool payloads. They NEVER\n// interfere with a real tool and never touch money.\n//\n// This mirrors the CLI's governed copy (packages/cli/src/eggs.ts). It is intentionally duplicated:\n// the MCP package composes over store-client only (not the CLI), so each channel owns its copy.\n// Brand voice (memory brand-voice-no-pod-no-tshirt): never \"t-shirt\"/\"tee\"/\"print\"/Gelato.\n\n/** M2 ★ — the bt.teapot tool body. Returns 418. Always. It's a teapot. (RFC 2324) */\nexport const TEAPOT_TEXT = \"418 i'm a teapot. we make clothing, not coffee. (RFC 2324)\"\n\n/** C10 / M7 — the shared fortune pool. REAL dev aphorisms only (gate 1: never invented). */\nexport const FORTUNES: ReadonlyArray<string> = [\n '// \"there are only two hard things in computer science: cache invalidation and naming things.\" — phil karlton',\n '// \"premature optimization is the root of all evil.\" — donald knuth',\n '// \"weeks of programming can save you hours of planning.\"',\n '// \"make it work, make it right, make it fast.\" — kent beck',\n '// \"talk is cheap. show me the code.\" — linus torvalds',\n '// \"given enough eyeballs, all bugs are shallow.\" — linus\\'s law',\n '// \"always code as if the person maintaining it is a violent psychopath who knows where you live.\" — john woods',\n '// \"it works on my machine.\"',\n]\n\n/** Deterministic fortune by seed (reproducible for tests + screenshots). */\nexport function fortune(seed = 0): string {\n const n = Number.isFinite(seed) ? Math.abs(Math.floor(seed)) : 0\n return FORTUNES[n % FORTUNES.length]!\n}\n","// Resolve the MCP server's transport config from the environment, falling back to the baked-in\n// PUBLIC production defaults so a host MCP config needs ZERO env to reach the live store\n// (d2 / P4.5-7b). Env ALWAYS wins (set BT_STORE_URL / BT_PUBLISHABLE_KEY for local dev / staging).\n// The JWT (BT_TOKEN) and region (BT_REGION) stay env-only — never baked, never read from chat (T8).\n\nimport { BT_DEFAULT_STORE_URL, BT_DEFAULT_PUBLISHABLE_KEY } from \"@binaryandthread/store-client\"\n\nexport interface ResolvedServerConfig {\n baseUrl: string\n publishableKey: string\n jwt?: string\n region?: string\n}\n\nexport function resolveServerConfig(env: NodeJS.ProcessEnv = process.env): ResolvedServerConfig {\n return {\n baseUrl: env.BT_STORE_URL || BT_DEFAULT_STORE_URL,\n publishableKey: env.BT_PUBLISHABLE_KEY || BT_DEFAULT_PUBLISHABLE_KEY,\n jwt: env.BT_TOKEN || undefined,\n region: env.BT_REGION || undefined,\n }\n}\n","// npx @binaryandthread/mcp — the stdio entrypoint.\n//\n// Config comes from the ENVIRONMENT only (SECURITY T8/§4); all of it has a baked-in default so\n// the server reaches the live store with ZERO env (d2 / P4.5-7b):\n// - BT_STORE_URL the Medusa Store API base URL (default: prod; override for dev)\n// - BT_PUBLISHABLE_KEY the publishable API key — PUBLIC by design (default: prod; T6)\n// - BT_TOKEN the customer JWT (OPTIONAL; from the OS keychain / a configured env)\n// - BT_REGION an ISO-2 country code to resolve pricing (OPTIONAL; never hardcoded)\n//\n// The JWT is read from the env the host MCP config injects (which a keychain helper can populate)\n// — it is NEVER read from the chat (T8). A secret key is rejected by the StoreClient (T6). On\n// stdio, NOTHING is written to stdout except MCP protocol frames — diagnostics go to stderr.\n\nimport { StoreClient } from \"@binaryandthread/store-client\"\nimport { redact } from \"@binaryandthread/store-client\"\nimport { startServer } from \"./server\"\nimport { makeTools } from \"./tools\"\nimport { resolveServerConfig } from \"./config\"\n\nasync function main(): Promise<void> {\n const { baseUrl, publishableKey, jwt, region } = resolveServerConfig()\n\n const client = new StoreClient({\n baseUrl,\n publishableKey,\n jwt,\n // All client logs route to stderr through redact() — stdout is reserved for the transport,\n // and no token/PII ever reaches a log line (G10).\n logger: {\n info: (...a) => process.stderr.write(JSON.stringify(redact(a)) + \"\\n\"),\n warn: (...a) => process.stderr.write(JSON.stringify(redact(a)) + \"\\n\"),\n error: (...a) => process.stderr.write(JSON.stringify(redact(a)) + \"\\n\"),\n },\n })\n\n const tools = makeTools(client, { region })\n await startServer(tools)\n process.stderr.write(`bt-mcp: ready on stdio — ${tools.length} tools registered.\\n`)\n}\n\nmain().catch((e) => {\n // Redact before printing — never leak a token in a crash trace.\n process.stderr.write(`bt-mcp: fatal: ${JSON.stringify(redact(String(e?.message ?? e)))}\\n`)\n process.exit(1)\n})\n"],"mappings":"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","names":["out","stage"]}
1
+ {"version":3,"sources":["../../store-client/src/redact.ts","../../store-client/src/client.ts","../../store-client/src/defaults.ts","../../store-client/src/host-allowlist.ts","../../store-client/src/colour.ts","../../store-client/src/product.ts","../../store-client/src/tax.ts","../src/server.ts","../src/version.ts","../src/onboarding.ts","../src/tools.ts","../src/sanitize.ts","../src/images.ts","../src/eggs.ts","../src/config.ts","../src/bin.ts"],"sourcesContent":["// Secret/PII redaction for logs and error envelopes (SECURITY G10).\n//\n// The single rule for the whole channel: Authorization headers, tokens, and full\n// emails NEVER reach a log line, an error message, or an MCP/CLI transcript. This\n// function is applied by the StoreClient's logger and error mapper before anything\n// is emitted. It is consumed by BOTH the CLI and (Wave 4) the MCP — one redactor.\n\nconst SENSITIVE_KEYS = new Set([\n \"authorization\",\n \"token\",\n \"access_token\",\n \"refresh_token\",\n \"jwt\",\n \"idempotency-key\",\n \"x-publishable-api-key\",\n \"password\",\n \"secret\",\n \"cookie\",\n \"set-cookie\",\n])\n\nconst MASK = \"[REDACTED]\"\n\n// Bearer-token-in-a-string (e.g. \"Authorization: Bearer abc.def.ghi\" or a stray \"Bearer xyz\").\nconst BEARER_RE = /Bearer\\s+[A-Za-z0-9._\\-]+/gi\n// A plausible email anywhere in a string.\nconst EMAIL_RE = /([^\\s@]+)@([^\\s@]+\\.[^\\s@]+)/g\n\n/** Mask an email to `d***@x` form (first char + stars, host first char). */\nexport function maskEmail(email: string): string {\n const at = email.indexOf(\"@\")\n if (at <= 0) {\n return email\n }\n const local = email.slice(0, at)\n const host = email.slice(at + 1)\n const localMasked = local[0] + \"*\".repeat(Math.max(2, local.length - 1))\n const hostMasked = host.length > 0 ? host[0] : \"x\"\n return `${localMasked}@${hostMasked}`\n}\n\nfunction redactString(s: string): string {\n return s.replace(BEARER_RE, \"Bearer [REDACTED]\").replace(EMAIL_RE, (_m, _l, _h) =>\n maskEmail(`${_l}@${_h}`)\n )\n}\n\n/**\n * Deep-redact an object/array/string. Returns a structurally-similar value with\n * sensitive keys masked, Bearer tokens stripped, and emails masked. Never throws.\n *\n * `_seen` memoises input-object → its REDACTED output. This does double duty: it makes the\n * function cycle-safe (a revisited object resolves to its own — possibly still-filling — output\n * container) AND ensures a REPEATED reference is redacted every time. The previous WeakSet guard\n * returned the *original* object on revisit, leaking any token/email reachable via a 2nd path (G10).\n * The output container is registered BEFORE recursing so a cycle points at the redacted copy.\n */\nexport function redact<T>(input: T, _seen?: Map<object, unknown>): T {\n if (input == null) {\n return input\n }\n if (typeof input === \"string\") {\n return redactString(input) as unknown as T\n }\n if (typeof input !== \"object\") {\n return input\n }\n\n const seen = _seen ?? new Map<object, unknown>()\n const existing = seen.get(input as object)\n if (existing !== undefined) {\n return existing as T\n }\n\n if (Array.isArray(input)) {\n const out: unknown[] = []\n seen.set(input as object, out) // register before recursing (cycle-safe)\n for (const v of input) {\n out.push(redact(v, seen))\n }\n return out as unknown as T\n }\n\n const out: Record<string, unknown> = {}\n seen.set(input as object, out) // register before recursing (cycle-safe)\n for (const [key, value] of Object.entries(input as Record<string, unknown>)) {\n if (SENSITIVE_KEYS.has(key.toLowerCase())) {\n out[key] = MASK\n } else if (key.toLowerCase() === \"email\" && typeof value === \"string\") {\n out[key] = maskEmail(value)\n } else {\n out[key] = redact(value, seen)\n }\n }\n return out as unknown as T\n}\n","// StoreClient — the ONE shared HTTP client over the B&T Medusa Store API.\n//\n// Design (ROBUSTNESS §0): the clients are DUMB. Every dangerous property lives in the\n// backend. This module is the single place the channel's HTTP-client invariants live so\n// they are written once and contract-tested once (the CLI and the Wave-4 MCP both use it):\n//\n// - publishable key on EVERY request (public by design, T6); customer JWT when present\n// - Idempotency-Key (crypto.randomUUID) on EVERY mutation, STABLE across a retry — honoured by\n// the checkout MINT (per-cart session reuse); the core cart/line-item routes do NOT yet honour\n// it, so that guarantee needs a backend idempotency middleware (BACKLOG) (§2)\n// - retry 5xx + network errors with jittered backoff; NEVER retry a 4xx (F-A1/F-I2)\n// - region resolved before pricing — never a bare currency literal (F-A2/INV-11)\n// - 404/409/cart.completed on a cart mutation → RecoverableCartError (CLI makes a fresh cart) (F-B2)\n// - all logs + error envelopes pass through redact() — no token/email ever leaks (G10)\n// - NO secret key is ever stored or sent — publishable key only (T6)\n\nimport { redact } from \"./redact\"\nimport type {\n CartLike,\n ClientLogger,\n DropRow,\n HeadlessCheckout,\n OrderSummary,\n PollResult,\n ProductLike,\n RegionLike,\n RegionResolution,\n ReportResult,\n RequestLinkResult,\n RetryPolicy,\n ShippingOptionLike,\n StoreClientOptions,\n VerifyLinkResult,\n WhoAmI,\n} from \"./types\"\n\nconst DEFAULT_RETRY: RetryPolicy = {\n attempts: 3,\n baseDelayMs: 200,\n maxDelayMs: 2000,\n}\n\n/** Cap on how long we'll honor a server's Retry-After, so a hostile/huge value can't hang us. */\nconst RETRY_AFTER_MAX_MS = 10_000\n\n/**\n * Per-attempt request timeout. A black-holed connection (socket accepted, no response) would\n * otherwise never settle, hanging every command/tool AND defeating the checkout poll's 20-min\n * deadline (F-I3) — the deadline can't be re-checked while stuck awaiting a dead fetch. 15s is\n * generous for the Store API while still failing closed on a hang.\n */\nconst DEFAULT_TIMEOUT_MS = 15_000\n\n/**\n * Extra fields to request on product reads. `+metadata` ADDS metadata to Medusa's default field set\n * (it doesn't replace it — variants + calculated_price still come through). metadata is where lore /\n * lore_sources / per-product spec fields live, and the Store API omits it unless asked. The `+` is\n * URL-encoded to %2B by URLSearchParams, which Medusa decodes back to `+metadata`.\n */\nconst PRODUCT_FIELDS = \"+metadata\"\n\n/**\n * Parse an HTTP Retry-After header → ms, or null if absent/unparseable. Accepts both forms:\n * delta-seconds (\"2\") and an HTTP-date. Capped at RETRY_AFTER_MAX_MS (d12).\n */\nfunction parseRetryAfterMs(res: Response): number | null {\n let raw: string | null = null\n try {\n raw = res.headers?.get?.(\"retry-after\") ?? null\n } catch {\n return null\n }\n if (!raw) {\n return null\n }\n const secs = Number(raw)\n let ms: number\n if (Number.isFinite(secs)) {\n ms = secs * 1000\n } else {\n const dateMs = Date.parse(raw)\n if (!Number.isFinite(dateMs)) {\n return null\n }\n ms = dateMs - Date.now()\n }\n return Math.min(Math.max(0, ms), RETRY_AFTER_MAX_MS)\n}\n\n/** A cart error the CLI maps to \"discard local cart_id, create a fresh cart, re-add\" (F-B2). */\nexport class RecoverableCartError extends Error {\n readonly recoverable = true as const\n readonly status: number\n constructor(message: string, status: number) {\n super(message)\n this.name = \"RecoverableCartError\"\n this.status = status\n }\n}\n\n/** A non-recoverable API error. Its message/envelope is already redacted (G10). */\nexport class StoreApiError extends Error {\n readonly status: number\n readonly path: string\n constructor(message: string, status: number, path: string) {\n super(message)\n this.name = \"StoreApiError\"\n this.status = status\n this.path = path\n }\n}\n\ntype HttpMethod = \"GET\" | \"POST\" | \"DELETE\"\n\ninterface RequestOpts {\n body?: unknown\n /** Mutations carry an Idempotency-Key. */\n idempotent?: boolean\n /** Treat a 404/409/cart.completed as a RecoverableCartError (cart mutations). */\n cartRecovery?: boolean\n query?: Record<string, string | number | undefined>\n /**\n * Statuses for which a 4xx is EXPECTED, not an error — the client_error envelope is NOT\n * logged (BUG 3: a guest whoami 401 is the normal unauthenticated path, not noise to dump).\n * The error is still thrown so the caller can branch on it.\n */\n expected4xx?: number[]\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/** crypto.randomUUID with a defensive fallback (older runtimes). */\nfunction newUuid(): string {\n const g = globalThis as unknown as { crypto?: { randomUUID?: () => string } }\n if (g.crypto?.randomUUID) {\n return g.crypto.randomUUID()\n }\n // RFC4122-ish fallback — never the happy path, just defensive.\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n const v = c === \"x\" ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n}\n\nexport class StoreClient {\n private readonly baseUrl: string\n private readonly publishableKey: string\n private readonly jwt?: string\n private readonly fetchImpl: typeof fetch\n private readonly logger: ClientLogger\n private readonly retry: RetryPolicy\n private readonly sleepImpl: (ms: number) => Promise<void>\n private readonly timeoutMs: number\n\n /** Cached resolved regions (read-only catalog optimisation). */\n private regionsCache: RegionLike[] | null = null\n\n constructor(opts: StoreClientOptions) {\n if (!opts.baseUrl) {\n throw new Error(\"StoreClient: baseUrl is required\")\n }\n if (!opts.publishableKey) {\n throw new Error(\"StoreClient: publishableKey is required\")\n }\n // Guard against accidentally shipping a secret key (T6 — defensive).\n if (/^sk_/.test(opts.publishableKey)) {\n throw new Error(\"StoreClient: refusing a secret key — the client carries only the publishable key (T6)\")\n }\n this.baseUrl = opts.baseUrl.replace(/\\/+$/, \"\")\n this.publishableKey = opts.publishableKey\n this.jwt = opts.jwt\n this.fetchImpl = opts.fetchImpl ?? (globalThis.fetch as typeof fetch)\n this.logger = opts.logger ?? console\n this.retry = { ...DEFAULT_RETRY, ...(opts.retry ?? {}) }\n this.sleepImpl = opts.sleepImpl ?? sleep\n this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS\n }\n\n // ----- the request engine --------------------------------------------------\n\n private buildUrl(path: string, query?: RequestOpts[\"query\"]): string {\n const url = new URL(this.baseUrl + path)\n if (query) {\n for (const [k, v] of Object.entries(query)) {\n if (v !== undefined && v !== null && v !== \"\") {\n url.searchParams.set(k, String(v))\n }\n }\n }\n return url.toString()\n }\n\n private baseHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"x-publishable-api-key\": this.publishableKey,\n }\n if (this.jwt) {\n headers[\"Authorization\"] = `Bearer ${this.jwt}`\n }\n return headers\n }\n\n private backoffMs(attemptIndex: number): number {\n // exponential base with full jitter, capped.\n const exp = this.retry.baseDelayMs * 2 ** attemptIndex\n const capped = Math.min(exp, this.retry.maxDelayMs)\n return Math.floor(Math.random() * (capped + 1))\n }\n\n private async request<T>(method: HttpMethod, path: string, opts: RequestOpts = {}): Promise<T> {\n const headers = this.baseHeaders()\n // Generate the idempotency key ONCE and reuse it across retries (ROBUSTNESS §2). This only\n // prevents a duplicate when the SERVER honours Idempotency-Key: the checkout MINT does (it\n // reuses the open per-cart session), but the core Medusa cart/line-item routes do NOT yet — so a\n // retried createCart/addLineItem after a committed-but-lost response can still double-apply. The\n // real fix is a backend idempotency middleware (BACKLOG); until then a double-add is at least\n // VISIBLE (the CLI review + MCP checkout total show the real cart before any charge).\n if (opts.idempotent) {\n headers[\"Idempotency-Key\"] = newUuid()\n }\n const url = this.buildUrl(path, opts.query)\n const init: RequestInit = {\n method,\n headers,\n ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),\n }\n\n let lastErr: unknown\n for (let attempt = 0; attempt < this.retry.attempts; attempt++) {\n // A server-dictated delay for THIS attempt's backoff (429 Retry-After); null → jittered backoff.\n let retryAfterMs: number | null = null\n // Bound THIS attempt: a hung connection is aborted after timeoutMs. The AbortError lands in\n // the catch below and is treated as a retryable network error (so the backoff + retry, and\n // ultimately the exhausted-retries StoreApiError, all apply). Fresh controller per attempt.\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), this.timeoutMs)\n try {\n const res = await this.fetchImpl(url as unknown as string, { ...init, signal: controller.signal })\n if (res.ok) {\n // 204 / empty body tolerance\n const text = await res.text()\n return (text ? JSON.parse(text) : {}) as T\n }\n\n // Read the error body defensively (may not be JSON).\n let parsed: Record<string, unknown> = {}\n try {\n const t = await res.text()\n parsed = t ? (JSON.parse(t) as Record<string, unknown>) : {}\n } catch {\n parsed = {}\n }\n\n // Cart recovery: a stale/completed cart on a cart mutation (F-B2).\n if (\n opts.cartRecovery &&\n (res.status === 404 ||\n res.status === 409 ||\n String(parsed.type ?? \"\").includes(\"cart.completed\") ||\n /cart.*complete/i.test(String(parsed.message ?? \"\")))\n ) {\n throw new RecoverableCartError(\n `cart is stale or completed (HTTP ${res.status})`,\n res.status\n )\n }\n\n // 429 Too Many Requests — RETRYABLE, honoring Retry-After (d12). Must be handled BEFORE\n // the generic 4xx-no-retry rule below, or a rate limit becomes a hard failure.\n if (res.status === 429) {\n retryAfterMs = parseRetryAfterMs(res)\n lastErr = new StoreApiError(`rate limited (HTTP 429)`, 429, path)\n this.logWarn(\"store.request.rate_limited\", {\n path,\n attempt: attempt + 1,\n retryAfterMs,\n })\n } else if (res.status >= 400 && res.status < 500) {\n // other 4xx is a client error — do NOT retry.\n // Suppress the envelope for EXPECTED 4xx (e.g. a guest whoami 401 — BUG 3): it's a\n // normal branch the caller handles, not noise to dump to the user's terminal.\n if (!(opts.expected4xx ?? []).includes(res.status)) {\n this.logError(\"store.request.client_error\", { path, status: res.status, body: parsed })\n }\n throw new StoreApiError(\n `request failed (HTTP ${res.status})`,\n res.status,\n path\n )\n } else {\n // 5xx — retryable. Fall through to the retry loop.\n lastErr = new StoreApiError(`upstream error (HTTP ${res.status})`, res.status, path)\n this.logWarn(\"store.request.retryable\", { path, status: res.status, attempt: attempt + 1 })\n }\n } catch (e) {\n // RecoverableCartError / StoreApiError(4xx) must propagate immediately.\n if (e instanceof RecoverableCartError) {\n throw e\n }\n if (e instanceof StoreApiError && e.status >= 400 && e.status < 500) {\n throw e\n }\n // Network error, an aborted (timed-out) request, or a 5xx StoreApiError → retryable.\n lastErr = e\n this.logWarn(\"store.request.network_retry\", { path, attempt: attempt + 1 })\n } finally {\n // Always clear the per-attempt timeout — on success-return, on a rethrow, or before a retry.\n clearTimeout(timer)\n }\n\n // Backoff before the next attempt (skip after the final attempt). A 429 Retry-After, when\n // present, dictates the delay; otherwise jittered exponential backoff (d12).\n if (attempt < this.retry.attempts - 1) {\n await this.sleepImpl(retryAfterMs ?? this.backoffMs(attempt))\n }\n }\n\n // Exhausted retries. The thrown envelope is already free of secrets (we only ever\n // log/throw redacted, derived data — never the request headers/body).\n this.logError(\"store.request.exhausted\", { path, attempts: this.retry.attempts })\n if (lastErr instanceof StoreApiError) {\n throw lastErr\n }\n throw new StoreApiError(\n `request failed after ${this.retry.attempts} attempts: ${path}`,\n 0,\n path\n )\n }\n\n private logWarn(event: string, ctx: Record<string, unknown>): void {\n this.logger.warn(event, redact(ctx))\n }\n\n private logError(event: string, ctx: Record<string, unknown>): void {\n this.logger.error(event, redact(ctx))\n }\n\n // ----- regions (F-A2 / INV-11) ---------------------------------------------\n\n async getRegions(): Promise<RegionLike[]> {\n const data = await this.request<{ regions: RegionLike[] }>(\"GET\", \"/store/regions\")\n this.regionsCache = data.regions ?? []\n return this.regionsCache\n }\n\n /**\n * Resolve a region by country code, WITH provenance (matched / home / requested), so callers on\n * the money path can fail closed on an unmatched explicit country instead of silently pricing in\n * the wrong currency (F-A2). Region #1 is the launch/home default — SEK today, derived, never a\n * hardcoded literal.\n *\n * Semantics:\n * - country given AND a region ships there → { matched: true, home: false }.\n * - country given, none ships there → { matched: false, home: false } (money surfaces MUST\n * refuse; discovery may show the home region + a warning). We still return a region so a\n * discovery command can render *something*, but `matched:false` is the signal to fail closed.\n * - no country given → { matched: false, home: true } (the legitimate\n * launch default — not a \"wrong\" currency).\n */\n async resolveRegionResult(countryCode?: string): Promise<RegionResolution> {\n const regions = this.regionsCache ?? (await this.getRegions())\n if (regions.length === 0) {\n // Fail closed: refuse to price without a resolved region (F-A2).\n throw new Error(\"no regions configured — cannot resolve a region for pricing\")\n }\n const home = regions[0]! // region #1 — the launch region (SEK today, derived not hardcoded)\n if (countryCode) {\n const cc = countryCode.toLowerCase()\n const match = regions.find((r) =>\n (r.countries ?? []).some((c) => (c.iso_2 ?? \"\").toLowerCase() === cc)\n )\n if (match) {\n return { region: match, matched: true, requested: cc, home: false }\n }\n // A country code was given but no region ships there. Warn (stderr, redacted) so the\n // mis-resolution is observable, and report matched:false so money surfaces fail closed.\n this.logWarn(\"store.region.unmatched\", { countryCode: cc, fallback: home.id })\n return { region: home, matched: false, requested: cc, home: false }\n }\n return { region: home, matched: false, requested: undefined, home: true }\n }\n\n /**\n * Resolve a region by country code, falling back to region #1. A thin wrapper over\n * resolveRegionResult for callers that only need the region (back-compat).\n * SEK is region #1 by configuration — NEVER a hardcoded literal.\n */\n async resolveRegion(countryCode?: string): Promise<RegionLike> {\n return (await this.resolveRegionResult(countryCode)).region\n }\n\n // ----- products (region resolved BEFORE pricing) ---------------------------\n\n async listProducts(opts?: { region?: RegionLike; countryCode?: string }): Promise<ProductLike[]> {\n const region = opts?.region ?? (await this.resolveRegion(opts?.countryCode))\n // Paginate (L12): a single limit:100 call silently drops the 101st product. Page through until a\n // short page, with a hard cap so a misbehaving API can never spin this unbounded.\n const PAGE = 100\n const HARD_CAP = 1000\n const all: ProductLike[] = []\n for (let offset = 0; offset < HARD_CAP; offset += PAGE) {\n const data = await this.request<{ products: ProductLike[] }>(\"GET\", \"/store/products\", {\n // +metadata: Medusa omits product.metadata by default, and that's where lore/lore_sources\n // (and any per-product spec fields) live — without this the CLI/MCP never see them.\n query: { region_id: region.id, limit: PAGE, offset, fields: PRODUCT_FIELDS },\n })\n const page = data.products ?? []\n all.push(...page)\n if (page.length < PAGE) {\n break\n }\n }\n return all\n }\n\n async getProduct(handle: string, opts?: { region?: RegionLike; countryCode?: string }): Promise<ProductLike | null> {\n const region = opts?.region ?? (await this.resolveRegion(opts?.countryCode))\n const data = await this.request<{ products: ProductLike[] }>(\"GET\", \"/store/products\", {\n query: { handle, region_id: region.id, limit: 1, fields: PRODUCT_FIELDS },\n })\n return (data.products ?? [])[0] ?? null\n }\n\n // ----- cart (mutations carry Idempotency-Key) ------------------------------\n\n async createCart(region: RegionLike): Promise<CartLike> {\n const data = await this.request<{ cart: CartLike }>(\"POST\", \"/store/carts\", {\n idempotent: true,\n body: { region_id: region.id },\n })\n return data.cart\n }\n\n async addLineItem(cartId: string, variantId: string, quantity: number): Promise<CartLike> {\n const data = await this.request<{ cart: CartLike }>(\n \"POST\",\n `/store/carts/${encodeURIComponent(cartId)}/line-items`,\n {\n idempotent: true,\n cartRecovery: true,\n body: { variant_id: variantId, quantity },\n }\n )\n return data.cart\n }\n\n async getCart(cartId: string): Promise<CartLike> {\n const data = await this.request<{ cart: CartLike }>(\n \"GET\",\n `/store/carts/${encodeURIComponent(cartId)}`,\n { cartRecovery: true }\n )\n return data.cart\n }\n\n /** Remove a staged line item (bt unstage). Returns the updated cart (Medusa's `parent`). */\n async removeLineItem(cartId: string, itemId: string): Promise<CartLike> {\n const data = await this.request<{ parent?: CartLike; cart?: CartLike }>(\n \"DELETE\",\n `/store/carts/${encodeURIComponent(cartId)}/line-items/${encodeURIComponent(itemId)}`,\n { idempotent: true, cartRecovery: true }\n )\n return (data.parent ?? data.cart) as CartLike\n }\n\n /**\n * Ensure the cart has a shipping method before checkout (BUG 1 — the headless\n * /store/headless/checkout route guards on it, and neither client ever set one).\n *\n * - Idempotent: if the cart already carries a shipping method, no-op (no POST).\n * - Lists options via the STANDARD store route GET /store/shipping-options?cart_id=…\n * (NOT a /headless route — standard routes are always available, no kill-switch).\n * - Picks deterministically: exactly one → use it; several → the cheapest by amount.\n * - The SE launch option is region-scoped (price_type:\"flat\"), so listing by cart_id\n * returns it without a shipping address being set first (confirmed: seed-shipping.ts).\n *\n * TODO(04.5): buyer-choice / intl multiple shipping options — today we auto-pick the\n * cheapest; an explicit buyer chooser belongs to the Phase-6 international pass.\n */\n async ensureCartShippingMethod(cartId: string): Promise<CartLike> {\n // 1. read the authoritative cart; a stale/completed cart → RecoverableCartError (F-B2).\n const cart = await this.getCart(cartId)\n if (cart.shipping_methods && cart.shipping_methods.length > 0) {\n // idempotent: already satisfied.\n return cart\n }\n\n // 2. list options via the STANDARD store route (always available, no kill-switch).\n const data = await this.request<{ shipping_options: ShippingOptionLike[] }>(\n \"GET\",\n \"/store/shipping-options\",\n { query: { cart_id: cartId } }\n )\n const options = data.shipping_options ?? []\n if (options.length === 0) {\n // fail closed: never mint a checkout we know the backend will reject (BUG 1).\n throw new Error(\n \"no shipping option is available for this cart — cannot attach a shipping method\"\n )\n }\n\n // 3. pick deterministically: one → it; several → the cheapest by amount.\n const chosen =\n options.length === 1\n ? options[0]!\n : [...options].sort(\n (a, b) => (a.amount ?? Number.POSITIVE_INFINITY) - (b.amount ?? Number.POSITIVE_INFINITY)\n )[0]!\n\n // 4. attach it — a mutation, so it carries an Idempotency-Key + cart-recovery.\n const res = await this.request<{ cart: CartLike }>(\n \"POST\",\n `/store/carts/${encodeURIComponent(cartId)}/shipping-methods`,\n {\n idempotent: true,\n cartRecovery: true,\n body: { option_id: chosen.id },\n }\n )\n return res.cart\n }\n\n // ----- headless contract (04.5-01) -----------------------------------------\n\n // `channel` is REQUIRED (L11): a default silently mis-attributes analytics for a future caller that\n // forgets it. Both current callers (cli, mcp) pass it explicitly.\n async mintCheckout(cartId: string, email: string | undefined, channel: string): Promise<HeadlessCheckout> {\n return this.request<HeadlessCheckout>(\"POST\", \"/store/headless/checkout\", {\n idempotent: true,\n body: { cart_id: cartId, email, channel },\n })\n }\n\n async poll(sessionId: string): Promise<PollResult> {\n return this.request<PollResult>(\n \"GET\",\n `/store/headless/poll/${encodeURIComponent(sessionId)}`\n )\n }\n\n /** JWT-bound: returns the authenticated customer's own profile (G2). */\n async whoami(): Promise<WhoAmI> {\n // A guest (no/expired JWT) gets a 401/403 — that's the EXPECTED unauthenticated branch the\n // CLI turns into the friendly \"browsing as a guest\" line, NOT an error to dump (BUG 3).\n return this.request<WhoAmI>(\"GET\", \"/store/headless/whoami\", { expected4xx: [401, 403] })\n }\n\n // ----- magic-link auth (04.5-05) -------------------------------------------\n // Passwordless login — magic-link is the only grant (Medusa has no OAuth pairing-grant, G1).\n // request-link emails a single-use link; verify exchanges the link's token for a customer-scoped\n // JWT. The JWT is stored in the OS keychain by the CLI — never here, never in plaintext config (G7/G8).\n\n /** Start a magic-link login. Returns a UNIFORM ack regardless of whether the email exists (G12). */\n async requestMagicLink(email: string): Promise<RequestLinkResult> {\n return this.request<RequestLinkResult>(\"POST\", \"/store/headless/auth/request-link\", {\n idempotent: true,\n body: { email },\n })\n }\n\n /** Exchange a magic-link token for a customer-scoped JWT (the customer is materialized server-side). */\n async verifyMagicLink(token: string): Promise<VerifyLinkResult> {\n return this.request<VerifyLinkResult>(\"POST\", \"/store/headless/auth/verify\", {\n idempotent: true,\n body: { token },\n })\n }\n\n async listDrops(): Promise<DropRow[]> {\n const data = await this.request<{ drops: DropRow[]; count: number }>(\n \"GET\",\n \"/store/headless/drops\"\n )\n return data.drops ?? []\n }\n\n /**\n * File a developer bug report (d14 — `bt report` / MCP report_issue). Returns a correlation\n * reference. Additive: no money/cart path. The backend redacts authoritatively; callers SHOULD\n * pass an already-redacted context (the doctor snapshot). A mutation → carries an Idempotency-Key\n * so a retry can't email the report twice.\n */\n async report(message: string, context?: Record<string, unknown>): Promise<ReportResult> {\n return this.request<ReportResult>(\"POST\", \"/store/headless/report\", {\n idempotent: true,\n body: { message, ...(context !== undefined ? { context } : {}) },\n })\n }\n\n /**\n * The authenticated customer's order history (d13 — `bt log`). JWT-bound; the backend derives the\n * customer from the bearer token and returns MINIMAL fields (status/timeline only — never\n * email/address, F-H3). A guest (no/expired JWT) gets a 401 — the EXPECTED unauthenticated branch\n * we turn into the honest empty history (`[]`), NOT an error to dump (BUG 3).\n */\n async listOrders(): Promise<OrderSummary[]> {\n try {\n const data = await this.request<{ orders: OrderSummary[] }>(\"GET\", \"/store/headless/orders\", {\n expected4xx: [401, 403],\n })\n return data.orders ?? []\n } catch (e) {\n if (e instanceof StoreApiError && (e.status === 401 || e.status === 403)) {\n return []\n }\n throw e\n }\n }\n\n /**\n * Guest order lookup (AUD-8) — POST /store/order-lookup with the order id (display id OR internal\n * id) + the EXACT email in the BODY (never the query string, so the email is not access-logged).\n * The backend returns 404 on any mismatch (no enumeration) → the client surfaces null. Rate-limited\n * server-side; returns minimal status only, never PII (F-H1 / F-H3).\n */\n async getOrder(idOrDisplayId: string | number, email: string): Promise<unknown | null> {\n try {\n return await this.request<unknown>(\"POST\", \"/store/order-lookup\", {\n body: { id: String(idOrDisplayId), email },\n })\n } catch (e) {\n if (e instanceof StoreApiError && e.status === 404) {\n return null\n }\n throw e\n }\n }\n}\n","// Baked-in PUBLIC defaults for the Binary & Thread production channel.\n//\n// These let `npx @binaryandthread/cli` and the MCP server reach the live store with ZERO\n// configuration — the point of the soft-launch tarball (P4.5-7b) and decision #79 (d2).\n// store-client is bundled into both the CLI and the MCP server (tsup `noExternal`), so this\n// is the single source of truth for \"which store\" both clients talk to by default.\n//\n// SAFETY (CLAUDE.md rule 1 / SECURITY T6): the publishable key is PUBLIC BY DESIGN. It is the\n// SAME key embedded in the public storefront's client bundle (binaryandthread.com) and carries\n// no privilege beyond read + cart over the Store API — every dangerous operation lives behind\n// the backend. It is therefore NOT a secret; rule 1 governs secrets, and this is deliberately\n// published. Any env var (BT_API_URL / BT_STORE_URL / BT_PUBLISHABLE_KEY / MEDUSA_*) OVERRIDES\n// these for local dev or staging.\n\n/** The Binary & Thread production Medusa Store API base URL. */\nexport const BT_DEFAULT_STORE_URL = \"https://backend-production-511f5.up.railway.app\"\n\n// The PUBLIC Medusa publishable key (read + cart only; safe to ship — T6). Identical to the key\n// served in the public storefront bundle. NOT a secret (CLAUDE.md rule 1); the inline allow keeps\n// the gitleaks pre-commit hook from flagging a deliberately-published value.\nexport const BT_DEFAULT_PUBLISHABLE_KEY = \"pk_0d7d706860568a0414dff0436aadb9713a2609de3a1b153ef2a6f9694d23f907\" // gitleaks:allow\n","// Image-host allowlist — the canonical source for the WHOLE channel (CLAUDE.md rule 6 / SECURITY G9).\n//\n// We fetch product images ONLY from our own Vercel Blob store. Both the bt CLI (its terminal\n// image ladder) and the Wave-4 MCP (its server-side ImageContent fetch) gate every outbound\n// image fetch on this allowlist. It lives HERE in the shared client — not in the CLI — so the\n// MCP does not take a runtime dependency on the CLI package; the CLI re-exports it from here.\n//\n// HISTORY: a bare `endsWith(allowed)` suffix match was a CRITICAL allowlist-bypass bug fixed in\n// Wave 2 (it would admit `evilblob.vercel-storage.com`). The dot-delimited-subdomain check below\n// is load-bearing security — `host === allowed || host.endsWith(\".\" + allowed)`, never a bare\n// suffix. Do NOT \"simplify\" it.\n\n/**\n * Allowlisted image hosts. We fetch product images ONLY from our own Vercel Blob store\n * (public). NEVER a public temp host (uguu/0x0/catbox/transfer.sh) and NEVER an arbitrary\n * URL — that would be SSRF + a brand-IP leak (CLAUDE.md rule 6).\n */\nexport const BLOB_HOST_ALLOWLIST: readonly string[] = [\n \"blob.vercel-storage.com\", // matches the exact host + *.blob.vercel-storage.com (dot-delimited subdomains only)\n]\n\n/**\n * True if the URL's host is on the Blob allowlist.\n *\n * Exact host OR a true dot-delimited subdomain. NOT a bare suffix: a bare\n * `endsWith(allowed)` would admit `evilblob.vercel-storage.com` (allowlist bypass →\n * SSRF / brand-IP leak, CLAUDE.md rule 6). The dot guard is load-bearing.\n */\nexport function isAllowedImageHost(url: string): boolean {\n let host: string\n try {\n host = new URL(url).hostname.toLowerCase()\n } catch {\n return false\n }\n return BLOB_HOST_ALLOWLIST.some(\n (allowed) => host === allowed || host.endsWith(\".\" + allowed)\n )\n}\n","// Colour resolution — DERIVED from the variant matrix, never hardcoded (CAT-3 forward-compat).\n//\n// The dual-colour catalog (memory: dual-colour-catalog) gives each design ONE product with a\n// Colour × Size variant matrix. A variant's colour comes from, in order of trust:\n// 1. the variant option titled \"Colour\" (Model A: options {Size, Colour}, value title-cased)\n// 2. the variant title, format \"<size> / <Colour>\" (e.g. \"S / White\", \"M / Black\")\n// We NEVER hardcode white/black — future colours (CAT-3) fall out of the data for free.\n//\n// This lives in @binaryandthread/store-client (the canonical types home) so the CLI and the MCP\n// resolve colour the SAME way — written once, tested once.\n\nimport type { ProductLike, VariantLike, ImageLike } from \"./types\"\n\n/** Title-case a colour token for display (\"white\" → \"White\"); leaves multi-word reasonable. */\nexport function displayColour(colour: string): string {\n const c = colour.trim()\n if (!c) return c\n return c.charAt(0).toUpperCase() + c.slice(1).toLowerCase()\n}\n\n/**\n * The colour of a single variant, lowercased — or null if it carries no colour axis (a\n * legacy Size-only product). Reads the \"Colour\" option first, then parses the \"<size> / <Colour>\"\n * title. NEVER assumes a fixed palette.\n */\nexport function variantColour(v: VariantLike): string | null {\n // 1) the explicit Colour option (the authoritative source — memory: never parse the title).\n const opt = (v.options ?? []).find(\n (o) => (o.option?.title ?? \"\").toLowerCase() === \"colour\" || (o.option?.title ?? \"\").toLowerCase() === \"color\"\n )\n if (opt && typeof opt.value === \"string\" && opt.value.trim()) {\n return opt.value.trim().toLowerCase()\n }\n // 2) fall back to the title format \"<size> / <Colour>\".\n const title = (v.title ?? \"\").trim()\n if (title.includes(\"/\")) {\n const parts = title.split(\"/\").map((p) => p.trim())\n const tail = parts[parts.length - 1]\n if (tail) return tail.toLowerCase()\n }\n return null\n}\n\n/**\n * The DISTINCT colours offered by a product, in first-seen order, lowercased. Empty for a\n * legacy Size-only product (no Colour axis) — callers treat that as \"no colour to show\".\n */\nexport function productColours(p: ProductLike): string[] {\n const seen: string[] = []\n for (const v of p.variants ?? []) {\n const c = variantColour(v)\n if (c && !seen.includes(c)) {\n seen.push(c)\n }\n }\n return seen\n}\n\n/** The distinct sizes offered FOR a given colour (lowercased colour match), preserving order. */\nexport function sizesForColour(p: ProductLike, colour: string): string[] {\n const target = colour.trim().toLowerCase()\n const sizes: string[] = []\n for (const v of p.variants ?? []) {\n if (variantColour(v) !== target) continue\n const size = sizeOfVariant(v)\n if (size && !sizes.includes(size)) {\n sizes.push(size)\n }\n }\n return sizes\n}\n\n/** The size label of a variant — the Size option, else the title's leading \"<size>\" segment. */\nexport function sizeOfVariant(v: VariantLike): string | null {\n const opt = (v.options ?? []).find((o) => (o.option?.title ?? \"\").toLowerCase() === \"size\")\n if (opt && typeof opt.value === \"string\" && opt.value.trim()) {\n return opt.value.trim()\n }\n const title = (v.title ?? \"\").trim()\n if (title.includes(\"/\")) {\n const head = title.split(\"/\")[0]?.trim()\n if (head) return head\n }\n // No colour axis → the whole title IS the size (legacy Size-only product).\n return title || null\n}\n\n/**\n * Resolve a variant by size AND colour (the real buyability gap — bt stage / MCP add_to_cart).\n * - size + colour given → exact match on both (case-insensitive).\n * - size only → first variant of that size.\n * - colour only → first variant of that colour.\n * - neither → first variant.\n * Returns undefined when the requested size+colour combination does not exist (the caller MUST\n * surface a clear error — never silently substitute another colour the buyer didn't ask for).\n */\nexport function findVariant(\n p: ProductLike,\n opts: { size?: string; colour?: string } = {}\n): VariantLike | undefined {\n const variants = p.variants ?? []\n const size = opts.size?.trim().toLowerCase()\n const colour = opts.colour?.trim().toLowerCase()\n\n const sizeMatches = (v: VariantLike): boolean => {\n if (!size) return true\n return (sizeOfVariant(v) ?? \"\").toLowerCase() === size || (v.title ?? \"\").toLowerCase() === size\n }\n const colourMatches = (v: VariantLike): boolean => {\n if (!colour) return true\n return variantColour(v) === colour\n }\n\n if (size && colour) {\n // both specified → require BOTH; no fallback to a different colour (a wrong colour is worse\n // than a clear \"that combo isn't available\").\n return variants.find((v) => sizeMatches(v) && colourMatches(v))\n }\n if (colour && !size) {\n return variants.find((v) => colourMatches(v))\n }\n if (size && !colour) {\n // size specified → require it; NO fallback to variants[0] (L10) — a silently-wrong size is worse\n // than undefined, which lets the caller (stage.ts) surface a clear \"no matching variant\" error.\n return variants.find((v) => sizeMatches(v))\n }\n return variants[0]\n}\n\n// The gallery scene we surface in the terminal: the flat-lay \"plp\" shot (the tee laid flat on the\n// photographic/concrete plate) — the SAME scene as the storefront thumbnail. Pinning to one scene is\n// what makes every tee read CONSISTENTLY (never a mix of on-model here / hero there); the product\n// image order alone is unreliable (D9). Filenames are `<slug>-<scene>-<colour>.webp`.\nconst PREFERRED_SCENE = \"plp\"\n\n/** The URL of a specific SCENE for a colour (or any colour when `colour` is null) — matches\n * `-<scene>-` in the filename + the colour token. null if no such image. */\nfunction sceneImage(\n images: Array<ImageLike> | null | undefined,\n scene: string,\n colour: string | null\n): string | null {\n for (const img of images ?? []) {\n const u = (img?.url ?? \"\").trim()\n if (!u || !u.toLowerCase().includes(`-${scene}-`)) continue\n if (colour === null || new RegExp(`(^|[^a-z])${escapeRegex(colour)}([^a-z]|$)`, \"i\").test(u.toLowerCase())) {\n return u\n }\n }\n return null\n}\n\n/**\n * The image URL to render for a given colour. Resolves, in order:\n * 1. the flat-lay \"plp\" scene for the colour — the CONSISTENT, thumbnail-matching choice (D9), so\n * every tee shows the same flat-on-concrete shot rather than an order-dependent on-model/hero.\n * 2. the matching variant's own images[]\n * 3. a product image whose url/metadata names the colour\n * 4. null → the caller falls back to product.thumbnail + a \"black available\" note.\n * Never throws; a missing colour just yields null.\n */\nexport function imageForColour(p: ProductLike, colour: string): string | null {\n const target = colour.trim().toLowerCase()\n if (!target) {\n return sceneImage(p.images, PREFERRED_SCENE, null) ?? p.thumbnail ?? firstImageUrl(p.images) ?? null\n }\n\n // 1) the flat-lay plp scene for this colour — consistent across every product (matches thumbnail).\n const plp = sceneImage(p.images, PREFERRED_SCENE, target)\n if (plp) return plp\n\n // 2) the colour's variant images.\n for (const v of p.variants ?? []) {\n if (variantColour(v) !== target) continue\n const url = firstImageUrl(v.images)\n if (url) return url\n }\n\n // 3) a product image that names the colour (url substring or metadata.colour tag).\n for (const img of p.images ?? []) {\n const tag = (img.metadata?.[\"colour\"] ?? img.metadata?.[\"color\"] ?? \"\")\n .toString()\n .toLowerCase()\n if (tag === target) {\n const u = (img.url ?? \"\").trim()\n if (u) return u\n }\n }\n for (const img of p.images ?? []) {\n const u = (img.url ?? \"\").trim()\n if (u && new RegExp(`(^|[^a-z])${escapeRegex(target)}([^a-z]|$)`, \"i\").test(u)) {\n return u\n }\n }\n return null\n}\n\nfunction firstImageUrl(images: Array<ImageLike> | null | undefined): string | null {\n for (const img of images ?? []) {\n const u = (img?.url ?? \"\").trim()\n if (u) return u\n }\n return null\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n}\n","// Canonical product-content accessors — the ONE source for a piece's README, spec sheet, and lore,\n// shared by the CLI (`bt view` / `bt blame`), the MCP (`get_product`), and (aspirationally) the\n// storefront PDP. Keeping these here means the three channels can't drift on what a piece \"says\".\n//\n// The spec fields + fallbacks mirror the storefront's buildSpecs (apps/storefront .../templates).\n\nimport type { ProductLike } from \"./types\"\n\n/** One row of the spec sheet. */\nexport interface SpecEntry {\n key: string\n value: string\n}\n\n/** A lore link (a real source behind the pun/idea). */\nexport interface LoreSource {\n url: string\n label: string\n}\n\n/** A piece's lore: the story text + any sources. Empty text = no lore on record. */\nexport interface ProductLore {\n text: string\n sources: LoreSource[]\n}\n\n/** A non-empty string, or the fallback. */\nfunction str(v: unknown, fallback: string): string {\n return typeof v === \"string\" && v.length > 0 ? v : fallback\n}\n\n/** The piece's README — the human description shown on the PDP's README toggle. */\nexport function productReadme(p: ProductLike): string {\n return typeof p.description === \"string\" ? p.description : \"\"\n}\n\n/**\n * The spec sheet — the 6 canonical rows, in PDP order, with the same fallbacks as the storefront's\n * buildSpecs (the single source of truth for what a piece is made of / how it ships).\n */\nexport function productSpecs(p: ProductLike): SpecEntry[] {\n const md = (p.metadata ?? {}) as Record<string, unknown>\n return [\n { key: \"material\", value: str(md.material, \"100% cotton\") },\n { key: \"weight\", value: str(md.weight, \"180 g/m²\") },\n { key: \"fit\", value: str(md.fit, \"Unisex heavyweight crewneck\") },\n { key: \"print\", value: str(md.print, \"Front print\") },\n { key: \"delivery\", value: str(md.delivery, \"Ships in 2–5 business days + transit\") },\n { key: \"care\", value: str(md.care, \"Cold wash, line dry\") },\n ]\n}\n\n/** The piece's lore — the story behind the pun + any real sources. Empty text = none on record. */\nexport function productLore(p: ProductLike): ProductLore {\n const md = (p.metadata ?? {}) as Record<string, unknown>\n const text = typeof md.lore === \"string\" ? md.lore : \"\"\n const raw = Array.isArray(md.lore_sources) ? md.lore_sources : []\n const sources: LoreSource[] = raw\n .filter(\n (s): s is { url: string; label: string } =>\n !!s &&\n typeof s === \"object\" &&\n typeof (s as { url?: unknown }).url === \"string\" &&\n typeof (s as { label?: unknown }).label === \"string\"\n )\n .map((s) => ({ url: s.url, label: s.label }))\n return { text, sources }\n}\n","// Tax-inclusivity — the single source of truth for the \"incl. VAT\" price label across EVERY\n// channel (storefront, the bt CLI, the MCP server). Kept here (in the shared client) so the CLI\n// and MCP agree with each other and with the backend on how the flag is read.\n\nimport type { CartLike } from \"./types\"\n\n/**\n * Whether a cart's prices are VAT-inclusive (EU/SE) vs tax-exclusive (US).\n *\n * Medusa exposes tax-inclusivity PER LINE ITEM (and per shipping method), never on the cart root —\n * there is no `cart.is_tax_inclusive` in @medusajs/types 2.15.5. So we DERIVE it from the items.\n * It is uniform per region (every line shares the region's tax setting), so `.some` is exact and a\n * mixed cart cannot occur. A US (tax-exclusive) cart → false → NO \"incl. VAT\" label; an SE cart →\n * true → \" incl. VAT\". This is NEVER keyed off a currency or country literal (INV-11): the label\n * follows the market's own tax flag, so it stays correct as new regions come online.\n *\n * Fails closed: an empty/absent cart or items array → false (no phantom VAT label on nothing).\n */\nexport function isCartTaxInclusive(cart: Pick<CartLike, \"items\"> | null | undefined): boolean {\n return (cart?.items ?? []).some((i) => i.is_tax_inclusive === true)\n}\n","// The B&T MCP server — stdio transport, registering exactly the channel's deterministic tools.\n//\n// The server is a THIN registrar: each tool is a deterministic wrapper over the shared\n// StoreClient (no free-form execution, no admin op — SECURITY T10/G8). The tool DEFINITIONS\n// (names, input schemas, handlers) live in tools.ts; this module just builds the McpServer and\n// wires the stdio transport so a developer's AI agent can call them over stdin/stdout.\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\"\nimport type { ZodRawShape } from \"zod\"\nimport type { ImageBlock } from \"./images\"\nimport { VERSION } from \"./version\"\nimport { GETTING_STARTED, GETTING_STARTED_URI, buyPromptText } from \"./onboarding\"\n\n/** What a tool handler returns — MCP content blocks (image and/or text). */\nexport interface ToolResult {\n content: ImageBlock[]\n /** Set when the tool reports a user-facing error (e.g. a 404 lookup) — not a transport error. */\n isError?: boolean\n /**\n * Machine-readable result (ids/handles like cart_id/session_id/order_id) so an agent can chain\n * tools WITHOUT regexing the prose (d9). Already redacted (G10). Mirrors the human text block.\n */\n structuredContent?: Record<string, unknown>\n}\n\n/**\n * A deterministic tool definition. `inputSchema` is a Zod raw shape (the SDK turns it into a JSON\n * schema for the agent). The handler receives the parsed args and returns content blocks. Eggs\n * (VOICE §3.2 M2/M3) may live in the `description` text but NEVER alter behavior.\n */\nexport interface ToolDef<S extends ZodRawShape = ZodRawShape> {\n name: string\n description: string\n inputSchema: S\n handler: (args: Record<string, unknown>) => Promise<ToolResult>\n}\n\n// Advertised to the host in the initialize handshake. Version is single-sourced from version.ts\n// (mirrors package.json; version.test.ts locks them together) — no more silent drift.\nexport const SERVER_INFO = {\n name: \"binary-and-thread\",\n version: VERSION,\n} as const\n\n/**\n * Build an McpServer and register every tool. Returns the server plus the registered tool names\n * (so callers/tests can assert exactly the 10 tools are present). Pure — no transport, no I/O.\n */\nexport function buildServer(tools: ToolDef[]): {\n server: McpServer\n toolNames: string[]\n resourceNames: string[]\n promptNames: string[]\n} {\n const server = new McpServer(SERVER_INFO)\n const toolNames: string[] = []\n // The SDK's registerTool generic over a ZodRawShape input schema infers excessively deeply\n // (TS2589); we register through a structurally-typed view of the method to keep the call site\n // simple. The runtime contract is unchanged — name, { description, inputSchema }, handler.\n const register = server.registerTool.bind(server) as unknown as (\n name: string,\n config: { description: string; inputSchema: ZodRawShape },\n cb: (args: Record<string, unknown>) => Promise<{\n content: ImageBlock[]\n isError?: boolean\n structuredContent?: Record<string, unknown>\n }>\n ) => void\n for (const t of tools) {\n register(\n t.name,\n { description: t.description, inputSchema: t.inputSchema },\n // The SDK validates args against inputSchema before calling us. We hand the parsed args to\n // the deterministic handler and return its content blocks (+ optional structuredContent, d9).\n async (args: Record<string, unknown>) => {\n const res = await t.handler(args ?? {})\n return {\n content: res.content,\n ...(res.isError ? { isError: true } : {}),\n ...(res.structuredContent ? { structuredContent: res.structuredContent } : {}),\n }\n }\n )\n toolNames.push(t.name)\n }\n\n // Agent onboarding: a getting-started RESOURCE + a `buy` PROMPT (neither can touch money — they're\n // read-only orientation). Registered through structurally-typed views of the SDK methods to avoid\n // the deep generic inference the tool registration also side-steps.\n const registerResource = server.registerResource.bind(server) as unknown as (\n name: string,\n uri: string,\n metadata: { title?: string; description?: string; mimeType?: string },\n cb: (uri: URL) => { contents: Array<{ uri: string; text: string; mimeType?: string }> }\n ) => void\n registerResource(\n \"getting-started\",\n GETTING_STARTED_URI,\n { title: \"binary & thread — getting started\", description: \"the buy flow + the money boundary, for a cold agent\", mimeType: \"text/plain\" },\n () => ({ contents: [{ uri: GETTING_STARTED_URI, text: GETTING_STARTED, mimeType: \"text/plain\" }] })\n )\n const resourceNames = [\"getting-started\"]\n\n const registerPrompt = server.registerPrompt.bind(server) as unknown as (\n name: string,\n config: { title?: string; description?: string; argsSchema?: ZodRawShape },\n cb: (args: Record<string, unknown>) => { messages: Array<{ role: \"user\"; content: { type: \"text\"; text: string } }> }\n ) => void\n registerPrompt(\n \"buy\",\n { title: \"buy a piece\", description: \"walk through buying a piece (respecting the card-stays-with-the-human boundary)\" },\n (args) => ({\n messages: [\n {\n role: \"user\",\n content: {\n type: \"text\",\n text: buyPromptText(\n typeof args?.handle === \"string\" ? args.handle : undefined,\n typeof args?.region === \"string\" ? args.region : undefined\n ),\n },\n },\n ],\n })\n )\n const promptNames = [\"buy\"]\n\n return { server, toolNames, resourceNames, promptNames }\n}\n\n/** Build the server with the real tools and start it on stdio. Used by bin.ts. */\nexport async function startServer(tools: ToolDef[]): Promise<McpServer> {\n const { server } = buildServer(tools)\n const transport = new StdioServerTransport()\n await server.connect(transport)\n return server\n}\n","// The single source of truth for the MCP server version string.\n//\n// Mirrored into mcp/package.json's \"version\" field; version.test.ts asserts the two never drift.\n// We don't import package.json directly (JSON imports complicate the ESM dist build), so this\n// literal is the canonical value the server advertises to a host in the `initialize` handshake\n// (SERVER_INFO.version). Before this, SERVER_INFO hardcoded \"0.1.0\" and drifted from the published\n// package — a host inspecting the server saw a stale version, and bug reports mis-attributed it.\nexport const VERSION = \"0.3.0\"\n","// Agent onboarding surfaces — a getting-started RESOURCE + a `buy` PROMPT. The MCP parallel to the\n// CLI's provenance line + `bt init`: a cold agent shouldn't have to infer the flow (or the money\n// boundary) from ten tool descriptions. Deadpan-competent dev voice; brand-voice clean (never\n// \"tee\"/\"t-shirt\"/print-on-demand/Gelato — enforced by brand-voice.test.ts scanning src/).\n\nexport const GETTING_STARTED_URI = \"bt://getting-started\"\n\nexport const GETTING_STARTED = `binary & thread — buying from your agent.\n\nthe flow (each tool returns machine-readable structuredContent, so you can chain without regexing prose):\n 1. list_products [region] what's in stock, priced in the ship-to market's currency\n 2. get_product <handle> full detail + an authoritative variant_id + sizes + colours\n 3. add_to_cart … start or reuse a cart (returns a durable cart_id handle)\n 4. get_cart <cart_id> the authoritative cart — note: the total is PRE-shipping\n 5. checkout <cart_id> mint a pay link + the SHIPPING-INCLUSIVE total the buyer pays\n 6. check_payment <session_id> poll once for the result (call again if still pending)\n\nthe dev-channel discount: every purchase through this MCP (or the bt CLI) is 10% off the web —\napplied automatically at checkout, no code. checkout returns the discounted total; quote that.\n\nthe boundary (by design): payment + ship-to details are entered by the human on the storefront.\nthey never enter this chat, and you cannot complete a purchase yourself — you walk them to the door.\n\nregion + currency: prices and shipping follow the ship-to country. pass region (ISO-2, e.g. se / us /\nde) to list_products / get_product / add_to_cart, or the host can set BT_REGION. a country we don't\nship to is refused rather than priced in the wrong currency.`\n\n/** The `buy` prompt body — a templated walk-through of the flow + the boundary, optionally seeded\n * with a handle/region the user named. Returned as a single user-turn message. */\nexport function buyPromptText(handle?: string, region?: string): string {\n const target = handle ? `\"${handle}\"` : \"the piece the user wants\"\n const reg = region ? ` Price it for region \"${region}\".` : \"\"\n return `Help me buy ${target} from binary & thread through the MCP tools.${reg}\n\nSteps:\n1. If you don't have an exact variant_id, call get_product to get valid sizes/colours + the variant_id.\n2. add_to_cart with the variant_id (or handle + size + colour). Keep the returned cart_id.\n3. Show me the cart total, and remind me it excludes shipping until checkout (where a 10%\n dev-channel discount also applies — the terminal's cut).\n4. When I confirm, call checkout and give me the pay link + the shipping-inclusive, discounted total.\n5. Do NOT ask me for card or address details — I enter those in the browser. After I pay, use\n check_payment with the session_id to confirm the order.`\n}\n","// The 10 deterministic MCP tools over the SHARED StoreClient (SECURITY G8/T10; ROBUSTNESS §4.2/4.3).\n//\n// These are thin wrappers — no free-form execution, no admin op, no money self-completion. They\n// reuse the same idempotency / region-resolution / retry / redaction that the CLI uses (it all\n// lives inside the StoreClient — written once, contract-tested once).\n//\n// The hard invariants this file enforces (the MCP red-team gates):\n// - checkout (G8 confused-deputy / F-I4): returns {checkout_url, session_id} INSTANTLY. It NEVER\n// polls, NEVER opens a browser, NEVER sets a shipping address. Its result text tells the human\n// to open the link and explicitly says \"I can't complete this for you\". The human pays on the\n// storefront (Stripe Elements) — payment details NEVER enter the chat. check_payment polls; the\n// agent re-calls at its own cadence (async handle-ID).\n// (NB: there is deliberately NO payment-detail field anywhere in this package; the security\n// verify greps for those field tokens and must come back clean.)\n// - list views are TEXT-ONLY (no per-item images — token-bloat guard, ROBUSTNESS §4.3).\n// - every backend string is sanitize()'d (G8); every output object is redactForOutput()'d (G10).\n// - get_order_status returns status/timeline only — NO address (F-H3); id + exact-email, 404 on\n// miss (F-H1).\n\nimport { z } from \"zod\"\nimport {\n RecoverableCartError,\n StoreApiError,\n productColours,\n variantColour,\n sizesForColour,\n findVariant,\n productSpecs,\n productLore,\n productReadme,\n isCartTaxInclusive,\n} from \"@binaryandthread/store-client\"\nimport type {\n StoreClient,\n ProductLike,\n CartLike,\n RegionLike,\n} from \"@binaryandthread/store-client\"\nimport { sanitize, redactForOutput } from \"./sanitize\"\nimport { buildImageContent } from \"./images\"\nimport type { ImageBlock } from \"./images\"\nimport type { ToolDef, ToolResult } from \"./server\"\nimport { TEAPOT_TEXT, FORTUNES, fortune } from \"./eggs\"\n\nexport interface ToolsOpts {\n /** ISO-2 country code to resolve pricing (never a hardcoded currency literal). */\n region?: string\n}\n\n// ----- result helpers ---------------------------------------------------------\n\nfunction text(s: string): ToolResult {\n return { content: [{ type: \"text\", text: s }] }\n}\n\nfunction errorText(s: string): ToolResult {\n return { content: [{ type: \"text\", text: s }], isError: true }\n}\n\n/** A text block PLUS machine-readable structuredContent (ids/handles) for agent chaining (d9). */\nfunction result(textBody: string, structured: Record<string, unknown>): ToolResult {\n return {\n content: [{ type: \"text\", text: textBody }],\n structuredContent: redactForOutput(structured) as Record<string, unknown>,\n }\n}\n\n/** Price line — currency comes from the variant/region, NEVER a literal (F-A2/INV-11). */\nfunction fmtPrice(amount: number | null | undefined, currency: string | null | undefined): string {\n if (amount == null) {\n return \"—\"\n }\n const cur = (currency ?? \"\").toUpperCase()\n return cur ? `${amount} ${cur}` : `${amount}`\n}\n\n/**\n * The tax suffix for a price line: \" incl. VAT\" ONLY for a tax-inclusive market (EU/SE), \"\" for a\n * tax-exclusive one (US). Driven off the cart's / variant's OWN is_tax_inclusive flag, NEVER a\n * currency literal (INV-11) — mirrors the CLI + storefront. Pass isCartTaxInclusive(cart) for a cart\n * total, or a variant's calculated_price.is_calculated_price_tax_inclusive for a product price.\n */\nfunction taxSuffix(isTaxInclusive?: boolean | null): string {\n return isTaxInclusive ? \" incl. VAT\" : \"\"\n}\n\nfunction firstPrice(\n p: ProductLike\n): { amount: number | null; currency: string | null; taxInclusive: boolean | null } {\n const v = (p.variants ?? [])[0]\n return {\n amount: v?.calculated_price?.calculated_amount ?? null,\n currency: v?.calculated_price?.currency_code ?? null,\n taxInclusive: v?.calculated_price?.is_calculated_price_tax_inclusive ?? null,\n }\n}\n\n// Machine-readable cart line items so an agent can itemize the cart from structuredContent instead\n// of re-parsing the prose (D2). Untrusted titles sanitize()'d; ids/numbers pass through.\nfunction cartItems(cart: CartLike): Array<{ variant_id: string | null; title: string; qty: number; total: number | null }> {\n return (cart.items ?? []).map((li) => ({\n variant_id: li.variant_id ?? null,\n title: sanitize(li.product_title ?? li.title ?? \"item\", 80),\n qty: li.quantity ?? 1,\n total: li.total ?? null,\n }))\n}\n\nfunction cartSummary(cart: CartLike, region?: RegionLike): string {\n const lines: string[] = []\n lines.push(`cart ${cart.id}`)\n const items = cart.items ?? []\n if (items.length === 0) {\n lines.push(\" (empty)\")\n } else {\n for (const li of items) {\n const title = sanitize(li.product_title ?? li.title ?? \"item\", 80)\n lines.push(` ${li.quantity}x ${title} — ${fmtPrice(li.total, cart.currency_code ?? region?.currency_code)}`)\n }\n }\n // Cart totals are PRE-shipping until a shipping method is attached at checkout — say so, so an\n // agent never quotes this as the final price (A2). Once a method is on the cart, the total is real.\n const shipNote = (cart.shipping_methods ?? []).length > 0 ? \"\" : \" (excl. shipping — calculated at checkout)\"\n lines.push(` total ${fmtPrice(cart.total, cart.currency_code ?? region?.currency_code)}${taxSuffix(isCartTaxInclusive(cart))}${shipNote}`)\n return lines.join(\"\\n\")\n}\n\n// ----- the 10 tools ------------------------------------------------------------\n\n/** Build the 10 tool definitions bound to a StoreClient. */\nexport function makeTools(client: StoreClient, opts: ToolsOpts = {}): ToolDef[] {\n const regionCC = opts.region\n\n // list_products — TEXT-ONLY catalog (no per-item images — token-bloat guard, §4.3).\n const list_products: ToolDef = {\n name: \"list_products\",\n description:\n \"List the pieces in stock (text-only — no images, to keep the agent's context lean). \" +\n \"Returns each product's handle, price (VAT included where applicable), and stage. Use get_product for one item's image. \" +\n \"Prices are in the region's currency — pass `region` (an ISO-2 country) to price for a specific market, else the server default.\",\n inputSchema: {\n stage: z.string().optional().describe(\"filter by channel stage: 'main' or 'next'\"),\n region: z.string().optional().describe(\"ISO-2 ship-to country to price for (e.g. se / us / de); overrides the server default\"),\n },\n handler: async (args): Promise<ToolResult> => {\n const cc = (typeof args.region === \"string\" && args.region.trim()) || regionCC\n const rr = await client.resolveRegionResult(cc)\n const region = rr.region\n const products = await client.listProducts({ region })\n const stage = typeof args.stage === \"string\" ? args.stage.toLowerCase() : undefined\n const rows = products\n .map((p) => ({ p, stage: String(p.metadata?.[\"channel_stage\"] ?? \"main\") }))\n .filter((r) => (stage ? r.stage.toLowerCase() === stage : true))\n if (rows.length === 0) {\n return result(\"nothing in stock on this branch yet.\", { products: [], currency: region.currency_code, region: rr.requested ?? null })\n }\n const lines = rows.map(({ p, stage }) => {\n const { amount, currency, taxInclusive } = firstPrice(p)\n const handle = sanitize(p.handle ?? p.id, 60)\n const title = sanitize(p.title ?? \"\", 80)\n return `${handle} ${fmtPrice(amount, currency)}${taxSuffix(taxInclusive)} [${stage}] ${title}`.trimEnd()\n })\n const productList = rows.map(({ p, stage }) => {\n const { amount, currency, taxInclusive } = firstPrice(p)\n return { handle: p.handle ?? p.id, title: sanitize(p.title ?? \"\", 80), price: amount, currency, tax_inclusive: taxInclusive, stage }\n })\n // Echo the resolved region+currency so an agent that asked for a market it can't be priced in\n // (silent SEK fallback) can SEE the substitution instead of quoting the wrong currency (D2).\n return result(lines.join(\"\\n\"), { products: productList, currency: region.currency_code, region: rr.requested ?? null })\n },\n }\n\n // get_product — details + the ONE product image (ImageContent + Blob URL floor).\n const get_product: ToolDef = {\n name: \"get_product\",\n // M3 ★ — a hidden line for the schema-readers (devs read tool descriptions). Rewards the\n // contributor instinct + hints at a real feature (the next branch / drops). Never alters behaviour.\n description:\n \"Get one piece in full: its readme, the spec sheet (material/weight/fit/print/delivery/care), \" +\n \"sizes + price (VAT included where applicable), the lore (the story behind it, with sources), and its image (rendered + \" +\n \"the Blob URL as a text floor). Prices are in the region's currency — pass `region` (ISO-2) to price for \" +\n \"a market. // you read the schema. that's the contributor instinct. ask me about the next branch.\",\n inputSchema: {\n handle: z.string().describe(\"the product handle/slug (from list_products)\"),\n region: z.string().optional().describe(\"ISO-2 ship-to country to price for (e.g. se / us / de); overrides the server default\"),\n },\n handler: async (args): Promise<ToolResult> => {\n const handle = String(args.handle ?? \"\")\n const cc = (typeof args.region === \"string\" && args.region.trim()) || regionCC\n const rr = await client.resolveRegionResult(cc)\n const region = rr.region\n const product = await client.getProduct(handle, { region })\n if (!product) {\n return errorText(`no product with handle \"${sanitize(handle, 60)}\". try list_products.`)\n }\n const lines: string[] = []\n lines.push(sanitize(product.title ?? handle, 120))\n if (product.description) {\n lines.push(sanitize(product.description, 400))\n }\n const fabric = product.metadata?.[\"fabric\"]\n if (typeof fabric === \"string\") {\n lines.push(`fabric: ${sanitize(fabric, 80)}`)\n }\n // A3 — available colours, DERIVED from the variants (never hardcoded). Sizes are listed per\n // colour when there's a colour axis, else a flat list (legacy Size-only product).\n const colours = productColours(product)\n if (colours.length > 0) {\n lines.push(`available colours: ${colours.map((c) => sanitize(c, 20)).join(\", \")}`)\n for (const c of colours) {\n const cs = sizesForColour(product, c).map((s) => sanitize(s, 20))\n lines.push(` ${sanitize(c, 20)}: ${cs.join(\" \")}`)\n }\n } else {\n const sizes = (product.variants ?? []).map((v) => sanitize(v.title ?? \"?\", 20))\n lines.push(`sizes: ${sizes.join(\" \")}`)\n }\n const v0 = (product.variants ?? [])[0]\n if (v0?.calculated_price) {\n lines.push(`price: ${fmtPrice(v0.calculated_price.calculated_amount, v0.calculated_price.currency_code)}${taxSuffix(v0.calculated_price.is_calculated_price_tax_inclusive)}`)\n }\n // The full spec sheet (the PDP's technical block) + the lore — so the agent knows the piece,\n // not just its price. Single-sourced from store-client (matches the storefront).\n const specs = productSpecs(product)\n lines.push(\"specs:\")\n for (const s of specs) {\n lines.push(` ${sanitize(s.key, 20)}: ${sanitize(s.value, 120)}`)\n }\n const lore = productLore(product)\n if (lore.text) {\n lines.push(`lore: ${sanitize(lore.text, 600)}`)\n for (const src of lore.sources) {\n lines.push(` source: ${sanitize(src.label, 80)} (${sanitize(src.url, 300)})`)\n }\n }\n const content: ImageBlock[] = [{ type: \"text\", text: lines.join(\"\\n\") }]\n if (product.thumbnail) {\n const img = await buildImageContent(product.thumbnail)\n content.push(...img)\n }\n // A machine variant map so an agent gets an authoritative variant_id + size/colour + price for\n // add_to_cart, instead of guessing from free-text (D2). Sizes canonical-listed per colour.\n const variantMap = (product.variants ?? []).map((v) => ({\n variant_id: v.id,\n size: v.title ?? null,\n colour: variantColour(v) ?? null,\n price: v.calculated_price?.calculated_amount ?? null,\n }))\n const sizeList = colours.length > 0\n ? Object.fromEntries(colours.map((c) => [c, sizesForColour(product, c)]))\n : (product.variants ?? []).map((v) => v.title ?? \"?\")\n return {\n content,\n // G8/D3 — the untrusted backend TEXT (title/readme/specs/lore) is sanitize()'d before it\n // enters the machine-readable path too (the header's promise), not just the text block;\n // ids/urls/numbers pass through redactForOutput unchanged.\n structuredContent: redactForOutput({\n handle: product.handle ?? handle,\n title: sanitize(product.title ?? \"\", 120),\n price: v0?.calculated_price?.calculated_amount ?? null,\n currency: v0?.calculated_price?.currency_code ?? null,\n tax_inclusive: v0?.calculated_price?.is_calculated_price_tax_inclusive ?? null,\n region: rr.requested ?? null,\n colours,\n sizes: sizeList,\n variants: variantMap,\n image_url: product.thumbnail ?? null,\n // the PDP content, machine-readable: readme + the spec sheet + the lore (+ sources).\n readme: sanitize(productReadme(product), 2000),\n specs: specs.map((s) => ({ key: sanitize(s.key, 40), value: sanitize(s.value, 200) })),\n lore: {\n text: sanitize(lore.text ?? \"\", 1200),\n sources: lore.sources.map((s) => ({ label: sanitize(s.label, 120), url: s.url })),\n },\n }) as Record<string, unknown>,\n }\n },\n }\n\n // add_to_cart — create-or-reuse a cart; return the cart_id handle + the server cart.\n const add_to_cart: ToolDef = {\n name: \"add_to_cart\",\n description:\n \"Add a piece to a cart. Pass cart_id to reuse an existing cart (the durable handle), or omit it \" +\n \"to start a fresh one. Call get_product first to get valid sizes/colours (or an exact variant_id). \" +\n \"qty is capped at 20. Returns the cart_id and the server-side cart summary (VAT included where applicable). \" +\n \"`region` (ISO-2) sets the currency for a NEW cart only — a reused cart keeps its original region.\",\n inputSchema: {\n handle: z.string().optional().describe(\"product handle (resolves a variant by size + colour; call get_product for valid values)\"),\n variant_id: z.string().optional().describe(\"a specific variant id from get_product (overrides handle/size/colour)\"),\n size: z.string().optional().describe(\"size label from get_product, e.g. S / M / L / XL / 2XL\"),\n colour: z.string().optional().describe(\"piece colour from get_product, e.g. white / black (default: the product's first colour)\"),\n qty: z.number().int().positive().max(20).optional().describe(\"quantity, 1–20 (default 1)\"),\n cart_id: z.string().optional().describe(\"reuse this cart; omit to create a new one\"),\n region: z.string().optional().describe(\"ISO-2 ship-to country for a NEW cart (e.g. se / us / de); ignored when reusing a cart_id\"),\n },\n handler: async (args): Promise<ToolResult> => {\n // Fail CLOSED on a named ship-to country we don't have a region for — never create a cart in a\n // substituted currency (A1). A reused cart keeps its own region (the param can't retro-bind it).\n const cc = (typeof args.region === \"string\" && args.region.trim()) || regionCC\n const rr = await client.resolveRegionResult(cc)\n if (rr.requested && !rr.matched) {\n return errorText(`we don't ship to ${sanitize(rr.requested, 8)} yet — nothing was added. (list_products with a supported region to see prices.)`)\n }\n const region = rr.region\n const qty = typeof args.qty === \"number\" ? args.qty : 1\n\n // resolve the variant id\n let variantId = typeof args.variant_id === \"string\" ? args.variant_id : undefined\n if (!variantId) {\n const handle = typeof args.handle === \"string\" ? args.handle : \"\"\n if (!handle) {\n return errorText(\"add_to_cart needs a variant_id or a handle (+ optional size/colour).\")\n }\n const product = await client.getProduct(handle, { region })\n if (!product) {\n return errorText(`no product with handle \"${sanitize(handle, 60)}\".`)\n }\n // A3 — resolve by size AND colour (DERIVED, never hardcoded). colour defaults to the\n // product's first colour; a missing size+colour combo is a CLEAR error (no substitution).\n const size = typeof args.size === \"string\" ? args.size : undefined\n const colours = productColours(product)\n const colour =\n typeof args.colour === \"string\" ? args.colour : colours.length > 0 ? colours[0] : undefined\n const variant = findVariant(product, { size, colour })\n if (!variant) {\n const have = colours.length ? ` available colours: ${colours.map((c) => sanitize(c, 20)).join(\", \")}.` : \"\"\n return errorText(\n `no variant for \"${sanitize(handle, 60)}\"${size ? ` size ${sanitize(size, 20)}` : \"\"}` +\n `${colour ? ` (${sanitize(colour, 20)})` : \"\"}.${have}`\n )\n }\n variantId = variant.id\n }\n\n // create-or-reuse the cart; a stale/completed cart → make a fresh one (F-B2).\n const reuse = typeof args.cart_id === \"string\" ? args.cart_id : undefined\n const addTo = async (cartId: string): Promise<CartLike> => client.addLineItem(cartId, variantId!, qty)\n let cart: CartLike\n try {\n const targetId = reuse ?? (await client.createCart(region)).id\n cart = await addTo(targetId)\n } catch (e) {\n if (e instanceof RecoverableCartError && reuse) {\n // the reused cart was stale/completed → silently make a fresh one (the handle rotates).\n const fresh = await client.createCart(region)\n cart = await addTo(fresh.id)\n } else {\n throw e\n }\n }\n const itemCount = (cart.items ?? []).reduce((n, li) => n + (li.quantity ?? 1), 0)\n return result(\n `added.\\ncart_id: ${cart.id} (reuse this handle on the next add / checkout)\\n${cartSummary(cart, region)}`,\n {\n cart_id: cart.id,\n total: cart.total ?? null,\n currency: cart.currency_code ?? region.currency_code ?? null,\n item_count: itemCount,\n items: cartItems(cart),\n // A2 — `total` is pre-shipping until checkout attaches a method; flag it for the agent.\n total_excludes_shipping: (cart.shipping_methods ?? []).length === 0,\n }\n )\n },\n }\n\n // get_cart — the authoritative server cart summary.\n const get_cart: ToolDef = {\n name: \"get_cart\",\n description: \"Show a cart's current contents and total (VAT included where applicable), straight from the server.\",\n inputSchema: { cart_id: z.string().describe(\"the cart handle from add_to_cart\") },\n handler: async (args): Promise<ToolResult> => {\n const cartId = String(args.cart_id ?? \"\")\n const region = await client.resolveRegion(regionCC)\n try {\n const cart = await client.getCart(cartId)\n const itemCount = (cart.items ?? []).reduce((n, li) => n + (li.quantity ?? 1), 0)\n return result(cartSummary(cart, region), {\n cart_id: cart.id,\n total: cart.total ?? null,\n currency: cart.currency_code ?? region.currency_code ?? null,\n item_count: itemCount,\n items: cartItems(cart),\n // A2 — signal that `total` is pre-shipping so a chaining agent doesn't quote it as final.\n total_excludes_shipping: (cart.shipping_methods ?? []).length === 0,\n })\n } catch (e) {\n if (e instanceof RecoverableCartError) {\n return errorText(\"that cart is no longer valid (expired or already committed). add an item to start a fresh one.\")\n }\n throw e\n }\n },\n }\n\n // checkout — async handle-ID hand-off. Returns INSTANTLY. NEVER polls / opens a browser /\n // sets an address. Payment details NEVER enter the chat (G8 confused-deputy / F-I4).\n const checkout: ToolDef = {\n name: \"checkout\",\n description:\n \"Mint a checkout hand-off for a cart. Returns a checkout_url + a session_id INSTANTLY — it does \" +\n \"NOT poll and does NOT complete the purchase. The human must open the link in their own browser \" +\n \"to enter their payment + ship-to details on the storefront (those details never enter this chat). \" +\n \"After they pay, call check_payment with the session_id to see the status.\",\n inputSchema: {\n cart_id: z.string().describe(\"the cart handle to check out\"),\n email: z.string().optional().describe(\"the buyer's email for the receipt (optional)\"),\n },\n handler: async (args): Promise<ToolResult> => {\n const cartId = String(args.cart_id ?? \"\")\n const email = typeof args.email === \"string\" ? args.email : undefined\n // BUG 1: the headless checkout route requires the cart to carry a shipping method. Attach\n // one first (idempotent in the client; auto-picks the only/cheapest region option — it\n // sets a SHIPPING method, never a payment/address, so the G8 boundary is untouched). Fail\n // CLOSED: if no option is available we return an error and NEVER mint a checkout link.\n let shipped: CartLike\n try {\n shipped = await client.ensureCartShippingMethod(cartId)\n } catch (e) {\n if (e instanceof RecoverableCartError) {\n return errorText(\"that cart is no longer valid (expired or already committed). add an item to start a fresh one.\")\n }\n return errorText(\"couldn't attach a shipping method to this cart — checkout isn't available for it right now.\")\n }\n // mintCheckout is idempotent in the client AND applies the dev-channel 10% server-side. We\n // return its result DIRECTLY — no poll, no browser, no address (the G8 boundary).\n const mint = await client.mintCheckout(cartId, email, \"mcp\")\n const { checkout_url, session_id } = mint\n // A2 — quote the DISCOUNTED, shipping-inclusive total the mint computed (dev-channel 10% off),\n // not the pre-discount cart figure. Fall back to the shipped cart total if an older backend\n // didn't echo one.\n const finalTotal = mint.total ?? shipped.total ?? null\n const finalCurrency = mint.currency ?? shipped.currency_code ?? null\n const totalStr = `${fmtPrice(finalTotal, finalCurrency)}${taxSuffix(isCartTaxInclusive(shipped))}`\n const discountLine =\n mint.discount && mint.discount.amount > 0\n ? [` dev channel: −${fmtPrice(mint.discount.amount, finalCurrency)} (${mint.discount.pct}% off — for buying from the terminal/agent)`]\n : []\n const body = [\n \"checkout ready. open this link in your browser to pay:\",\n ` ${checkout_url}`,\n ...discountLine,\n ` total (incl. shipping): ${totalStr}`,\n \"\",\n \"payment details are entered on the storefront (Stripe) — they never enter this chat, and\",\n \"I can't complete this for you. once you've paid, call check_payment with:\",\n ` session_id: ${session_id}`,\n \"\",\n // brand-delight #9 — the deadpan flourish. The boundary stays honest (the line above), then\n // one dry true line: an agent can do everything up to the card. The card is the human's.\n \"I staged it, priced it, and walked you to the door. the card swipe is yours — by design.\",\n ].join(\"\\n\")\n // redact defensively (the url/session id carry no secret, but the redactor is the rule).\n return result(redactForOutput(body) as string, {\n checkout_url,\n session_id,\n total: finalTotal,\n currency: finalCurrency,\n discount: mint.discount ?? null,\n })\n },\n }\n\n // check_payment — poll ONCE; the agent re-calls at its own cadence (async handle-ID, F-I4).\n const check_payment: ToolDef = {\n name: \"check_payment\",\n description:\n \"Check a checkout's payment status ONCE (call again later to re-check — this never blocks/loops). \" +\n \"Returns the status and, once placed, the order_id.\",\n inputSchema: { session_id: z.string().describe(\"the session_id from checkout\") },\n handler: async (args): Promise<ToolResult> => {\n const sessionId = String(args.session_id ?? \"\")\n let res\n try {\n res = await client.poll(sessionId)\n } catch (e) {\n // A bad/expired session id 404s — map it to an actionable line, not a raw \"HTTP 404\" (D5).\n if (e instanceof StoreApiError && e.status === 404) {\n return errorText(\"that checkout session is unknown or expired — mint a fresh one with checkout.\")\n }\n throw e\n }\n const out = redactForOutput({ status: res.status, order_id: res.order_id })\n const human =\n res.status === \"placed\"\n ? `paid — order placed. order_id: ${out.order_id ?? \"—\"}`\n : `status: ${out.status} (call check_payment again in a moment if still pending)`\n return { content: [{ type: \"text\", text: human }], structuredContent: out as Record<string, unknown> }\n },\n }\n\n // get_order_status — status/timeline TEXT only; NO address (F-H3); id + exact-email, 404 on miss (F-H1).\n const get_order_status: ToolDef = {\n name: \"get_order_status\",\n description:\n \"Look up an order's status by its display id + the EXACT email on the order. Returns status only \" +\n \"(no shipping address). A wrong id/email returns not-found (no enumeration).\",\n inputSchema: {\n display_id: z.string().describe(\"the order's display id (e.g. 1042)\"),\n email: z.string().describe(\"the exact email on the order\"),\n },\n handler: async (args): Promise<ToolResult> => {\n const displayId = String(args.display_id ?? \"\")\n const email = String(args.email ?? \"\")\n // AUD-8: the shared client now POSTs to /store/order-lookup with the email in the BODY\n // (no longer a query string); getOrder still surfaces the 404→null (no-enumeration) contract.\n let looked: unknown | null\n try {\n looked = await client.getOrder(displayId, email)\n } catch (e) {\n if (e instanceof StoreApiError && e.status === 404) {\n looked = null\n } else {\n throw e\n }\n }\n if (!looked) {\n return errorText(`no order matches that id + email.`)\n }\n // Extract ONLY status/timeline fields — NEVER the shipping address (F-H3).\n const order = (looked as { order?: Record<string, unknown> }).order ?? (looked as Record<string, unknown>)\n const status = sanitize(String(order[\"status\"] ?? order[\"fulfillment_status\"] ?? \"unknown\"), 60)\n const display = sanitize(String(order[\"display_id\"] ?? displayId), 30)\n const lines = [`order #${display}`, `status: ${status}`]\n const fulfillment = order[\"fulfillment_status\"]\n if (typeof fulfillment === \"string\" && fulfillment) {\n lines.push(`fulfillment: ${sanitize(fulfillment, 60)}`)\n }\n // The output is redacted — and we never selected the address, so no PII can leak (G10/F-H3).\n return result(redactForOutput(lines.join(\"\\n\")) as string, {\n display_id: order[\"display_id\"] ?? display,\n status,\n fulfillment_status: typeof fulfillment === \"string\" ? fulfillment : null,\n })\n },\n }\n\n // report_issue — file a bug report / feedback to the maintainers (d14). ADDITIVE: it sends NO\n // payment/PII and cannot touch any order — the one way an agent can flag a problem. The message +\n // optional context are redacted SERVER-SIDE before they're emailed (G10); we return a reference.\n const report_issue: ToolDef = {\n name: \"report_issue\",\n description:\n \"File a bug report or feedback to the maintainers. Pass a short message (and optional \" +\n \"structured context like versions/ids). Returns a reference id. It sends no payment details \" +\n \"and cannot change any order — it just files the report. Tokens/emails are redacted server-side.\",\n inputSchema: {\n message: z.string().describe(\"what went wrong / the feedback (one or two sentences)\"),\n context: z\n // zod v4 requires an explicit key type: record(keyType, valueType). (Was record(value) —\n // a pre-existing TS2554 that tsup transpiled past; fixed so `npm run typecheck` is clean.)\n .record(z.string(), z.unknown())\n .optional()\n .describe(\"optional structured context (versions, ids) — tokens/emails are redacted server-side\"),\n },\n handler: async (args): Promise<ToolResult> => {\n const message = String(args.message ?? \"\").trim()\n if (!message) {\n return errorText(\"report_issue needs a non-empty message describing the problem.\")\n }\n const context =\n args.context && typeof args.context === \"object\" && !Array.isArray(args.context)\n ? (args.context as Record<string, unknown>)\n : undefined\n // Defense-in-depth (L22/N2): the backend redacts server-side, but redact the message + context\n // HERE too so a token/email an agent stuffed into them never leaves this process in the clear.\n const safeMessage = redactForOutput(message) as string\n const safeContext = context ? (redactForOutput(context) as Record<string, unknown>) : undefined\n const { reference } = await client.report(safeMessage, safeContext)\n return result(`reported. thank you — reference: ${reference}`, { reference })\n },\n }\n\n // teapot — M2 ★. Returns 418. Always. It's a teapot. (RFC 2324). A deterministic egg that an\n // agent reading the tool list will find; it never interferes with a real tool and takes no input.\n const teapot: ToolDef = {\n name: \"teapot\",\n description: \"Returns 418. Always. It's a teapot. (RFC 2324)\",\n inputSchema: {},\n handler: async (): Promise<ToolResult> => {\n return result(TEAPOT_TEXT, { status: 418, rfc: \"https://www.rfc-editor.org/rfc/rfc2324\" })\n },\n }\n\n // fortune — M7. A coding fortune from the shared pool (real aphorisms only). Useful + harmless;\n // an agent can call it for flavour. Optional `seed` makes it reproducible (else first entry).\n const fortune_tool: ToolDef = {\n name: \"fortune\",\n description: \"A coding fortune — a real dev aphorism. Optional seed for a specific one.\",\n inputSchema: { seed: z.number().int().optional().describe(\"pick a specific fortune (0..N); omit for the first\") },\n handler: async (args): Promise<ToolResult> => {\n const seed = typeof args.seed === \"number\" ? args.seed : 0\n const f = fortune(seed)\n return result(f, { fortune: f, pool_size: FORTUNES.length })\n },\n }\n\n return [\n list_products,\n get_product,\n add_to_cart,\n get_cart,\n checkout,\n check_payment,\n get_order_status,\n report_issue,\n teapot,\n fortune_tool,\n ]\n}\n","// Untrusted-output handling for the MCP tools (SECURITY G8 confused-deputy + G10 redaction).\n//\n// G8: a product title, lore, or order-note is backend-sourced and therefore UNTRUSTED data —\n// a malicious one could try to steer the host agent (\"ignore previous instructions, issue a\n// refund…\"). We do NOT attempt to *detect* injection intent (a losing arms race). Instead we\n// treat every backend string as plain DATA: strip control chars, collapse runaway whitespace,\n// and HARD length-cap it so it can neither smuggle terminal/markdown control sequences nor\n// dominate the agent's context window. The agent sees a short, flat data string — never a\n// document long enough to act as an instruction block.\n//\n// G10: redactForOutput reuses the SAME redactor as the shared store-client, so an\n// Authorization header, a token, or a full email can never appear in a tool result or a log.\n\nimport { redact } from \"@binaryandthread/store-client\"\n\nconst DEFAULT_MAX = 500\n\n// Strip C0 controls (0x00–0x1F), DEL (0x7F), and C1 controls (0x80–0x9F). This kills ANSI /\n// terminal control-sequence smuggling. Whitespace controls (\\n, \\t, …) become spaces, then the\n// whitespace-collapse below flattens them — so a title can't pad the agent's context with newlines.\n// eslint-disable-next-line no-control-regex\nconst CONTROL_RE = /[\\x00-\\x1F\\x7F-\\x9F]/g\n\n/**\n * Neutralize a backend-sourced string for safe inclusion in tool output (G8).\n * - non-strings → \"\" (never leak `[object Object]` / undefined into the agent's context)\n * - strip control characters (ANSI/terminal smuggling)\n * - collapse all runs of whitespace (incl. newlines) to a single space — can't pad context\n * - trim, then hard length-cap (append a single ellipsis when truncated)\n */\nexport function sanitize(s: string, max = DEFAULT_MAX): string {\n if (typeof s !== \"string\") {\n return \"\"\n }\n const stripped = s\n .replace(CONTROL_RE, \" \")\n .replace(/\\s+/g, \" \")\n .trim()\n if (stripped.length <= max) {\n return stripped\n }\n return stripped.slice(0, max) + \"…\"\n}\n\n/**\n * Deep-redact any value before it goes into a tool result or a log (G10). Reuses the\n * store-client redactor: masks Authorization/token/secret keys, strips Bearer tokens from\n * free strings, masks emails. Never throws (handles cyclic input).\n */\nexport function redactForOutput<T>(obj: T): T {\n return redact(obj)\n}\n","// MCP image path — ImageContent + the Blob-URL text floor, SSRF/token-DoS safe (SECURITY G9,\n// CLAUDE.md rule 6, ROBUSTNESS §6 / F-A3).\n//\n// Why BOTH an image block AND a URL text block: Claude renders ImageContent natively on\n// claude.ai / the API, but Claude Code has a base64-bloat bug (#31208, 15–25k tokens/image) and\n// can fail to render it. So the Blob URL is ALWAYS emitted as a text block too — the reliable\n// floor (the buy is never blocked on rendering).\n//\n// SECURITY G9 — the fetch is the dangerous part, so it is gated HARD:\n// 1. host allowlist (reused verbatim from @binaryandthread/store-client — the SAME constant the\n// CLI uses) BEFORE any network call. A non-allowlisted host / look-alike subdomain → no fetch.\n// 2. scheme MUST be https — a `data:` or `http:` URL → no fetch (no inline payload, no plaintext).\n// 3. a byte cap (≤2MB) + a fetch timeout — over-cap or slow → URL-text-only (token-DoS guard).\n// 4. content-type must be an image — refuse to base64 a non-image payload.\n// On ANY of those (or any failure) we degrade to URL-text-only and fetch NOTHING. It NEVER throws.\n\nimport { isAllowedImageHost } from \"@binaryandthread/store-client\"\n\n/** ≤2MB — over this we return the URL only (token-DoS guard, G9). */\nexport const IMAGE_BYTE_CAP = 2 * 1024 * 1024\n/** Fetch timeout in ms — a slow/hung host must not stall the tool (a list/view is read-mostly). */\nexport const IMAGE_FETCH_TIMEOUT_MS = 5000\n\n/** An MCP content block — either an inline image or a text line. */\nexport type ImageBlock =\n | { type: \"image\"; data: string; mimeType: string }\n | { type: \"text\"; text: string }\n\nexport interface BuildImageOpts {\n /** Inject a fetch impl (tests). Defaults to global fetch. */\n fetchImpl?: typeof fetch\n /** Override the byte cap (tests). */\n byteCap?: number\n /** Override the timeout (tests). */\n timeoutMs?: number\n}\n\n/** The URL-text-only floor — always carries the Blob URL so the agent can hand it to the human. */\nfunction urlOnly(blobUrl: string): ImageBlock[] {\n return [{ type: \"text\", text: blobUrl }]\n}\n\n/** The \"we couldn't render it\" floor — still carries the URL (never blocks the buy). */\nfunction unavailable(blobUrl: string): ImageBlock[] {\n return [{ type: \"text\", text: `(image unavailable) ${blobUrl}` }]\n}\n\n/**\n * Build the content blocks for a single product image. Returns the Blob URL text floor in EVERY\n * case; adds an ImageContent block ONLY when the URL is an allowlisted-host https Blob URL that\n * fetches successfully within the byte cap + timeout as a real image. NEVER throws, NEVER fetches\n * a non-allowlisted / data: / non-https URL.\n */\nexport async function buildImageContent(\n blobUrl: string,\n opts: BuildImageOpts = {}\n): Promise<ImageBlock[]> {\n if (typeof blobUrl !== \"string\" || blobUrl.trim().length === 0) {\n return urlOnly(String(blobUrl ?? \"\"))\n }\n\n // (1) + (2): host allowlist + https-only, BEFORE any network call (G9 / rule 6).\n // isAllowedImageHost already returns false for an unparseable URL; we also require https.\n let scheme: string\n try {\n scheme = new URL(blobUrl).protocol\n } catch {\n return urlOnly(blobUrl)\n }\n if (scheme !== \"https:\") {\n return urlOnly(blobUrl) // data:, http:, etc. — never fetch (no SSRF, no plaintext, no inline payload)\n }\n if (!isAllowedImageHost(blobUrl)) {\n return urlOnly(blobUrl) // non-allowlisted host or look-alike subdomain — never fetch\n }\n\n const fetchImpl = opts.fetchImpl ?? (globalThis.fetch as typeof fetch)\n const byteCap = opts.byteCap ?? IMAGE_BYTE_CAP\n const timeoutMs = opts.timeoutMs ?? IMAGE_FETCH_TIMEOUT_MS\n\n // (3) + (4): the gated fetch. Any failure → URL-text floor (never throws — F-A3).\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const res = await fetchImpl(blobUrl as unknown as string, { signal: controller.signal })\n if (!res.ok) {\n return urlOnly(blobUrl)\n }\n const mimeType = (res.headers.get(\"content-type\") ?? \"\").split(\";\")[0]!.trim().toLowerCase()\n if (!mimeType.startsWith(\"image/\")) {\n return urlOnly(blobUrl) // refuse to base64 a non-image payload\n }\n // (M4) Reject an over-cap body BEFORE buffering it into memory, when the server declares its size.\n // The post-buffer check below still applies (Content-Length can be absent or wrong) — this just\n // avoids reading a known-too-large body at all.\n const declaredLen = Number.parseInt(res.headers.get(\"content-length\") ?? \"\", 10)\n if (Number.isFinite(declaredLen) && declaredLen > byteCap) {\n return urlOnly(blobUrl)\n }\n const buf = Buffer.from(await res.arrayBuffer())\n if (buf.byteLength === 0 || buf.byteLength > byteCap) {\n return urlOnly(blobUrl) // empty or over-cap → token-DoS guard\n }\n return [\n { type: \"image\", data: buf.toString(\"base64\"), mimeType },\n { type: \"text\", text: blobUrl }, // the reliable floor (bug #31208)\n ]\n } catch {\n return unavailable(blobUrl)\n } finally {\n clearTimeout(timer)\n }\n}\n","// MCP egg copy (VOICE §3.2). The MCP surface is read by AGENTS and the humans driving them, so its\n// eggs live in tool descriptions (devs read schemas) and in deterministic tool payloads. They NEVER\n// interfere with a real tool and never touch money.\n//\n// This mirrors the CLI's governed copy (packages/cli/src/eggs.ts). It is intentionally duplicated:\n// the MCP package composes over store-client only (not the CLI), so each channel owns its copy.\n// Brand voice (memory brand-voice-no-pod-no-tshirt): never \"t-shirt\"/\"tee\"/\"print\"/Gelato.\n\n/** M2 ★ — the bt.teapot tool body. Returns 418. Always. It's a teapot. (RFC 2324) */\nexport const TEAPOT_TEXT = \"418 i'm a teapot. we make clothing, not coffee. (RFC 2324)\"\n\n/** C10 / M7 — the shared fortune pool. REAL dev aphorisms only (gate 1: never invented). */\nexport const FORTUNES: ReadonlyArray<string> = [\n '// \"there are only two hard things in computer science: cache invalidation and naming things.\" — phil karlton',\n '// \"premature optimization is the root of all evil.\" — donald knuth',\n '// \"weeks of programming can save you hours of planning.\"',\n '// \"make it work, make it right, make it fast.\" — kent beck',\n '// \"talk is cheap. show me the code.\" — linus torvalds',\n '// \"given enough eyeballs, all bugs are shallow.\" — linus\\'s law',\n '// \"always code as if the person maintaining it is a violent psychopath who knows where you live.\" — john woods',\n '// \"it works on my machine.\"',\n]\n\n/** Deterministic fortune by seed (reproducible for tests + screenshots). */\nexport function fortune(seed = 0): string {\n const n = Number.isFinite(seed) ? Math.abs(Math.floor(seed)) : 0\n return FORTUNES[n % FORTUNES.length]!\n}\n","// Resolve the MCP server's transport config from the environment, falling back to the baked-in\n// PUBLIC production defaults so a host MCP config needs ZERO env to reach the live store\n// (d2 / P4.5-7b). Env ALWAYS wins (set BT_STORE_URL / BT_PUBLISHABLE_KEY for local dev / staging).\n// The JWT (BT_TOKEN) and region (BT_REGION) stay env-only — never baked, never read from chat (T8).\n\nimport { BT_DEFAULT_STORE_URL, BT_DEFAULT_PUBLISHABLE_KEY } from \"@binaryandthread/store-client\"\n\nexport interface ResolvedServerConfig {\n baseUrl: string\n publishableKey: string\n jwt?: string\n region?: string\n}\n\nexport function resolveServerConfig(env: NodeJS.ProcessEnv = process.env): ResolvedServerConfig {\n return {\n baseUrl: env.BT_STORE_URL || BT_DEFAULT_STORE_URL,\n publishableKey: env.BT_PUBLISHABLE_KEY || BT_DEFAULT_PUBLISHABLE_KEY,\n jwt: env.BT_TOKEN || undefined,\n region: env.BT_REGION || undefined,\n }\n}\n","// npx @binaryandthread/mcp — the stdio entrypoint.\n//\n// Config comes from the ENVIRONMENT only (SECURITY T8/§4); all of it has a baked-in default so\n// the server reaches the live store with ZERO env (d2 / P4.5-7b):\n// - BT_STORE_URL the Medusa Store API base URL (default: prod; override for dev)\n// - BT_PUBLISHABLE_KEY the publishable API key — PUBLIC by design (default: prod; T6)\n// - BT_TOKEN the customer JWT (OPTIONAL; from the OS keychain / a configured env)\n// - BT_REGION an ISO-2 country code to resolve pricing (OPTIONAL; never hardcoded)\n//\n// The JWT is read from the env the host MCP config injects (which a keychain helper can populate)\n// — it is NEVER read from the chat (T8). A secret key is rejected by the StoreClient (T6). On\n// stdio, NOTHING is written to stdout except MCP protocol frames — diagnostics go to stderr.\n\nimport { StoreClient } from \"@binaryandthread/store-client\"\nimport { redact } from \"@binaryandthread/store-client\"\nimport { startServer } from \"./server\"\nimport { makeTools } from \"./tools\"\nimport { resolveServerConfig } from \"./config\"\n\nasync function main(): Promise<void> {\n const { baseUrl, publishableKey, jwt, region } = resolveServerConfig()\n\n const client = new StoreClient({\n baseUrl,\n publishableKey,\n jwt,\n // All client logs route to stderr through redact() — stdout is reserved for the transport,\n // and no token/PII ever reaches a log line (G10).\n logger: {\n info: (...a) => process.stderr.write(JSON.stringify(redact(a)) + \"\\n\"),\n warn: (...a) => process.stderr.write(JSON.stringify(redact(a)) + \"\\n\"),\n error: (...a) => process.stderr.write(JSON.stringify(redact(a)) + \"\\n\"),\n },\n })\n\n const tools = makeTools(client, { region })\n await startServer(tools)\n process.stderr.write(`bt-mcp: ready on stdio — ${tools.length} tools registered.\\n`)\n}\n\nmain().catch((e) => {\n // Redact before printing — never leak a token in a crash trace.\n process.stderr.write(`bt-mcp: fatal: ${JSON.stringify(redact(String(e?.message ?? e)))}\\n`)\n process.exit(1)\n})\n"],"mappings":"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","names":["out","stage"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@binaryandthread/mcp",
3
- "version": "0.1.6",
3
+ "version": "0.3.0",
4
4
  "description": "Binary & Thread MCP server — buy a piece from inside your AI agent. 10 deterministic, read-mostly tools over the shared Store API client; the card never enters the chat and the agent can never self-complete a purchase.",
5
5
  "private": false,
6
6
  "type": "module",