@reactionary/acp 0.10.41 → 0.10.43
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 +52 -57
- package/index.js +11 -0
- package/lib/acp-discounts.js +113 -0
- package/lib/acp-idempotency.js +119 -0
- package/lib/acp-mcp.js +206 -0
- package/lib/acp-payment-handlers.js +38 -0
- package/lib/acp-schemas.js +240 -70
- package/lib/acp-webhooks.js +72 -0
- package/lib/reactionary-acp-server.js +1156 -253
- package/package.json +3 -3
- package/src/index.d.ts +4 -0
- package/src/lib/acp-discounts.d.ts +45 -0
- package/src/lib/acp-idempotency.d.ts +23 -0
- package/src/lib/acp-mcp.d.ts +21 -0
- package/src/lib/acp-payment-handlers.d.ts +64 -0
- package/src/lib/acp-schemas.d.ts +766 -209
- package/src/lib/acp-webhooks.d.ts +48 -0
- package/src/lib/reactionary-acp-server.d.ts +343 -39
package/lib/acp-schemas.js
CHANGED
|
@@ -1,12 +1,41 @@
|
|
|
1
|
+
import { CurrencySchema } from "@reactionary/core";
|
|
1
2
|
import * as z from "zod";
|
|
3
|
+
import { ACPDiscountsRequestSchema, ACPRejectedDiscountSchema } from "./acp-discounts.js";
|
|
2
4
|
const ACPItemSchema = z.object({
|
|
3
5
|
id: z.string().min(1),
|
|
4
|
-
quantity: z.
|
|
6
|
+
quantity: z.number().positive().optional(),
|
|
7
|
+
name: z.string().optional(),
|
|
8
|
+
unit_amount: z.int().optional()
|
|
5
9
|
});
|
|
6
|
-
const
|
|
10
|
+
const ACPCompanyInfoSchema = z.object({
|
|
7
11
|
name: z.string().min(1),
|
|
12
|
+
tax_id: z.string().optional(),
|
|
13
|
+
department: z.string().optional(),
|
|
14
|
+
cost_center: z.string().optional()
|
|
15
|
+
});
|
|
16
|
+
const ACPLoyaltyInfoSchema = z.object({
|
|
17
|
+
tier: z.string().optional(),
|
|
18
|
+
points_balance: z.int().optional(),
|
|
19
|
+
member_since: z.string().optional()
|
|
20
|
+
});
|
|
21
|
+
const ACPTaxExemptionSchema = z.object({
|
|
22
|
+
certificate_id: z.string().min(1),
|
|
23
|
+
certificate_type: z.enum(["resale", "exempt_organization", "government"]),
|
|
24
|
+
exempt_regions: z.array(z.string()).optional(),
|
|
25
|
+
expires_at: z.string().optional()
|
|
26
|
+
});
|
|
27
|
+
const ACPBuyerSchema = z.object({
|
|
28
|
+
first_name: z.string().optional(),
|
|
29
|
+
last_name: z.string().optional(),
|
|
30
|
+
full_name: z.string().optional(),
|
|
8
31
|
email: z.email(),
|
|
9
|
-
phone_number: z.string().optional()
|
|
32
|
+
phone_number: z.string().optional(),
|
|
33
|
+
customer_id: z.string().optional(),
|
|
34
|
+
account_type: z.enum(["guest", "registered", "business"]).optional(),
|
|
35
|
+
authentication_status: z.enum(["authenticated", "guest", "requires_signin"]).optional(),
|
|
36
|
+
company: ACPCompanyInfoSchema.optional(),
|
|
37
|
+
loyalty: ACPLoyaltyInfoSchema.optional(),
|
|
38
|
+
tax_exemption: ACPTaxExemptionSchema.optional()
|
|
10
39
|
});
|
|
11
40
|
const ACPAddressSchema = z.object({
|
|
12
41
|
name: z.string().min(1),
|
|
@@ -15,113 +44,254 @@ const ACPAddressSchema = z.object({
|
|
|
15
44
|
city: z.string().min(1),
|
|
16
45
|
state: z.string().min(1),
|
|
17
46
|
country: z.string().min(1),
|
|
18
|
-
postal_code: z.string().min(1)
|
|
19
|
-
|
|
47
|
+
postal_code: z.string().min(1)
|
|
48
|
+
});
|
|
49
|
+
const ACPFulfillmentDetailsSchema = z.object({
|
|
50
|
+
name: z.string().optional(),
|
|
51
|
+
phone_number: z.string().optional(),
|
|
52
|
+
email: z.email().optional(),
|
|
53
|
+
address: ACPAddressSchema.optional()
|
|
54
|
+
});
|
|
55
|
+
const ACPSelectedFulfillmentOptionSchema = z.object({
|
|
56
|
+
type: z.enum(["shipping", "digital", "pickup", "local_delivery"]),
|
|
57
|
+
option_id: z.string().min(1),
|
|
58
|
+
item_ids: z.array(z.string())
|
|
59
|
+
});
|
|
60
|
+
const ACPFlatMetadataSchema = z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).refine((metadata) => Object.keys(metadata).length <= 20, { error: "metadata may hold at most 20 keys" }).refine((metadata) => JSON.stringify(metadata).length <= 4096, { error: "metadata may be at most 4KB" });
|
|
61
|
+
const ACPAffiliateAttributionSchema = z.object({
|
|
62
|
+
provider: z.string().min(1),
|
|
63
|
+
token: z.string().optional(),
|
|
64
|
+
publisher_id: z.string().optional(),
|
|
65
|
+
campaign_id: z.string().optional(),
|
|
66
|
+
creative_id: z.string().optional(),
|
|
67
|
+
sub_id: z.string().optional(),
|
|
68
|
+
source: z.object({
|
|
69
|
+
type: z.enum(["url", "platform", "unknown"]),
|
|
70
|
+
url: z.url().optional()
|
|
71
|
+
}).optional(),
|
|
72
|
+
issued_at: z.iso.datetime({ offset: true }).optional(),
|
|
73
|
+
expires_at: z.iso.datetime({ offset: true }).optional(),
|
|
74
|
+
metadata: ACPFlatMetadataSchema.optional(),
|
|
75
|
+
touchpoint: z.enum(["first", "last"]).optional()
|
|
76
|
+
}).refine(
|
|
77
|
+
(attribution) => attribution.token !== void 0 || attribution.publisher_id !== void 0,
|
|
78
|
+
{ error: "token or publisher_id is required", path: ["token"] }
|
|
79
|
+
);
|
|
80
|
+
const ACP_INTERVENTION_TYPES = ["3ds", "biometric", "address_verification"];
|
|
81
|
+
const ACPAgentCapabilitiesSchema = z.object({
|
|
82
|
+
interventions: z.object({
|
|
83
|
+
supported: z.array(z.string()).optional(),
|
|
84
|
+
display_context: z.string().optional(),
|
|
85
|
+
redirect_context: z.string().optional(),
|
|
86
|
+
max_redirects: z.int().min(0).optional(),
|
|
87
|
+
max_interaction_depth: z.int().min(1).optional()
|
|
88
|
+
}).optional(),
|
|
89
|
+
extensions: z.array(z.string()).optional()
|
|
20
90
|
});
|
|
21
91
|
const ACPCreateCheckoutSessionRequestSchema = z.object({
|
|
92
|
+
capabilities: ACPAgentCapabilitiesSchema,
|
|
93
|
+
locale: z.string().optional(),
|
|
94
|
+
timezone: z.string().optional(),
|
|
95
|
+
affiliate_attribution: ACPAffiliateAttributionSchema.optional(),
|
|
96
|
+
discounts: ACPDiscountsRequestSchema.optional(),
|
|
97
|
+
/** Deprecated alias of `discounts.codes`. */
|
|
98
|
+
coupons: z.array(z.string().min(1)).optional(),
|
|
22
99
|
buyer: ACPBuyerSchema.optional(),
|
|
23
|
-
|
|
24
|
-
|
|
100
|
+
line_items: z.array(ACPItemSchema).min(1),
|
|
101
|
+
currency: z.string().refine(
|
|
102
|
+
(currency) => CurrencySchema.safeParse(currency.toUpperCase()).success,
|
|
103
|
+
"Expected an ISO 4217 currency code"
|
|
104
|
+
),
|
|
105
|
+
fulfillment_details: ACPFulfillmentDetailsSchema.optional()
|
|
25
106
|
});
|
|
26
107
|
const ACPUpdateCheckoutSessionRequestSchema = z.object({
|
|
27
108
|
buyer: ACPBuyerSchema.optional(),
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
109
|
+
line_items: z.array(ACPItemSchema).min(1).optional(),
|
|
110
|
+
discounts: ACPDiscountsRequestSchema.optional(),
|
|
111
|
+
/** Deprecated alias of `discounts.codes`. */
|
|
112
|
+
coupons: z.array(z.string().min(1)).optional(),
|
|
113
|
+
// null clears the field; absent leaves it unchanged.
|
|
114
|
+
fulfillment_details: ACPFulfillmentDetailsSchema.nullable().optional(),
|
|
115
|
+
selected_fulfillment_options: z.array(ACPSelectedFulfillmentOptionSchema).nullable().optional()
|
|
31
116
|
});
|
|
32
117
|
const ACPPaymentDataSchema = z.object({
|
|
33
|
-
|
|
34
|
-
|
|
118
|
+
handler_id: z.string().min(1),
|
|
119
|
+
instrument: z.object({
|
|
120
|
+
type: z.string().min(1),
|
|
121
|
+
credential: z.looseObject({
|
|
122
|
+
type: z.string().min(1),
|
|
123
|
+
token: z.string().min(1)
|
|
124
|
+
})
|
|
125
|
+
}),
|
|
35
126
|
billing_address: ACPAddressSchema.optional()
|
|
36
127
|
});
|
|
128
|
+
const ACPAuthenticationMetadataSchema = z.looseObject({
|
|
129
|
+
channel: z.looseObject({}).optional(),
|
|
130
|
+
acquirer_details: z.object({
|
|
131
|
+
acquirer_bin: z.string().max(11),
|
|
132
|
+
acquirer_country: z.string().length(2),
|
|
133
|
+
acquirer_merchant_id: z.string().max(35),
|
|
134
|
+
merchant_name: z.string().max(40),
|
|
135
|
+
requestor_id: z.string().max(35).optional()
|
|
136
|
+
}),
|
|
137
|
+
directory_server: z.enum(["american_express", "mastercard", "visa"]),
|
|
138
|
+
flow_preference: z.object({
|
|
139
|
+
type: z.enum(["challenge", "frictionless"]),
|
|
140
|
+
challenge: z.object({ type: z.enum(["mandated", "preferred"]).optional() }).optional(),
|
|
141
|
+
frictionless: z.object({ type: z.enum(["low_risk"]).optional() }).optional()
|
|
142
|
+
}).optional()
|
|
143
|
+
});
|
|
144
|
+
const ACP_AUTHENTICATED_OUTCOMES = ["authenticated", "attempt_acknowledged", "informational"];
|
|
145
|
+
const ACPAuthenticationResultSchema = z.object({
|
|
146
|
+
outcome: z.enum([
|
|
147
|
+
"abandoned",
|
|
148
|
+
"attempt_acknowledged",
|
|
149
|
+
"authenticated",
|
|
150
|
+
"canceled",
|
|
151
|
+
"denied",
|
|
152
|
+
"informational",
|
|
153
|
+
"internal_error",
|
|
154
|
+
"not_supported",
|
|
155
|
+
"processing_error",
|
|
156
|
+
"rejected"
|
|
157
|
+
]),
|
|
158
|
+
outcome_details: z.object({
|
|
159
|
+
three_ds_cryptogram: z.string(),
|
|
160
|
+
electronic_commerce_indicator: z.enum(["01", "02", "05", "06", "07"]),
|
|
161
|
+
transaction_id: z.string(),
|
|
162
|
+
version: z.string()
|
|
163
|
+
}).optional()
|
|
164
|
+
}).refine(
|
|
165
|
+
(result) => result.outcome_details !== void 0 || !ACP_AUTHENTICATED_OUTCOMES.includes(result.outcome),
|
|
166
|
+
{ error: "outcome_details is required for this outcome", path: ["outcome_details"] }
|
|
167
|
+
);
|
|
37
168
|
const ACPCompleteCheckoutSessionRequestSchema = z.object({
|
|
38
169
|
buyer: ACPBuyerSchema.optional(),
|
|
39
|
-
payment_data: ACPPaymentDataSchema
|
|
170
|
+
payment_data: ACPPaymentDataSchema,
|
|
171
|
+
authentication_result: ACPAuthenticationResultSchema.optional(),
|
|
172
|
+
affiliate_attribution: ACPAffiliateAttributionSchema.optional(),
|
|
173
|
+
/** The buyer's marketing consent decisions for the offered channels. */
|
|
174
|
+
marketing_consents: z.array(z.object({ channel: z.string().min(1), opted_in: z.boolean() })).optional()
|
|
175
|
+
});
|
|
176
|
+
const ACPCartCreateRequestSchema = z.object({
|
|
177
|
+
line_items: z.array(ACPItemSchema).min(1),
|
|
178
|
+
buyer: ACPBuyerSchema.optional(),
|
|
179
|
+
locale: z.string().optional()
|
|
180
|
+
});
|
|
181
|
+
const ACPCartUpdateRequestSchema = z.object({
|
|
182
|
+
line_items: z.array(ACPItemSchema),
|
|
183
|
+
buyer: ACPBuyerSchema.optional()
|
|
184
|
+
});
|
|
185
|
+
const ACPCartStateSchema = z.looseObject({
|
|
186
|
+
id: z.string(),
|
|
187
|
+
cartId: z.string(),
|
|
188
|
+
sessionId: z.string().optional(),
|
|
189
|
+
agentId: z.string().optional(),
|
|
190
|
+
buyer: ACPBuyerSchema.optional(),
|
|
191
|
+
expiresAt: z.string()
|
|
192
|
+
});
|
|
193
|
+
const ACP_INTENT_TRACE_REASON_CODES = [
|
|
194
|
+
"price_sensitivity",
|
|
195
|
+
"shipping_cost",
|
|
196
|
+
"shipping_speed",
|
|
197
|
+
"product_fit",
|
|
198
|
+
"trust_security",
|
|
199
|
+
"returns_policy",
|
|
200
|
+
"payment_options",
|
|
201
|
+
"comparison",
|
|
202
|
+
"timing_deferred",
|
|
203
|
+
"other"
|
|
204
|
+
];
|
|
205
|
+
const ACPIntentTraceSchema = z.object({
|
|
206
|
+
reason_code: z.string().min(1).transform((code) => ACP_INTENT_TRACE_REASON_CODES.includes(code) ? code : "other"),
|
|
207
|
+
trace_summary: z.string().max(500).optional(),
|
|
208
|
+
metadata: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional()
|
|
209
|
+
});
|
|
210
|
+
const ACPCancelCheckoutSessionRequestSchema = z.object({
|
|
211
|
+
intent_trace: ACPIntentTraceSchema.optional()
|
|
40
212
|
});
|
|
41
213
|
const ACPCheckoutSessionStateSchema = z.looseObject({
|
|
42
214
|
id: z.string(),
|
|
43
215
|
// The ACP session owning the backend cart, resumed for later requests.
|
|
44
216
|
sessionId: z.string().optional(),
|
|
217
|
+
/** The authenticated agent that created the session and may access it. */
|
|
218
|
+
agentId: z.string().optional(),
|
|
45
219
|
cartId: z.string(),
|
|
220
|
+
/** BCP 47 locale and IANA timezone requested on creation. */
|
|
221
|
+
locale: z.string().optional(),
|
|
222
|
+
timezone: z.string().optional(),
|
|
223
|
+
createdAt: z.string().optional(),
|
|
224
|
+
updatedAt: z.string().optional(),
|
|
225
|
+
/** What the agent declared on creation; capabilities are write-only. */
|
|
226
|
+
agentCapabilities: ACPAgentCapabilitiesSchema.optional(),
|
|
227
|
+
/** ISO 4217 currency requested on creation, lower case. */
|
|
228
|
+
currency: z.string().optional(),
|
|
46
229
|
// Set once completion has created the real reactionary checkout.
|
|
47
230
|
checkoutId: z.string().optional(),
|
|
48
231
|
status: z.enum([
|
|
49
232
|
"not_ready_for_payment",
|
|
50
233
|
"ready_for_payment",
|
|
234
|
+
"authentication_required",
|
|
51
235
|
"in_progress",
|
|
236
|
+
"complete_in_progress",
|
|
52
237
|
"completed",
|
|
53
238
|
"canceled"
|
|
54
239
|
]),
|
|
55
240
|
buyer: ACPBuyerSchema.optional(),
|
|
56
|
-
|
|
241
|
+
/** Affiliate attribution by touchpoint; write-only, never returned. */
|
|
242
|
+
firstTouchAttribution: z.looseObject({ provider: z.string() }).optional(),
|
|
243
|
+
lastTouchAttribution: z.looseObject({ provider: z.string() }).optional(),
|
|
244
|
+
/** Marketing consents submitted on completion, reported with the order. */
|
|
245
|
+
marketingConsents: z.array(z.looseObject({ channel: z.string(), opted_in: z.boolean() })).optional(),
|
|
246
|
+
/** Why the agent canceled the session; write-only, never returned. */
|
|
247
|
+
intentTrace: z.looseObject({
|
|
248
|
+
reason_code: z.string(),
|
|
249
|
+
trace_summary: z.string().optional(),
|
|
250
|
+
metadata: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional()
|
|
251
|
+
}).optional(),
|
|
252
|
+
/** Discount codes submitted (discount extension) and those rejected. */
|
|
253
|
+
discountCodes: z.array(z.string()).optional(),
|
|
254
|
+
rejectedDiscounts: z.array(ACPRejectedDiscountSchema).optional(),
|
|
255
|
+
/** The 3DS metadata the session awaits an authentication result for. */
|
|
256
|
+
authenticationMetadata: ACPAuthenticationMetadataSchema.optional(),
|
|
257
|
+
fulfillmentDetails: ACPFulfillmentDetailsSchema.optional(),
|
|
258
|
+
// The backend checkout has one shipping method, so one option is kept.
|
|
57
259
|
fulfillmentOptionId: z.string().optional(),
|
|
58
260
|
orderId: z.string().optional()
|
|
59
261
|
});
|
|
60
|
-
const
|
|
61
|
-
plain: z.string().optional(),
|
|
62
|
-
html: z.string().optional(),
|
|
63
|
-
markdown: z.string().optional()
|
|
64
|
-
});
|
|
65
|
-
const ACPMediaSchema = z.object({
|
|
66
|
-
url: z.string(),
|
|
67
|
-
alt_text: z.string().optional()
|
|
68
|
-
});
|
|
69
|
-
const ACPFeedPriceSchema = z.object({
|
|
70
|
-
amount: z.int(),
|
|
71
|
-
currency: z.string().length(3)
|
|
72
|
-
});
|
|
73
|
-
const ACPAvailabilitySchema = z.object({
|
|
74
|
-
available: z.boolean().optional(),
|
|
75
|
-
status: z.string().optional()
|
|
76
|
-
});
|
|
77
|
-
const ACPBarcodeSchema = z.object({
|
|
78
|
-
type: z.string(),
|
|
79
|
-
value: z.string()
|
|
80
|
-
});
|
|
81
|
-
const ACPVariantOptionSchema = z.object({
|
|
82
|
-
name: z.string(),
|
|
83
|
-
value: z.string()
|
|
84
|
-
});
|
|
85
|
-
const ACPFeedVariantSchema = z.object({
|
|
262
|
+
const ACPOrderRecordSchema = z.looseObject({
|
|
86
263
|
id: z.string(),
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
list_price: ACPFeedPriceSchema.optional(),
|
|
93
|
-
availability: ACPAvailabilitySchema.optional(),
|
|
94
|
-
variant_options: z.array(ACPVariantOptionSchema).optional(),
|
|
95
|
-
media: z.array(ACPMediaSchema).optional()
|
|
96
|
-
});
|
|
97
|
-
const ACPFeedProductSchema = z.object({
|
|
98
|
-
id: z.string(),
|
|
99
|
-
title: z.string().optional(),
|
|
100
|
-
description: ACPDescriptionSchema.optional(),
|
|
101
|
-
url: z.string().optional(),
|
|
102
|
-
media: z.array(ACPMediaSchema).optional(),
|
|
103
|
-
variants: z.array(ACPFeedVariantSchema).min(1)
|
|
104
|
-
});
|
|
105
|
-
const ACPProductFeedResponseSchema = z.object({
|
|
106
|
-
target_country: z.string().length(2).optional(),
|
|
107
|
-
products: z.array(ACPFeedProductSchema)
|
|
264
|
+
checkoutSessionId: z.string(),
|
|
265
|
+
/** The ACP session that placed the order; backends scope orders to it. */
|
|
266
|
+
sessionId: z.string().optional(),
|
|
267
|
+
/** The agent that placed the order, whose webhook receives its events. */
|
|
268
|
+
agentId: z.string().optional()
|
|
108
269
|
});
|
|
109
270
|
export {
|
|
110
271
|
ACPAddressSchema,
|
|
111
|
-
|
|
112
|
-
|
|
272
|
+
ACPAffiliateAttributionSchema,
|
|
273
|
+
ACPAgentCapabilitiesSchema,
|
|
274
|
+
ACPAuthenticationMetadataSchema,
|
|
275
|
+
ACPAuthenticationResultSchema,
|
|
113
276
|
ACPBuyerSchema,
|
|
277
|
+
ACPCancelCheckoutSessionRequestSchema,
|
|
278
|
+
ACPCartCreateRequestSchema,
|
|
279
|
+
ACPCartStateSchema,
|
|
280
|
+
ACPCartUpdateRequestSchema,
|
|
114
281
|
ACPCheckoutSessionStateSchema,
|
|
282
|
+
ACPCompanyInfoSchema,
|
|
115
283
|
ACPCompleteCheckoutSessionRequestSchema,
|
|
116
284
|
ACPCreateCheckoutSessionRequestSchema,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
ACPFeedProductSchema,
|
|
120
|
-
ACPFeedVariantSchema,
|
|
285
|
+
ACPFulfillmentDetailsSchema,
|
|
286
|
+
ACPIntentTraceSchema,
|
|
121
287
|
ACPItemSchema,
|
|
122
|
-
|
|
288
|
+
ACPLoyaltyInfoSchema,
|
|
289
|
+
ACPOrderRecordSchema,
|
|
123
290
|
ACPPaymentDataSchema,
|
|
124
|
-
|
|
291
|
+
ACPSelectedFulfillmentOptionSchema,
|
|
292
|
+
ACPTaxExemptionSchema,
|
|
125
293
|
ACPUpdateCheckoutSessionRequestSchema,
|
|
126
|
-
|
|
294
|
+
ACP_AUTHENTICATED_OUTCOMES,
|
|
295
|
+
ACP_INTENT_TRACE_REASON_CODES,
|
|
296
|
+
ACP_INTERVENTION_TYPES
|
|
127
297
|
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
const DEFAULT_RETRY_DELAYS_MS = [500, 2e3, 1e4, 6e4, 3e5];
|
|
3
|
+
const REQUEST_TIMEOUT_MS = 5e3;
|
|
4
|
+
class ACPOrderWebhooks {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
this.retryDelaysMs = options.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
|
|
8
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
9
|
+
for (const endpoint of options.endpoints) {
|
|
10
|
+
if (!endpoint.secret || !this.isAllowedUrl(endpoint.url)) {
|
|
11
|
+
throw new Error(`ACP webhook endpoint ${endpoint.url} needs an https URL and a secret.`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
options;
|
|
16
|
+
retryDelaysMs;
|
|
17
|
+
fetchImpl;
|
|
18
|
+
/** The endpoint for an agent's orders, if any. */
|
|
19
|
+
getEndpoint(agentId) {
|
|
20
|
+
return this.options.endpoints.find((endpoint) => agentId !== void 0 && endpoint.agentId === agentId) ?? this.options.endpoints.find((endpoint) => endpoint.agentId === void 0);
|
|
21
|
+
}
|
|
22
|
+
/** Sends an event without waiting for it; the order is the full current state. */
|
|
23
|
+
deliver(endpoint, type, order) {
|
|
24
|
+
const body = JSON.stringify({ type, data: order });
|
|
25
|
+
const requestId = `evt_${crypto.randomUUID()}`;
|
|
26
|
+
void this.attempt(endpoint, body, requestId, 0);
|
|
27
|
+
}
|
|
28
|
+
async attempt(endpoint, body, requestId, attempt) {
|
|
29
|
+
let failure;
|
|
30
|
+
try {
|
|
31
|
+
const response = await this.fetchImpl(endpoint.url, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
"content-type": "application/json",
|
|
35
|
+
"request-id": requestId,
|
|
36
|
+
"merchant-signature": signWebhookPayload(body, endpoint.secret)
|
|
37
|
+
},
|
|
38
|
+
body,
|
|
39
|
+
redirect: "manual",
|
|
40
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
41
|
+
});
|
|
42
|
+
if (response.ok) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
failure = `HTTP ${response.status}`;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
failure = error instanceof Error ? error.message : String(error);
|
|
48
|
+
}
|
|
49
|
+
const delay = this.retryDelaysMs[attempt];
|
|
50
|
+
if (delay === void 0) {
|
|
51
|
+
console.error(`ACP: giving up on webhook ${requestId} to ${endpoint.url} after ${attempt + 1} attempts: ${failure}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
setTimeout(() => void this.attempt(endpoint, body, requestId, attempt + 1), delay).unref();
|
|
55
|
+
}
|
|
56
|
+
isAllowedUrl(value) {
|
|
57
|
+
try {
|
|
58
|
+
const { protocol } = new URL(value);
|
|
59
|
+
return protocol === "https:" || protocol === "http:" && Boolean(this.options.allowInsecureUrls);
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function signWebhookPayload(body, secret, timestamp = Math.floor(Date.now() / 1e3)) {
|
|
66
|
+
const signature = createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
|
|
67
|
+
return `t=${timestamp},v1=${signature}`;
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
ACPOrderWebhooks,
|
|
71
|
+
signWebhookPayload
|
|
72
|
+
};
|