@solvapay/mcp-core 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -238,6 +238,9 @@ function limitsAsSignals(limits) {
238
238
  if (!limits) return null;
239
239
  return limits;
240
240
  }
241
+ function creditsUnused(customer) {
242
+ return !(0, import_server.creditSignals)(limitsAsSignals(customer?.limits)).isCreditBased;
243
+ }
241
244
  function preferLimitsPlan(purchase, limits) {
242
245
  const limitsRef = limits?.planRef;
243
246
  if (!limitsRef) return false;
@@ -252,6 +255,12 @@ function balanceSummary(customer) {
252
255
  if (!row) return null;
253
256
  return row.replace(/^Balance:\s*/, "");
254
257
  }
258
+ function identityRow(customer) {
259
+ const ref = customer?.ref?.trim();
260
+ if (!ref) return null;
261
+ const email = customer?.email?.trim();
262
+ return email ? `Signed in as: ${email} \xB7 ${ref}` : `Signed in as: ${ref}`;
263
+ }
255
264
  function balanceRow(customer) {
256
265
  if (!customer?.balance) return null;
257
266
  const credits = customer.balance.credits ?? 0;
@@ -593,8 +602,7 @@ function narrateAccountBody(input) {
593
602
  } else {
594
603
  position = "After your first call";
595
604
  }
596
- const creditsUnused = !(0, import_server.creditSignals)(limitsAsSignals(customer?.limits)).isCreditBased;
597
- return `${product} is on ${planName}${priceBit}. ${position}. ` + (creditsUnused ? `Credits are not used on this plan. Call \`${VIEWER_TOOL_NAME}\` with view: 'checkout' to switch.` : `Call \`${VIEWER_TOOL_NAME}\` with view: 'checkout' to switch.`);
605
+ return `${product} is on ${planName}${priceBit}. ${position}. ` + (creditsUnused(customer) ? `Credits are not used on this plan. Call \`${VIEWER_TOOL_NAME}\` with view: 'checkout' to switch.` : `Call \`${VIEWER_TOOL_NAME}\` with view: 'checkout' to switch.`);
598
606
  }
599
607
  if (state === "E") {
600
608
  const left2 = remainingOfTotal(usage);
@@ -617,7 +625,7 @@ function narrateAccountBody(input) {
617
625
  return otherShape !== "free" && otherShape !== "trial";
618
626
  });
619
627
  const carryOn = others.length > 0 ? ` ${joinOr(others.map((item) => carryOnFragment(item, customer)))}.` : "";
620
- const antiTrap = credits > 0 && planShape !== "usage-based" ? ` Adding credits will not help, because ${planName} does not spend them.` : "";
628
+ const antiTrap = creditsUnused(customer) ? ` Adding credits will not help, because ${planName} does not spend them.` : "";
621
629
  const priceBit = planPriceBit(plan);
622
630
  return `${product} is on ${planName}${priceBit}. ${capBit}.${consequence}.${antiTrap}${carryOn} Call \`${VIEWER_TOOL_NAME}\` with view: 'checkout' to switch plan.`;
623
631
  }
@@ -704,6 +712,8 @@ function narrateManageAccount(data, options) {
704
712
  now
705
713
  })
706
714
  );
715
+ const identity = identityRow(customer);
716
+ if (identity) lines.push(identity);
707
717
  if (state === "A" || state === "H") {
708
718
  const bal = balanceRow(customer);
709
719
  if (bal) lines.push(bal);
@@ -904,6 +914,15 @@ function enrichPurchase(purchase) {
904
914
  ...enrichedPlanSnapshot !== void 0 ? { planSnapshot: enrichedPlanSnapshot } : {}
905
915
  };
906
916
  }
917
+ function signedInCustomerRefOnly(getCustomerRef) {
918
+ return (extra) => {
919
+ const ref = getCustomerRef(extra);
920
+ if (typeof ref !== "string") return null;
921
+ const trimmed = ref.trim();
922
+ if (!trimmed || trimmed === "anonymous") return null;
923
+ return trimmed;
924
+ };
925
+ }
907
926
  function defaultGetCustomerRef(extra) {
908
927
  const candidates = [
909
928
  extra?.http?.authInfo?.extra?.customer_ref,
@@ -1029,13 +1048,9 @@ var VIEW_PRIORITY = ["checkout", "topup", "account"];
1029
1048
  function hasActivePlan(customer, productRef) {
1030
1049
  return selectActivePlanPurchase(customer?.purchase?.purchases, productRef) !== null;
1031
1050
  }
1032
- function isOutOfCredits(customer) {
1033
- const credits = customer?.balance?.credits;
1034
- return credits === 0;
1035
- }
1036
1051
  function deriveDefaultView(data, enabledViews = new Set(SOLVAPAY_MCP_ADVERTISED_VIEW_KINDS)) {
1037
1052
  const customer = data.customer;
1038
- const preferred = !hasActivePlan(customer, data.productRef) ? "checkout" : isOutOfCredits(customer) ? "topup" : "account";
1053
+ const preferred = !hasActivePlan(customer, data.productRef) ? "checkout" : customer?.nextAction === "topup" ? "topup" : "account";
1039
1054
  if (enabledViews.has(preferred)) return preferred;
1040
1055
  for (const view of VIEW_PRIORITY) {
1041
1056
  if (enabledViews.has(view)) return view;
@@ -1251,12 +1266,10 @@ var createBootstrapProductError = (productResult) => {
1251
1266
  return err;
1252
1267
  };
1253
1268
  function createBuildBootstrapPayload(options) {
1254
- const {
1255
- solvaPay,
1256
- productRef,
1257
- publicBaseUrl,
1258
- getCustomerRef = defaultGetCustomerRef
1259
- } = options;
1269
+ const { solvaPay, productRef, publicBaseUrl } = options;
1270
+ const getCustomerRef = signedInCustomerRefOnly(
1271
+ options.getCustomerRef ?? defaultGetCustomerRef
1272
+ );
1260
1273
  const fetchPublishableKey = async () => {
1261
1274
  try {
1262
1275
  const platform = await solvaPay.apiClient.getPlatformConfig?.();
@@ -1311,18 +1324,16 @@ function createBuildBootstrapPayload(options) {
1311
1324
  }
1312
1325
  }
1313
1326
  const checkoutPurpose = view === "topup" || view !== "checkout" && view !== "auto-recharge" && limits?.paywallReason === "topup_required" ? "credit_topup" : void 0;
1314
- const checkoutResult = await wrapError(
1327
+ const checkoutResult = customerRef ? await wrapError(
1315
1328
  createMcpCheckoutSession(
1316
- buildSolvaPayRequest(extra, {
1317
- getCustomerRef: () => customerRef ?? "anonymous"
1318
- }),
1329
+ buildRequest(extra),
1319
1330
  {
1320
1331
  productRef,
1321
1332
  ...checkoutPurpose ? { purpose: checkoutPurpose } : {}
1322
1333
  },
1323
1334
  { solvaPay }
1324
1335
  )
1325
- );
1336
+ ) : await unauthenticated();
1326
1337
  if ((0, import_server4.isErrorResult)(merchantResult)) {
1327
1338
  throw createBootstrapMerchantError(merchantResult);
1328
1339
  }
@@ -1345,19 +1356,16 @@ function createBuildBootstrapPayload(options) {
1345
1356
  } : null;
1346
1357
  const activePurchase2 = selectActivePlanPurchase(enrichedPurchase?.purchases, productRef);
1347
1358
  const usage = customerRef ? (0, import_server4.deriveUsageSnapshot)({
1348
- // Consumption comes from `limits.used` (or `limit - remaining`)
1349
- // inside `deriveUsageSnapshot`. `purchase.usage.used` is only
1350
- // ever reset to zero by the backend and must not be the source.
1351
- used: 0,
1352
1359
  periodStart: activePurchase2?.usage?.periodStart,
1353
1360
  periodEnd: activePurchase2?.usage?.periodEnd,
1354
1361
  purchaseRef: activePurchase2?.reference,
1355
1362
  limits
1356
1363
  }) : null;
1357
1364
  const signals = limits ? (0, import_server4.creditSignals)(limits) : null;
1358
- const state = limits ? (0, import_server4.classifyPaywallState)(limits) : null;
1359
1365
  const customer = customerRef ? {
1360
1366
  ref: customerRef,
1367
+ email: purchase?.email?.trim() ? purchase.email.trim() : null,
1368
+ name: purchase?.name?.trim() ? purchase.name.trim() : null,
1361
1369
  purchase: enrichedPurchase,
1362
1370
  paymentMethod: okOrNull(paymentMethodResult),
1363
1371
  balance: okOrNull(balanceResult),
@@ -1367,8 +1375,9 @@ function createBuildBootstrapPayload(options) {
1367
1375
  ...signals?.remainingCalls !== void 0 ? { remainingCalls: signals.remainingCalls } : {},
1368
1376
  ...signals?.creditsPerCall !== void 0 ? { creditsPerCall: signals.creditsPerCall } : {},
1369
1377
  ...signals?.shortfallCredits !== void 0 ? { shortfallCredits: signals.shortfallCredits } : {},
1378
+ ...signals ? { isCreditBased: signals.isCreditBased } : {},
1370
1379
  ...limits?.autoRecharge !== void 0 ? { autoRecharge: limits.autoRecharge } : {},
1371
- ...state ? { nextAction: (0, import_server4.nextActionFor)(state) } : {}
1380
+ ...limits?.withinLimits === false ? { nextAction: (0, import_server4.nextActionFor)((0, import_server4.classifyPaywallState)(limits)) } : {}
1372
1381
  } : null;
1373
1382
  const checkout = okOrNull(checkoutResult);
1374
1383
  const portal = okOrNull(portalResult);
@@ -1479,11 +1488,13 @@ function buildSolvaPayDescriptors(options) {
1479
1488
  views = DEFAULT_VIEWS,
1480
1489
  csp,
1481
1490
  apiBaseUrl,
1482
- getCustomerRef = defaultGetCustomerRef,
1483
1491
  onToolCall,
1484
1492
  onToolResult,
1485
1493
  branding
1486
1494
  } = options;
1495
+ const getCustomerRef = signedInCustomerRefOnly(
1496
+ options.getCustomerRef ?? defaultGetCustomerRef
1497
+ );
1487
1498
  const toolIcons = deriveIcons(branding);
1488
1499
  if (!/^https?:\/\//i.test(publicBaseUrl)) {
1489
1500
  throw new Error(
@@ -1561,6 +1572,11 @@ function buildSolvaPayDescriptors(options) {
1561
1572
  publicBaseUrl,
1562
1573
  getCustomerRef
1563
1574
  });
1575
+ const buildDefaultBootstrapPayload = async (extra) => {
1576
+ const data = await buildBootstrapPayload("account", extra);
1577
+ data.view = deriveDefaultView(data, enabledViews);
1578
+ return data;
1579
+ };
1564
1580
  const advertisedViewList = SOLVAPAY_MCP_ADVERTISED_VIEW_KINDS.filter(
1565
1581
  (view) => enabledViews.has(view)
1566
1582
  );
@@ -1590,10 +1606,9 @@ function buildSolvaPayDescriptors(options) {
1590
1606
  });
1591
1607
  }
1592
1608
  const mode = parseMode(args.mode);
1593
- const data = await buildBootstrapPayload(requested ?? "account", extra);
1594
- const view = requested ?? deriveDefaultView(data, enabledViews);
1595
- data.view = view;
1596
- return narratedToolResult(view, data, mode, {
1609
+ const data = requested === void 0 ? await buildDefaultBootstrapPayload(extra) : await buildBootstrapPayload(requested, extra);
1610
+ if (requested !== void 0) data.view = requested;
1611
+ return narratedToolResult(data.view, data, mode, {
1597
1612
  ...toolMeta,
1598
1613
  "openai/widgetSessionId": crypto.randomUUID()
1599
1614
  });
@@ -1950,10 +1965,11 @@ function buildSolvaPayDescriptors(options) {
1950
1965
  title: "SolvaPay bootstrap",
1951
1966
  description: "Current merchant/product/plans/customer snapshot for the embedded UI. Widgets read this idempotently when the host scrubs structuredContent from tool results.",
1952
1967
  mimeType: SOLVAPAY_BOOTSTRAP_MIME_TYPE,
1953
- // View is an echoed routing label — the widget resolves the actual
1954
- // surface from host context (`inferViewFromHost`), so any view kind
1955
- // produces identical merchant/product/plans/customer data.
1956
- readPayload: (extra) => buildBootstrapPayload("account", extra)
1968
+ // View is the server's default for this customer (checkout with no
1969
+ // active plan, topup on a top-up gate, account otherwise) — the same
1970
+ // choice the viewer makes when `view` is omitted. The widget trusts
1971
+ // this field; it does not resolve the surface from host context.
1972
+ readPayload: (extra) => buildDefaultBootstrapPayload(extra)
1957
1973
  };
1958
1974
  return { tools, resource, prompts, docsResources, bootstrapResource, buildBootstrapPayload };
1959
1975
  }
package/dist/index.d.cts CHANGED
@@ -82,6 +82,14 @@ type BootstrapPlan = components['schemas']['Plan'];
82
82
  */
83
83
  interface BootstrapCustomer {
84
84
  ref: string;
85
+ /**
86
+ * Signed-in identity from the customer record. Projected off the
87
+ * purchase-check result, which carries them only because that helper
88
+ * looks the customer up. Null when that sub-read returned none.
89
+ * The whole `customer` is null for an anonymous caller.
90
+ */
91
+ email: string | null;
92
+ name: string | null;
85
93
  purchase: PurchaseCheckResult | null;
86
94
  paymentMethod: PaymentMethodInfo | null;
87
95
  balance: CustomerBalanceResult | null;
@@ -101,9 +109,14 @@ interface BootstrapCustomer {
101
109
  creditsPerCall?: number;
102
110
  /** `max(0, creditsPerCall - balance)` when both are known. */
103
111
  shortfallCredits?: number;
112
+ /**
113
+ * True when the limits response carries credit-balance fields.
114
+ * False means the plan does not spend credits.
115
+ */
116
+ isCreditBased?: boolean;
104
117
  /** Per-provider auto-recharge snapshot from the limits response. */
105
118
  autoRecharge?: LimitAutoRechargeDto;
106
- /** Same next action the gate would name for this limits result. */
119
+ /** Present only when the latest `checkLimits` blocked the call. */
107
120
  nextAction?: PaywallNextAction;
108
121
  }
109
122
  /**
@@ -725,16 +738,6 @@ declare const ToolErrorEnvelopeSchema: z.ZodObject<{
725
738
  * programmatic consumers (e.g. the React transport).
726
739
  */
727
740
  declare function enrichPurchase(purchase: Record<string, unknown>): Record<string, unknown>;
728
- /**
729
- * Default extractor for `customer_ref` out of the MCP OAuth bridge
730
- * (what `auth-bridge.ts` populates), trimmed. Returns `null` when no
731
- * ref is present.
732
- *
733
- * Checks `extra.http.authInfo` first — the official SDK v2 location —
734
- * then falls back to the flat v1 `extra.authInfo` still emitted by some
735
- * third-party adapters. Reading only the flat location silently
736
- * de-authenticated every tool call under SDK v2.
737
- */
738
741
  declare function defaultGetCustomerRef(extra?: McpToolExtra): string | null;
739
742
  interface BuildSolvaPayRequestOptions {
740
743
  method?: string;
@@ -911,7 +914,7 @@ interface PurchaseShape {
911
914
  };
912
915
  }
913
916
  interface UsageShape {
914
- used?: number;
917
+ used?: number | null;
915
918
  total?: number | null;
916
919
  remaining?: number | null;
917
920
  periodEnd?: string;
@@ -934,7 +937,9 @@ interface LimitsShape {
934
937
  };
935
938
  }
936
939
  interface CustomerShape {
937
- ref?: string;
940
+ ref?: string | null;
941
+ email?: string | null;
942
+ name?: string | null;
938
943
  balance?: {
939
944
  credits?: number | null;
940
945
  displayCurrency?: string;
@@ -984,7 +989,7 @@ declare function uiPlaceholder(view: SolvaPayMcpViewKind, data: BootstrapPayload
984
989
  * The three surfaces share one payload; omitting `view` must still land
985
990
  * somewhere useful instead of failing. Rule:
986
991
  * - no active plan → checkout (plans)
987
- * - active plan and zero credits → topup
992
+ * - active plan and blocked by a top-up gate → topup
988
993
  * - otherwise → account
989
994
  *
990
995
  * When the preferred view is disabled on this server, fall through the
package/dist/index.d.ts CHANGED
@@ -82,6 +82,14 @@ type BootstrapPlan = components['schemas']['Plan'];
82
82
  */
83
83
  interface BootstrapCustomer {
84
84
  ref: string;
85
+ /**
86
+ * Signed-in identity from the customer record. Projected off the
87
+ * purchase-check result, which carries them only because that helper
88
+ * looks the customer up. Null when that sub-read returned none.
89
+ * The whole `customer` is null for an anonymous caller.
90
+ */
91
+ email: string | null;
92
+ name: string | null;
85
93
  purchase: PurchaseCheckResult | null;
86
94
  paymentMethod: PaymentMethodInfo | null;
87
95
  balance: CustomerBalanceResult | null;
@@ -101,9 +109,14 @@ interface BootstrapCustomer {
101
109
  creditsPerCall?: number;
102
110
  /** `max(0, creditsPerCall - balance)` when both are known. */
103
111
  shortfallCredits?: number;
112
+ /**
113
+ * True when the limits response carries credit-balance fields.
114
+ * False means the plan does not spend credits.
115
+ */
116
+ isCreditBased?: boolean;
104
117
  /** Per-provider auto-recharge snapshot from the limits response. */
105
118
  autoRecharge?: LimitAutoRechargeDto;
106
- /** Same next action the gate would name for this limits result. */
119
+ /** Present only when the latest `checkLimits` blocked the call. */
107
120
  nextAction?: PaywallNextAction;
108
121
  }
109
122
  /**
@@ -725,16 +738,6 @@ declare const ToolErrorEnvelopeSchema: z.ZodObject<{
725
738
  * programmatic consumers (e.g. the React transport).
726
739
  */
727
740
  declare function enrichPurchase(purchase: Record<string, unknown>): Record<string, unknown>;
728
- /**
729
- * Default extractor for `customer_ref` out of the MCP OAuth bridge
730
- * (what `auth-bridge.ts` populates), trimmed. Returns `null` when no
731
- * ref is present.
732
- *
733
- * Checks `extra.http.authInfo` first — the official SDK v2 location —
734
- * then falls back to the flat v1 `extra.authInfo` still emitted by some
735
- * third-party adapters. Reading only the flat location silently
736
- * de-authenticated every tool call under SDK v2.
737
- */
738
741
  declare function defaultGetCustomerRef(extra?: McpToolExtra): string | null;
739
742
  interface BuildSolvaPayRequestOptions {
740
743
  method?: string;
@@ -911,7 +914,7 @@ interface PurchaseShape {
911
914
  };
912
915
  }
913
916
  interface UsageShape {
914
- used?: number;
917
+ used?: number | null;
915
918
  total?: number | null;
916
919
  remaining?: number | null;
917
920
  periodEnd?: string;
@@ -934,7 +937,9 @@ interface LimitsShape {
934
937
  };
935
938
  }
936
939
  interface CustomerShape {
937
- ref?: string;
940
+ ref?: string | null;
941
+ email?: string | null;
942
+ name?: string | null;
938
943
  balance?: {
939
944
  credits?: number | null;
940
945
  displayCurrency?: string;
@@ -984,7 +989,7 @@ declare function uiPlaceholder(view: SolvaPayMcpViewKind, data: BootstrapPayload
984
989
  * The three surfaces share one payload; omitting `view` must still land
985
990
  * somewhere useful instead of failing. Rule:
986
991
  * - no active plan → checkout (plans)
987
- * - active plan and zero credits → topup
992
+ * - active plan and blocked by a top-up gate → topup
988
993
  * - otherwise → account
989
994
  *
990
995
  * When the preferred view is disabled on this server, fall through the
package/dist/index.js CHANGED
@@ -147,6 +147,9 @@ function limitsAsSignals(limits) {
147
147
  if (!limits) return null;
148
148
  return limits;
149
149
  }
150
+ function creditsUnused(customer) {
151
+ return !creditSignals(limitsAsSignals(customer?.limits)).isCreditBased;
152
+ }
150
153
  function preferLimitsPlan(purchase, limits) {
151
154
  const limitsRef = limits?.planRef;
152
155
  if (!limitsRef) return false;
@@ -161,6 +164,12 @@ function balanceSummary(customer) {
161
164
  if (!row) return null;
162
165
  return row.replace(/^Balance:\s*/, "");
163
166
  }
167
+ function identityRow(customer) {
168
+ const ref = customer?.ref?.trim();
169
+ if (!ref) return null;
170
+ const email = customer?.email?.trim();
171
+ return email ? `Signed in as: ${email} \xB7 ${ref}` : `Signed in as: ${ref}`;
172
+ }
164
173
  function balanceRow(customer) {
165
174
  if (!customer?.balance) return null;
166
175
  const credits = customer.balance.credits ?? 0;
@@ -502,8 +511,7 @@ function narrateAccountBody(input) {
502
511
  } else {
503
512
  position = "After your first call";
504
513
  }
505
- const creditsUnused = !creditSignals(limitsAsSignals(customer?.limits)).isCreditBased;
506
- return `${product} is on ${planName}${priceBit}. ${position}. ` + (creditsUnused ? `Credits are not used on this plan. Call \`${VIEWER_TOOL_NAME}\` with view: 'checkout' to switch.` : `Call \`${VIEWER_TOOL_NAME}\` with view: 'checkout' to switch.`);
514
+ return `${product} is on ${planName}${priceBit}. ${position}. ` + (creditsUnused(customer) ? `Credits are not used on this plan. Call \`${VIEWER_TOOL_NAME}\` with view: 'checkout' to switch.` : `Call \`${VIEWER_TOOL_NAME}\` with view: 'checkout' to switch.`);
507
515
  }
508
516
  if (state === "E") {
509
517
  const left2 = remainingOfTotal(usage);
@@ -526,7 +534,7 @@ function narrateAccountBody(input) {
526
534
  return otherShape !== "free" && otherShape !== "trial";
527
535
  });
528
536
  const carryOn = others.length > 0 ? ` ${joinOr(others.map((item) => carryOnFragment(item, customer)))}.` : "";
529
- const antiTrap = credits > 0 && planShape !== "usage-based" ? ` Adding credits will not help, because ${planName} does not spend them.` : "";
537
+ const antiTrap = creditsUnused(customer) ? ` Adding credits will not help, because ${planName} does not spend them.` : "";
530
538
  const priceBit = planPriceBit(plan);
531
539
  return `${product} is on ${planName}${priceBit}. ${capBit}.${consequence}.${antiTrap}${carryOn} Call \`${VIEWER_TOOL_NAME}\` with view: 'checkout' to switch plan.`;
532
540
  }
@@ -613,6 +621,8 @@ function narrateManageAccount(data, options) {
613
621
  now
614
622
  })
615
623
  );
624
+ const identity = identityRow(customer);
625
+ if (identity) lines.push(identity);
616
626
  if (state === "A" || state === "H") {
617
627
  const bal = balanceRow(customer);
618
628
  if (bal) lines.push(bal);
@@ -813,6 +823,15 @@ function enrichPurchase(purchase) {
813
823
  ...enrichedPlanSnapshot !== void 0 ? { planSnapshot: enrichedPlanSnapshot } : {}
814
824
  };
815
825
  }
826
+ function signedInCustomerRefOnly(getCustomerRef) {
827
+ return (extra) => {
828
+ const ref = getCustomerRef(extra);
829
+ if (typeof ref !== "string") return null;
830
+ const trimmed = ref.trim();
831
+ if (!trimmed || trimmed === "anonymous") return null;
832
+ return trimmed;
833
+ };
834
+ }
816
835
  function defaultGetCustomerRef(extra) {
817
836
  const candidates = [
818
837
  extra?.http?.authInfo?.extra?.customer_ref,
@@ -945,13 +964,9 @@ var VIEW_PRIORITY = ["checkout", "topup", "account"];
945
964
  function hasActivePlan(customer, productRef) {
946
965
  return selectActivePlanPurchase(customer?.purchase?.purchases, productRef) !== null;
947
966
  }
948
- function isOutOfCredits(customer) {
949
- const credits = customer?.balance?.credits;
950
- return credits === 0;
951
- }
952
967
  function deriveDefaultView(data, enabledViews = new Set(SOLVAPAY_MCP_ADVERTISED_VIEW_KINDS)) {
953
968
  const customer = data.customer;
954
- const preferred = !hasActivePlan(customer, data.productRef) ? "checkout" : isOutOfCredits(customer) ? "topup" : "account";
969
+ const preferred = !hasActivePlan(customer, data.productRef) ? "checkout" : customer?.nextAction === "topup" ? "topup" : "account";
955
970
  if (enabledViews.has(preferred)) return preferred;
956
971
  for (const view of VIEW_PRIORITY) {
957
972
  if (enabledViews.has(view)) return view;
@@ -1193,12 +1208,10 @@ var createBootstrapProductError = (productResult) => {
1193
1208
  return err;
1194
1209
  };
1195
1210
  function createBuildBootstrapPayload(options) {
1196
- const {
1197
- solvaPay,
1198
- productRef,
1199
- publicBaseUrl,
1200
- getCustomerRef = defaultGetCustomerRef
1201
- } = options;
1211
+ const { solvaPay, productRef, publicBaseUrl } = options;
1212
+ const getCustomerRef = signedInCustomerRefOnly(
1213
+ options.getCustomerRef ?? defaultGetCustomerRef
1214
+ );
1202
1215
  const fetchPublishableKey = async () => {
1203
1216
  try {
1204
1217
  const platform = await solvaPay.apiClient.getPlatformConfig?.();
@@ -1253,18 +1266,16 @@ function createBuildBootstrapPayload(options) {
1253
1266
  }
1254
1267
  }
1255
1268
  const checkoutPurpose = view === "topup" || view !== "checkout" && view !== "auto-recharge" && limits?.paywallReason === "topup_required" ? "credit_topup" : void 0;
1256
- const checkoutResult = await wrapError(
1269
+ const checkoutResult = customerRef ? await wrapError(
1257
1270
  createMcpCheckoutSession(
1258
- buildSolvaPayRequest(extra, {
1259
- getCustomerRef: () => customerRef ?? "anonymous"
1260
- }),
1271
+ buildRequest(extra),
1261
1272
  {
1262
1273
  productRef,
1263
1274
  ...checkoutPurpose ? { purpose: checkoutPurpose } : {}
1264
1275
  },
1265
1276
  { solvaPay }
1266
1277
  )
1267
- );
1278
+ ) : await unauthenticated();
1268
1279
  if (isErrorResult(merchantResult)) {
1269
1280
  throw createBootstrapMerchantError(merchantResult);
1270
1281
  }
@@ -1287,19 +1298,16 @@ function createBuildBootstrapPayload(options) {
1287
1298
  } : null;
1288
1299
  const activePurchase2 = selectActivePlanPurchase(enrichedPurchase?.purchases, productRef);
1289
1300
  const usage = customerRef ? deriveUsageSnapshot({
1290
- // Consumption comes from `limits.used` (or `limit - remaining`)
1291
- // inside `deriveUsageSnapshot`. `purchase.usage.used` is only
1292
- // ever reset to zero by the backend and must not be the source.
1293
- used: 0,
1294
1301
  periodStart: activePurchase2?.usage?.periodStart,
1295
1302
  periodEnd: activePurchase2?.usage?.periodEnd,
1296
1303
  purchaseRef: activePurchase2?.reference,
1297
1304
  limits
1298
1305
  }) : null;
1299
1306
  const signals = limits ? creditSignals2(limits) : null;
1300
- const state = limits ? classifyPaywallState(limits) : null;
1301
1307
  const customer = customerRef ? {
1302
1308
  ref: customerRef,
1309
+ email: purchase?.email?.trim() ? purchase.email.trim() : null,
1310
+ name: purchase?.name?.trim() ? purchase.name.trim() : null,
1303
1311
  purchase: enrichedPurchase,
1304
1312
  paymentMethod: okOrNull(paymentMethodResult),
1305
1313
  balance: okOrNull(balanceResult),
@@ -1309,8 +1317,9 @@ function createBuildBootstrapPayload(options) {
1309
1317
  ...signals?.remainingCalls !== void 0 ? { remainingCalls: signals.remainingCalls } : {},
1310
1318
  ...signals?.creditsPerCall !== void 0 ? { creditsPerCall: signals.creditsPerCall } : {},
1311
1319
  ...signals?.shortfallCredits !== void 0 ? { shortfallCredits: signals.shortfallCredits } : {},
1320
+ ...signals ? { isCreditBased: signals.isCreditBased } : {},
1312
1321
  ...limits?.autoRecharge !== void 0 ? { autoRecharge: limits.autoRecharge } : {},
1313
- ...state ? { nextAction: nextActionFor(state) } : {}
1322
+ ...limits?.withinLimits === false ? { nextAction: nextActionFor(classifyPaywallState(limits)) } : {}
1314
1323
  } : null;
1315
1324
  const checkout = okOrNull(checkoutResult);
1316
1325
  const portal = okOrNull(portalResult);
@@ -1421,11 +1430,13 @@ function buildSolvaPayDescriptors(options) {
1421
1430
  views = DEFAULT_VIEWS,
1422
1431
  csp,
1423
1432
  apiBaseUrl,
1424
- getCustomerRef = defaultGetCustomerRef,
1425
1433
  onToolCall,
1426
1434
  onToolResult,
1427
1435
  branding
1428
1436
  } = options;
1437
+ const getCustomerRef = signedInCustomerRefOnly(
1438
+ options.getCustomerRef ?? defaultGetCustomerRef
1439
+ );
1429
1440
  const toolIcons = deriveIcons(branding);
1430
1441
  if (!/^https?:\/\//i.test(publicBaseUrl)) {
1431
1442
  throw new Error(
@@ -1503,6 +1514,11 @@ function buildSolvaPayDescriptors(options) {
1503
1514
  publicBaseUrl,
1504
1515
  getCustomerRef
1505
1516
  });
1517
+ const buildDefaultBootstrapPayload = async (extra) => {
1518
+ const data = await buildBootstrapPayload("account", extra);
1519
+ data.view = deriveDefaultView(data, enabledViews);
1520
+ return data;
1521
+ };
1506
1522
  const advertisedViewList = SOLVAPAY_MCP_ADVERTISED_VIEW_KINDS.filter(
1507
1523
  (view) => enabledViews.has(view)
1508
1524
  );
@@ -1532,10 +1548,9 @@ function buildSolvaPayDescriptors(options) {
1532
1548
  });
1533
1549
  }
1534
1550
  const mode = parseMode(args.mode);
1535
- const data = await buildBootstrapPayload(requested ?? "account", extra);
1536
- const view = requested ?? deriveDefaultView(data, enabledViews);
1537
- data.view = view;
1538
- return narratedToolResult(view, data, mode, {
1551
+ const data = requested === void 0 ? await buildDefaultBootstrapPayload(extra) : await buildBootstrapPayload(requested, extra);
1552
+ if (requested !== void 0) data.view = requested;
1553
+ return narratedToolResult(data.view, data, mode, {
1539
1554
  ...toolMeta,
1540
1555
  "openai/widgetSessionId": crypto.randomUUID()
1541
1556
  });
@@ -1892,10 +1907,11 @@ function buildSolvaPayDescriptors(options) {
1892
1907
  title: "SolvaPay bootstrap",
1893
1908
  description: "Current merchant/product/plans/customer snapshot for the embedded UI. Widgets read this idempotently when the host scrubs structuredContent from tool results.",
1894
1909
  mimeType: SOLVAPAY_BOOTSTRAP_MIME_TYPE,
1895
- // View is an echoed routing label — the widget resolves the actual
1896
- // surface from host context (`inferViewFromHost`), so any view kind
1897
- // produces identical merchant/product/plans/customer data.
1898
- readPayload: (extra) => buildBootstrapPayload("account", extra)
1910
+ // View is the server's default for this customer (checkout with no
1911
+ // active plan, topup on a top-up gate, account otherwise) — the same
1912
+ // choice the viewer makes when `view` is omitted. The widget trusts
1913
+ // this field; it does not resolve the surface from host context.
1914
+ readPayload: (extra) => buildDefaultBootstrapPayload(extra)
1899
1915
  };
1900
1916
  return { tools, resource, prompts, docsResources, bootstrapResource, buildBootstrapPayload };
1901
1917
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solvapay/mcp-core",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Framework-neutral MCP contracts for the SolvaPay SDK (tool names, descriptors, payable handler, paywall meta, CSP, bootstrap payload, OAuth discovery JSON builders, bearer/JWT helpers).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -41,7 +41,7 @@
41
41
  "typescript": "^5.9.3",
42
42
  "vitest": "^4.1.2",
43
43
  "zod": "^4.3.6",
44
- "@solvapay/server": "2.9.0",
44
+ "@solvapay/server": "2.10.0",
45
45
  "@solvapay/test-utils": "^0.0.0"
46
46
  },
47
47
  "scripts": {