@nuanu-ai/agentify-contracts 0.3.2
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/LICENSE +201 -0
- package/NOTICE +2 -0
- package/README.md +32 -0
- package/dist/api.d.ts +1948 -0
- package/dist/api.js +956 -0
- package/dist/card.d.ts +423 -0
- package/dist/card.js +700 -0
- package/dist/envelope.d.ts +174 -0
- package/dist/envelope.js +101 -0
- package/dist/events.d.ts +56 -0
- package/dist/events.js +54 -0
- package/dist/evm-address.d.ts +77 -0
- package/dist/evm-address.js +265 -0
- package/dist/handler.d.ts +90 -0
- package/dist/handler.js +102 -0
- package/dist/index.d.ts +1008 -0
- package/dist/index.js +167 -0
- package/dist/merchant.d.ts +281 -0
- package/dist/merchant.js +433 -0
- package/dist/order-status.d.ts +47 -0
- package/dist/order-status.js +102 -0
- package/dist/order.d.ts +50 -0
- package/dist/order.js +78 -0
- package/dist/param-spec.d.ts +145 -0
- package/dist/param-spec.js +147 -0
- package/dist/primitives.d.ts +103 -0
- package/dist/primitives.js +130 -0
- package/dist/quote.d.ts +79 -0
- package/dist/quote.js +92 -0
- package/dist/receipt.d.ts +79 -0
- package/dist/receipt.js +83 -0
- package/dist/results.d.ts +230 -0
- package/dist/results.js +269 -0
- package/dist/selling.d.ts +24 -0
- package/dist/selling.js +35 -0
- package/package.json +46 -0
package/dist/api.js
ADDED
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP surface: which calls exist, at which addresses, with which document
|
|
3
|
+
* going each way.
|
|
4
|
+
*
|
|
5
|
+
* It is written as data rather than as prose because two programs read it. The
|
|
6
|
+
* gateway serves these calls and the SDK makes them, and each of them
|
|
7
|
+
* previously would have learned the surface from a document a person had
|
|
8
|
+
* transcribed — which is how one side ends up posting to `/v0/order/:id/deliver`
|
|
9
|
+
* while the other serves `/v0/orders/:id/deliver`, and how a response shape
|
|
10
|
+
* agreed in a meeting becomes two shapes in two repositories. Here the table is
|
|
11
|
+
* the agreement, and both sides import it.
|
|
12
|
+
*
|
|
13
|
+
* What the table does not carry is deliberate. There is no header name for the
|
|
14
|
+
* merchant key, no status codes, no base address, no timeouts and no retry
|
|
15
|
+
* policy. `auth` says which door a call is behind, not how the door is built;
|
|
16
|
+
* the key's header, the status code a refusal comes back under and where the
|
|
17
|
+
* service lives are the gateway's, and a number invented here would read as a
|
|
18
|
+
* decision nobody took. What belongs here is what both sides have to agree on
|
|
19
|
+
* or fail: the address, the method, the door, and the documents.
|
|
20
|
+
*
|
|
21
|
+
* The paths are written the way an express-style router writes them — a colon
|
|
22
|
+
* and a lowercase name, `/v0/orders/:order_id/deliver`. That form was chosen
|
|
23
|
+
* over the braces of OpenAPI for one reason: the gateway can mount these
|
|
24
|
+
* strings unchanged, so the side that must never be wrong about an address is
|
|
25
|
+
* the side that does no translation at all. The SDK is the side that
|
|
26
|
+
* substitutes, and it does not write its own substitution either: `pathParamsOf`
|
|
27
|
+
* and `expandPath` below are here so that the encoding is done once. That is
|
|
28
|
+
* not a nicety. An identifier in this contract may contain a slash and a space
|
|
29
|
+
* — "SKU 100/1" is one we accept — and pasted into an address unencoded it
|
|
30
|
+
* becomes two path segments and a different route.
|
|
31
|
+
*
|
|
32
|
+
* What each route names is the document it answers with when the call works.
|
|
33
|
+
* How it says no is not a per-route question and is not written route by
|
|
34
|
+
* route: every refusal on this surface, from any call, arrives in the one
|
|
35
|
+
* envelope `ErrorEnvelopeSchema` describes below.
|
|
36
|
+
*
|
|
37
|
+
* Two ways of answering for an order sit in the table and they are not the
|
|
38
|
+
* same thing, which is worth reading before either is used. What a merchant's
|
|
39
|
+
* handler returns — delivered, refused, taken on — goes to the answer route,
|
|
40
|
+
* in every mode, and the SDK sends it without the merchant asking. `deliver`
|
|
41
|
+
* and `refuse` are the asynchronous mode's explicit closure verbs, called
|
|
42
|
+
* later by a merchant who took an order on and now has the goods or has run
|
|
43
|
+
* out of them. The order machine keeps the same two apart, and in the
|
|
44
|
+
* synchronous mode it is the returned answer that exists and the explicit
|
|
45
|
+
* calls that do not. The addendum of 2026-08-26 to ADR-0004 settles this; §2
|
|
46
|
+
* of that decision, read alone, leaves the synchronous handler with no address
|
|
47
|
+
* at all.
|
|
48
|
+
*/
|
|
49
|
+
import { z } from "zod";
|
|
50
|
+
import { CardSchema, MerchantCardSchema, PublicCardSchema } from "./card.js";
|
|
51
|
+
import { WorkerEnvelopeSchema } from "./envelope.js";
|
|
52
|
+
import { AcceptanceSchema, DeliverySchema, HandlerAnswerSchema, RefusalSchema } from "./handler.js";
|
|
53
|
+
import { CabinetKeySchema, DisabledKeySchema, ForgottenCabinetKeySchema, IssuedKeySchema, IssueKeyRequestSchema, MerchantKeyListSchema, PayoutWalletRequestSchema, PayoutWalletSchema, RegisteredMerchantSchema, RegistrationRequestSchema, SellerNameRequestSchema, SellerNameSchema, } from "./merchant.js";
|
|
54
|
+
import { OrderSchema } from "./order.js";
|
|
55
|
+
import { OrderStatusSchema } from "./order-status.js";
|
|
56
|
+
import { ParamNameSchema } from "./param-spec.js";
|
|
57
|
+
import { IdentifierSchema, SalePriceSchema } from "./primitives.js";
|
|
58
|
+
import { QuoteResponseSchema } from "./quote.js";
|
|
59
|
+
import { ReceiptSchema } from "./receipt.js";
|
|
60
|
+
import { CallErrorSchema, OrderCallResultSchema, PublishResultSchema } from "./results.js";
|
|
61
|
+
import { SellingStateSchema } from "./selling.js";
|
|
62
|
+
/**
|
|
63
|
+
* An order together with the word for where it stands.
|
|
64
|
+
*
|
|
65
|
+
* The order itself carries no state — reading state back is a separate call,
|
|
66
|
+
* and this is that call's document. It is the order with one field added
|
|
67
|
+
* rather than the order nested inside a wrapper, because to the merchant it is
|
|
68
|
+
* one order that has a status, and a wrapper would exist only to keep a strict
|
|
69
|
+
* parser happy. The consequence is worth naming: this is a different document
|
|
70
|
+
* from an order off the worker stream, and `OrderSchema` refuses it. That is
|
|
71
|
+
* the intended reading, not an accident of strictness.
|
|
72
|
+
*/
|
|
73
|
+
export const OrderWithStatusSchema = OrderSchema.extend({
|
|
74
|
+
/** Where the order stands, in the words an agent and a merchant both read. */
|
|
75
|
+
status: OrderStatusSchema,
|
|
76
|
+
});
|
|
77
|
+
/**
|
|
78
|
+
* Every order the merchant asked for.
|
|
79
|
+
*
|
|
80
|
+
* An object rather than a bare array, because a bare array is the one shape
|
|
81
|
+
* that cannot grow. The day this list needs a cursor or a count, an array
|
|
82
|
+
* would have to become an object and every reader that already parses it would
|
|
83
|
+
* break at once.
|
|
84
|
+
*/
|
|
85
|
+
export const OrderListSchema = z.strictObject({
|
|
86
|
+
orders: z.array(OrderWithStatusSchema),
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* A merchant's own catalog: their cards, and whether they are selling at all.
|
|
90
|
+
*
|
|
91
|
+
* The merchant's own word sits beside the cards rather than being left to be
|
|
92
|
+
* inferred from them, because the two answer different questions and a screen
|
|
93
|
+
* needs both. Every card of a merchant who has stopped all selling reads
|
|
94
|
+
* paused, and nothing in the cards themselves says whether that is five
|
|
95
|
+
* separate decisions or one — which is exactly the difference between resuming
|
|
96
|
+
* a card and resuming the catalog.
|
|
97
|
+
*
|
|
98
|
+
* An object rather than a bare array for the reason the order list gives, and
|
|
99
|
+
* one more: a bare array has nowhere to put the merchant's own word at all.
|
|
100
|
+
*/
|
|
101
|
+
export const MerchantCardListSchema = z
|
|
102
|
+
.strictObject({
|
|
103
|
+
/** Whether this merchant is taking new orders at all. */
|
|
104
|
+
selling: SellingStateSchema,
|
|
105
|
+
cards: z.array(MerchantCardSchema),
|
|
106
|
+
})
|
|
107
|
+
.meta({
|
|
108
|
+
description: "A merchant's own catalog: every card they have published, and whether they are taking new orders at all. The merchant's own selling word is here as well as on each card because the two are different facts, and neither can be worked out from the other. A card reads open only where all of it holds at once: the merchant is selling, the card is not paused in its own right, the merchant has a name for it to be sold under, and — on a deployment that settles on a real chain — a wallet for the money to be paid into. So a card can read paused while this field reads open, and this field does not say which of the reasons it was. What does say is the refusal at the publish and the merchant's own settings, each of which names the piece that is missing. This document does not say whether it is the whole catalog: paging is not designed, and when it is, this object grows the field that answers it.",
|
|
109
|
+
});
|
|
110
|
+
/**
|
|
111
|
+
* Every receipt this merchant has.
|
|
112
|
+
*
|
|
113
|
+
* A receipt exists from the moment the money moves, so this is the list a
|
|
114
|
+
* merchant reconciles their wallet against. What it cannot show is worth
|
|
115
|
+
* knowing before it is reconciled from: a purchase that ended before any
|
|
116
|
+
* payment leaves no receipt at all, and no receipt is written while it is
|
|
117
|
+
* unknown whether the buyer was charged — `ReceiptSchema` works both through.
|
|
118
|
+
*
|
|
119
|
+
* An object rather than a bare array, so the day it needs paging it grows the
|
|
120
|
+
* field that says so instead of changing shape under every reader.
|
|
121
|
+
*/
|
|
122
|
+
export const ReceiptListSchema = z
|
|
123
|
+
.strictObject({
|
|
124
|
+
receipts: z.array(ReceiptSchema),
|
|
125
|
+
})
|
|
126
|
+
.meta({
|
|
127
|
+
description: "The receipts of one merchant. A receipt is written when the money moves, so a purchase that ended before any payment is not here, and neither is one whose payment outcome is still unknown — that receipt is written if and when the answer arrives. This document does not say whether it is the whole list: paging is not designed, and the absence of a field about it is not a promise that there is no more.",
|
|
128
|
+
});
|
|
129
|
+
/**
|
|
130
|
+
* The question a merchant puts in the query string when listing orders.
|
|
131
|
+
*
|
|
132
|
+
* `open=true` narrows the list to the orders that are still owed something —
|
|
133
|
+
* which includes the two that stay open after the purchase itself is over, an
|
|
134
|
+
* order owing a refund and one delivered but never paid for. Leaving it out
|
|
135
|
+
* asks for everything.
|
|
136
|
+
*
|
|
137
|
+
* The value is text and not a boolean, because that is what a query string
|
|
138
|
+
* carries. Written as a boolean it would export as one, and an engineer
|
|
139
|
+
* generating a client from that document would build a request nobody can
|
|
140
|
+
* send. The two words are exact on purpose: a merchant who wrote `open=1`,
|
|
141
|
+
* meant "only the open ones" and silently received all of them would reconcile
|
|
142
|
+
* their books against the wrong list.
|
|
143
|
+
*/
|
|
144
|
+
export const OrderListQuerySchema = z
|
|
145
|
+
.strictObject({
|
|
146
|
+
open: z.enum(["true", "false"]).optional(),
|
|
147
|
+
})
|
|
148
|
+
.meta({
|
|
149
|
+
description: 'Which orders to list. Written as text because a query string carries text. "true" narrows the list to the orders that are still owed something, which includes the two that stay open after the purchase itself is over: an order owing a refund, and one delivered but never paid for. Leaving the field out asks for everything.',
|
|
150
|
+
});
|
|
151
|
+
/**
|
|
152
|
+
* What a worker asks of one poll.
|
|
153
|
+
*
|
|
154
|
+
* Neither field carries a ceiling, and the absence is deliberate. A first
|
|
155
|
+
* draft put a maximum on each and called them bounds on the format; they were
|
|
156
|
+
* not. The bounds this package does write down are anchored in something
|
|
157
|
+
* outside itself — eighteen fractional digits because that is what an ERC-20
|
|
158
|
+
* token carries — and no such anchor exists for a number of seconds or a
|
|
159
|
+
* number of messages. A card is already held to that rule from the other side:
|
|
160
|
+
* it takes any whole positive number of seconds for its deadlines rather than
|
|
161
|
+
* inventing a limit that would read as a decision nobody took. How long the
|
|
162
|
+
* gateway will actually hold a request and how many envelopes it will actually
|
|
163
|
+
* return are the gateway's, and it is the one that has to answer for them.
|
|
164
|
+
*
|
|
165
|
+
* The two lower bounds stay, because each refuses something that is not a
|
|
166
|
+
* request at all rather than something that is merely large.
|
|
167
|
+
*/
|
|
168
|
+
export const WorkerPollRequestSchema = z
|
|
169
|
+
.strictObject({
|
|
170
|
+
/**
|
|
171
|
+
* How long the gateway may hold the request open waiting for something to
|
|
172
|
+
* arrive. Zero is allowed and means a drain: answer with whatever is
|
|
173
|
+
* queued right now and come straight back, which is what a worker shutting
|
|
174
|
+
* down asks for. The gateway will hold it for its own window at most, and
|
|
175
|
+
* that window is shorter than anything worth asking for.
|
|
176
|
+
*/
|
|
177
|
+
wait_seconds: z.int().min(0).optional(),
|
|
178
|
+
/**
|
|
179
|
+
* At most this many envelopes in the answer. One is the smallest request
|
|
180
|
+
* that means anything — at most zero envelopes is a call asking for
|
|
181
|
+
* nothing, answered with an empty batch forever and indistinguishable from
|
|
182
|
+
* a quiet queue.
|
|
183
|
+
*/
|
|
184
|
+
max: z.int().min(1).optional(),
|
|
185
|
+
})
|
|
186
|
+
.meta({
|
|
187
|
+
description: "What a worker asks of one poll. Both fields may be left out, and then the gateway's own defaults apply. Neither carries a ceiling here, because a ceiling on a wait or on a batch would be a policy number and the policy is the gateway's: it answers with its own window and its own batch size whatever is asked for.",
|
|
188
|
+
});
|
|
189
|
+
/**
|
|
190
|
+
* What a poll answers with.
|
|
191
|
+
*
|
|
192
|
+
* The list may be empty, and an empty batch is the ordinary answer to a quiet
|
|
193
|
+
* window rather than a failure. A worker that read it as one would tear down
|
|
194
|
+
* and rebuild its subscription every time nothing happened.
|
|
195
|
+
*
|
|
196
|
+
* The contract version rides here because this is the call every worker makes
|
|
197
|
+
* first and then forever. The SDK checks it and refuses to start against a
|
|
198
|
+
* gateway speaking another dialect, which is the difference between failing at
|
|
199
|
+
* startup and failing on somebody's first order.
|
|
200
|
+
*/
|
|
201
|
+
export const WorkerPollResponseSchema = z.strictObject({
|
|
202
|
+
/** The version of the contract this gateway speaks. */
|
|
203
|
+
contract_version: z.string().regex(/\S/, "a gateway names the contract version it speaks"),
|
|
204
|
+
envelopes: z.array(WorkerEnvelopeSchema),
|
|
205
|
+
});
|
|
206
|
+
/**
|
|
207
|
+
* The failure branch every order call shares: it did not go through, here is
|
|
208
|
+
* why, and here is whether calling again could change that.
|
|
209
|
+
*/
|
|
210
|
+
const OrderCallFailedSchema = z.strictObject({
|
|
211
|
+
ok: z.literal(false),
|
|
212
|
+
error: CallErrorSchema,
|
|
213
|
+
});
|
|
214
|
+
/**
|
|
215
|
+
* What delivering or refusing an order comes back as.
|
|
216
|
+
*
|
|
217
|
+
* `ok` is a value rather than a key, and it is the same envelope every
|
|
218
|
+
* merchant-facing call of this surface answers in; the reasons are written down
|
|
219
|
+
* once, in `results.ts`, rather than argued again at each route. What this
|
|
220
|
+
* route adds to them is a word for which success it was: the marker of success
|
|
221
|
+
* is one and the same for a first delivery and for a repeated one, so a
|
|
222
|
+
* merchant who wrote `if (result === "delivered")` would have turned their own
|
|
223
|
+
* safe retry into a failure branch, and branching on `ok` and recording the
|
|
224
|
+
* word is what this shape makes the easy thing to do.
|
|
225
|
+
*
|
|
226
|
+
* The failure is one `error` and not a list, and that is not an asymmetry with
|
|
227
|
+
* the publish. A call fails for one reason, wherever it is; what may be wrong
|
|
228
|
+
* in several places at once is the document that was sent, and those findings
|
|
229
|
+
* ride inside the error under `problems` — which is exactly what a delivery
|
|
230
|
+
* that is not what its card declares comes back with.
|
|
231
|
+
*
|
|
232
|
+
* Nothing here travels as an exception. A merchant's integration code is
|
|
233
|
+
* expected to read this, branch on it and write some of it down.
|
|
234
|
+
*/
|
|
235
|
+
export const OrderCallResponseSchema = z.discriminatedUnion("ok", [
|
|
236
|
+
z.strictObject({ ok: z.literal(true), result: OrderCallResultSchema }),
|
|
237
|
+
OrderCallFailedSchema,
|
|
238
|
+
]);
|
|
239
|
+
/**
|
|
240
|
+
* What taking an order on comes back as.
|
|
241
|
+
*
|
|
242
|
+
* The success carries no word, and that is a choice rather than a gap. The
|
|
243
|
+
* published results include `accepted`, so there is a word to carry; this
|
|
244
|
+
* route declines to carry it. Underneath, taking on an order that is waiting
|
|
245
|
+
* and taking on one already delivered are two different answers, and this
|
|
246
|
+
* route reports both as the same bare success — a merchant who takes on an
|
|
247
|
+
* order they have already finished learns nothing new here, which is what
|
|
248
|
+
* makes a repeat harmless. The answer route is where the word earns its
|
|
249
|
+
* place, because it carries whichever of the three things a handler returned
|
|
250
|
+
* and its success has to name which. `ok: true` here is a whole sentence on
|
|
251
|
+
* its own: true is true in every language, and the shape leaves room for a
|
|
252
|
+
* word beside it the day this route needs to tell two successes apart.
|
|
253
|
+
*
|
|
254
|
+
* Repeats are ordinary here. Delivery is at least once, so an order already
|
|
255
|
+
* taken on is taken on again every time it is redelivered, and an answer with
|
|
256
|
+
* no word in it has nothing to get wrong on the second pass.
|
|
257
|
+
*
|
|
258
|
+
* The failures are the same ones the other order calls have — accepting an
|
|
259
|
+
* order that has already closed, or accepting in a mode where acceptance does
|
|
260
|
+
* not exist.
|
|
261
|
+
*/
|
|
262
|
+
export const OrderAcceptResponseSchema = z.discriminatedUnion("ok", [
|
|
263
|
+
z.strictObject({ ok: z.literal(true) }),
|
|
264
|
+
OrderCallFailedSchema,
|
|
265
|
+
]);
|
|
266
|
+
/**
|
|
267
|
+
* What answering a price question comes back as.
|
|
268
|
+
*
|
|
269
|
+
* One fact, because one fact is what the merchant acts on. A merchant who set
|
|
270
|
+
* stock aside against the question needs to know whether the answer arrived in
|
|
271
|
+
* time to price the purchase; if it did not, they can release what they held.
|
|
272
|
+
*
|
|
273
|
+
* `used: false` also covers a question we no longer hold at all — a worker
|
|
274
|
+
* that restarted and replayed an envelope from an hour ago. The two are folded
|
|
275
|
+
* into one word deliberately, because the merchant does the same thing in
|
|
276
|
+
* either case, and the fold is written down here rather than left to be
|
|
277
|
+
* discovered. The flag is required, not defaulted: read as used, a missing
|
|
278
|
+
* flag leaves stock held forever; read as unused, it releases stock out from
|
|
279
|
+
* under a sale that is going through.
|
|
280
|
+
*/
|
|
281
|
+
export const QuoteAnswerAckSchema = z
|
|
282
|
+
.strictObject({
|
|
283
|
+
used: z.boolean(),
|
|
284
|
+
})
|
|
285
|
+
.meta({
|
|
286
|
+
description: "Whether the merchant's answer arrived in time to price the purchase it was asked for. False also covers a question we no longer hold — a worker replaying an old envelope after a restart — because in both cases the answer priced nothing and stock held against it can be released.",
|
|
287
|
+
});
|
|
288
|
+
/**
|
|
289
|
+
* What an agent sends to buy.
|
|
290
|
+
*
|
|
291
|
+
* The parameters are always there, empty for a product that needs no input,
|
|
292
|
+
* for the same reason an order's are: nobody downstream should have to tell
|
|
293
|
+
* "no parameters" from "the field did not arrive".
|
|
294
|
+
*
|
|
295
|
+
* This document holds the envelope and not the contents. Whether these
|
|
296
|
+
* particular values fit this particular card is checked against that card's
|
|
297
|
+
* own declaration at the moment of purchase — one card, one check, one error
|
|
298
|
+
* message — and no schema written in advance of a catalog can do it.
|
|
299
|
+
*/
|
|
300
|
+
export const PurchaseRequestSchema = z
|
|
301
|
+
.strictObject({
|
|
302
|
+
// The same dropped key as everywhere this contract parses free-form names;
|
|
303
|
+
// see `PROTOTYPE_KEY_IS_DROPPED` in `param-spec.ts`.
|
|
304
|
+
params: z.record(ParamNameSchema, z.unknown()),
|
|
305
|
+
})
|
|
306
|
+
.meta({
|
|
307
|
+
description: "What an agent supplies to buy: the purchase parameters, empty for a product that needs none. The names are held to the shape a card could have declared and no further — that these values fit this card is checked against that card at the moment of purchase.",
|
|
308
|
+
});
|
|
309
|
+
/**
|
|
310
|
+
* What became of a purchase, told to the agent that made it.
|
|
311
|
+
*
|
|
312
|
+
* Four fields, and the shape of the list is the decision rather than its
|
|
313
|
+
* length. This document is what the buyer is owed — where their order stands,
|
|
314
|
+
* what it cost them, and the goods once those exist — and it is deliberately
|
|
315
|
+
* smaller than the merchant's own view of the same order. It carries no
|
|
316
|
+
* merchant, no merchant's key for the product, none of the parameters the
|
|
317
|
+
* buyer sent and nothing about any other order, because whoever holds an
|
|
318
|
+
* order's identifier can read it (ADR-0011) and an answer assembled from the
|
|
319
|
+
* merchant's record would hand all of that to whoever guessed one.
|
|
320
|
+
*
|
|
321
|
+
* Both of the agent's doors answer in it, and that is one concept rather than
|
|
322
|
+
* two: the purchase and the later read are the same question — where does my
|
|
323
|
+
* order stand — asked at two moments. Two documents for it would be two
|
|
324
|
+
* readers for an integrator to write, and the one that came back from the
|
|
325
|
+
* purchase was the merchant's own, with the merchant's key for the product and
|
|
326
|
+
* the buyer's own parameters in it. What a purchase answers with beyond this
|
|
327
|
+
* document is the payment exchange that precedes it, which is a header and not
|
|
328
|
+
* a document at all.
|
|
329
|
+
*
|
|
330
|
+
* The two nullable fields are required rather than optional, and that is the
|
|
331
|
+
* fifth gate written into a shape. `price` is null for an order nobody ever
|
|
332
|
+
* named a price for — a product that was gone, a price question that went
|
|
333
|
+
* unanswered — and `delivered` is null while there are no goods to hand over.
|
|
334
|
+
* Left out instead, either would be a silence a reader cannot tell from an
|
|
335
|
+
* oversight; present and null, it is a fact.
|
|
336
|
+
*
|
|
337
|
+
* The status vocabulary folds several of the machine's endings into `rejected`
|
|
338
|
+
* on the argument that the reason travels separately, in the refusal code, and
|
|
339
|
+
* `refusal` is where it travels. It is optional because most endings have no
|
|
340
|
+
* refusal behind them, and what remains coarse is said in that field's own
|
|
341
|
+
* words rather than left to be discovered: a product that was gone and a
|
|
342
|
+
* payment that failed its check both still arrive as a bare `rejected`,
|
|
343
|
+
* because neither was worded by anybody.
|
|
344
|
+
*/
|
|
345
|
+
export const AgentOrderStatusSchema = z
|
|
346
|
+
.strictObject({
|
|
347
|
+
order_id: IdentifierSchema,
|
|
348
|
+
status: OrderStatusSchema,
|
|
349
|
+
/**
|
|
350
|
+
* The price this order was priced at, or null where nobody ever named one
|
|
351
|
+
* for it. It is the order's own price and not the card's number: a card
|
|
352
|
+
* with a price check is priced at what the check answered.
|
|
353
|
+
*
|
|
354
|
+
* "Priced at" and not "sold for", and the difference is worth the extra
|
|
355
|
+
* word. An order that was priced and then ended without a sale — the
|
|
356
|
+
* product was gone, a deadline ran out, the merchant left — still carries
|
|
357
|
+
* the number it was priced at, because that is what the buyer was asked
|
|
358
|
+
* for. Whether any money moved is the status's business and not this
|
|
359
|
+
* field's, and a reader taking this for an amount charged would be
|
|
360
|
+
* reconciling against sales that never happened.
|
|
361
|
+
*/
|
|
362
|
+
price: SalePriceSchema.nullable(),
|
|
363
|
+
/**
|
|
364
|
+
* The goods, once they are the buyer's — the delivery as the merchant
|
|
365
|
+
* wrote it, and null until then.
|
|
366
|
+
*
|
|
367
|
+
* "Once they are the buyer's" is narrower than "once the merchant handed
|
|
368
|
+
* them over", and the gap between the two is a real one. A synchronous
|
|
369
|
+
* delivery whose charge did not go through leaves goods sitting on an
|
|
370
|
+
* order nothing was paid for; the purchase itself refuses to hand those
|
|
371
|
+
* over and waits for a repeat that carries the payment, and this document
|
|
372
|
+
* says null for exactly as long as that is true.
|
|
373
|
+
*/
|
|
374
|
+
delivered: DeliverySchema.nullable(),
|
|
375
|
+
/**
|
|
376
|
+
* Whether the money behind this purchase was real.
|
|
377
|
+
*
|
|
378
|
+
* The receipt a merchant reads carries this word already, and the buyer's
|
|
379
|
+
* own view of the same purchase is the one place it matters more: a
|
|
380
|
+
* sandbox settles against nothing (ADR-0008) and every other field here
|
|
381
|
+
* reads exactly as it would after a real charge. Without it, this document
|
|
382
|
+
* is indistinguishable from proof of a purchase that moved money, which is
|
|
383
|
+
* the one thing it must never be mistaken for.
|
|
384
|
+
*/
|
|
385
|
+
test: z.boolean(),
|
|
386
|
+
/**
|
|
387
|
+
* Why the merchant would not sell, where a merchant's refusal is what
|
|
388
|
+
* closed this order — their own code and their own sentence, carried
|
|
389
|
+
* across word for word.
|
|
390
|
+
*
|
|
391
|
+
* It is the shape a merchant's handler answers in (`RefusalSchema`) and
|
|
392
|
+
* not a copy of it, because it is the same fact travelling one hop
|
|
393
|
+
* further: what the merchant wrote is what the agent reads, and two
|
|
394
|
+
* shapes for that would be two chances to say it differently. The code is
|
|
395
|
+
* what an agent branches on — `out_of_stock`, `invalid_params`,
|
|
396
|
+
* `cannot_fulfill`, or a word of the merchant's own, so an unfamiliar one
|
|
397
|
+
* has to fall through to the sentence rather than break a reader. The
|
|
398
|
+
* sentence is what it can show a person.
|
|
399
|
+
*
|
|
400
|
+
* Optional, and what its absence means is the whole of the fifth gate
|
|
401
|
+
* here. It is absent exactly where no merchant refused this order, and
|
|
402
|
+
* that covers most of the endings — a deadline that ran out, a charge
|
|
403
|
+
* that failed its check, a product that was simply gone. Those last two
|
|
404
|
+
* are worth naming because they still arrive as a bare `rejected`: a
|
|
405
|
+
* price answer of "not available" carries no words at all, and a payment
|
|
406
|
+
* this gateway would not vouch for is refused at the door in an error
|
|
407
|
+
* envelope of its own rather than described here. So an absent pair says
|
|
408
|
+
* "no refusal to quote", never "there was one and we dropped it", and a
|
|
409
|
+
* present pair is always somebody's actual answer rather than a word this
|
|
410
|
+
* gateway picked for them.
|
|
411
|
+
*/
|
|
412
|
+
refusal: RefusalSchema.optional(),
|
|
413
|
+
})
|
|
414
|
+
.meta({
|
|
415
|
+
description: 'What became of one purchase, in the words an agent and a merchant both read: where the order stands, what it was priced at, the goods once they are the buyer\'s, and why the merchant would not sell where that is what ended it. It is smaller than the merchant\'s own view of the same order on purpose — no merchant, no merchant\'s own key for the product, none of the purchase parameters and nothing about any other order. The price is what the buyer was asked for and not proof that anything was charged: an order that was priced and then ended without a sale still carries it, and the status is what says which happened. A null price means nobody ever named one for this order, and a null delivery means there are no goods here to hand over; both fields are always present, because an absent field is a silence a reader cannot tell from an oversight. "refusal" is the exception and is present only where a merchant refused: their own short code to branch on and their own sentence to show, carried across unchanged. The code is an open set — "out_of_stock", "invalid_params" and "cannot_fulfill" are read the same way by everybody, and a merchant whose reason fits none of them sends their own word, so an unfamiliar code has to fall through to the sentence rather than break a reader. An absent "refusal" means there is no refusal to quote and never that one was dropped, which leaves two endings still coarse: "rejected" also covers a product that was gone and a payment that failed its check, and neither of those was worded by anybody — the first because a price answer of "not available" carries no words, the second because it is refused at the door in an error envelope instead. A null delivery is likewise not a promise that no goods were ever made: a purchase whose charge failed or went unanswered can leave goods the buyer has not paid for, and this document withholds them rather than describing them. Every answer says whether the money behind the purchase was real: a gateway settling against nothing produces every other field here exactly as a real charge would, so a reader taking this for proof of a payment has to read that word first.',
|
|
416
|
+
});
|
|
417
|
+
/**
|
|
418
|
+
* The catalog as an agent reads it.
|
|
419
|
+
*
|
|
420
|
+
* An object rather than a bare array, so that the day it needs paging it can
|
|
421
|
+
* grow the field that says so instead of changing shape under every reader.
|
|
422
|
+
* Until then it makes no claim about completeness, and that is stated in the
|
|
423
|
+
* document itself: an agent must not read the absence of a field about paging
|
|
424
|
+
* as a promise that there is nothing more.
|
|
425
|
+
*/
|
|
426
|
+
export const CatalogPageSchema = z
|
|
427
|
+
.strictObject({
|
|
428
|
+
items: z.array(PublicCardSchema),
|
|
429
|
+
})
|
|
430
|
+
.meta({
|
|
431
|
+
description: "Products offered for sale, as an agent reads them. This document does not say whether it is the whole catalog: paging is not designed, and when it is, this object grows the field that answers it. Until then the absence of such a field is not a promise that there is no more.",
|
|
432
|
+
});
|
|
433
|
+
/**
|
|
434
|
+
* The methods this surface uses.
|
|
435
|
+
*
|
|
436
|
+
* `DELETE` is here for the one call that removes rows rather than marking them,
|
|
437
|
+
* and it earns the third verb rather than being folded into a POST: what it
|
|
438
|
+
* does to a key is not what disabling one does, and a surface that spelled both
|
|
439
|
+
* as a POST would be inviting a reader to think it was.
|
|
440
|
+
*/
|
|
441
|
+
export const HTTP_METHODS = Object.freeze(["GET", "POST", "DELETE"]);
|
|
442
|
+
/**
|
|
443
|
+
* Which door a call is behind.
|
|
444
|
+
*
|
|
445
|
+
* `merchant_key` is a key the merchant holds, sent as a bearer token; it names
|
|
446
|
+
* the merchant it belongs to, and a call behind this door acts on that
|
|
447
|
+
* merchant's cards, orders and receipts and on nobody else's. `none` is a
|
|
448
|
+
* call anybody may make — the catalog an agent browses, and the purchase,
|
|
449
|
+
* where the payment is what stands in for authorisation.
|
|
450
|
+
*
|
|
451
|
+
* `order_id` is the agent's own door and there is exactly one route behind it:
|
|
452
|
+
* knowing an order's identifier is what stands in for a key (ADR-0011). What
|
|
453
|
+
* that leans on is the identifier being generated from a random source and long
|
|
454
|
+
* enough not to be guessed — not on it being secret, which it is not. It
|
|
455
|
+
* travels in the payment challenge, on the merchant's stream and in the
|
|
456
|
+
* merchant's receipts, so the merchant who sold an order holds every identifier
|
|
457
|
+
* they would need to read one of these documents. That is why ownership of an
|
|
458
|
+
* order is settled by the verified payer and not by the identifier, and why the
|
|
459
|
+
* document behind this door carries only what the buyer is owed.
|
|
460
|
+
*
|
|
461
|
+
* It is a separate word from `none` because the two are not the same call: a
|
|
462
|
+
* route marked `none` answers everybody the same thing, and this one answers
|
|
463
|
+
* about the order the caller named and about no other. Whoever mounts it reads
|
|
464
|
+
* that difference off the word rather than out of a sentence. The weakness is
|
|
465
|
+
* stated in the decision rather than hidden: anyone who obtains an identifier
|
|
466
|
+
* can read that order, and the first real payment is the trigger to narrow it.
|
|
467
|
+
*
|
|
468
|
+
* `undecided` is not a scheme at all. It is the honest word for a route whose
|
|
469
|
+
* door nobody has chosen yet, and it exists because "I do not know" and "I
|
|
470
|
+
* know there is no door" cannot be the same value. A gateway must refuse to
|
|
471
|
+
* mount a route marked this way until somebody decides, which is exactly the
|
|
472
|
+
* behaviour wanted: the alternative is a route quietly serving everybody
|
|
473
|
+
* because `none` was the closest word to hand.
|
|
474
|
+
*/
|
|
475
|
+
export const AUTH_MODES = Object.freeze(["merchant_key", "none", "order_id", "undecided"]);
|
|
476
|
+
/**
|
|
477
|
+
* The header the merchant's key travels in, and the two helpers that put a key
|
|
478
|
+
* into it and read one back out.
|
|
479
|
+
*
|
|
480
|
+
* `auth` above says which door a call is behind and not how the door is built,
|
|
481
|
+
* and that omission is deliberate — but how the door is built is still a thing
|
|
482
|
+
* the gateway and the SDK have to agree on exactly, because a mismatch is a call
|
|
483
|
+
* that fails with an authorisation error while both repositories look correct.
|
|
484
|
+
* They had agreed, by each writing the same two strings down apart from the
|
|
485
|
+
* other: the gateway parsed an `Authorization: Bearer` header and the SDK sent
|
|
486
|
+
* one. That is agreement by luck, and it is the very thing this table exists to
|
|
487
|
+
* remove for the addresses and the documents. So the strings live here now, in
|
|
488
|
+
* the one place both sides already import, and a change to either is a change in
|
|
489
|
+
* one file rather than a silent drift between two.
|
|
490
|
+
*
|
|
491
|
+
* The name is `authorization` and the scheme is `Bearer`. The name
|
|
492
|
+
* is lower-case because that is how a client writes a header and how the wire
|
|
493
|
+
* carries it; a reader on the gateway matches it without regard to case anyway.
|
|
494
|
+
*/
|
|
495
|
+
export const MERCHANT_KEY_HEADER = "authorization";
|
|
496
|
+
/** The scheme the merchant's key travels under. */
|
|
497
|
+
const MERCHANT_KEY_SCHEME = "Bearer";
|
|
498
|
+
/**
|
|
499
|
+
* The value of the {@link MERCHANT_KEY_HEADER} header for a given key — what the
|
|
500
|
+
* SDK sends, so that what the gateway parses is never guessed at.
|
|
501
|
+
*/
|
|
502
|
+
export const merchantKeyHeaderValue = (key) => `${MERCHANT_KEY_SCHEME} ${key}`;
|
|
503
|
+
// Built once and matched case-insensitively: an auth scheme is case-insensitive
|
|
504
|
+
// (RFC 7235 §2.1), so a client that wrote "bearer" holds a key that is correct,
|
|
505
|
+
// and rejecting it would cost that merchant an afternoon on a key that works.
|
|
506
|
+
const MERCHANT_KEY_PATTERN = new RegExp(`^${MERCHANT_KEY_SCHEME}[ \\t]+(\\S+)$`, "i");
|
|
507
|
+
/**
|
|
508
|
+
* The key inside a merchant-key header value, or null where there is not one.
|
|
509
|
+
*
|
|
510
|
+
* Null is every way the value is not a bearer token this contract issued: no
|
|
511
|
+
* header at all, another scheme, or the scheme with nothing after it. The caller
|
|
512
|
+
* is handed a key it can compare or nothing to compare, and never a half-parsed
|
|
513
|
+
* string that a constant-time comparison would then match against a prefix of
|
|
514
|
+
* the real key.
|
|
515
|
+
*/
|
|
516
|
+
export const merchantKeyFrom = (headerValue) => {
|
|
517
|
+
if (headerValue === undefined) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
const match = MERCHANT_KEY_PATTERN.exec(headerValue.trim());
|
|
521
|
+
return match?.[1] ?? null;
|
|
522
|
+
};
|
|
523
|
+
/**
|
|
524
|
+
* How every call on this surface says no.
|
|
525
|
+
*
|
|
526
|
+
* One shape, for every refusal of every route, whatever it is about and
|
|
527
|
+
* whatever status it arrives under: an object whose only field is `error`, and
|
|
528
|
+
* inside it a machine-readable `code` and a sentence a person can act on. It
|
|
529
|
+
* is declared here, once, rather than being described route by route — a
|
|
530
|
+
* refusal shape written down twenty-six times is a refusal shape that is
|
|
531
|
+
* already twenty-six shapes, and the one thing a caller does with every call
|
|
532
|
+
* it makes is find out whether the call was refused.
|
|
533
|
+
*
|
|
534
|
+
* Both fields are required, and the sentence is the one worth arguing for. A
|
|
535
|
+
* refusal with a code and no words is a refusal only the code's author can
|
|
536
|
+
* read: an integrator meeting it for the first time has a string to search our
|
|
537
|
+
* source for and nothing to act on, and the caller printing it has an empty
|
|
538
|
+
* space where the reason belongs. That is why this is required rather than
|
|
539
|
+
* optional, and why a refusal that carries findings — a card's problems, a
|
|
540
|
+
* body that did not fit — carries them beside the sentence rather than instead
|
|
541
|
+
* of it.
|
|
542
|
+
*
|
|
543
|
+
* Inside `error` the shape is open, and that is deliberate in both directions.
|
|
544
|
+
* A refusal may say more about itself than these two fields, and several do:
|
|
545
|
+
* where an order ended, whether the payment layer might vouch for a second
|
|
546
|
+
* attempt, which fields of a document did not fit. What no refusal may do is
|
|
547
|
+
* ride beside the envelope rather than inside it, which is why the outer
|
|
548
|
+
* object is closed: `{ "error": … }` and nothing else is how a reader tells a
|
|
549
|
+
* refusal from a document that happens to mention an error.
|
|
550
|
+
*
|
|
551
|
+
* The set of codes is open, and is not enumerated here. A gateway meets
|
|
552
|
+
* situations this contract has not named, and a closed list would leave it
|
|
553
|
+
* choosing between the nearest wrong word and no answer at all. What is
|
|
554
|
+
* promised is the shape, so that a caller can always find out that it was
|
|
555
|
+
* refused and always have something to print.
|
|
556
|
+
*/
|
|
557
|
+
export const ErrorEnvelopeSchema = z
|
|
558
|
+
.strictObject({
|
|
559
|
+
error: z.looseObject({
|
|
560
|
+
/** The name of the refusal, for the code that branches on it. */
|
|
561
|
+
code: z.string().regex(/\S/, "a refusal carries a code"),
|
|
562
|
+
/** The same refusal in words, for whoever has to do something about it. */
|
|
563
|
+
message: z.string().regex(/\S/, "a refusal carries an explanation a person can read"),
|
|
564
|
+
/**
|
|
565
|
+
* Whether making this same call again could succeed.
|
|
566
|
+
*
|
|
567
|
+
* A fact about this refusal, not an instruction: how long to wait, how
|
|
568
|
+
* many times, and whether to bother at all are the caller's, who knows
|
|
569
|
+
* what the call was worth. What the caller cannot know from outside is
|
|
570
|
+
* whether the door is shut or merely was shut a moment ago, and that is
|
|
571
|
+
* the one thing said here.
|
|
572
|
+
*
|
|
573
|
+
* The gateway assigns it conservatively. True is claimed only where
|
|
574
|
+
* repeating this very call is genuinely the way through — the call that
|
|
575
|
+
* fell over before anything was decided, the answer that is not ready
|
|
576
|
+
* yet. Everything definitive, and everything arguable, is false: a
|
|
577
|
+
* caller who stops early loses one call they could have made again,
|
|
578
|
+
* and a caller who loops against a door that will never open spends
|
|
579
|
+
* until somebody notices.
|
|
580
|
+
*/
|
|
581
|
+
retryable: z.boolean(),
|
|
582
|
+
}),
|
|
583
|
+
})
|
|
584
|
+
.meta({
|
|
585
|
+
description: 'How every call on this surface says no. One shape for every refusal of every route, whatever it is about and whatever HTTP status it arrives under: an object whose only field is "error", carrying a machine-readable code, a sentence a person can act on, and "retryable". All three are always present — a refusal with a code and no words is one only its author can read, and one that will not say whether calling again could work leaves the caller to guess between a retry loop against a door that never opens and an abandoned call that would have gone through. "retryable" is a fact about this refusal and not an instruction: it answers "could this same call succeed if it were made again", and how long to wait, how often, and whether to bother at all stay with the caller, who is the one who knows what the call was worth. This gateway assigns it conservatively — true only where repeating the very same call is genuinely the way through, such as a call that fell over before anything was decided or an answer that is not ready yet; everything definitive, and everything arguable, is false. The code is an open set here on purpose: a gateway meets situations this contract has not named, and what this schema promises is the shape rather than the vocabulary, so an unfamiliar code parses and stays readable. The codes this gateway sends today are published beside it as a list, to switch over rather than to validate against — a consumer covers those and keeps a default arm for the one it has never seen, where the sentence in this envelope is what it shows. Inside "error" a refusal may carry more about itself — where an order ended, which fields of a document did not fit — and a reader that does not recognise those must still be able to read the three that are always there. Nothing rides beside the envelope: a body carrying anything at the top level other than "error" is not a refusal, and reading one as a refusal would turn a document that merely mentions an error into a call that failed.',
|
|
586
|
+
});
|
|
587
|
+
/**
|
|
588
|
+
* Every code this contract's gateway refuses a call with today.
|
|
589
|
+
*
|
|
590
|
+
* Named after the field it lands in — `error.code` — rather than "refusal
|
|
591
|
+
* code", which this contract already uses for something else: what a
|
|
592
|
+
* merchant's handler answers when it cannot deliver an order it was given
|
|
593
|
+
* (`RefusalCode` in `handler.ts`). Those are a merchant's words about one
|
|
594
|
+
* order; these are the surface's words about one call, and a reader who ran
|
|
595
|
+
* the two together would go looking for `out_of_stock` here.
|
|
596
|
+
*
|
|
597
|
+
* Two halves, and a consumer that takes only one of them will be wrong about
|
|
598
|
+
* this surface sooner or later.
|
|
599
|
+
*
|
|
600
|
+
* The wire is open. `ErrorEnvelopeSchema` holds `code` to a non-blank string
|
|
601
|
+
* and not to this list, deliberately and for the reason `results.ts` gives
|
|
602
|
+
* about its own codes: a situation nobody anticipated has to reach the caller
|
|
603
|
+
* in its own words rather than break the parse on the way. A gateway newer
|
|
604
|
+
* than the client, another implementation of this surface, or something
|
|
605
|
+
* standing in front of one, can all answer with a code that is not here, and
|
|
606
|
+
* the answer is still a refusal and still readable.
|
|
607
|
+
*
|
|
608
|
+
* So this list is what to switch over, not what to validate against. A
|
|
609
|
+
* consumer covers these exhaustively — the compiler tells them when the list
|
|
610
|
+
* grows — and keeps a default arm for the code it has never seen, which is not
|
|
611
|
+
* an error path: the envelope's `message` is required precisely so that arm
|
|
612
|
+
* has a sentence to show, written by the side that knows why. A consumer that
|
|
613
|
+
* refused an unknown code, or rendered nothing for it, would turn a version
|
|
614
|
+
* skew into a blank screen over a refusal the gateway explained perfectly
|
|
615
|
+
* well.
|
|
616
|
+
*
|
|
617
|
+
* The list is alphabetical because it is looked up rather than read through,
|
|
618
|
+
* and it is the codes and not their meanings: what each one means to whoever
|
|
619
|
+
* has to act on it belongs to the route that sends it and to the sentence the
|
|
620
|
+
* refusal carries.
|
|
621
|
+
*/
|
|
622
|
+
export const ERROR_CODES = Object.freeze([
|
|
623
|
+
"body_too_large",
|
|
624
|
+
"body_undecodable",
|
|
625
|
+
"call_refused",
|
|
626
|
+
"charset_unsupported",
|
|
627
|
+
"encoding_unsupported",
|
|
628
|
+
"gateway_failed",
|
|
629
|
+
"key_made_for_a_cabinet",
|
|
630
|
+
"key_opened_this_call",
|
|
631
|
+
"malformed_body",
|
|
632
|
+
"malformed_query",
|
|
633
|
+
"merchant_departed",
|
|
634
|
+
"no_such_item",
|
|
635
|
+
"no_such_key",
|
|
636
|
+
"no_such_order",
|
|
637
|
+
"no_such_route",
|
|
638
|
+
"not_a_cabinet_key",
|
|
639
|
+
"not_authorised",
|
|
640
|
+
"not_invited",
|
|
641
|
+
"not_selling",
|
|
642
|
+
"not_this_purchase",
|
|
643
|
+
"order_closed_before_it_was_priced",
|
|
644
|
+
"order_not_priced_yet",
|
|
645
|
+
"params_do_not_fit",
|
|
646
|
+
"payment_already_spent",
|
|
647
|
+
"payment_not_taken",
|
|
648
|
+
"payment_not_verified",
|
|
649
|
+
]);
|
|
650
|
+
/**
|
|
651
|
+
* Every call of the surface, under the name it is known by in both programs.
|
|
652
|
+
*
|
|
653
|
+
* The name is the key, so two routes cannot share one; a test holds the rest —
|
|
654
|
+
* that no two sit at one address under one method, and that every schema named
|
|
655
|
+
* here is one the contract registry publishes, since the registry and this
|
|
656
|
+
* table are the whole of what a reader outside TypeScript has.
|
|
657
|
+
*
|
|
658
|
+
* Two prefixes are in this table and they are two different kinds of address.
|
|
659
|
+
* `/v0` is the merchant's API, and it is versioned for the ordinary reason a
|
|
660
|
+
* classic API is: an integrator writes code against it, and the number is what
|
|
661
|
+
* lets that code keep working while the shapes underneath it change. `/x402` is
|
|
662
|
+
* the storefront — the catalog an agent browses, the address it buys at, and
|
|
663
|
+
* the door it comes back to for its own order — and it carries no version at
|
|
664
|
+
* all. Those three addresses are not something an integrator holds in a client
|
|
665
|
+
* library; they are a product identity. A discovery catalog keys a listing on
|
|
666
|
+
* the address it was given, strangers' agents construct them from a base and an
|
|
667
|
+
* identifier, and both of those outlive any dialect we speak. So the protocol
|
|
668
|
+
* versions where the protocol can be read — the `x402Version` field inside the
|
|
669
|
+
* challenge — and the address stays put. A version segment in a storefront path
|
|
670
|
+
* would be a promise to keep every old segment answering forever, made to
|
|
671
|
+
* catalogs we do not run.
|
|
672
|
+
*
|
|
673
|
+
* Which door a call is behind is still `auth` and never the prefix. The two
|
|
674
|
+
* happen to agree today, and a reader who took that for the rule would mount
|
|
675
|
+
* the next route by its address; `apps/gateway/src/http/server.ts` says what
|
|
676
|
+
* that costs.
|
|
677
|
+
*/
|
|
678
|
+
export const API_ROUTES = Object.freeze({
|
|
679
|
+
publish_card: {
|
|
680
|
+
method: "POST",
|
|
681
|
+
path: "/v0/catalog/publish",
|
|
682
|
+
auth: "merchant_key",
|
|
683
|
+
description: "Publishes one product, or says what is wrong with the card. Republishing under the same merchant_item_id is how a card is changed rather than how a second one appears. The catalog identifier comes back beside ok, and from then on it is what an agent, a purchase and a receipt all use. A card that is not published comes back as ok:false under the code card_rejected, never retryable, with every finding named in the error's problems. Two of the findings this can come back with are not about the card at all. A merchant who has not set the name their products are sold under gets a finding whose code is no_seller_name, and the way past it is POST /v0/seller-name: a card published without one reaches a buyer's agent inside a payment request that names no seller. And on a deployment that settles on a real chain, a merchant who has set no wallet gets one whose code is no_payout_wallet, with POST /v0/payout-wallet as the way past it: the money from that card's sales is paid to the merchant's own address directly, and without one there is nowhere for it to go. Both come back in the same problems list as whatever is wrong with the card, so one answer carries everything standing between this card and the catalog.",
|
|
684
|
+
request: CardSchema,
|
|
685
|
+
response: { document: PublishResultSchema },
|
|
686
|
+
},
|
|
687
|
+
list_merchant_cards: {
|
|
688
|
+
method: "GET",
|
|
689
|
+
path: "/v0/cards",
|
|
690
|
+
auth: "merchant_key",
|
|
691
|
+
description: "The cards published under the key this call was made with, each whole and with the word it is selling under, together with whether the merchant is taking new orders at all. It is the catalog of the one merchant that key belongs to and of nobody else. It is not the public catalog: that one is unscoped, spans every merchant, carries our identifier in place of the merchant's key, and leaves out a card that is off sale — which is the one card a merchant goes looking for when they want it back on sale.",
|
|
692
|
+
response: { document: MerchantCardListSchema },
|
|
693
|
+
},
|
|
694
|
+
pause_card: {
|
|
695
|
+
method: "POST",
|
|
696
|
+
path: "/v0/cards/:item_id/pause",
|
|
697
|
+
auth: "merchant_key",
|
|
698
|
+
description: "Takes one card off sale. No new order is taken for it, and the orders already accepted for it play out in the ordinary way — a pause closes nothing. Calling it on a card that is already paused changes nothing and answers the same way, so a retry after a dropped connection is safe. Republishing a paused card changes the card and leaves it paused: a merchant editing a price is not asking for it to go back on sale.",
|
|
699
|
+
response: { document: MerchantCardSchema },
|
|
700
|
+
},
|
|
701
|
+
resume_card: {
|
|
702
|
+
method: "POST",
|
|
703
|
+
path: "/v0/cards/:item_id/resume",
|
|
704
|
+
auth: "merchant_key",
|
|
705
|
+
description: "Puts one card back on sale. Where the merchant has stopped all selling this lifts only this card's own pause, and the card goes on refusing new orders until selling is resumed — the answer says so in both of its words, and that is the case a merchant is most likely to misread.",
|
|
706
|
+
response: { document: MerchantCardSchema },
|
|
707
|
+
},
|
|
708
|
+
pause_selling: {
|
|
709
|
+
method: "POST",
|
|
710
|
+
path: "/v0/selling/pause",
|
|
711
|
+
auth: "merchant_key",
|
|
712
|
+
description: "Stops all selling for this merchant. No new order is taken for any card, and the orders already accepted play out in the ordinary way — this is a pause and not a departure, so nothing open is closed and this call creates no debt. The answer is the whole catalog, because every card's word changed. A merchant who has already left is refused rather than paused: their orders are closed and their refunds owed, and a pause would describe none of that.",
|
|
713
|
+
response: { document: MerchantCardListSchema },
|
|
714
|
+
},
|
|
715
|
+
resume_selling: {
|
|
716
|
+
method: "POST",
|
|
717
|
+
path: "/v0/selling/resume",
|
|
718
|
+
auth: "merchant_key",
|
|
719
|
+
description: "Starts selling again. Cards paused in their own right stay paused: stopping all selling did not forget which they were, and putting them all back on sale would sell products their merchant took off. The answer is the whole catalog, so which cards actually came back is a fact rather than an inference. A merchant who has left is refused: leaving closed the orders that were open and left refunds owed, and this switch unwinds none of it, so a departure is not undone here.",
|
|
720
|
+
response: { document: MerchantCardListSchema },
|
|
721
|
+
},
|
|
722
|
+
register_merchant: {
|
|
723
|
+
method: "POST",
|
|
724
|
+
path: "/v0/merchants",
|
|
725
|
+
auth: "none",
|
|
726
|
+
description: "Makes a merchant and the key whoever registered them will call as them with — both of those or neither. It takes no key because nobody registering has one yet; what stands in the door instead is an invitation code out of the gateway's own configuration, and a gateway with no code configured refuses every registration in the same words a wrong code gets, so this call is not a way of finding out whether registration is open. The key comes back once and is readable nowhere afterwards, and it is a key made for a cabinet: it is in no list of the merchant's keys and is not disabled through them, so no row for it comes back here — only the key itself. What the new merchant does not have is a name: they are listed under nothing until somebody sets one at POST /v0/seller-name, and until then publishing a card is refused, so a cabinet that registers a person and takes them straight to a publish screen has built a dead end. Nothing about an account, an address or a password reaches this call either: those belong to whatever signs a person in, on the other side of it. Unlike every other call on this surface that writes something, a repeat of this one is not safe and there is nothing here that could make it so: two calls make two merchants, and the caller holds a key to only the second. A caller whose connection drops before the answer arrives cannot find out from here whether the first landed — it has no key with which to ask — and the merchant it may have made cannot be swept away afterwards, because a merchant is what every card, order and receipt is owned by.",
|
|
727
|
+
request: RegistrationRequestSchema,
|
|
728
|
+
response: { document: RegisteredMerchantSchema },
|
|
729
|
+
},
|
|
730
|
+
get_seller_name: {
|
|
731
|
+
method: "GET",
|
|
732
|
+
path: "/v0/seller-name",
|
|
733
|
+
auth: "merchant_key",
|
|
734
|
+
description: "What the merchant this call's own key belongs to is listed under: the name a discovery catalog shows beside their products and a buyer's agent is shown beside the price. Null is the ordinary answer for a merchant who has not chosen one, and it is an answer rather than a refusal — a merchant with no name exists and has a settings screen to draw. This is not read off a card: the name belongs to the merchant, so every card of theirs carries the same one and none of them carries it.",
|
|
735
|
+
response: { document: SellerNameSchema },
|
|
736
|
+
},
|
|
737
|
+
set_seller_name: {
|
|
738
|
+
method: "POST",
|
|
739
|
+
path: "/v0/seller-name",
|
|
740
|
+
auth: "merchant_key",
|
|
741
|
+
description: "Sets what the merchant this call's own key belongs to is listed under. The answer is the name as it now stands, read back from what was written rather than echoed, so a screen showing it afterwards is showing what is true. Setting the same name twice changes nothing and answers the same way, so a retry after a dropped connection is safe. A name outside the rule of the catalog that will carry it is refused and nothing is written, which leaves the merchant listed under whatever they had before. What this call will not do is take a name away: it goes from no name to a name and from one name to another, and null is refused, because without a name there is nobody for a payment request to name as the seller and every card the merchant has published would quietly come off sale — ending the selling under the name of editing a setting; somebody reaching for that wants either a different name, which is this same call, or an end to selling, which is the pause, and the pause leaves their cards where they can put them back on sale. One more thing is worth knowing before a screen is built on this: the name here is not the name a person reads in a list of merchants at a terminal. Those are two fields, held to two different rules, and only this one ever leaves us.",
|
|
742
|
+
request: SellerNameRequestSchema,
|
|
743
|
+
response: { document: SellerNameSchema },
|
|
744
|
+
},
|
|
745
|
+
get_payout_wallet: {
|
|
746
|
+
method: "GET",
|
|
747
|
+
path: "/v0/payout-wallet",
|
|
748
|
+
auth: "merchant_key",
|
|
749
|
+
description: "The address the sales of the merchant this call's own key belongs to are paid into. Payments here are not held by anybody on the way: a buyer's agent pays this address directly and no balance of the merchant's is ever held on our side, which is why the address has to be theirs and why this call exists. Null is the ordinary answer for a merchant who has set none, and it is an answer rather than a refusal — a merchant with no wallet exists and has a settings screen to draw. The address comes back in the mixed-case spelling a wallet shows, whichever of the two accepted spellings was sent to set it, so a screen showing it shows what the merchant copied out of their wallet character for character.",
|
|
750
|
+
response: { document: PayoutWalletSchema },
|
|
751
|
+
},
|
|
752
|
+
set_payout_wallet: {
|
|
753
|
+
method: "POST",
|
|
754
|
+
path: "/v0/payout-wallet",
|
|
755
|
+
auth: "merchant_key",
|
|
756
|
+
description: "Sets the address the sales of the merchant this call's own key belongs to are paid into. The answer is the address as it now stands, read back from what was written rather than echoed, in the mixed-case spelling a wallet shows. Setting the same address twice changes nothing and answers the same way, so a retry after a dropped connection is safe. An address whose capital letters do not agree with the rest of it is refused and nothing is written, because those capitals are a checksum and letters that disagree mean a character is wrong — and an address that is wrong is another perfectly good address belonging to somebody else. What this call will not do is take an address away: null is refused, because without an address there is nowhere to send the money and every published card would quietly come off sale — ending the selling under the name of editing a setting; somebody reaching for that wants either a different address, which is this same call, or an end to selling, which is the pause. On a deployment that settles on a real chain, a merchant with no wallet set here cannot publish a card, and the refusal at the publish says so.",
|
|
757
|
+
request: PayoutWalletRequestSchema,
|
|
758
|
+
response: { document: PayoutWalletSchema },
|
|
759
|
+
},
|
|
760
|
+
list_keys: {
|
|
761
|
+
method: "GET",
|
|
762
|
+
path: "/v0/keys",
|
|
763
|
+
auth: "merchant_key",
|
|
764
|
+
description: "The keys the merchant this call's own key belongs to made for their own code, the revoked ones among them, and never the keys themselves. A merchant who has only ever signed into a cabinet has none of these, and an empty list is that answer rather than a fault: the key a cabinet calls with is made for the cabinet and is in no list here, because the merchant did not issue it and cannot disable it. Whether the list is all of the rest is not something this document claims either: paging is not designed, and the absence of a field about it is not a promise that there is no more. The answer also names the key the call was made with, as this_call, and that field is the reason this is not a bare list: a merchant cannot disable the key their own call was made with, so a screen drawn without knowing which key that is would offer a button the gateway refuses. That identifier is not among the keys listed when the caller is a cabinet, and a client that looked it up among the rows has to be built for finding none. Every row also says when a call was last seen on that key, which is the field a screen offering to revoke one is worth drawing at all — and it is a recorded answer rather than an exact one, lagging by minutes, and blank both for a key nothing has called and for one made before this gateway began recording, which nothing here tells apart. This call is itself one of those calls, so the key it was made with reads as used at about this moment, whether or not it appears in the list.",
|
|
765
|
+
response: { document: MerchantKeyListSchema },
|
|
766
|
+
},
|
|
767
|
+
issue_key: {
|
|
768
|
+
method: "POST",
|
|
769
|
+
path: "/v0/keys",
|
|
770
|
+
auth: "merchant_key",
|
|
771
|
+
description: "Issues another key for the merchant's own code, to the merchant this call's own key belongs to. The key is generated here and never taken from the caller, and it comes back exactly once — what is kept afterwards is a digest, so nothing can show it again. A merchant with several keys can hand one to each worker and revoke one without touching the others, which is the whole reason a key is a row. This call cannot make the other kind of key: what a cabinet calls with is asked for at POST /v0/keys/cabinet, and a key made here is one the merchant sees, names and revokes.",
|
|
772
|
+
request: IssueKeyRequestSchema,
|
|
773
|
+
response: { document: IssuedKeySchema },
|
|
774
|
+
},
|
|
775
|
+
disable_key: {
|
|
776
|
+
method: "POST",
|
|
777
|
+
path: "/v0/keys/:key_id/disable",
|
|
778
|
+
auth: "merchant_key",
|
|
779
|
+
description: "Stops one of this merchant's keys working, from that instant, and touches no other key. Disabling a key that is already disabled changes nothing and answers the same way, keeping the instant it was first revoked at, so a retry after a dropped connection is safe. Three refusals are worth knowing before a screen is built on this. A key belonging to another merchant is answered exactly as a key that does not exist, so this call is not a way of counting somebody else's keys. A key made for a cabinet is refused under key_made_for_a_cabinet, whoever asks and however they came by its identifier: this call reaches the keys a merchant issued for their own code and nothing else, and replacing the one a cabinet holds is POST /v0/keys/cabinet and the forgetting beside it. And the key this call was made with cannot be disabled by it — that one click and no more: the refusal is about the key in front of it, so a merchant holding two keys of their own can still disable either with the other, and two such calls at one moment can leave them with none of their own. That is not refused here or anywhere, and what it costs is their own code going quiet rather than the way back in, which is a key of the other kind.",
|
|
780
|
+
response: { document: DisabledKeySchema },
|
|
781
|
+
},
|
|
782
|
+
issue_cabinet_key: {
|
|
783
|
+
method: "POST",
|
|
784
|
+
path: "/v0/keys/cabinet",
|
|
785
|
+
auth: "merchant_key",
|
|
786
|
+
description: "Makes a key for a cabinet to call as this merchant with, and hands it back once. It is a key of a different kind from the ones at /v0/keys: the merchant did not ask for it, never sees it, and it appears in no list of theirs — so nothing comes back but the key itself. What this is for is a cabinet that replaces its own credential every time somebody signs in, which is what keeps a copy of a cabinet's database from being a set of working keys for long. The call is refused to a key made for the merchant's own code, under not_a_cabinet_key: these two calls are the cabinet's own, and a merchant asking for one would be asking for a credential to a cabinet they are not standing in.",
|
|
787
|
+
response: { document: CabinetKeySchema },
|
|
788
|
+
},
|
|
789
|
+
forget_cabinet_key: {
|
|
790
|
+
method: "DELETE",
|
|
791
|
+
path: "/v0/keys/cabinet",
|
|
792
|
+
auth: "merchant_key",
|
|
793
|
+
description: "Removes the key this call was made with, and no other. It is removed rather than revoked: a merchant never issued one, never sees one and would never read a revoked one back, so a row kept for the history would be history for nobody. There are no parameters, and that is not a convenience — a key belonging to anybody else, this merchant included, is unreachable here by construction, because the only way to name a key is to be holding it. What a caller does with this is put a key of its own beyond use once it has stopped signing in with it: ask for a fresh key, write it down where the account is kept, then forget the one that was there. A caller that forgets first can be left naming a key that no longer exists, which is the one order that locks somebody out. Made a second time with the same key it is refused as a key that does not exist, which is safe and is the confirmation the first one landed: either way that key is gone and nothing else has moved. It is refused to a key made for the merchant's own code, under not_a_cabinet_key: those are revoked from the list they appear on, at an instant their owner can read back, and removing the row outright would take that history away.",
|
|
794
|
+
response: { document: ForgottenCabinetKeySchema },
|
|
795
|
+
},
|
|
796
|
+
get_order: {
|
|
797
|
+
method: "GET",
|
|
798
|
+
path: "/v0/orders/:order_id",
|
|
799
|
+
auth: "merchant_key",
|
|
800
|
+
description: "One order and the state it is in. Reading state back is not a convenience: a worker that restarted has to be able to find out what it still owes without resting on its own database alone.",
|
|
801
|
+
response: { document: OrderWithStatusSchema },
|
|
802
|
+
},
|
|
803
|
+
list_orders: {
|
|
804
|
+
method: "GET",
|
|
805
|
+
path: "/v0/orders",
|
|
806
|
+
auth: "merchant_key",
|
|
807
|
+
description: "Orders and the states they are in. With open=true, only the ones still owed something — which includes the two that stay open after the purchase itself is over, an order owing a refund and one delivered but never paid for. One kind of order is not in this list at all, with or without the flag: one that closed before anybody named a price for it, because the product was gone or a price question went unanswered. Every row here is written in a document that carries a sale price and those orders have none, so they are readable one at a time by their identifier, where the refusal says what became of them. A merchant reconciling against this list is reconciling against the orders that were priced.",
|
|
808
|
+
query: OrderListQuerySchema,
|
|
809
|
+
response: { document: OrderListSchema },
|
|
810
|
+
},
|
|
811
|
+
list_receipts: {
|
|
812
|
+
method: "GET",
|
|
813
|
+
path: "/v0/receipts",
|
|
814
|
+
auth: "merchant_key",
|
|
815
|
+
description: "The receipts belonging to the key this call was made with: what was paid, when the payment executed, when that price was fixed for the sale, the moment the price behind it was true, and what became of the order. Those three moments are three different things and none of them stands in for another: on a card whose price is checked at the purchase, the price is fixed when the merchant answers, and the buyer may pay some time after that. Every receipt says whether the money behind it was real, and a reader reconciling against a wallet has to read that field rather than assume. Two silences are the receipt's own — a purchase that ended before any payment leaves no receipt, and none is written while it is unknown whether the buyer was charged. A third belongs to whoever serves this and is not a property of the shape: a gateway decides when it writes a receipt, and one that writes them only as goods are released has none for a payment that executed at the purchase and has not been delivered yet. That is money taken with nothing here to show it, so this list is not by itself an account of what was received. Whose receipts these are is settled by the key: the merchant it belongs to, and nobody else.",
|
|
816
|
+
response: { document: ReceiptListSchema },
|
|
817
|
+
},
|
|
818
|
+
poll_worker: {
|
|
819
|
+
method: "POST",
|
|
820
|
+
path: "/v0/worker/poll",
|
|
821
|
+
auth: "merchant_key",
|
|
822
|
+
description: "Draws the next batch off this merchant's stream. The request is held open until something arrives or the wait window closes, and then answers with a batch of envelopes or an empty one; an empty batch is the ordinary answer to a quiet window and is not a failure. Delivery is not one rule for everything on this stream. An order arrives at least once: a merchant who does not answer is sent it again, and each attempt is a fresh envelope with an identifier of its own, so the only thing two attempts at one order have in common is the order they carry — which is why a handler recognises a repeat by the order's own identifier inside the payload and never by the envelope's. Nothing else here is ever re-offered. A price question our patience runs out on is resolved on our side rather than asked again, and what that costs the purchase depends on the card's mode. An event has no reply of any kind, so one that went into a response that never reached its worker is simply gone, and nothing afterwards says that anything went missing. Guarding against a repeat is right for an order and pointless for the other two; counting on a message to arrive is safe for an order and not for an event.",
|
|
823
|
+
request: WorkerPollRequestSchema,
|
|
824
|
+
response: { document: WorkerPollResponseSchema },
|
|
825
|
+
},
|
|
826
|
+
answer_order: {
|
|
827
|
+
method: "POST",
|
|
828
|
+
path: "/v0/orders/:order_id/answer",
|
|
829
|
+
auth: "merchant_key",
|
|
830
|
+
description: "What the merchant's handler returned for an order it was given: the goods, a refusal, or an acceptance. The SDK sends this itself, in every mode, and it is the only way a synchronous answer reaches us at all — there the handler's return is the delivery and the refusal, and the explicit deliver and refuse calls do not apply. An acceptance is answered with the word accepted — the order is taken on, and the goods follow through the deliver call — or with already_delivered when the order closed before the acceptance reached us, which a redelivered order makes ordinary rather than a fault. A synchronous answer that arrives after its deadline is not an error: the work exists and a repeat purchase collects it, and the answer says so in the word purchase_already_closed.",
|
|
831
|
+
request: HandlerAnswerSchema,
|
|
832
|
+
response: { document: OrderCallResponseSchema },
|
|
833
|
+
},
|
|
834
|
+
deliver_order: {
|
|
835
|
+
method: "POST",
|
|
836
|
+
path: "/v0/orders/:order_id/deliver",
|
|
837
|
+
auth: "merchant_key",
|
|
838
|
+
description: "The goods for an order the merchant took on earlier — the asynchronous mode's closure verb, called by the merchant rather than by the SDK. Idempotent by the order's identifier: called again after a dropped connection it delivers nothing twice and charges nothing twice, so repeating it is safe and keeping a note of what was already sent is not needed. A late call is accepted too — where the delivery deadline has passed and the refund has not yet been paid out, delivering closes the debt.",
|
|
839
|
+
request: DeliverySchema,
|
|
840
|
+
response: { document: OrderCallResponseSchema },
|
|
841
|
+
},
|
|
842
|
+
refuse_order: {
|
|
843
|
+
method: "POST",
|
|
844
|
+
path: "/v0/orders/:order_id/refuse",
|
|
845
|
+
auth: "merchant_key",
|
|
846
|
+
description: "A final \"this cannot be delivered\" for an order already taken on — the asynchronous mode's other closure verb. It exists only where an acceptance came first; in the synchronous mode the handler's own answer is the refusal and travels on the answer route instead, which is what the error code not_applicable_in_mode says from the other side. A temporary failure is not a refusal — it is an exception, a dead process or a dropped connection, and it is answered with another delivery.",
|
|
847
|
+
request: RefusalSchema,
|
|
848
|
+
response: { document: OrderCallResponseSchema },
|
|
849
|
+
},
|
|
850
|
+
accept_order: {
|
|
851
|
+
method: "POST",
|
|
852
|
+
path: "/v0/orders/:order_id/accept",
|
|
853
|
+
auth: "merchant_key",
|
|
854
|
+
description: "Takes an order on: the merchant will deliver, and says how long they expect it to take when they know. An empty body is a complete answer. The same order is taken on again every time it is redelivered, and the success carries no word: taking on an order that is already delivered succeeds here too, and this route does not tell the two apart. The answer route does, because it carries whichever of the three things a handler returned.",
|
|
855
|
+
request: AcceptanceSchema,
|
|
856
|
+
response: { document: OrderAcceptResponseSchema },
|
|
857
|
+
},
|
|
858
|
+
answer_quote: {
|
|
859
|
+
method: "POST",
|
|
860
|
+
path: "/v0/quotes/:price_id/answer",
|
|
861
|
+
auth: "merchant_key",
|
|
862
|
+
description: "The price and availability for a question that came off the worker stream, against the price_id that question carried. The acknowledgement says whether the answer arrived in time to price the purchase; when it did not, stock held against the question can be released.",
|
|
863
|
+
request: QuoteResponseSchema,
|
|
864
|
+
response: { document: QuoteAnswerAckSchema },
|
|
865
|
+
},
|
|
866
|
+
list_catalog: {
|
|
867
|
+
method: "GET",
|
|
868
|
+
path: "/x402/catalog",
|
|
869
|
+
auth: "none",
|
|
870
|
+
description: "Products offered for sale, as an agent reads them. Each is the projection of a published card: our catalog identifier rather than the merchant's own key, and nothing about how the merchant's price is asked for beyond the fact that it will be asked again.",
|
|
871
|
+
response: { document: CatalogPageSchema },
|
|
872
|
+
},
|
|
873
|
+
purchase_item: {
|
|
874
|
+
method: "POST",
|
|
875
|
+
path: "/x402/:item_id/purchase",
|
|
876
|
+
auth: "none",
|
|
877
|
+
description: "Buying one product. The payment is what stands in for authorisation, so there is no key on this call. It begins with the payment exchange of the x402 protocol: a call with no payment on it is answered with a challenge, which travels in a header and carries no document. What a paid purchase is answered with is the state of the order it made — the same document the status route answers with, whatever the card's mode and whether the purchase ended in the goods or in something else, so an agent that bought and an agent that came back later read one shape. No receipt of ours is in it: a receipt is the merchant's record of the sale and is read behind the merchant's key. What an agent is told about its own money is the price and the word for whether that money was real, and, where the payment executed as the last step of this exchange, the settlement the payment layer signs into a header on this answer — a card whose money moves as the order is opened has already spent that step, so no settlement comes back here and the two fields are the whole of it. The address answers on GET as well as on POST, because that is how the validators and crawlers that list a paid resource ask for it. An unpaid call that carries no document — a GET, or a POST with nothing or with an empty document — is answered with the challenge and never a purchase, and a GET is never read for payment; the challenge describes the purchase, which is a POST with a JSON body. An unpaid POST that carries that document is answered with the price of the very order it opened, and a document of some other shape is refused with the fields that are wrong. A product that is not on sale answers neither method with a challenge: it is refused, so that a catalog built from these challenges never carries a product nobody can buy.",
|
|
878
|
+
request: PurchaseRequestSchema,
|
|
879
|
+
also_answers_on: ["GET"],
|
|
880
|
+
response: { document: AgentOrderStatusSchema },
|
|
881
|
+
},
|
|
882
|
+
get_order_status: {
|
|
883
|
+
method: "GET",
|
|
884
|
+
path: "/x402/orders/:order_id/status",
|
|
885
|
+
auth: "order_id",
|
|
886
|
+
description: "What became of a purchase, for the agent that made it: where the order stands, what it sold for, and the goods once they are the buyer's. It is the route an agent that bought a product whose goods come later collects them on, and without it half a catalogue takes money and hands back an order nobody can act on. Knowing the order's identifier is the proof (ADR-0011), so this call takes no key: an agent has no account and no registration, and the identifier is handed to exactly one party. No answer of ours hands the agent this address either — it is built from the order's identifier and the address the agent already bought at, which is why it is written the way a stranger would write it and carries no version to guess at. Two things follow for whoever mounts it. Which door a call is behind is read off auth and never off the address, whatever the prefixes happen to agree on today. And an identifier that names no order must be answered exactly as any other unknown one is, or the refusal becomes a way of counting the orders behind it.",
|
|
887
|
+
response: { document: AgentOrderStatusSchema },
|
|
888
|
+
},
|
|
889
|
+
});
|
|
890
|
+
/**
|
|
891
|
+
* The shape of a path parameter's name: lowercase, the way the fields on the
|
|
892
|
+
* wire are written, so an address reads like the documents it carries.
|
|
893
|
+
*/
|
|
894
|
+
const PARAMETER_NAME = "[a-z][a-z0-9_]*";
|
|
895
|
+
// Built fresh on each call rather than kept in a module constant, because a
|
|
896
|
+
// global regular expression carries a cursor between calls and sharing one
|
|
897
|
+
// between a scan and a replace is a bug waiting for the second caller.
|
|
898
|
+
const pathParameterPattern = () => new RegExp(`:(${PARAMETER_NAME})`, "g");
|
|
899
|
+
/** The parameters an address takes, in the order it writes them. */
|
|
900
|
+
export const pathParamsOf = (path) => [...path.matchAll(pathParameterPattern())].map((match) => match[1] ?? "");
|
|
901
|
+
/**
|
|
902
|
+
* The address of one call, with its parameters filled in and encoded.
|
|
903
|
+
*
|
|
904
|
+
* Encoding is the reason this is not left to the caller. An identifier in this
|
|
905
|
+
* contract may hold a slash and a space — "SKU 100/1" is one we accept — and
|
|
906
|
+
* concatenated into a path unencoded it becomes two segments and a different
|
|
907
|
+
* route.
|
|
908
|
+
*
|
|
909
|
+
* A missing value and a value for a parameter the address does not take are
|
|
910
|
+
* both refused, and both are refused by throwing rather than by returning
|
|
911
|
+
* something. Everything this package returns rather than throws is a condition
|
|
912
|
+
* a consumer is expected to handle; this is not one of those. An address built
|
|
913
|
+
* with a hole in it is a bug in the caller, and the request it would send
|
|
914
|
+
* reads in a log as a route that exists and answers nothing to anybody.
|
|
915
|
+
*
|
|
916
|
+
* Three refusals are less obvious and each is a way an address stops being the
|
|
917
|
+
* one that was meant. A value is looked up with `Object.hasOwn`, because a
|
|
918
|
+
* parameter named `constructor` would otherwise find something on the
|
|
919
|
+
* prototype and expand into the source of a function. A value of "." or ".."
|
|
920
|
+
* is refused, because a relative-URL resolver walks it and the request lands
|
|
921
|
+
* somewhere else entirely. And a value that is not a string is refused rather
|
|
922
|
+
* than stringified: `null` would otherwise become the four letters that spell
|
|
923
|
+
* it, and the SDK is not always the one holding the types.
|
|
924
|
+
*/
|
|
925
|
+
export const expandPath = (path, values) => {
|
|
926
|
+
const wanted = pathParamsOf(path);
|
|
927
|
+
const unwanted = Object.keys(values).filter((name) => !wanted.includes(name));
|
|
928
|
+
if (unwanted.length > 0) {
|
|
929
|
+
throw new TypeError(`"${path}" takes ${wanted.length === 0 ? "no path parameters" : wanted.join(", ")}, so there is nowhere to put ${unwanted.join(", ")}`);
|
|
930
|
+
}
|
|
931
|
+
return path.replace(pathParameterPattern(), (_match, name) => {
|
|
932
|
+
const value = Object.hasOwn(values, name) ? values[name] : undefined;
|
|
933
|
+
if (typeof value !== "string" || value === "") {
|
|
934
|
+
throw new TypeError(`"${path}" needs a value for ":${name}" and was given none`);
|
|
935
|
+
}
|
|
936
|
+
if (value === "." || value === "..") {
|
|
937
|
+
throw new TypeError(`"${path}" was given ${JSON.stringify(value)} for ":${name}", which is a step through the path rather than a value in it`);
|
|
938
|
+
}
|
|
939
|
+
return encodeURIComponent(value);
|
|
940
|
+
});
|
|
941
|
+
};
|
|
942
|
+
/**
|
|
943
|
+
* The routes a gateway may serve: every route whose door somebody has chosen.
|
|
944
|
+
*
|
|
945
|
+
* It exists because the natural way to mount a table is to ask whether a route
|
|
946
|
+
* needs the merchant key and to treat everything else as open — and that
|
|
947
|
+
* reading serves a route nobody has chosen a door for to the whole world.
|
|
948
|
+
* Mounting from this list instead makes the safe reading the easy one, and a
|
|
949
|
+
* route appears here the day its door is decided.
|
|
950
|
+
*
|
|
951
|
+
* Every route in the table has a door today, so this list is the whole of it.
|
|
952
|
+
* That is a fact about the table and not a reason to mount from `API_ROUTES`
|
|
953
|
+
* directly: the next route to be written down before it is designed is exactly
|
|
954
|
+
* the one this filter is for.
|
|
955
|
+
*/
|
|
956
|
+
export const mountableRoutes = () => Object.entries(API_ROUTES).filter(([, route]) => route.auth !== "undecided");
|