@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/dist/order.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The order, as it reaches the merchant's handler.
3
+ *
4
+ * By the time an order exists the purchase has already been checked on our
5
+ * side: the card was found, the parameters passed the card's own declaration,
6
+ * the price was settled and the payment was verified. Invalid purchases, stale
7
+ * prices and payments that did not verify never reach a handler, which is why
8
+ * this schema is short.
9
+ *
10
+ * Everything here answers a question the handler would otherwise have to ask
11
+ * us: what to deliver, to whom, under which of its own keys, at what price,
12
+ * and whether the money behind it is real.
13
+ *
14
+ * What state an order is in is not a field here. Reading state back — one
15
+ * order, or every order still open — is a separate call, and the words it
16
+ * answers with are `OrderStatusSchema` in `order-status.ts`.
17
+ *
18
+ * A second thing an order does not carry is its deadline. The design canon
19
+ * says an order carries one and the portal says a synchronous order does, but
20
+ * the portal's own table of what an order consists of does not list it, and
21
+ * this schema follows the table. The consequence stands either way: this
22
+ * package is the whole of the SDK's dependency tree, so nothing in it lets a
23
+ * synchronous handler learn how long it has — and in that mode the budget is
24
+ * ours, not something the merchant set and could look up. Which of the two
25
+ * readings is right is a question for whoever settles the numbers.
26
+ *
27
+ * One shape the documentation promises is still missing, and it is named here
28
+ * rather than left to be found as an absence. In the mode where the merchant
29
+ * is asked before the money moves, a confirmation request reaches the same
30
+ * subscription as the orders and is marked as a request rather than an order —
31
+ * no payment has happened, and delivering against one is not allowed. Nothing
32
+ * in this package carries that mark, so the two would be indistinguishable on
33
+ * the wire. Rather than leave that as a trap, the card refuses to publish in
34
+ * that mode at all until the request has a shape; see `card.ts`.
35
+ */
36
+ import { z } from "zod";
37
+ import { ParamNameSchema } from "./param-spec.js";
38
+ import { IdentifierSchema, SalePriceSchema } from "./primitives.js";
39
+ export const OrderSchema = z.strictObject({
40
+ /**
41
+ * The order's identifier, which is also its idempotency key: the same string
42
+ * on every redelivery. Delivery is at-least-once, so a handler that answers
43
+ * from this key instead of delivering twice is not being careful, it is
44
+ * being correct.
45
+ */
46
+ id: IdentifierSchema,
47
+ /** The merchant's own key for the product, the one their database uses. */
48
+ merchant_item_id: IdentifierSchema,
49
+ /**
50
+ * The purchase parameters, already checked against the card's declaration.
51
+ * Always present, empty for a card that takes no input — a handler should
52
+ * never have to tell "no parameters" from "the field did not arrive".
53
+ */
54
+ // One key is dropped here rather than carried or refused; the reason is at
55
+ // `PROTOTYPE_KEY_IS_DROPPED` in `param-spec.ts`.
56
+ params: z.record(ParamNameSchema, z.unknown()),
57
+ /**
58
+ * What the product was actually sold for, which is not always what the card
59
+ * says: a card with a price check sells at the price the check answered.
60
+ * Both moments travel with it, so the sale can be written down as it stands.
61
+ */
62
+ price: SalePriceSchema,
63
+ /**
64
+ * The identifier of the price question this sale came out of, absent when
65
+ * there was none — a card without a price check sells from its own price and
66
+ * no question was ever asked. A merchant who set stock aside under this
67
+ * identifier can release it here.
68
+ */
69
+ price_id: IdentifierSchema.optional(),
70
+ /**
71
+ * Whether this is a test order.
72
+ *
73
+ * Required rather than defaulted. The safe reading of a missing flag is not
74
+ * obvious in either direction, and a test order taken for a live one is a
75
+ * real delivery made against money that does not exist.
76
+ */
77
+ test: z.boolean(),
78
+ });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The small schema language a card writes its purchase parameters and its
3
+ * delivery result in, and the compiler that turns one of those declarations
4
+ * into a check.
5
+ *
6
+ * Both halves live here on purpose. The gateway validates an agent's purchase
7
+ * against the card's `params`, and the SDK holds a merchant's delivery to the
8
+ * card's `result`; if each side compiled the declaration its own way, a card
9
+ * would mean two different things and the difference would only show up after
10
+ * someone had paid.
11
+ *
12
+ * The language is deliberately small — four scalar types and nothing else.
13
+ * The portal names the exact schema of parameters as still open, and a richer
14
+ * language invented here (enumerations, lists, nested objects) would be a
15
+ * contract nobody agreed to. It grows when a real card needs it to.
16
+ */
17
+ import { z } from "zod";
18
+ /**
19
+ * The types a card may declare.
20
+ *
21
+ * Every one of them has a check behind it in `paramSpecToValidator`. A type
22
+ * this list does not carry would promise the agent a validation that nobody
23
+ * performs, which is why the enumeration and the compiler sit in one file.
24
+ */
25
+ export declare const ParamTypeSchema: z.ZodEnum<{
26
+ string: "string";
27
+ number: "number";
28
+ boolean: "boolean";
29
+ integer: "integer";
30
+ }>;
31
+ /**
32
+ * The name of one parameter.
33
+ *
34
+ * A name becomes a property in JSON on both sides of the wire and a segment of
35
+ * the error path a merchant reads, so it is held to the shape of a plain
36
+ * identifier. Dots would collide with that path notation and spaces would make
37
+ * the name unquotable in an error. Both `access_url` and `expiresAt` are fine:
38
+ * which of the two a merchant writes is theirs to choose.
39
+ *
40
+ * One name never gets this far: see `PROTOTYPE_KEY_IS_DROPPED` below.
41
+ */
42
+ export declare const ParamNameSchema: z.ZodString;
43
+ /**
44
+ * One declared parameter: what type it holds, whether it has to be there, and
45
+ * a line of human words explaining what it is for.
46
+ *
47
+ * What silence about `required` means depends on which of a card's two
48
+ * declarations this is, and the compiler below is where that is decided. In
49
+ * the purchase parameters a field with no flag is one the agent may leave out;
50
+ * in the delivery result it is one that arrives. Setting the flag says the
51
+ * same thing in both directions, and it is the only way to say the opposite.
52
+ */
53
+ export declare const FieldSpecSchema: z.ZodObject<{
54
+ type: z.ZodEnum<{
55
+ string: "string";
56
+ number: "number";
57
+ boolean: "boolean";
58
+ integer: "integer";
59
+ }>;
60
+ required: z.ZodOptional<z.ZodBoolean>;
61
+ title: z.ZodOptional<z.ZodString>;
62
+ }, z.core.$strict>;
63
+ /**
64
+ * The one silent loss in this contract, written down once because five places
65
+ * have it and a note on one of them was worse than none.
66
+ *
67
+ * A key named `__proto__` is removed by zod while a record is parsed, before
68
+ * any check of ours runs — so it is neither carried nor refused, and nobody is
69
+ * told. It applies wherever this contract parses a record of free-form names,
70
+ * and there are five of those: a card's `params` and `result` here, an order's
71
+ * `params`, a purchase's `params`, a price question's `params`, and a delivery.
72
+ * The check compiled below is not a sixth. It is built from a spec that has
73
+ * already been through the record above, so a name that would have been
74
+ * dropped is gone before the compiler ever sees it.
75
+ *
76
+ * Every one of those is a place the portal promises to pass values through
77
+ * unchanged, so the loss is worth knowing about even though nothing can
78
+ * legitimately reach it: no card can declare such a field, so a merchant only
79
+ * meets this by sending a name nobody asked for. The behaviour is zod's, not
80
+ * ours, and it is the reason the compiler below can never be handed a name
81
+ * that would set a prototype instead of declaring a field. A test holds it in
82
+ * place, so a change in zod is noticed here rather than in production.
83
+ */
84
+ export declare const PROTOTYPE_KEY_IS_DROPPED = "__proto__";
85
+ /** A whole declaration: the parameters of a purchase, or a delivery result. */
86
+ export declare const ParamSpecSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
87
+ type: z.ZodEnum<{
88
+ string: "string";
89
+ number: "number";
90
+ boolean: "boolean";
91
+ integer: "integer";
92
+ }>;
93
+ required: z.ZodOptional<z.ZodBoolean>;
94
+ title: z.ZodOptional<z.ZodString>;
95
+ }, z.core.$strict>>;
96
+ export type ParamType = z.infer<typeof ParamTypeSchema>;
97
+ export type FieldSpec = z.infer<typeof FieldSpecSchema>;
98
+ export type ParamSpec = z.infer<typeof ParamSpecSchema>;
99
+ /**
100
+ * One declared field as a merchant may write it on a card: the whole spec, or
101
+ * the type word alone.
102
+ *
103
+ * The short form belongs to the card and is opened out where a card is parsed,
104
+ * so this type says what may be written and never what is stored or read back.
105
+ * The rule and the reasons are in `card.ts`, beside the other two short forms.
106
+ */
107
+ export type FieldSpecInput = FieldSpec | ParamType;
108
+ /** A whole declaration as a merchant may write it, field by field. */
109
+ export type ParamSpecInput = Record<string, FieldSpecInput>;
110
+ /**
111
+ * Which of a card's two declarations is being compiled.
112
+ *
113
+ * It decides two things, and both come from the same difference: a purchase is
114
+ * input the agent is being asked for, and a delivery is a promise the card
115
+ * made to it before it paid.
116
+ *
117
+ * What silence about `required` means. In a purchase the portal writes
118
+ * `required: true` only where it means it, so a field with no flag is one the
119
+ * agent may leave out. In a delivery a declared field is one that arrives — a
120
+ * flag-less field treated as optional would make the promise unenforceable,
121
+ * and a merchant could deliver an empty object against a card that advertised
122
+ * three fields. A delivery field that genuinely may be absent says so with
123
+ * `required: false`.
124
+ *
125
+ * And whether a string may be empty. It may in a purchase and may not in a
126
+ * delivery, for the reason written beside the check itself.
127
+ */
128
+ export type ParamSpecDirection = "purchase" | "delivery";
129
+ /**
130
+ * Turns a declaration into the check that an instance of it is held to.
131
+ *
132
+ * Keys the declaration does not carry are refused, in both directions. The two
133
+ * alternatives both lie: stripping them is truncation that nobody is told
134
+ * about, and passing them through hands the merchant's handler fields the card
135
+ * never declared — a card that does not describe what arrives, and a way to
136
+ * push input into someone else's code. In a delivery an undeclared field means
137
+ * the agent paid for something other than the result it read before paying.
138
+ *
139
+ * One thing this cannot refuse is a key named `__proto__`; see
140
+ * `PROTOTYPE_KEY_IS_DROPPED` above.
141
+ *
142
+ * The spec is expected to have been parsed by `ParamSpecSchema` already; that
143
+ * is what the parameter type says.
144
+ */
145
+ export declare const paramSpecToValidator: (spec: ParamSpec, direction: ParamSpecDirection) => z.ZodType;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * The small schema language a card writes its purchase parameters and its
3
+ * delivery result in, and the compiler that turns one of those declarations
4
+ * into a check.
5
+ *
6
+ * Both halves live here on purpose. The gateway validates an agent's purchase
7
+ * against the card's `params`, and the SDK holds a merchant's delivery to the
8
+ * card's `result`; if each side compiled the declaration its own way, a card
9
+ * would mean two different things and the difference would only show up after
10
+ * someone had paid.
11
+ *
12
+ * The language is deliberately small — four scalar types and nothing else.
13
+ * The portal names the exact schema of parameters as still open, and a richer
14
+ * language invented here (enumerations, lists, nested objects) would be a
15
+ * contract nobody agreed to. It grows when a real card needs it to.
16
+ */
17
+ import { z } from "zod";
18
+ /**
19
+ * The types a card may declare.
20
+ *
21
+ * Every one of them has a check behind it in `paramSpecToValidator`. A type
22
+ * this list does not carry would promise the agent a validation that nobody
23
+ * performs, which is why the enumeration and the compiler sit in one file.
24
+ */
25
+ export const ParamTypeSchema = z.enum(["string", "number", "integer", "boolean"]);
26
+ /**
27
+ * The name of one parameter.
28
+ *
29
+ * A name becomes a property in JSON on both sides of the wire and a segment of
30
+ * the error path a merchant reads, so it is held to the shape of a plain
31
+ * identifier. Dots would collide with that path notation and spaces would make
32
+ * the name unquotable in an error. Both `access_url` and `expiresAt` are fine:
33
+ * which of the two a merchant writes is theirs to choose.
34
+ *
35
+ * One name never gets this far: see `PROTOTYPE_KEY_IS_DROPPED` below.
36
+ */
37
+ export const ParamNameSchema = z
38
+ .string()
39
+ .regex(/^[A-Za-z][A-Za-z0-9_]*$/, "a parameter name starts with a letter and carries letters, digits and underscores");
40
+ /**
41
+ * One declared parameter: what type it holds, whether it has to be there, and
42
+ * a line of human words explaining what it is for.
43
+ *
44
+ * What silence about `required` means depends on which of a card's two
45
+ * declarations this is, and the compiler below is where that is decided. In
46
+ * the purchase parameters a field with no flag is one the agent may leave out;
47
+ * in the delivery result it is one that arrives. Setting the flag says the
48
+ * same thing in both directions, and it is the only way to say the opposite.
49
+ */
50
+ export const FieldSpecSchema = z.strictObject({
51
+ type: ParamTypeSchema,
52
+ required: z.boolean().optional(),
53
+ title: z.string().regex(/\S/, "a title must not be empty or blank").optional(),
54
+ });
55
+ /**
56
+ * The one silent loss in this contract, written down once because five places
57
+ * have it and a note on one of them was worse than none.
58
+ *
59
+ * A key named `__proto__` is removed by zod while a record is parsed, before
60
+ * any check of ours runs — so it is neither carried nor refused, and nobody is
61
+ * told. It applies wherever this contract parses a record of free-form names,
62
+ * and there are five of those: a card's `params` and `result` here, an order's
63
+ * `params`, a purchase's `params`, a price question's `params`, and a delivery.
64
+ * The check compiled below is not a sixth. It is built from a spec that has
65
+ * already been through the record above, so a name that would have been
66
+ * dropped is gone before the compiler ever sees it.
67
+ *
68
+ * Every one of those is a place the portal promises to pass values through
69
+ * unchanged, so the loss is worth knowing about even though nothing can
70
+ * legitimately reach it: no card can declare such a field, so a merchant only
71
+ * meets this by sending a name nobody asked for. The behaviour is zod's, not
72
+ * ours, and it is the reason the compiler below can never be handed a name
73
+ * that would set a prototype instead of declaring a field. A test holds it in
74
+ * place, so a change in zod is noticed here rather than in production.
75
+ */
76
+ export const PROTOTYPE_KEY_IS_DROPPED = "__proto__";
77
+ /** A whole declaration: the parameters of a purchase, or a delivery result. */
78
+ export const ParamSpecSchema = z.record(ParamNameSchema, FieldSpecSchema, {
79
+ // What a value that is not a declaration at all is told. The default —
80
+ // "expected record, received string" — names a word from the schema library
81
+ // rather than from the card, and it lands on the one mistake the short form
82
+ // invites: a merchant who has learned that `access_url: 'string'` declares a
83
+ // field writes `result: 'string'` and has declared nothing. The sentence
84
+ // shows the shape they meant.
85
+ //
86
+ // Only where something was written. A declaration that is simply absent is a
87
+ // different finding — a required field nobody filled in — and returning
88
+ // nothing here leaves zod's own words for it, which say exactly that.
89
+ error: (issue) => issue.input === undefined
90
+ ? undefined
91
+ : "a declaration names each field and gives its type, as in { access_url: 'string' }; a type word on its own declares one field and not the whole of it",
92
+ });
93
+ const checkFor = (type, direction) => {
94
+ switch (type) {
95
+ case "string":
96
+ // A delivered string carries something. This is the one place the two
97
+ // directions hold the same declared type to different rules, and the
98
+ // asymmetry is the point rather than an oversight.
99
+ //
100
+ // In a purchase, an empty string is input the agent chose to give: a
101
+ // note left blank, a name it does not have. Refusing it would be this
102
+ // package inventing a requirement no card asked for.
103
+ //
104
+ // In a delivery the field is a promise about what the buyer receives,
105
+ // and an empty access code is not a shorter access code — it is nothing,
106
+ // arriving under the name of something. Without this the check refuses
107
+ // `{}` and accepts `{access_code: ""}`, which are the same outcome for
108
+ // the buyer, and only one of them would be caught. Every other string on
109
+ // this wire is already held this way — a title, a description, a refusal
110
+ // code and its message all refuse to be blank — and the one the buyer
111
+ // actually paid for was the exception.
112
+ return direction === "delivery"
113
+ ? z.string().regex(/\S/, "a delivered field carries something, and this one is blank")
114
+ : z.string();
115
+ case "number":
116
+ return z.number();
117
+ case "integer":
118
+ return z.int();
119
+ case "boolean":
120
+ return z.boolean();
121
+ }
122
+ };
123
+ /**
124
+ * Turns a declaration into the check that an instance of it is held to.
125
+ *
126
+ * Keys the declaration does not carry are refused, in both directions. The two
127
+ * alternatives both lie: stripping them is truncation that nobody is told
128
+ * about, and passing them through hands the merchant's handler fields the card
129
+ * never declared — a card that does not describe what arrives, and a way to
130
+ * push input into someone else's code. In a delivery an undeclared field means
131
+ * the agent paid for something other than the result it read before paying.
132
+ *
133
+ * One thing this cannot refuse is a key named `__proto__`; see
134
+ * `PROTOTYPE_KEY_IS_DROPPED` above.
135
+ *
136
+ * The spec is expected to have been parsed by `ParamSpecSchema` already; that
137
+ * is what the parameter type says.
138
+ */
139
+ export const paramSpecToValidator = (spec, direction) => {
140
+ const shape = {};
141
+ for (const [name, field] of Object.entries(spec)) {
142
+ const check = checkFor(field.type, direction);
143
+ const required = field.required ?? direction === "delivery";
144
+ shape[name] = required ? check : check.optional();
145
+ }
146
+ return z.strictObject(shape);
147
+ };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The vocabulary every other contract in this package is built from: a sum of
3
+ * money, a currency, a moment in time, an identifier.
4
+ *
5
+ * Two rules run through the file and are worth stating once.
6
+ *
7
+ * Money never travels as a floating point number. An amount is a decimal
8
+ * string, because 0.1 + 0.2 is not 0.3 in binary floating point and a price is
9
+ * a promise about someone else's money. A merchant who writes "5.00" gets
10
+ * "5.00" back, trailing zero and all, in the order and in the receipt.
11
+ *
12
+ * These schemas bound formats, not business policy. How long a title may be,
13
+ * how far away a deadline may sit, how stale a price may get — those numbers
14
+ * are named before the pilot and belong to the gateway. A number invented here
15
+ * would read as a decision that nobody took. Where a bound belongs to the
16
+ * format itself, it is written down together with the reason.
17
+ */
18
+ import { z } from "zod";
19
+ /**
20
+ * A decimal number written as text: digits, optionally a dot and more digits.
21
+ *
22
+ * Exponent notation, thousands separators and a leading plus are out, because
23
+ * the same money must always be the same string — a receipt compares badly
24
+ * when "5.00" and "5.0e0" both mean five dollars. Leading zeros go for the
25
+ * same reason. Negative amounts are out because nothing in this contract sells
26
+ * for less than nothing; a refund is a direction, not a sign on a price.
27
+ *
28
+ * The bounds are the format's own. Eighteen fractional digits is the most an
29
+ * ERC-20 token carries, so any stablecoin amount fits exactly; eighteen digits
30
+ * before the dot is past any price a catalog will hold. Beyond that a string
31
+ * of digits is not a price and refusing it early is cheaper than carrying it.
32
+ */
33
+ export declare const AmountSchema: z.ZodString;
34
+ /**
35
+ * A currency code, upper case.
36
+ *
37
+ * Three to eight characters, starting with a letter, digits allowed after it.
38
+ * A card is priced in one currency and settled in another and this contract
39
+ * does not yet say which of the two it carries, so the shape has to admit
40
+ * both: a national code is three letters (`USD`), and a token ticker is longer
41
+ * and sometimes carries a digit (`USDC`, `USD1`). Eight admits every ticker
42
+ * this contract has had to carry; which currencies are accepted is the
43
+ * gateway's to decide.
44
+ *
45
+ * What this does not do is check membership in any list. We hold no currency
46
+ * table, and a schema that pretended to would be claiming knowledge the
47
+ * package does not have. Which currencies the gateway accepts is a gateway
48
+ * question, and it is not answered here.
49
+ */
50
+ export declare const CurrencyCodeSchema: z.ZodString;
51
+ /** A sum of money: how much, in what. */
52
+ export declare const MoneySchema: z.ZodObject<{
53
+ amount: z.ZodString;
54
+ currency: z.ZodString;
55
+ }, z.core.$strict>;
56
+ /**
57
+ * A moment in time, ISO 8601, with its zone.
58
+ *
59
+ * The zone is not decoration. What arrives here is parsed into an instant and
60
+ * then kept — `as_of` travels into the order the merchant's handler is given
61
+ * and into the record of the sale, which is what somebody reconciles a charge
62
+ * against months later — and a local time with no offset is not an instant. It
63
+ * is an hour of the day, and two machines reading it would land on two
64
+ * different moments. Both an explicit offset and `Z` are accepted; what a
65
+ * merchant's clock reports is their business, as long as it names a point in
66
+ * time.
67
+ */
68
+ export declare const TimestampSchema: z.ZodISODateTime;
69
+ export declare const IdentifierSchema: z.ZodString;
70
+ /**
71
+ * The price a purchase actually went through at.
72
+ *
73
+ * Four fields because the merchant's handler is meant to write the sale down
74
+ * without looking the card up: the sum and its currency, `at` — the moment this
75
+ * price was fixed for this sale, and `as_of` — the moment the price behind it
76
+ * was true. The two differ on purpose. A merchant whose answer lands at 10:15,
77
+ * out of a price list published at 10:00, sells at an `at` of 10:15 and an
78
+ * `as_of` of 10:00; a card with no price check is quoted off its own snapshot
79
+ * the instant the agent asks, so `at` is that instant and `as_of` is when the
80
+ * card's price was published.
81
+ *
82
+ * Neither of them is when the money moved, and a handler that read `at` as the
83
+ * charge would file the sale under the wrong minute. On a card with a price
84
+ * check the agent may take as long as it likes between the answer and the
85
+ * payment, and in the synchronous mode the charge executes after the goods. The
86
+ * moment of payment is on the receipt, as `paid_at`, and is not in this shape.
87
+ *
88
+ * A card sold from its own price and never checked live also carries `as_of`:
89
+ * the moment that card price was last published. That is a fact, not a guess,
90
+ * and it keeps the handler from having to treat a missing field as "fresh".
91
+ */
92
+ export declare const SalePriceSchema: z.ZodObject<{
93
+ amount: z.ZodString;
94
+ currency: z.ZodString;
95
+ at: z.ZodISODateTime;
96
+ as_of: z.ZodISODateTime;
97
+ }, z.core.$strict>;
98
+ export type Amount = z.infer<typeof AmountSchema>;
99
+ export type CurrencyCode = z.infer<typeof CurrencyCodeSchema>;
100
+ export type Money = z.infer<typeof MoneySchema>;
101
+ export type Timestamp = z.infer<typeof TimestampSchema>;
102
+ export type Identifier = z.infer<typeof IdentifierSchema>;
103
+ export type SalePrice = z.infer<typeof SalePriceSchema>;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The vocabulary every other contract in this package is built from: a sum of
3
+ * money, a currency, a moment in time, an identifier.
4
+ *
5
+ * Two rules run through the file and are worth stating once.
6
+ *
7
+ * Money never travels as a floating point number. An amount is a decimal
8
+ * string, because 0.1 + 0.2 is not 0.3 in binary floating point and a price is
9
+ * a promise about someone else's money. A merchant who writes "5.00" gets
10
+ * "5.00" back, trailing zero and all, in the order and in the receipt.
11
+ *
12
+ * These schemas bound formats, not business policy. How long a title may be,
13
+ * how far away a deadline may sit, how stale a price may get — those numbers
14
+ * are named before the pilot and belong to the gateway. A number invented here
15
+ * would read as a decision that nobody took. Where a bound belongs to the
16
+ * format itself, it is written down together with the reason.
17
+ */
18
+ import { z } from "zod";
19
+ /**
20
+ * A decimal number written as text: digits, optionally a dot and more digits.
21
+ *
22
+ * Exponent notation, thousands separators and a leading plus are out, because
23
+ * the same money must always be the same string — a receipt compares badly
24
+ * when "5.00" and "5.0e0" both mean five dollars. Leading zeros go for the
25
+ * same reason. Negative amounts are out because nothing in this contract sells
26
+ * for less than nothing; a refund is a direction, not a sign on a price.
27
+ *
28
+ * The bounds are the format's own. Eighteen fractional digits is the most an
29
+ * ERC-20 token carries, so any stablecoin amount fits exactly; eighteen digits
30
+ * before the dot is past any price a catalog will hold. Beyond that a string
31
+ * of digits is not a price and refusing it early is cheaper than carrying it.
32
+ */
33
+ export const AmountSchema = z
34
+ .string()
35
+ .regex(/^(?:0|[1-9]\d{0,17})(?:\.\d{1,18})?$/, 'an amount is an exact decimal number written as text, such as "5.00"');
36
+ /**
37
+ * A currency code, upper case.
38
+ *
39
+ * Three to eight characters, starting with a letter, digits allowed after it.
40
+ * A card is priced in one currency and settled in another and this contract
41
+ * does not yet say which of the two it carries, so the shape has to admit
42
+ * both: a national code is three letters (`USD`), and a token ticker is longer
43
+ * and sometimes carries a digit (`USDC`, `USD1`). Eight admits every ticker
44
+ * this contract has had to carry; which currencies are accepted is the
45
+ * gateway's to decide.
46
+ *
47
+ * What this does not do is check membership in any list. We hold no currency
48
+ * table, and a schema that pretended to would be claiming knowledge the
49
+ * package does not have. Which currencies the gateway accepts is a gateway
50
+ * question, and it is not answered here.
51
+ */
52
+ export const CurrencyCodeSchema = z
53
+ .string()
54
+ .regex(/^[A-Z][A-Z0-9]{2,7}$/, 'a currency code is written in upper case, such as "USD"');
55
+ /** A sum of money: how much, in what. */
56
+ export const MoneySchema = z.strictObject({
57
+ amount: AmountSchema,
58
+ currency: CurrencyCodeSchema,
59
+ });
60
+ /**
61
+ * A moment in time, ISO 8601, with its zone.
62
+ *
63
+ * The zone is not decoration. What arrives here is parsed into an instant and
64
+ * then kept — `as_of` travels into the order the merchant's handler is given
65
+ * and into the record of the sale, which is what somebody reconciles a charge
66
+ * against months later — and a local time with no offset is not an instant. It
67
+ * is an hour of the day, and two machines reading it would land on two
68
+ * different moments. Both an explicit offset and `Z` are accepted; what a
69
+ * merchant's clock reports is their business, as long as it names a point in
70
+ * time.
71
+ */
72
+ export const TimestampSchema = z.iso.datetime({ offset: true });
73
+ /**
74
+ * An identifier — ours or the merchant's.
75
+ *
76
+ * Three things are asked of it, and each one is a way an identifier stops
77
+ * identifying. It cannot be blank, because a blank string is what a missing
78
+ * value turns into on its way through a template and an order keyed by "" is
79
+ * an order nobody finds again. It cannot begin or end with whitespace: a
80
+ * merchant's key is what republishing a card matches on, and
81
+ * `"access-monthly "` would look identical to `"access-monthly"` in every log
82
+ * and every screen while quietly creating the second card the portal promises
83
+ * cannot appear. And it carries no control characters, which nothing means to
84
+ * type and which make a key unreadable wherever it is printed.
85
+ *
86
+ * The shape beyond that is deliberately left alone: the merchant's own key is
87
+ * theirs and our own key formats are ours to change. So is the length. A
88
+ * ceiling would be a policy number, and the numbers of this contract are named
89
+ * before the pilot rather than invented here; until then the bound on an
90
+ * identifier is whatever the transport carrying it allows.
91
+ */
92
+ const UNPRINTABLE = "\\u0000-\\u001f\\u007f\\u200b-\\u200f\\u2060-\\u2064\\ufeff";
93
+ export const IdentifierSchema = z.string().regex(
94
+ // One pattern, not three checks, because a rule written as a refinement is
95
+ // dropped from the JSON Schema export without a word — the defect this
96
+ // package has already paid for twice. As a pattern it crosses intact, and a
97
+ // generated client refuses the same keys we do.
98
+ //
99
+ // It reads: a first character that is neither whitespace nor unprintable,
100
+ // then anything printable, then a last character under the same rule as the
101
+ // first. One character on its own is allowed; nothing at all is not.
102
+ new RegExp(`^[^\\s${UNPRINTABLE}](?:[^${UNPRINTABLE}]*[^\\s${UNPRINTABLE}])?$`, "u"), "an identifier must not be empty, padded with whitespace, or carry characters that show nothing");
103
+ /**
104
+ * The price a purchase actually went through at.
105
+ *
106
+ * Four fields because the merchant's handler is meant to write the sale down
107
+ * without looking the card up: the sum and its currency, `at` — the moment this
108
+ * price was fixed for this sale, and `as_of` — the moment the price behind it
109
+ * was true. The two differ on purpose. A merchant whose answer lands at 10:15,
110
+ * out of a price list published at 10:00, sells at an `at` of 10:15 and an
111
+ * `as_of` of 10:00; a card with no price check is quoted off its own snapshot
112
+ * the instant the agent asks, so `at` is that instant and `as_of` is when the
113
+ * card's price was published.
114
+ *
115
+ * Neither of them is when the money moved, and a handler that read `at` as the
116
+ * charge would file the sale under the wrong minute. On a card with a price
117
+ * check the agent may take as long as it likes between the answer and the
118
+ * payment, and in the synchronous mode the charge executes after the goods. The
119
+ * moment of payment is on the receipt, as `paid_at`, and is not in this shape.
120
+ *
121
+ * A card sold from its own price and never checked live also carries `as_of`:
122
+ * the moment that card price was last published. That is a fact, not a guess,
123
+ * and it keeps the handler from having to treat a missing field as "fresh".
124
+ */
125
+ export const SalePriceSchema = z.strictObject({
126
+ amount: AmountSchema,
127
+ currency: CurrencyCodeSchema,
128
+ at: TimestampSchema,
129
+ as_of: TimestampSchema,
130
+ });