@carrierllc/mcp 0.13.24 → 0.13.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carrierllc/mcp",
3
- "version": "0.13.24",
3
+ "version": "0.13.26",
4
4
  "description": "Carrier MCP \u2014 natural-language control of MVNO/eSIM fleets via eSIMVault OCS. Stdio mode for direct integration with Claude Desktop, Cursor, Windsurf, and MCP-compatible clients. Ships the `carrier` CLI (plugin install + white-label eSIM storefront scaffold).",
5
5
  "license": "MIT",
6
6
  "author": "Carrier (Lifecycle Innovations Limited)",
@@ -51,7 +51,7 @@ MCP over stdio (these drive host CLIs on your machine, so they are local-only):
51
51
 
52
52
  ```
53
53
  list_deploy_targets dir: ./storefront
54
- deploy_storefront dir: ./storefront [target, name, skip_build, push_secrets, verify, domain]
54
+ deploy_storefront dir: ./storefront [target, name, force, skip_build, push_secrets, verify, domain]
55
55
  provision_storefront_clerk dir: ./storefront [name, production, no_create, url]
56
56
  ```
57
57
 
@@ -102,7 +102,32 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
102
102
  }
103
103
  const channel = normalizeChannel(body.channel);
104
104
  const stripeKey = process.env.STRIPE_SECRET_KEY;
105
- const catalog = await fetchTemplateCatalog();
105
+ // The deployment selects the public shop, never request-body scope. Read
106
+ // its canonical id and plans together so fulfilment cannot fall back to
107
+ // the legacy catalog or charge a stale/foreign tenant's price.
108
+ const slug = process.env.NEXT_PUBLIC_STOREFRONT_SLUG;
109
+ let storefrontId: string | null = null;
110
+ let catalog;
111
+ if (slug) {
112
+ const base = (process.env.NEXT_PUBLIC_CARRIER_API_URL ?? "").replace(/\/+$/, "");
113
+ if (!base || !/^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/.test(slug)) {
114
+ return NextResponse.json({ error: "Storefront unavailable" }, { status: 503 });
115
+ }
116
+ const response = await fetch(`${base}/api/hosted/${encodeURIComponent(slug)}`, { cache: "no-store" });
117
+ if (!response.ok) return NextResponse.json({ error: "Storefront unavailable" }, { status: 503 });
118
+ const shop = await response.json();
119
+ if (!shop || typeof shop !== "object" || Array.isArray(shop) ||
120
+ typeof shop.id !== "string" || !/^sf_[A-Za-z0-9_-]{20}$/.test(shop.id) ||
121
+ typeof shop.name !== "string" || !shop.name.trim() || typeof shop.url !== "string" ||
122
+ (shop.status !== undefined && shop.status !== "published") ||
123
+ shop.slug !== slug || !Array.isArray(shop.plans)) {
124
+ return NextResponse.json({ error: "Storefront unavailable" }, { status: 503 });
125
+ }
126
+ storefrontId = shop.id;
127
+ catalog = { source: "live" as const, templates: shop.plans };
128
+ } else {
129
+ catalog = await fetchTemplateCatalog();
130
+ }
106
131
 
107
132
  const decision = decideGuestCheckout({
108
133
  catalog,
@@ -179,6 +204,10 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
179
204
  // session-level copy is what fulfilment keys off to provision the eSIM —
180
205
  // without it the plan is lost and the order is stuck "activating".
181
206
  "metadata[templateId]": templateId,
207
+ ...(storefrontId ? {
208
+ "metadata[storefront_id]": storefrontId,
209
+ "payment_intent_data[metadata][storefront_id]": storefrontId,
210
+ } : {}),
182
211
  "payment_intent_data[metadata][templateId]": templateId,
183
212
  // Where the sale came from. Written alongside templateId and by the same
184
213
  // reasoning: attribution recovered later from Stripe reports is guesswork,
@@ -102,6 +102,14 @@ function apiKey(): string {
102
102
  return process.env.CARRIER_API_KEY ?? "";
103
103
  }
104
104
 
105
+ /** Order tenant scope comes only from deployment config, never buyer input. */
106
+ function orderPath(orderId: string, action = ""): string | null {
107
+ const path = `/api/orders/${encodeURIComponent(orderId)}${action}`;
108
+ const slug = process.env.NEXT_PUBLIC_STOREFRONT_SLUG;
109
+ if (slug && !/^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/.test(slug)) return null;
110
+ return slug ? `${path}?storefront=${encodeURIComponent(slug)}` : path;
111
+ }
112
+
105
113
  function sampleCatalog(reason: CatalogDegradedReason): TemplateCatalog {
106
114
  return { templates: MOCK_TEMPLATES, source: "sample", reason };
107
115
  }
@@ -190,12 +198,15 @@ export async function fetchOrder(orderId: string): Promise<OrderLookup> {
190
198
  if (!base) return { ok: false, reason: "unconfigured" };
191
199
  if (!/^[A-Za-z0-9_-]{1,256}$/.test(orderId)) return { ok: false, reason: "not-found" };
192
200
 
201
+ const path = orderPath(orderId);
202
+ if (path === null) return { ok: false, reason: "unconfigured" };
203
+
193
204
  const headers: Record<string, string> = { Accept: "application/json" };
194
205
  const key = apiKey();
195
206
  if (key) headers.Authorization = `Bearer ${key}`;
196
207
 
197
208
  try {
198
- const res = await fetch(`${base}/api/orders/${encodeURIComponent(orderId)}`, {
209
+ const res = await fetch(`${base}${path}`, {
199
210
  headers,
200
211
  cache: "no-store",
201
212
  });
@@ -228,12 +239,15 @@ async function orderScopedCall<T>(
228
239
  if (!base) return { ok: false, reason: "unconfigured" };
229
240
  if (!/^[A-Za-z0-9_-]{1,256}$/.test(orderId)) return { ok: false, reason: "not-found" };
230
241
 
242
+ const scopedPath = orderPath(orderId, path);
243
+ if (scopedPath === null) return { ok: false, reason: "unconfigured" };
244
+
231
245
  const headers: Record<string, string> = { Accept: "application/json", ...(init?.headers as Record<string, string>) };
232
246
  const key = apiKey();
233
247
  if (key) headers.Authorization = `Bearer ${key}`;
234
248
 
235
249
  try {
236
- const res = await fetch(`${base}/api/orders/${encodeURIComponent(orderId)}${path}`, {
250
+ const res = await fetch(`${base}${scopedPath}`, {
237
251
  ...init,
238
252
  headers,
239
253
  cache: "no-store",