@reactionary/ucp 0.10.31 → 0.10.33

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
@@ -83,7 +83,9 @@ The official discovery profile is served from `/.well-known/ucp`. The profile ad
83
83
  - capabilities derived from the configured Reactionary client
84
84
  - configured merchant metadata
85
85
  - configured public keys
86
- - configured payment handlers, if supplied
86
+ - configured payment handlers (`profile.paymentHandlers`), if supplied; the same handlers are echoed in checkout responses' `ucp.payment_handlers`
87
+
88
+ Payment handlers are static configuration. The handler `id` is forwarded as `paymentMethod.paymentProcessor` when a payment instrument is selected, so it must match a payment provider id in the backend (for example Medusa's `pp_system_default`).
87
89
 
88
90
  Capability advertisement is intentionally derived from the instantiated client:
89
91
 
@@ -1,4 +1,5 @@
1
1
  import { getCapability, hasCapability } from "./reactionary-ucp-capabilities.js";
2
+ const UCP_DOCS_BASE_URL = "https://ucp.dev/2026-08-25";
2
3
  function getRequestRoute(request, profile) {
3
4
  const url = new URL(request.url);
4
5
  const endpointPath = profile ? new URL(profile.endpoint).pathname.replace(/\/$/, "") : "";
@@ -21,7 +22,9 @@ function createUCPProfile(client, profile) {
21
22
  {
22
23
  version: "2026-08-25",
23
24
  transport: "rest",
24
- endpoint
25
+ endpoint,
26
+ spec: `${UCP_DOCS_BASE_URL}/specification/overview/`,
27
+ schema: `${UCP_DOCS_BASE_URL}/services/shopping/rest.openapi.json`
25
28
  }
26
29
  ]
27
30
  },
@@ -41,29 +44,31 @@ function createUCPProfile(client, profile) {
41
44
  function createUcpCapabilities(client) {
42
45
  const capabilities = {};
43
46
  if (hasCapability(client, "product-search")) {
44
- capabilities["dev.ucp.shopping.catalog.search"] = [createUcpCapability()];
47
+ capabilities["dev.ucp.shopping.catalog.search"] = [createUcpCapability("catalog/search", "catalog_search")];
45
48
  }
46
49
  if (hasCapability(client, "product")) {
47
- capabilities["dev.ucp.shopping.catalog.lookup"] = [createUcpCapability()];
50
+ capabilities["dev.ucp.shopping.catalog.lookup"] = [createUcpCapability("catalog/lookup", "catalog_lookup")];
48
51
  }
49
52
  if (hasCapability(client, "cart")) {
50
- capabilities["dev.ucp.shopping.cart"] = [createUcpCapability()];
53
+ capabilities["dev.ucp.shopping.cart"] = [createUcpCapability("cart", "cart")];
51
54
  const cart = getCapability(client, "cart");
52
55
  if (cart && typeof Reflect.get(cart, "applyCouponCode") === "function") {
53
- capabilities["dev.ucp.shopping.discount"] = [createUcpCapability()];
56
+ capabilities["dev.ucp.shopping.discount"] = [createUcpCapability("extensions/discount", "discount")];
54
57
  }
55
58
  }
56
59
  if (hasCapability(client, "checkout")) {
57
- capabilities["dev.ucp.shopping.checkout"] = [createUcpCapability()];
60
+ capabilities["dev.ucp.shopping.checkout"] = [createUcpCapability("checkout", "checkout")];
58
61
  }
59
62
  if (hasCapability(client, "order")) {
60
- capabilities["dev.ucp.shopping.order"] = [createUcpCapability()];
63
+ capabilities["dev.ucp.shopping.order"] = [createUcpCapability("order", "order")];
61
64
  }
62
65
  return capabilities;
63
66
  }
64
- function createUcpCapability() {
67
+ function createUcpCapability(specPath, schemaName) {
65
68
  return {
66
- version: "2026-08-25"
69
+ version: "2026-08-25",
70
+ spec: `${UCP_DOCS_BASE_URL}/specification/shopping/${specPath}/`,
71
+ schema: `${UCP_DOCS_BASE_URL}/schemas/shopping/${schemaName}.json`
67
72
  };
68
73
  }
69
74
  export {
@@ -1,5 +1,5 @@
1
1
  import { jsonResponse } from "./reactionary-ucp-http.js";
2
- async function handleRestRequest(request, client, path, sessionId, sessionStore) {
2
+ async function handleRestRequest(request, client, path, sessionId, sessionStore, paymentHandlers = {}) {
3
3
  if (request.method === "POST" && path === "/catalog/search") {
4
4
  return jsonResponse(await handleCatalogSearch(client, await parseJsonBody(request)));
5
5
  }
@@ -54,12 +54,12 @@ async function handleRestRequest(request, client, path, sessionId, sessionStore)
54
54
  "REST POST /checkout-sessions",
55
55
  body,
56
56
  sessionStore,
57
- async () => jsonResponse(await handleCreateCheckout(client, body), { status: 201 })
57
+ async () => jsonResponse(await handleCreateCheckout(client, body, paymentHandlers), { status: 201 })
58
58
  );
59
59
  }
60
60
  const checkoutMatch = /^\/checkout-sessions\/([^/]+)$/.exec(path);
61
61
  if (checkoutMatch && request.method === "GET") {
62
- return jsonResponse(await handleGetCheckout(client, decodeURIComponent(checkoutMatch[1])));
62
+ return jsonResponse(await handleGetCheckout(client, decodeURIComponent(checkoutMatch[1]), paymentHandlers));
63
63
  }
64
64
  if (checkoutMatch && request.method === "PUT") {
65
65
  const body = await parseJsonBody(request);
@@ -69,7 +69,7 @@ async function handleRestRequest(request, client, path, sessionId, sessionStore)
69
69
  `REST PUT /checkout-sessions/${decodeURIComponent(checkoutMatch[1])}`,
70
70
  body,
71
71
  sessionStore,
72
- async () => jsonResponse(await handleUpdateCheckout(client, decodeURIComponent(checkoutMatch[1]), body))
72
+ async () => jsonResponse(await handleUpdateCheckout(client, decodeURIComponent(checkoutMatch[1]), body, paymentHandlers))
73
73
  );
74
74
  }
75
75
  const checkoutCompleteMatch = /^\/checkout-sessions\/([^/]+)\/complete$/.exec(path);
@@ -80,7 +80,7 @@ async function handleRestRequest(request, client, path, sessionId, sessionStore)
80
80
  `REST POST /checkout-sessions/${decodeURIComponent(checkoutCompleteMatch[1])}/complete`,
81
81
  {},
82
82
  sessionStore,
83
- async () => jsonResponse(await handleCompleteCheckout(client, decodeURIComponent(checkoutCompleteMatch[1])))
83
+ async () => jsonResponse(await handleCompleteCheckout(client, decodeURIComponent(checkoutCompleteMatch[1]), paymentHandlers))
84
84
  );
85
85
  }
86
86
  const checkoutCancelMatch = /^\/checkout-sessions\/([^/]+)\/cancel$/.exec(path);
@@ -297,7 +297,7 @@ async function handleCancelCart(client, cartId) {
297
297
  }
298
298
  return current.success ? toUcpCart(current.value) : createEmptyUcpCart(cartId);
299
299
  }
300
- async function handleCreateCheckout(client, body) {
300
+ async function handleCreateCheckout(client, body, paymentHandlers) {
301
301
  if (!client.checkout) {
302
302
  return createUCPError("not_available", "Checkout capability is not available.");
303
303
  }
@@ -309,9 +309,9 @@ async function handleCreateCheckout(client, body) {
309
309
  if (!result.success) {
310
310
  return createUCPError("checkout_create_failed", "Checkout session creation failed.");
311
311
  }
312
- return toUcpCheckout(result.value);
312
+ return toUcpCheckout(result.value, paymentHandlers);
313
313
  }
314
- async function handleGetCheckout(client, checkoutId) {
314
+ async function handleGetCheckout(client, checkoutId, paymentHandlers) {
315
315
  if (!client.checkout) {
316
316
  return createUCPError("not_available", "Checkout capability is not available.");
317
317
  }
@@ -319,9 +319,9 @@ async function handleGetCheckout(client, checkoutId) {
319
319
  if (!result.success) {
320
320
  return createUCPError("not_found", `Checkout was not found: ${checkoutId}`);
321
321
  }
322
- return toUcpCheckout(result.value);
322
+ return toUcpCheckout(result.value, paymentHandlers);
323
323
  }
324
- async function handleUpdateCheckout(client, checkoutId, body) {
324
+ async function handleUpdateCheckout(client, checkoutId, body, paymentHandlers) {
325
325
  if (!client.checkout) {
326
326
  return createUCPError("not_available", "Checkout capability is not available.");
327
327
  }
@@ -353,9 +353,9 @@ async function handleUpdateCheckout(client, checkoutId, body) {
353
353
  }
354
354
  checkout = paymentResult.value;
355
355
  }
356
- return toUcpCheckout(checkout);
356
+ return toUcpCheckout(checkout, paymentHandlers);
357
357
  }
358
- async function handleCompleteCheckout(client, checkoutId) {
358
+ async function handleCompleteCheckout(client, checkoutId, paymentHandlers) {
359
359
  if (!client.checkout) {
360
360
  return createUCPError("not_available", "Checkout capability is not available.");
361
361
  }
@@ -363,7 +363,7 @@ async function handleCompleteCheckout(client, checkoutId) {
363
363
  if (!result.success) {
364
364
  return createUCPError("checkout_complete_failed", "Checkout completion failed.");
365
365
  }
366
- return toUcpCheckout(result.value, "completed");
366
+ return toUcpCheckout(result.value, paymentHandlers, "completed");
367
367
  }
368
368
  async function handleGetOrder(client, orderId) {
369
369
  if (!client.order) {
@@ -380,7 +380,7 @@ async function handleGetOrder(client, orderId) {
380
380
  permalink_url: "",
381
381
  line_items: [],
382
382
  currency: getMoneyCurrency(result.value.price?.grandTotal),
383
- totals: toUcpTotals(result.value.price?.grandTotal),
383
+ totals: toUcpCostTotals(result.value.price),
384
384
  fulfillment: {
385
385
  expectations: [],
386
386
  events: []
@@ -487,7 +487,7 @@ function toUcpCart(cart) {
487
487
  id: cart.identifier.key,
488
488
  line_items: cart.items.map(toUcpCartLineItem),
489
489
  currency: getMoneyCurrency(cart.price?.grandTotal),
490
- totals: toUcpTotals(cart.price?.grandTotal),
490
+ totals: toUcpCostTotals(cart.price),
491
491
  ucp: createUcpSuccessMetadata()
492
492
  };
493
493
  }
@@ -516,20 +516,38 @@ function toUcpCartLineItem(lineItem) {
516
516
  totals: toUcpTotals(lineItem.price.totalPrice)
517
517
  };
518
518
  }
519
- function toUcpCheckout(checkout, status) {
519
+ function toUcpCheckout(checkout, paymentHandlers, status) {
520
520
  return {
521
521
  id: checkout.identifier.key,
522
522
  status: status ?? (checkout.readyForFinalization ? "ready_for_complete" : "incomplete"),
523
523
  line_items: checkout.items.map(toUcpCartLineItem),
524
524
  currency: getMoneyCurrency(checkout.price?.grandTotal),
525
- totals: toUcpTotals(checkout.price?.grandTotal),
525
+ totals: toUcpCostTotals(checkout.price),
526
526
  links: [],
527
- ucp: createUcpCheckoutSuccessMetadata()
527
+ ucp: createUcpCheckoutSuccessMetadata(paymentHandlers)
528
528
  };
529
529
  }
530
530
  function toUcpDescription(text) {
531
531
  return text ? { plain: text } : { plain: "" };
532
532
  }
533
+ function toUcpCostTotals(price) {
534
+ const totals = [
535
+ { type: "subtotal", amount: getMoneyValue(price.totalProductPrice) }
536
+ ];
537
+ const optionalTotals = [
538
+ ["discount", -Math.abs(getMoneyValue(price.totalDiscount))],
539
+ ["fulfillment", getMoneyValue(price.totalShipping)],
540
+ ["tax", getMoneyValue(price.totalTax)],
541
+ ["fee", getMoneyValue(price.totalSurcharge)]
542
+ ];
543
+ for (const [type, amount] of optionalTotals) {
544
+ if (amount !== 0) {
545
+ totals.push({ type, amount });
546
+ }
547
+ }
548
+ totals.push({ type: "total", amount: getMoneyValue(price.grandTotal) });
549
+ return totals;
550
+ }
533
551
  function toUcpTotals(amount) {
534
552
  return [
535
553
  {
@@ -556,13 +574,21 @@ function createUcpSuccessMetadata() {
556
574
  status: "success"
557
575
  };
558
576
  }
559
- function createUcpCheckoutSuccessMetadata() {
577
+ function createUcpCheckoutSuccessMetadata(paymentHandlers) {
578
+ const candidate = paymentHandlers;
560
579
  return {
561
580
  version: "2026-08-25",
562
581
  status: "success",
563
- payment_handlers: {}
582
+ payment_handlers: isUcpCheckoutPaymentHandlers(candidate) ? candidate : {}
564
583
  };
565
584
  }
585
+ function isUcpCheckoutPaymentHandlers(value) {
586
+ return typeof value === "object" && value !== null && Object.values(value).every(
587
+ (handlers) => Array.isArray(handlers) && handlers.every(
588
+ (handler) => typeof handler === "object" && handler !== null && typeof Reflect.get(handler, "version") === "string"
589
+ )
590
+ );
591
+ }
566
592
  function createUCPError(code, message) {
567
593
  return {
568
594
  ucp: {
@@ -75,7 +75,8 @@ class ReactionaryUCPServer {
75
75
  client,
76
76
  route.path,
77
77
  sessionId,
78
- this.sessionStore
78
+ this.sessionStore,
79
+ this.options.profile?.paymentHandlers
79
80
  );
80
81
  if (restResponse) {
81
82
  return restResponse;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactionary/ucp",
3
- "version": "0.10.31",
3
+ "version": "0.10.33",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "types": "./src/index.d.ts",
@@ -9,7 +9,7 @@
9
9
  "url": "git+https://github.com/Solteq/reactionary.git"
10
10
  },
11
11
  "dependencies": {
12
- "@reactionary/core": "0.10.31",
12
+ "@reactionary/core": "0.10.33",
13
13
  "zod": "4.1.9"
14
14
  },
15
15
  "devDependencies": {
@@ -3,13 +3,16 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
3
3
  import type { components } from './ucp-shopping.openapi.js';
4
4
  type UCPService = components['schemas']['base'];
5
5
  type UCPCapability = components['schemas']['$defs-base'];
6
- type UCPPaymentHandler = components['schemas']['payment_handler_$defs-base'];
6
+ export type UCPPaymentHandler = components['schemas']['entity'] & {
7
+ available_instruments?: components['schemas']['available_payment_instrument'][];
8
+ };
9
+ export type UCPPaymentHandlers = Record<string, UCPPaymentHandler[]>;
7
10
  export interface ReactionaryUCPProfile {
8
11
  ucp: {
9
12
  version: string;
10
13
  services: Record<string, UCPService[]>;
11
14
  capabilities: Record<string, UCPCapability[]>;
12
- payment_handlers?: Record<string, UCPPaymentHandler[]>;
15
+ payment_handlers?: UCPPaymentHandlers;
13
16
  };
14
17
  keys: Array<Record<string, unknown>>;
15
18
  merchant: {
@@ -1,6 +1,6 @@
1
- import type { ReactionaryUCPClient } from './reactionary-ucp-common.js';
1
+ import type { ReactionaryUCPClient, UCPPaymentHandlers } from './reactionary-ucp-common.js';
2
2
  import type { ReactionaryUCPSessionStore } from './reactionary-ucp-session-store.js';
3
- export declare function handleRestRequest(request: Request, client: ReactionaryUCPClient, path: string, sessionId: string, sessionStore: ReactionaryUCPSessionStore): Promise<Response | undefined>;
3
+ export declare function handleRestRequest(request: Request, client: ReactionaryUCPClient, path: string, sessionId: string, sessionStore: ReactionaryUCPSessionStore, paymentHandlers?: UCPPaymentHandlers): Promise<Response | undefined>;
4
4
  export declare class UCPHttpError extends Error {
5
5
  readonly status: number;
6
6
  readonly body: unknown;