@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:
|
|
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:
|
|
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:
|
|
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: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactionary/ucp",
|
|
3
|
-
"version": "0.10.
|
|
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.
|
|
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']['
|
|
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?:
|
|
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;
|