cope-sdk 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -180,7 +180,7 @@ var CHECKOUT_MOUNT_MAX_ATTEMPTS = 20;
180
180
  var CHECKOUT_READY_TIMEOUT_MS = 8e3;
181
181
  var SUPPORTED_ONE_CLICK_CHARGE_TERMS_VERSIONS = /* @__PURE__ */ new Set(["oto-v1"]);
182
182
  function seg(id) {
183
- return encodeURIComponent(id);
183
+ return encodeURIComponent(String(id));
184
184
  }
185
185
  function isRecord2(value) {
186
186
  return typeof value === "object" && value !== null;
@@ -212,10 +212,28 @@ function parseErrorBody(body) {
212
212
  function isApiErrorEntry(value) {
213
213
  return isRecord2(value) && typeof value.code === "string" && typeof value.message === "string";
214
214
  }
215
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
216
+ var ASCTIME = /^[A-Z][a-z]{2} ([A-Z][a-z]{2}) ([ \d]\d) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/;
217
+ function asctimeToUtc([, monthName, day, hh, mm, ss, year]) {
218
+ const fields = [Number(year), MONTHS.indexOf(monthName ?? ""), Number((day ?? "").trim()), Number(hh), Number(mm), Number(ss)];
219
+ const at = Date.UTC(fields[0], fields[1], fields[2], fields[3], fields[4], fields[5]);
220
+ const back = new Date(at);
221
+ const roundTrip = [
222
+ back.getUTCFullYear(),
223
+ back.getUTCMonth(),
224
+ back.getUTCDate(),
225
+ back.getUTCHours(),
226
+ back.getUTCMinutes(),
227
+ back.getUTCSeconds()
228
+ ];
229
+ return roundTrip.every((value, i) => value === fields[i]) ? at : Number.NaN;
230
+ }
215
231
  function parseRetryAfter(value) {
216
- if (value === null || value.trim() === "") return null;
217
- if (/^\d+$/.test(value.trim())) return Number(value.trim());
218
- const at = Date.parse(value);
232
+ if (value === null) return null;
233
+ const trimmed = value.trim();
234
+ if (/^\d+$/.test(trimmed)) return Number(trimmed);
235
+ const asctime = ASCTIME.exec(trimmed);
236
+ const at = asctime ? asctimeToUtc(asctime) : / GMT$/.test(trimmed) ? Date.parse(trimmed) : Number.NaN;
219
237
  return Number.isNaN(at) ? null : Math.max(0, Math.ceil((at - Date.now()) / 1e3));
220
238
  }
221
239
  function bodyRequestId(body) {
@@ -386,7 +404,7 @@ function parseCheckoutEmbedEvent(data) {
386
404
  }
387
405
  return null;
388
406
  }
389
- var CopeCart = class {
407
+ var Cope = class {
390
408
  publishableKey;
391
409
  baseUrl;
392
410
  checkoutBaseUrl;
@@ -404,7 +422,7 @@ var CopeCart = class {
404
422
  throw new CopeError('Invalid publishableKey. Must start with "cope_pk_".');
405
423
  }
406
424
  if (typeof window !== "undefined" && window.location?.protocol === "http:" && window.location.hostname !== "localhost") {
407
- throw new CopeError("CopeCart SDK requires HTTPS. Refusing to operate on an insecure page.");
425
+ throw new CopeError("COPE SDK requires HTTPS. Refusing to operate on an insecure page.");
408
426
  }
409
427
  this.publishableKey = options.publishableKey;
410
428
  const baseOrigin = originOnly(options.baseUrl ?? DEFAULT_BASE_URL);
@@ -442,9 +460,11 @@ var CopeCart = class {
442
460
  async addLine(cartId, payload) {
443
461
  return normalizeCart(await this.post(`/carts/${seg(cartId)}/lines`, payload));
444
462
  }
463
+ /** `lineId` is the line's `id`, an integer; a string of its digits is accepted too. */
445
464
  async updateLine(cartId, lineId2, payload) {
446
465
  return normalizeCart(await this.patch(`/carts/${seg(cartId)}/lines/${seg(lineId2)}`, payload));
447
466
  }
467
+ /** `lineId` is the line's `id`, an integer; a string of its digits is accepted too. */
448
468
  async removeLine(cartId, lineId2) {
449
469
  return normalizeCart(await this.del(`/carts/${seg(cartId)}/lines/${seg(lineId2)}`));
450
470
  }
@@ -476,6 +496,7 @@ var CopeCart = class {
476
496
  ...input.attribution ? { attribution: input.attribution } : {},
477
497
  ...input.intendedPaymentMethod != null ? { intended_payment_method: input.intendedPaymentMethod } : {},
478
498
  ...input.metadata ? { metadata: input.metadata } : {},
499
+ ...input.collectBuyerIdentityAtCheckout != null ? { collect_buyer_identity_at_checkout: input.collectBuyerIdentityAtCheckout } : {},
479
500
  lines: lines.map((line) => ({
480
501
  product_id: line.productId,
481
502
  ...line.planId !== void 0 ? { plan_id: line.planId } : {},
@@ -499,7 +520,8 @@ var CopeCart = class {
499
520
  status: checkout.status,
500
521
  expiresAt: checkout.expiresAt,
501
522
  totals: checkout.totals,
502
- ...input.intendedPaymentMethod != null ? { intendedPaymentMethod: input.intendedPaymentMethod } : {}
523
+ ...input.intendedPaymentMethod != null ? { intendedPaymentMethod: input.intendedPaymentMethod } : {},
524
+ ...raw.field_errors?.length ? { fieldErrors: raw.field_errors } : {}
503
525
  };
504
526
  }
505
527
  async cancelCheckout(checkoutId) {
@@ -982,15 +1004,20 @@ function projectLine(line) {
982
1004
  unitPriceMinorUnits: line.price_snapshot?.unit_price_minor_units ?? null
983
1005
  };
984
1006
  }
985
- function projectCheckout(checkout) {
1007
+ function projectCheckout(checkout, lines) {
986
1008
  return {
987
1009
  checkoutUrl: checkout.checkoutUrl,
988
1010
  status: checkout.status,
989
1011
  expiresAt: checkout.expiresAt,
990
1012
  totalMinorUnits: checkout.totals.total_minor_units,
991
- currency: checkout.totals.currency
1013
+ currency: checkout.totals.currency,
1014
+ buyerNext: buyerNextFor(lines)
992
1015
  };
993
1016
  }
1017
+ function buyerNextFor(lines) {
1018
+ const waiver = lines.some((line) => line.requires_shipping === false) ? ", waive their right of withdrawal (a product in this cart does not ship)" : lines.some((line) => line.requires_shipping === void 0) ? ", waive their right of withdrawal if a product in the cart does not ship" : "";
1019
+ return `Send the buyer to checkoutUrl. There they accept the terms${waiver}, and pay. You cannot do any of these for them.`;
1020
+ }
994
1021
  var GUIDANCE = {
995
1022
  invalid_cart_ref: "The cartRef is not valid here. Start again with create_cart.",
996
1023
  cart_expired: "This cart is finished: ordered (the buyer has paid), expired or abandoned. For a new purchase, start again with create_cart.",
@@ -1004,6 +1031,7 @@ var GUIDANCE = {
1004
1031
  offer_sourced_no_promo: "This cart comes from a negotiated offer; promo codes do not apply.",
1005
1032
  checkout_expired: "The checkout expired. Call start_checkout again.",
1006
1033
  tax_address_invalid: "The buyer address is incomplete or invalid for tax. Ask the buyer and call set_buyer_identity.",
1034
+ missing_tax_location: "Totals need the buyer's country and postal code. Ask the buyer and call set_buyer_identity, then get_cart.",
1007
1035
  tax_service_unavailable: "Tax could not be calculated right now. Retry shortly.",
1008
1036
  business_checkout_hold: "This store cannot take orders right now. Tell the buyer to try later.",
1009
1037
  rate_limited: "Too many requests. Wait retryAfter seconds, then retry.",
@@ -1054,7 +1082,7 @@ var CONFLICT_WAITS_MS = [500, 1e3, 2e3];
1054
1082
  var cartRef = z.string().min(1).max(2048).describe("The cartRef returned by create_cart. Pass it back unchanged.");
1055
1083
  var ID = /^[A-Za-z0-9_-]{1,100}$/;
1056
1084
  var productId = z.string().regex(ID).describe("The product id, e.g. from the store page");
1057
- var lineId = z.string().regex(ID).describe("A lineId from the cart");
1085
+ var lineId = z.union([z.number().int().positive(), z.string().regex(/^\d{1,19}$/)]).describe("A lineId from the cart, as get_cart returned it");
1058
1086
  var address = z.object({
1059
1087
  line1: z.string(),
1060
1088
  line2: z.string().optional(),
@@ -1071,10 +1099,10 @@ function registerCheckoutTools(server, ctx) {
1071
1099
  return refusal(error) ?? fail(error);
1072
1100
  }
1073
1101
  };
1074
- const newClient = () => new CopeCart({ ...ctx.clientOptions, persistence: "none" });
1102
+ const newClient = () => new Cope({ ...ctx.clientOptions, persistence: "none" });
1075
1103
  const openCart = async (ref) => {
1076
1104
  const creds = await ctx.codec.decode(ref);
1077
- return { creds, cope: new CopeCart({ ...ctx.clientOptions, cart: creds }) };
1105
+ return { creds, cope: new Cope({ ...ctx.clientOptions, cart: creds }) };
1078
1106
  };
1079
1107
  server.registerTool("get_product", {
1080
1108
  description: "Look up a product the store sells: name, plans and prices. Prices are in minor units (cents).",
@@ -1101,8 +1129,14 @@ function registerCheckoutTools(server, ctx) {
1101
1129
  try {
1102
1130
  return { cart: projectCart(await cope.reprice(creds.id)) };
1103
1131
  } catch (error) {
1104
- if (!hasCode(error, "cart_not_repriceable")) throw error;
1105
- return { cart: projectCart(await cope.getCart(creds.id)) };
1132
+ if (hasCode(error, "cart_not_repriceable")) return { cart: projectCart(await cope.getCart(creds.id)) };
1133
+ if (hasCode(error, "missing_tax_location")) {
1134
+ return {
1135
+ cart: projectCart(await cope.getCart(creds.id)),
1136
+ totalsPending: "Totals need the buyer's country and postal code. Call set_buyer_identity when the buyer gives them."
1137
+ };
1138
+ }
1139
+ throw error;
1106
1140
  }
1107
1141
  }));
1108
1142
  server.registerTool("add_line", {
@@ -1215,17 +1249,19 @@ function registerCheckoutTools(server, ctx) {
1215
1249
  return { cart: projectCart(cart) };
1216
1250
  }));
1217
1251
  server.registerTool("start_checkout", {
1218
- description: "Create the checkout and return the URL where the buyer reviews, accepts the terms and pays. Calling it again returns the same checkout while it is open.",
1252
+ description: "Create the checkout and return the URL where the buyer reviews, gives the consents the cart needs and pays; buyerNext in the result says which. Calling it again returns the same checkout while it is open.",
1219
1253
  inputSchema: z.object({ cartRef }),
1220
1254
  annotations: { idempotentHint: true }
1221
1255
  }, ({ cartRef: ref }) => run(async () => {
1222
1256
  const { creds, cope } = await openCart(ref);
1223
1257
  const payload = { success_url: ctx.checkout.successUrl, cancel_url: ctx.checkout.cancelUrl, consents: [] };
1224
- return projectCheckout(await startCheckout(cope, creds, payload));
1258
+ const { checkout, cart } = await startCheckout(cope, creds, payload);
1259
+ return projectCheckout(checkout, cart.lines);
1225
1260
  }));
1226
1261
  }
1227
1262
  async function startCheckout(cope, creds, payload) {
1228
- const inCheckout = (await cope.getCart(creds.id)).state === "checkout_in_progress";
1263
+ const cart = await cope.getCart(creds.id);
1264
+ const inCheckout = cart.state === "checkout_in_progress";
1229
1265
  let open = null;
1230
1266
  for (let n = 0; n < MAX_CHECKOUT_KEYS; n++) {
1231
1267
  let result;
@@ -1241,9 +1277,9 @@ async function startCheckout(cope, creds, payload) {
1241
1277
  if (Date.parse(result.expiresAt) > Date.now()) open = result;
1242
1278
  continue;
1243
1279
  }
1244
- if ((await cope.getCart(creds.id)).state !== "active") return result;
1280
+ if ((await cope.getCart(creds.id)).state !== "active") return { checkout: result, cart };
1245
1281
  }
1246
- if (open) return open;
1282
+ if (open) return { checkout: open, cart };
1247
1283
  if (inCheckout) {
1248
1284
  throw new CopeApiError(409, [{ code: "checkout_not_recoverable", message: "The open checkout expired or cannot be recovered" }]);
1249
1285
  }
@@ -1265,8 +1301,11 @@ function hasCode(error, code) {
1265
1301
  return error instanceof CopeApiError && error.code === code;
1266
1302
  }
1267
1303
 
1304
+ // package.json
1305
+ var version = "0.1.1";
1306
+
1268
1307
  // src/mcp/handler.ts
1269
- var SERVER_INFO = { name: "cope-checkout", version: "0.1.0" };
1308
+ var SERVER_INFO = { name: "cope-checkout", version };
1270
1309
  function createCopeMcpHandler(options) {
1271
1310
  if (options.allowedHosts.length === 0) throw new Error("allowedHosts must name at least one host");
1272
1311
  parseCartRefKeys(options.cartRefKeys);