@reactionary/acp 0.0.0-stage → 0.0.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/README.md +156 -2
- package/index.js +2 -0
- package/lib/acp-schemas.js +123 -0
- package/lib/reactionary-acp-server.js +815 -0
- package/package.json +18 -4
- package/src/index.d.ts +2 -0
- package/src/lib/acp-schemas.d.ts +303 -0
- package/src/lib/reactionary-acp-server.d.ts +100 -0
package/README.md
CHANGED
|
@@ -1,3 +1,157 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @reactionary/acp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
HTTP adapter for exposing Reactionary checkout capabilities through the Agentic Commerce Protocol (ACP) checkout surface.
|
|
4
|
+
|
|
5
|
+
The package keeps Reactionary core protocol-neutral: host applications mount this adapter and provide a request-scoped Reactionary client factory.
|
|
6
|
+
|
|
7
|
+
## Implemented surface
|
|
8
|
+
|
|
9
|
+
The adapter implements the merchant-hosted ACP checkout endpoints:
|
|
10
|
+
|
|
11
|
+
- `GET /product_feeds/{id}/products`
|
|
12
|
+
- `POST /checkout_sessions`
|
|
13
|
+
- `POST /checkout_sessions/{checkout_session_id}`
|
|
14
|
+
- `GET /checkout_sessions/{checkout_session_id}`
|
|
15
|
+
- `POST /checkout_sessions/{checkout_session_id}/complete`
|
|
16
|
+
- `POST /checkout_sessions/{checkout_session_id}/cancel`
|
|
17
|
+
|
|
18
|
+
It also serves a readiness document from `GET` / `HEAD`.
|
|
19
|
+
|
|
20
|
+
If the adapter is mounted under `/acp`, both `/checkout_sessions/...` and `/acp/checkout_sessions/...` paths are understood. Set `basePath` when mounting somewhere else.
|
|
21
|
+
|
|
22
|
+
## Product feed generation
|
|
23
|
+
|
|
24
|
+
`GET /product_feeds/{id}/products` generates an ACP-compatible product feed from the configured feed definition whose key matches `{id}`.
|
|
25
|
+
|
|
26
|
+
The feed enumeration and serialization pipeline is shared with `@reactionary/feeds`; this adapter keeps the ACP-compatible route shape while delegating normalization and ACP product feed output to the reusable feed package.
|
|
27
|
+
|
|
28
|
+
Each feed definition provides:
|
|
29
|
+
|
|
30
|
+
- `languageContext` — applied to the `RequestContext` before the Reactionary client is created.
|
|
31
|
+
- `search` — a `ProductSearchIdentifierSchema`-compatible search object passed to `productSearch.queryByTerm`.
|
|
32
|
+
- `productUrlBase` — either a base URL that the product slug is resolved against, or a URL template supporting `{lang}` and `{slug}` placeholders.
|
|
33
|
+
|
|
34
|
+
By default it returns the ACP JSON shape:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"target_country": "FI",
|
|
39
|
+
"products": []
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`target_country` is derived from `languageContext.locale` when the locale includes a region, e.g. `fi-FI` -> `FI`.
|
|
44
|
+
|
|
45
|
+
For file generation, request JSONL:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
curl "https://example.com/acp/product_feeds/default/products?format=jsonl" > products.jsonl
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The first implementation streams the response directly instead of generating a download URL. That keeps the adapter stateless and lets callers pipe the result to a file, object storage upload, or feed-ingestion job. A signed download URL can be layered on later by a host application or a storage-backed feed job.
|
|
52
|
+
|
|
53
|
+
## Required Reactionary capabilities
|
|
54
|
+
|
|
55
|
+
The server validates the supplied client during construction and on every request. If the client does not expose the operations required by ACP checkout, the server does not initialize.
|
|
56
|
+
|
|
57
|
+
Required operations:
|
|
58
|
+
|
|
59
|
+
- `cart.createCart`
|
|
60
|
+
- `cart.add`
|
|
61
|
+
- `cart.getById`
|
|
62
|
+
- `checkout.initiateCheckoutForCart`
|
|
63
|
+
- `checkout.getById`
|
|
64
|
+
- `checkout.setShippingAddress`
|
|
65
|
+
- `checkout.getAvailableShippingMethods`
|
|
66
|
+
- `checkout.getAvailablePaymentMethods`
|
|
67
|
+
- `checkout.setShippingInstruction`
|
|
68
|
+
- `checkout.addPaymentInstruction`
|
|
69
|
+
- `checkout.finalizeCheckout`
|
|
70
|
+
- `productSearch.queryByTerm`
|
|
71
|
+
- `product.getBySKU`
|
|
72
|
+
- `price.getListPrice`
|
|
73
|
+
- `price.getCustomerPrice`
|
|
74
|
+
- `inventory.getBySKU`
|
|
75
|
+
|
|
76
|
+
This is deliberate because Reactionary clients can be built with different capability sets. A partially capable client should fail fast instead of advertising ACP checkout.
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { ReactionaryACPServer } from '@reactionary/acp';
|
|
82
|
+
|
|
83
|
+
const acp = new ReactionaryACPServer((requestContext) =>
|
|
84
|
+
createReactionaryClient({ contextOverrides: requestContext }),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
export const POST = (request: Request) => acp.fetch(request);
|
|
88
|
+
export const GET = (request: Request) => acp.fetch(request);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For Node HTTP:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { createServer } from 'node:http';
|
|
95
|
+
import { ReactionaryACPServer } from '@reactionary/acp';
|
|
96
|
+
|
|
97
|
+
const acp = new ReactionaryACPServer(createClient);
|
|
98
|
+
const handler = acp.toNodeHandler();
|
|
99
|
+
|
|
100
|
+
createServer((request, response) => {
|
|
101
|
+
void handler(request, response);
|
|
102
|
+
}).listen(3000);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Session handling
|
|
106
|
+
|
|
107
|
+
The server uses two pieces of state:
|
|
108
|
+
|
|
109
|
+
- `RequestContext.session`, keyed by the `acp-session-id` header.
|
|
110
|
+
- ACP checkout-session state, keyed by `checkout_session_id`.
|
|
111
|
+
|
|
112
|
+
If the request does not include `acp-session-id`, the server creates a new one and returns it in the response.
|
|
113
|
+
|
|
114
|
+
State is stored through the Reactionary `Cache` interface. The default is `MemoryCache`, which is useful for local development only.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
new ReactionaryACPServer(createClient, {
|
|
118
|
+
sessionCache: redisCache,
|
|
119
|
+
sessionTtlSeconds: 60 * 60,
|
|
120
|
+
checkoutSessionTtlSeconds: 60 * 60,
|
|
121
|
+
productFeed: {
|
|
122
|
+
feeds: {
|
|
123
|
+
finnish: {
|
|
124
|
+
languageContext: {
|
|
125
|
+
locale: 'fi-FI',
|
|
126
|
+
currencyCode: 'EUR',
|
|
127
|
+
},
|
|
128
|
+
search: {
|
|
129
|
+
term: '',
|
|
130
|
+
facets: [],
|
|
131
|
+
filters: ['market:fi'],
|
|
132
|
+
paginationOptions: {
|
|
133
|
+
pageNumber: 1,
|
|
134
|
+
pageSize: 50,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
productUrlBase: 'https://shop.example/{lang}/products/{slug}',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Mapping notes
|
|
145
|
+
|
|
146
|
+
- ACP item `id` maps to Reactionary `ProductVariantIdentifier.sku`.
|
|
147
|
+
- ACP amounts are returned as integer minor units.
|
|
148
|
+
- ACP product feed variant `price` comes from `price.getCustomerPrice`, which includes active customer/global campaign prices and can fall back to list prices in providers.
|
|
149
|
+
- ACP product feed variant `list_price` comes from `price.getListPrice`.
|
|
150
|
+
- ACP product feed availability comes from `inventory.getBySKU`.
|
|
151
|
+
- ACP payment data is passed as checkout payment-instruction protocol data with key `delegated_payment_token`.
|
|
152
|
+
- Fulfillment options are sourced from `checkout.getAvailableShippingMethods`.
|
|
153
|
+
- Payment provider information is sourced from `checkout.getAvailablePaymentMethods`, with a configurable fallback through `paymentProvider`.
|
|
154
|
+
|
|
155
|
+
Use `@reactionary/feeds` directly when you want Google Merchant, sitemap XML, or PriceRunner outputs from the same feed definitions.
|
|
156
|
+
|
|
157
|
+
Product-feed upsert/push APIs and order webhooks are separate ACP surfaces and are not implemented yet.
|
package/index.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
const ACPItemSchema = z.object({
|
|
3
|
+
id: z.string().min(1),
|
|
4
|
+
quantity: z.int().min(1)
|
|
5
|
+
});
|
|
6
|
+
const ACPBuyerSchema = z.object({
|
|
7
|
+
name: z.string().min(1),
|
|
8
|
+
email: z.email(),
|
|
9
|
+
phone_number: z.string().optional()
|
|
10
|
+
});
|
|
11
|
+
const ACPAddressSchema = z.object({
|
|
12
|
+
name: z.string().min(1),
|
|
13
|
+
line_one: z.string().min(1),
|
|
14
|
+
line_two: z.string().optional(),
|
|
15
|
+
city: z.string().min(1),
|
|
16
|
+
state: z.string().min(1),
|
|
17
|
+
country: z.string().min(1),
|
|
18
|
+
postal_code: z.string().min(1),
|
|
19
|
+
phone_number: z.string().optional()
|
|
20
|
+
});
|
|
21
|
+
const ACPCreateCheckoutSessionRequestSchema = z.object({
|
|
22
|
+
buyer: ACPBuyerSchema.optional(),
|
|
23
|
+
items: z.array(ACPItemSchema).min(1),
|
|
24
|
+
fulfillment_address: ACPAddressSchema.optional()
|
|
25
|
+
});
|
|
26
|
+
const ACPUpdateCheckoutSessionRequestSchema = z.object({
|
|
27
|
+
buyer: ACPBuyerSchema.optional(),
|
|
28
|
+
items: z.array(ACPItemSchema).min(1).optional(),
|
|
29
|
+
fulfillment_address: ACPAddressSchema.optional(),
|
|
30
|
+
fulfillment_option_id: z.string().optional()
|
|
31
|
+
});
|
|
32
|
+
const ACPPaymentDataSchema = z.object({
|
|
33
|
+
token: z.string().min(1),
|
|
34
|
+
provider: z.enum(["stripe", "adyen", "braintree"]),
|
|
35
|
+
billing_address: ACPAddressSchema.optional()
|
|
36
|
+
});
|
|
37
|
+
const ACPCompleteCheckoutSessionRequestSchema = z.object({
|
|
38
|
+
buyer: ACPBuyerSchema.optional(),
|
|
39
|
+
payment_data: ACPPaymentDataSchema
|
|
40
|
+
});
|
|
41
|
+
const ACPCheckoutSessionStateSchema = z.looseObject({
|
|
42
|
+
id: z.string(),
|
|
43
|
+
cartId: z.string(),
|
|
44
|
+
checkoutId: z.string(),
|
|
45
|
+
status: z.enum([
|
|
46
|
+
"not_ready_for_payment",
|
|
47
|
+
"ready_for_payment",
|
|
48
|
+
"completed",
|
|
49
|
+
"canceled"
|
|
50
|
+
]),
|
|
51
|
+
buyer: ACPBuyerSchema.optional(),
|
|
52
|
+
fulfillmentAddress: ACPAddressSchema.optional(),
|
|
53
|
+
fulfillmentOptionId: z.string().optional(),
|
|
54
|
+
orderId: z.string().optional()
|
|
55
|
+
});
|
|
56
|
+
const ACPDescriptionSchema = z.object({
|
|
57
|
+
plain: z.string().optional(),
|
|
58
|
+
html: z.string().optional(),
|
|
59
|
+
markdown: z.string().optional()
|
|
60
|
+
});
|
|
61
|
+
const ACPMediaSchema = z.object({
|
|
62
|
+
url: z.string(),
|
|
63
|
+
alt_text: z.string().optional()
|
|
64
|
+
});
|
|
65
|
+
const ACPFeedPriceSchema = z.object({
|
|
66
|
+
amount: z.int(),
|
|
67
|
+
currency: z.string().length(3)
|
|
68
|
+
});
|
|
69
|
+
const ACPAvailabilitySchema = z.object({
|
|
70
|
+
available: z.boolean().optional(),
|
|
71
|
+
status: z.string().optional()
|
|
72
|
+
});
|
|
73
|
+
const ACPBarcodeSchema = z.object({
|
|
74
|
+
type: z.string(),
|
|
75
|
+
value: z.string()
|
|
76
|
+
});
|
|
77
|
+
const ACPVariantOptionSchema = z.object({
|
|
78
|
+
name: z.string(),
|
|
79
|
+
value: z.string()
|
|
80
|
+
});
|
|
81
|
+
const ACPFeedVariantSchema = z.object({
|
|
82
|
+
id: z.string(),
|
|
83
|
+
title: z.string(),
|
|
84
|
+
description: ACPDescriptionSchema.optional(),
|
|
85
|
+
url: z.string().optional(),
|
|
86
|
+
barcodes: z.array(ACPBarcodeSchema).optional(),
|
|
87
|
+
price: ACPFeedPriceSchema.optional(),
|
|
88
|
+
list_price: ACPFeedPriceSchema.optional(),
|
|
89
|
+
availability: ACPAvailabilitySchema.optional(),
|
|
90
|
+
variant_options: z.array(ACPVariantOptionSchema).optional(),
|
|
91
|
+
media: z.array(ACPMediaSchema).optional()
|
|
92
|
+
});
|
|
93
|
+
const ACPFeedProductSchema = z.object({
|
|
94
|
+
id: z.string(),
|
|
95
|
+
title: z.string().optional(),
|
|
96
|
+
description: ACPDescriptionSchema.optional(),
|
|
97
|
+
url: z.string().optional(),
|
|
98
|
+
media: z.array(ACPMediaSchema).optional(),
|
|
99
|
+
variants: z.array(ACPFeedVariantSchema).min(1)
|
|
100
|
+
});
|
|
101
|
+
const ACPProductFeedResponseSchema = z.object({
|
|
102
|
+
target_country: z.string().length(2).optional(),
|
|
103
|
+
products: z.array(ACPFeedProductSchema)
|
|
104
|
+
});
|
|
105
|
+
export {
|
|
106
|
+
ACPAddressSchema,
|
|
107
|
+
ACPAvailabilitySchema,
|
|
108
|
+
ACPBarcodeSchema,
|
|
109
|
+
ACPBuyerSchema,
|
|
110
|
+
ACPCheckoutSessionStateSchema,
|
|
111
|
+
ACPCompleteCheckoutSessionRequestSchema,
|
|
112
|
+
ACPCreateCheckoutSessionRequestSchema,
|
|
113
|
+
ACPDescriptionSchema,
|
|
114
|
+
ACPFeedPriceSchema,
|
|
115
|
+
ACPFeedProductSchema,
|
|
116
|
+
ACPFeedVariantSchema,
|
|
117
|
+
ACPItemSchema,
|
|
118
|
+
ACPMediaSchema,
|
|
119
|
+
ACPPaymentDataSchema,
|
|
120
|
+
ACPProductFeedResponseSchema,
|
|
121
|
+
ACPUpdateCheckoutSessionRequestSchema,
|
|
122
|
+
ACPVariantOptionSchema
|
|
123
|
+
};
|