@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/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
|
+
});
|