@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.
@@ -0,0 +1,269 @@
1
+ /**
2
+ * What our calls hand back — the successes as well as the failures.
3
+ *
4
+ * None of it travels as an exception. A merchant's integration code has to
5
+ * read these, branch on them and write some of them down, and an exception is
6
+ * the wrong shape for something you are expected to handle rather than to be
7
+ * surprised by. That is a rule about the whole surface, which is why publishing
8
+ * a card and answering for an order share a file even though they are
9
+ * different calls.
10
+ *
11
+ * The words below are wire names, not prose: a merchant's code branches on
12
+ * them, so rewording one is changing the contract.
13
+ *
14
+ * Every one of those answers arrives in one envelope. `ok` is `true` or it is
15
+ * `false` and nothing else says which; a success carries its own fields beside
16
+ * it, and a failure carries `error`. That `ok` is a value and not a key that
17
+ * may or may not be present is the part worth arguing, because the two look
18
+ * alike from inside TypeScript and do not behave alike anywhere else. A key
19
+ * with an object under it reads as false in Python and in PHP — `{"ok": {}}`
20
+ * is falsy in both — so the one idiom an engineer would write says yes to one
21
+ * answer and no to another, and the JSON Schema export exists for exactly that
22
+ * engineer. As a literal it is also a discriminator: it crosses into the export
23
+ * as a `const` on each branch, which a generator can switch on, where
24
+ * "whichever key happens to be present" is something a reader has to work out.
25
+ *
26
+ * Two words carry what went wrong, and they are not two names for one thing.
27
+ * `error` is why a call did not go through: one object, always, with a code to
28
+ * branch on, a sentence to print, and a flag saying whether repeating the call
29
+ * could change the outcome. `problems` is the list of findings about what was
30
+ * sent — the fields at fault — and it rides inside `error` wherever there are
31
+ * any. A call fails for one reason; a document can be wrong in several places
32
+ * at once, and that is the whole of the difference between the singular and the
33
+ * plural here.
34
+ */
35
+ import { z } from "zod";
36
+ import { IdentifierSchema } from "./primitives.js";
37
+ /**
38
+ * How answering for an order can succeed.
39
+ *
40
+ * "Answering" rather than "delivering or refusing", because two of these reach
41
+ * us from a third surface — a handler's own return — rather than from either
42
+ * call. `accepted` is how an asynchronous handler answers while the delivery
43
+ * is still ahead of it, and `purchase_already_closed` belongs to the
44
+ * synchronous mode, where neither `deliver` nor `refuse` exists at all, as the
45
+ * error code `not_applicable_in_mode` says from the other direction.
46
+ *
47
+ * Each one is a different thing for the merchant to do next, which is why none
48
+ * of them collapses into another. `accepted` is a handler taking the order on: the
49
+ * goods are owed and will follow through the separate `deliver` call, and the
50
+ * merchant's own record of the order should say it is under way. It is a
51
+ * success and belongs among these rather than among the error codes, and that
52
+ * placement is the whole of what it does for a merchant: every handler answer
53
+ * is posted to the answer route by the SDK without being asked to, and an
54
+ * integration that reads anything but a success as trouble would otherwise
55
+ * open a case on every asynchronous order it sells.
56
+ *
57
+ * `delivered` is the sale closing. `already_delivered` is their own retry
58
+ * landing twice — safe, and no second delivery is wanted.
59
+ *
60
+ * That one is worth a warning, because the merchant-facing pages say a
61
+ * repeated call returns "the same success" and this returns a different word
62
+ * for it. Both are true of the effect — nothing is delivered twice and nothing
63
+ * is charged twice — but a merchant who reads that sentence and writes
64
+ * `if (result === "delivered")` turns their own safe retry into a failure
65
+ * branch. The answer these words travel in is built so that nobody has to: a
66
+ * consumer branches on `ok` and records the word, which stays right when this
67
+ * list grows another entry.
68
+ *
69
+ * One kind of consumer it does not stay right for, said here because the list
70
+ * has grown once already and will again. A client that checks the answer
71
+ * against its own copy of this schema — our own SDK does, and so does anything
72
+ * generated from the JSON Schema export — reads an unknown word as a document
73
+ * it cannot parse rather than as a success it cannot name, and reports the
74
+ * call as having gone wrong. So a word added here has to reach those clients
75
+ * before the gateway starts sending it. The version handshake would be the
76
+ * place to catch that. The published SDK keeps this schema strict, and every
77
+ * new word moves `CONTRACT_VERSION` before the gateway sends it. An old worker
78
+ * then stops at its version handshake rather than calling a successful answer
79
+ * unreadable after the merchant has already acted on it (ADR-0006).
80
+ *
81
+ * `debt_closed_by_delivery` says the delivery deadline had already passed and
82
+ * the goods went out anyway, closing a debt instead of completing a sale; the
83
+ * merchant may want to know that happened. `refused` is the refusal taking
84
+ * effect. `purchase_already_closed` is the synchronous handler that came back
85
+ * after its deadline: not an error, because nothing went wrong on their side —
86
+ * the work exists and a repeat purchase will collect it, and the only thing to
87
+ * do is write the case down.
88
+ */
89
+ export const ORDER_CALL_RESULTS = Object.freeze([
90
+ "accepted",
91
+ "delivered",
92
+ "already_delivered",
93
+ "debt_closed_by_delivery",
94
+ "refused",
95
+ "purchase_already_closed",
96
+ ]);
97
+ export const OrderCallResultSchema = z.enum(ORDER_CALL_RESULTS);
98
+ /**
99
+ * The error codes those calls answer with.
100
+ *
101
+ * A list of what we promise to mean the same way, not a gate: the code below
102
+ * stays an open string, because an error nobody anticipated has to reach the
103
+ * merchant in its own words rather than be flattened into the nearest of
104
+ * three. `refund_already_settled` — the debt was paid back, so there is
105
+ * nothing left to deliver against. `order_already_closed` — the order reached
106
+ * an ending that no call reopens. `not_applicable_in_mode` — the call does not
107
+ * exist for this card's mode, as refusing separately does not in the
108
+ * synchronous one, where the handler's own answer is the refusal.
109
+ * `delivery_does_not_match_card` — the goods are not the ones the card for
110
+ * this order declares it delivers, so nothing was written down; the message
111
+ * says whether the order still stands or has already ended, and the fields
112
+ * that did not fit are named one by one in the error's `problems`.
113
+ *
114
+ * The fourth is the only one of them a merchant fixes rather than records, and
115
+ * it is the reason it is promised rather than left to the open set. It is
116
+ * marked retryable where the order is still open, and that retry is not the
117
+ * same retry the other three mean: those say "the call may not have reached
118
+ * us, send it again", while this says the call arrived and was understood and
119
+ * the goods in it were wrong. Sending the same delivery again produces the
120
+ * same refusal until the goods change, and a merchant told only "retryable"
121
+ * would loop on it — which is why the code is named and the message says which
122
+ * field is wrong.
123
+ *
124
+ * On an order that has already ended it is not retryable, and the message says
125
+ * the ending. The goods being wrong and the sale being over are both true
126
+ * there, and only the second decides what he can do next: told the order still
127
+ * stood, a merchant would make the goods a second time for a purchase nobody
128
+ * can complete.
129
+ *
130
+ * Adding to this list is safe in a way that adding a success word is not: the
131
+ * code is an open string on the wire, so an older client parses an unfamiliar
132
+ * one and reads it as the failure it is. What it loses is the meaning, which is
133
+ * what this list and the dictionary below carry.
134
+ */
135
+ export const ORDER_CALL_ERROR_CODES = Object.freeze([
136
+ "refund_already_settled",
137
+ "order_already_closed",
138
+ "not_applicable_in_mode",
139
+ "delivery_does_not_match_card",
140
+ ]);
141
+ /**
142
+ * One thing wrong with what was sent, in a place, in a code and in words.
143
+ *
144
+ * This is one finding and never a whole answer. Wherever a call refuses what it
145
+ * was handed, the findings travel as a list of these under `problems` inside
146
+ * the error: a card that could not be published names every field standing
147
+ * between it and the catalog, a delivery that is not what its card declares
148
+ * names the fields that did not fit, and the local check a merchant runs before
149
+ * publishing — `checkCard` in the SDK — answers in this same shape, so a
150
+ * finding read on their own machine and a finding read off the wire need no
151
+ * two readers.
152
+ *
153
+ * The empty path is the part two readers were left to infer differently, so it
154
+ * is said in the exported description as well as here.
155
+ */
156
+ export const ProblemSchema = z
157
+ .strictObject({
158
+ /**
159
+ * Which field the finding is about, as the path to it — `["params",
160
+ * "email", "type"]`. An empty path is a statement rather than a missing
161
+ * value, and it covers two kinds of finding that a merchant tells apart by
162
+ * the words rather than by the shape: one about the whole of what was
163
+ * sent, and one about the merchant sending it — their having set no name
164
+ * for buyers to read, or no wallet for their sales to be paid into.
165
+ * Leaving the field out entirely would make an empty path
166
+ * indistinguishable from a path nobody filled in.
167
+ */
168
+ path: z.array(z.string()),
169
+ /** What kind of finding it is, for the code that reads it. */
170
+ code: z.string().regex(/\S/, "a finding carries a code"),
171
+ /** The same finding in words, for the person who has to fix the card. */
172
+ message: z.string().regex(/\S/, "a finding carries an explanation a person can read"),
173
+ })
174
+ .meta({
175
+ description: 'One thing wrong with what was sent: where it is, a code for the program that reads it, and the same finding in words for the person who has to fix it. The path names the field, innermost last — ["params", "email", "type"] — and an empty path is a statement rather than a missing value: the finding is about the whole of what was sent, or about the sender — a merchant with no name set for buyers, or no wallet set for their sales to be paid into, is refused with an empty path too — and not about any one field, which is also what a document that could not be read at all produces. The field is always present for that reason, because an absent path and an empty one would be indistinguishable. Findings never travel alone: they arrive as the problems list inside the error of a call that refused what it was handed — a card that was not published, a delivery that is not what its card declares, a body or a set of purchase parameters that did not fit.',
176
+ });
177
+ /**
178
+ * Why a call did not go through: a code to branch on, a sentence to print, and
179
+ * whether repeating it could change the outcome.
180
+ *
181
+ * One shape for every call on the merchant's surface. Publishing a card and
182
+ * delivering an order fail for entirely different reasons, and a merchant who
183
+ * had to learn two shapes for "this did not happen" would be learning the same
184
+ * lesson twice.
185
+ *
186
+ * The flag is the reason this shape exists at all. "The connection dropped,
187
+ * call again — the call is idempotent" and "nothing you do will change this,
188
+ * write the case down" need different code on the merchant's side, and a
189
+ * merchant left to guess turns one of them into a retry loop and the other into
190
+ * an order nobody comes back to. The refund that has already been paid out is
191
+ * the example of the second kind. It is required rather than defaulted for the
192
+ * same reason: both readings of a missing flag are expensive.
193
+ *
194
+ * `problems` is present where the call is refusing what it was handed rather
195
+ * than reporting a state of the world, and it is the field that turns "no" into
196
+ * an edit. It is optional because most failures have nothing to point at: a
197
+ * refund already settled is about the order and not about a field of the
198
+ * request. Where it is there it is never empty, and a call whose whole answer
199
+ * is "refused, and here is nothing" — a publish — requires it.
200
+ */
201
+ export const CallErrorSchema = z.strictObject({
202
+ code: z.string().regex(/\S/, "an error carries a code").meta({
203
+ // Same reason as the refusal code: the dictionary travels with the field
204
+ // or it does not reach the reader the export exists for.
205
+ description: 'Why the call did not go through. The set is open, and five are promised to mean one thing each — but not on the same calls, and which call a code can arrive on is part of what is promised about it. Publishing a card is refused with one word and no other: "card_rejected" (the card was not published, and every finding standing between it and the catalog is named in the error\'s problems — the fields at fault, and the merchant\'s own missing name or payout wallet where those are what is missing; it is never retryable, because the same card gets the same answer and what changes the outcome is fixing what the problems name). The calls that close an order — delivering, refusing, taking one on — are refused with the other four, and never with the first: "refund_already_settled" (the debt was paid back, so there is nothing left to deliver against). "order_already_closed" (the order reached an ending that no call reopens). "not_applicable_in_mode" (the call does not exist for this card\'s mode — refusing separately does not, in the synchronous one, where the handler\'s own answer is the refusal). "delivery_does_not_match_card" (the goods are not the ones the card for this order declares it delivers — nothing was written down, the problems name the fields that did not fit, and the message says whether the order still stands or has already ended). The last of those is retryable in a different sense from a lost connection: the call arrived and was understood, so sending the same goods again gives the same refusal, and what clears it is delivering what the card declares. It is not retryable at all where the order has already ended, because there is nothing left to deliver against.',
206
+ }),
207
+ message: z.string().regex(/\S/, "an error carries an explanation a person can read"),
208
+ retryable: z.boolean(),
209
+ /**
210
+ * The findings, where the call is refusing what it was handed.
211
+ *
212
+ * Never empty when it is there: a list with nothing in it says "these are the
213
+ * things at fault" and names none of them, which is the one answer a merchant
214
+ * cannot act on.
215
+ *
216
+ * Described for the export as well as here, for the reason the two codes are:
217
+ * a reader who has only the JSON Schema has only what travels in it, and this
218
+ * is the field the whole shape is named after.
219
+ */
220
+ problems: z.array(ProblemSchema).min(1).optional().meta({
221
+ description: 'What was wrong with what was sent, one finding at a time: where it is, a code for the program that reads it, and the same finding in words. Present where the call is refusing what it was handed — a card that was not published, a delivery that is not what its card declares — and absent where the refusal is about a state of the world instead: a refund already settled is about the order, not about a field of the request. Never empty where it is present, and a refused publish always carries it. This field is the complete account of what stands in the way, and it is the one to read the findings from. The error\'s "message" is a single line written to be read in a log and does not carry the list: it says how many findings there are, quotes one or a few of them, and marks the place where a long one was cut — so a reader can always tell a short refusal from a shortened account of a long one.',
222
+ }),
223
+ });
224
+ /**
225
+ * The code a refused publish comes back under.
226
+ *
227
+ * Promised rather than left to the open set, because it is the one refusal of
228
+ * this surface a merchant meets on their first afternoon, and every one of them
229
+ * writes the branch that reads it.
230
+ */
231
+ export const CARD_REJECTED = "card_rejected";
232
+ /**
233
+ * The error a refused publish carries: the shared shape, with the findings made
234
+ * required.
235
+ *
236
+ * "Refused, and here is nothing" is the one answer a merchant cannot act on,
237
+ * and publishing is the call where that would be easiest to send — a card is
238
+ * refused precisely because something about it is wrong, so there is always
239
+ * something to name. Not every finding is about the card. A merchant who has
240
+ * set no name for buyers to read is refused here too, and so is one who has set
241
+ * no wallet for their sales to be paid into; both ride in the same list, so one
242
+ * answer carries everything standing between this card and the catalog rather
243
+ * than handing it over one round trip at a time.
244
+ */
245
+ const PublishRefusalSchema = CallErrorSchema.extend({
246
+ problems: z.array(ProblemSchema).min(1),
247
+ });
248
+ /**
249
+ * The answer to publishing a card: the catalog id, or what stands in its way.
250
+ *
251
+ * `ok` says which, as it does on every other call of this surface, and the
252
+ * catalog identifier sits at the top level of the success beside it rather than
253
+ * nested under a word. There is one fact in a successful publish and this is
254
+ * it.
255
+ *
256
+ * The refusal is `CARD_REJECTED` and it is not retryable: publishing the same
257
+ * card again gets the same answer, and what changes the outcome is fixing what
258
+ * the findings name. That is a different thing from the retry a dropped
259
+ * connection asks for, which is why the flag says no here rather than being
260
+ * left for the merchant to infer from the code.
261
+ */
262
+ export const PublishResultSchema = z.discriminatedUnion("ok", [
263
+ z.strictObject({
264
+ ok: z.literal(true),
265
+ /** Our catalog identifier, from now on in catalogs, orders and receipts. */
266
+ id: IdentifierSchema,
267
+ }),
268
+ z.strictObject({ ok: z.literal(false), error: PublishRefusalSchema }),
269
+ ]);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Whether a merchant is taking new orders.
3
+ *
4
+ * These are wire names for a switch the merchant presses and the order machine
5
+ * reads. The machine keeps the same three words at the birth of an order and
6
+ * refuses to create one for two of them; a second list written anywhere else
7
+ * would be a screen saying one thing while the machine did another, with the
8
+ * gateway translating in between and neither side failing. A test at the seam
9
+ * holds the two lists identical.
10
+ *
11
+ * Three words and not a boolean, because leaving is not a heavier pause. A
12
+ * pause takes the cards off sale and lets the orders already accepted play out;
13
+ * leaving closes those orders and leaves the merchant owing refunds on whatever
14
+ * was paid for and never delivered. A merchant who read one word for both would
15
+ * expect a departure to be undone the way a pause is.
16
+ */
17
+ import { z } from "zod";
18
+ export declare const SELLING_STATES: readonly ["open", "paused", "departed"];
19
+ export declare const SellingStateSchema: z.ZodEnum<{
20
+ open: "open";
21
+ paused: "paused";
22
+ departed: "departed";
23
+ }>;
24
+ export type SellingState = z.infer<typeof SellingStateSchema>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Whether a merchant is taking new orders.
3
+ *
4
+ * These are wire names for a switch the merchant presses and the order machine
5
+ * reads. The machine keeps the same three words at the birth of an order and
6
+ * refuses to create one for two of them; a second list written anywhere else
7
+ * would be a screen saying one thing while the machine did another, with the
8
+ * gateway translating in between and neither side failing. A test at the seam
9
+ * holds the two lists identical.
10
+ *
11
+ * Three words and not a boolean, because leaving is not a heavier pause. A
12
+ * pause takes the cards off sale and lets the orders already accepted play out;
13
+ * leaving closes those orders and leaves the merchant owing refunds on whatever
14
+ * was paid for and never delivered. A merchant who read one word for both would
15
+ * expect a departure to be undone the way a pause is.
16
+ */
17
+ import { z } from "zod";
18
+ export const SELLING_STATES = Object.freeze([
19
+ /** Selling: new orders are taken. */
20
+ "open",
21
+ /**
22
+ * Paused: no new order is taken, and the orders already accepted play out in
23
+ * the ordinary way. Nothing about an open order changes when a pause begins.
24
+ */
25
+ "paused",
26
+ /**
27
+ * Gone: the merchant left. Their cards are off sale, the orders that were
28
+ * open closed with them, and the money for anything paid for and not
29
+ * delivered is theirs to return.
30
+ */
31
+ "departed",
32
+ ]);
33
+ export const SellingStateSchema = z.enum(SELLING_STATES).meta({
34
+ description: 'Whether a merchant is taking new orders. "open" — new orders are taken. "paused" — no new order is taken and the orders already accepted play out in the ordinary way, so a pause never closes anything. "departed" — the merchant left, their cards are off sale, the orders that were open closed with them, and the money for anything paid for and not delivered is theirs to return. Leaving is not a heavier pause: it is not reachable by pausing, and it is not undone by resuming.',
35
+ });
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@nuanu-ai/agentify-contracts",
3
+ "version": "0.3.2",
4
+ "type": "module",
5
+ "description": "The Agentify wire contract as code: the schemas for cards, orders, price checks and receipts that the gateway and the merchant SDK both read.",
6
+ "keywords": [
7
+ "x402",
8
+ "agent-commerce",
9
+ "zod-schemas",
10
+ "json-schema"
11
+ ],
12
+ "homepage": "https://app.agentify.ad/docs/",
13
+ "license": "Apache-2.0",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/nuanu-ai/agentify.git",
17
+ "directory": "packages/contracts"
18
+ },
19
+ "engines": {
20
+ "node": ">=24"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "LICENSE",
25
+ "NOTICE"
26
+ ],
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "dependencies": {
37
+ "zod": "^4.4.3"
38
+ },
39
+ "devDependencies": {
40
+ "vitest": "^4.1.11"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc -p tsconfig.build.json",
44
+ "typecheck": "tsc --noEmit -p tsconfig.json"
45
+ }
46
+ }