@nuanu-ai/agentify-contracts 0.5.0 → 0.7.0

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/merchant.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * wallet their sales are paid into, and the keys they open the door with.
4
4
  *
5
5
  * The first two belong together because registering is the act that produces
6
- * both: one call makes the merchant and the key its cabinet will call as them
6
+ * both: one call makes the merchant and the key its dashboard will call as them
7
7
  * with, and what comes back carries that key once. Split across two files, a
8
8
  * reader working out what registering leaves a merchant holding would have to
9
9
  * read both to find that it is a key of a kind no list here carries. The name is here for the same reason read the other way round — it is a
@@ -25,7 +25,7 @@
25
25
  * nothing, while a flag answers only half.
26
26
  */
27
27
  import { z } from "zod";
28
- import { ServiceNameSchema } from "./card.js";
28
+ import { SellerSchema, SellerSiteSchema, ServiceNameSchema } from "./card.js";
29
29
  import { EvmAddressSchema } from "./evm-address.js";
30
30
  import { IdentifierSchema, TimestampSchema } from "./primitives.js";
31
31
  /**
@@ -162,8 +162,8 @@ export const MerchantKeySchema = z
162
162
  * broken.
163
163
  *
164
164
  * It is not always one of the keys beside it, and that is the thing a reader is
165
- * likeliest to assume and be wrong about. A cabinet calls with a key made for a
166
- * cabinet, and those are in nobody's list, so a client that looked this
165
+ * likeliest to assume and be wrong about. A dashboard calls with a key made for a
166
+ * dashboard, and those are in nobody's list, so a client that looked this
167
167
  * identifier up among the rows would find nothing — which is an answer rather
168
168
  * than an error, and a screen has to be built for it.
169
169
  *
@@ -176,7 +176,7 @@ export const MerchantKeyListSchema = z
176
176
  * The keys this merchant made for their own code, the revoked ones among
177
177
  * them.
178
178
  *
179
- * The keys a cabinet holds are not here and never will be: the merchant did
179
+ * The keys a dashboard holds are not here and never will be: the merchant did
180
180
  * not issue one and has nothing to do with one. A list is what somebody
181
181
  * acts on, and a row nobody has any business acting on is a row that only
182
182
  * raises the question of why it will not go away.
@@ -186,7 +186,7 @@ export const MerchantKeyListSchema = z
186
186
  this_call: IdentifierSchema,
187
187
  })
188
188
  .meta({
189
- description: "The keys one merchant made for their own code, working and revoked together, and the identifier of the key this very call was made with. That last field is here because a merchant cannot disable the key they are holding: without it a screen would offer a button the gateway refuses. It is not always among the keys listed — a cabinet calls with a key of its own, and those are in no list here — so a client matching it against the rows has to be built for finding none. The keys a cabinet holds are left out entirely: they are not issued by the merchant and cannot be revoked by them. This document does not say whether it is the whole list either — paging is not designed, and the absence of a field about it is not a promise that there is no more.",
189
+ description: "The keys one merchant made for their own code, working and revoked together, and the identifier of the key this very call was made with. That last field is here because a merchant cannot disable the key they are holding: without it a screen would offer a button the gateway refuses. It is not always among the keys listed — a dashboard calls with a key of its own, and those are in no list here — so a client matching it against the rows has to be built for finding none. The keys a dashboard holds are left out entirely: they are not issued by the merchant and cannot be revoked by them. This document does not say whether it is the whole list either — paging is not designed, and the absence of a field about it is not a promise that there is no more.",
190
190
  });
191
191
  /** What a merchant sends to have a key made. */
192
192
  export const IssueKeyRequestSchema = z
@@ -211,7 +211,7 @@ export const IssuedKeySchema = z
211
211
  secret: KeySecretSchema,
212
212
  })
213
213
  .meta({
214
- description: "A key as it comes back from being issued: the row a merchant will see in their list from now on, and the key itself. It carries the key once. Three answers in this contract carry one — this, what registering gives back, and the key a cabinet asks for — and nothing else does, because what is written down on our side is a digest. A key that is lost is replaced by a new one rather than read back.",
214
+ description: "A key as it comes back from being issued: the row a merchant will see in their list from now on, and the key itself. It carries the key once. Three answers in this contract carry one — this, what registering gives back, and the key a dashboard asks for — and nothing else does, because what is written down on our side is a digest. A key that is lost is replaced by a new one rather than read back.",
215
215
  });
216
216
  /**
217
217
  * A key that has been revoked, as it now stands.
@@ -228,10 +228,10 @@ export const DisabledKeySchema = z
228
228
  description: "The key that was just revoked, with the instant it stopped working on it, so a merchant reads back what happened rather than taking the call's word for it. Revoking a key that was already revoked answers this same way and keeps the first instant, because that is the true one and a retry after a dropped connection must not rewrite it.",
229
229
  });
230
230
  /**
231
- * A key made for a cabinet, which is the secret and nothing else.
231
+ * A key made for a dashboard, which is the secret and nothing else.
232
232
  *
233
233
  * Every other answer that makes a key carries the row beside it, and this one
234
- * cannot. A key made for a cabinet is in no merchant's list — they did not
234
+ * cannot. A key made for a dashboard is in no merchant's list — they did not
235
235
  * issue it and have no reason to know it exists — so an identifier here would
236
236
  * name a row that no screen of theirs draws and no call of theirs reaches: not
237
237
  * the list it is absent from, and not the revoking, which takes the keys a
@@ -239,13 +239,13 @@ export const DisabledKeySchema = z
239
239
  * is put it on the row of whoever just signed in, and that is the whole of what
240
240
  * it needs.
241
241
  */
242
- export const CabinetKeySchema = z
242
+ export const DashboardKeySchema = z
243
243
  .strictObject({
244
244
  /** The only moment this is readable. Nothing on our side keeps it. */
245
245
  secret: KeySecretSchema,
246
246
  })
247
247
  .meta({
248
- description: "A key made for a cabinet to call as one merchant, carried once and readable nowhere afterwards. There is no row beside it and there is nothing to put one: a key made this way is in no merchant's list of keys, and the call that revokes a key refuses this kind by name — so an identifier for it would name something no answer shows and no call acts on. Whoever asked for this holds it until they ask for another.",
248
+ description: "A key made for a dashboard to call as one merchant, carried once and readable nowhere afterwards. There is no row beside it and there is nothing to put one: a key made this way is in no merchant's list of keys, and the call that revokes a key refuses this kind by name — so an identifier for it would name something no answer shows and no call acts on. Whoever asked for this holds it until they ask for another.",
249
249
  });
250
250
  /**
251
251
  * That the key a call was made with is gone.
@@ -257,7 +257,7 @@ export const CabinetKeySchema = z
257
257
  * is done, and it is said in a field rather than left to a status code so that
258
258
  * a client reads one document and not two kinds of evidence.
259
259
  */
260
- export const ForgottenCabinetKeySchema = z
260
+ export const ForgottenDashboardKeySchema = z
261
261
  .strictObject({
262
262
  /** The key this call was made with no longer exists. */
263
263
  forgotten: z.literal(true),
@@ -284,11 +284,20 @@ export const ForgottenCabinetKeySchema = z
284
284
  */
285
285
  export const SellerNameSchema = z
286
286
  .strictObject({
287
- /** What buyers read beside this merchant's products, or nothing. */
288
- seller_name: ServiceNameSchema.nullable(),
287
+ /**
288
+ * What buyers read beside this merchant's products, or nothing — read back
289
+ * by the rule an agent's `seller` reads it by, which leaves the plain-text
290
+ * rule to the door that writes it.
291
+ */
292
+ seller_name: SellerSchema.shape.name,
293
+ /**
294
+ * The https address of the merchant's own shop, where an agent takes what
295
+ * an order cannot answer (ADR-0034), or nothing where none was given.
296
+ */
297
+ seller_site: SellerSiteSchema.nullable(),
289
298
  })
290
299
  .meta({
291
- description: "The name a merchant's products are sold under: what a discovery catalog lists them under and what a buyer's agent is shown beside the price. Null means nobody has chosen one, which is where every merchant starts. The field is always present rather than left out when there is no name: an absent field would be indistinguishable from a client that dropped it. What a name may be is the catalog's rule rather than ours — at most 32 characters of printable ASCII — because a name outside it is dropped there in silence, so it is refused here where somebody is told. A merchant with no name cannot publish a card: a card published without one reaches a buyer's agent inside a payment request that names no seller at all.",
300
+ description: "The name a merchant's products are sold under: what a discovery catalog lists them under and what a buyer's agent is shown beside the price. Null means nobody has chosen one, which is where every merchant starts. The field is always present rather than left out when there is no name: an absent field would be indistinguishable from a client that dropped it. What a name may be is the catalog's rule rather than ours — at most 32 characters of printable ASCII — because a name outside it is dropped there in silence, so it is refused here where somebody is told. A merchant with no name cannot publish a card: a card published without one reaches a buyer's agent inside a payment request that names no seller at all. seller_site is the https address of the merchant's own shop, which every agent reads beside the name on their cards and orders as where to take what an order cannot answer; null means none was given.",
292
301
  });
293
302
  /**
294
303
  * What a merchant sends to change what their products are sold under.
@@ -303,7 +312,7 @@ export const SellerNameSchema = z
303
312
  * which is this same call, or an end to selling, which is the pause — and the
304
313
  * pause leaves their cards where they can find them again.
305
314
  *
306
- * So it is two documents rather than one, and a cabinet still reads back the
315
+ * So it is two documents rather than one, and a dashboard still reads back the
307
316
  * shape it sent. The message on a null is written here rather than left to a
308
317
  * type error, because "expected string, received null" describes the shape and
309
318
  * says nothing about which act the sender was reaching for.
@@ -311,7 +320,8 @@ export const SellerNameSchema = z
311
320
  export const SellerNameRequestSchema = z
312
321
  .strictObject({
313
322
  /**
314
- * What buyers are to read beside this merchant's products.
323
+ * What buyers are to read beside this merchant's products, where it is
324
+ * changing.
315
325
  *
316
326
  * The rule lives once, in `ServiceNameSchema`, and this reaches it through
317
327
  * a string that carries its own words for "this is not a name at all". A
@@ -320,18 +330,36 @@ export const SellerNameRequestSchema = z
320
330
  */
321
331
  seller_name: z
322
332
  .string({
323
- // A field that is missing is a client with a bug and a field holding
324
- // null is a client with a misunderstanding. Only the second gets this
325
- // sentence; the first falls through to the ordinary words about a
326
- // field that is not there, which is what its author needs to read.
333
+ // A field holding null is a client with a misunderstanding, and only
334
+ // that gets this sentence.
327
335
  error: (issue) => issue.input === undefined
328
336
  ? undefined
329
337
  : "a seller name cannot be taken away, only changed: a merchant who wants to stop being listed pauses their selling, which leaves their cards where they can put them back on sale",
330
338
  })
331
- .pipe(ServiceNameSchema),
339
+ .pipe(ServiceNameSchema)
340
+ .optional(),
341
+ /**
342
+ * The https address of the merchant's own shop, where it is changing
343
+ * (ADR-0034). Like the name it is changed and never taken away: an agent
344
+ * holding an order that named a site has been told where to go, and a
345
+ * site that vanished from the same order would leave it nowhere.
346
+ */
347
+ seller_site: z
348
+ .string({
349
+ error: (issue) => issue.input === undefined
350
+ ? undefined
351
+ : "a seller's site cannot be taken away, only changed: send the address it has moved to",
352
+ })
353
+ .pipe(SellerSiteSchema)
354
+ .optional(),
355
+ })
356
+ .refine((asked) => asked.seller_name !== undefined || asked.seller_site !== undefined, {
357
+ // A client that dropped both fields has a bug, and is told what this call
358
+ // takes rather than anything about taking a name away.
359
+ message: "a request names seller_name, seller_site or both: one that names neither changes nothing",
332
360
  })
333
361
  .meta({
334
- description: "What a merchant sends to change the name their products are sold under. The same rule as the answer — at most 32 characters of printable ASCII, the catalog's rule rather than ours — and one difference: null is refused. A merchant goes from no name to a name and from one name to another, never back to none, because a payment request names the seller and there would be nobody to name: every card they have published would come off sale, which is an end to their selling arriving under the name of editing a setting. Somebody reaching for null wants one of two other things: a different name, which is this call with a different value, or an end to selling, which is the pause.",
362
+ description: "What a merchant sends to change the name their products are sold under, the address of their shop's own site, or both; a field left out stays as it was, and one of the two has to be there. The name is held to the catalog's rule rather than ours — at most 32 characters of printable ASCII — and is plain text. The site is an https origin and nothing after it, such as https://shop.example. Null is refused for either. A merchant goes from no name to a name and from one name to another, never back to none, because a payment request names the seller and there would be nobody to name: every card they have published would come off sale, which is an end to their selling arriving under the name of editing a setting. Somebody reaching for null wants one of two other things: a different name, which is this call with a different value, or an end to selling, which is the pause. A site is changed the same way and never taken away.",
335
363
  });
336
364
  /**
337
365
  * A change of the wallet that has been asked for, announced, and has not taken
@@ -339,7 +367,7 @@ export const SellerNameRequestSchema = z
339
367
  *
340
368
  * It exists because a replacement does not apply at once where the money is
341
369
  * real (ADR-0019). The address a merchant is paid at is the one setting whose
342
- * change redirects money, and any key of theirs reaches it — the cabinet's, or
370
+ * change redirects money, and any key of theirs reaches it — the dashboard's, or
343
371
  * one sitting in their own server's environment — so on the live deployment a
344
372
  * replacement is told to every account of the merchant first and takes effect
345
373
  * forty-eight hours after that. What this document says is the two facts a
@@ -475,10 +503,10 @@ export const RegistrationRequestSchema = z
475
503
  description: "What somebody sends to become a merchant: the invitation code they were given, and nothing else. The name their products are sold under is deliberately not here — it is a public answer, and asked for on the way in it is answered by somebody with no products and no idea what the name is for; it is set afterwards, and changed afterwards, through the merchant's own call for it. Nothing about an account is here either: an address and a password belong to whatever signs the person in, and are never sent to the gateway. A document carrying either is refused rather than trimmed, because a field accepted and dropped is somebody believing they said something.",
476
504
  });
477
505
  /**
478
- * What registering answers with: a merchant and the key their cabinet will call
506
+ * What registering answers with: a merchant and the key their dashboard will call
479
507
  * as them with.
480
508
  *
481
- * The key is made for a cabinet rather than for the merchant's own code, and
509
+ * The key is made for a dashboard rather than for the merchant's own code, and
482
510
  * that is what the caller of this route is. So it is in no list: a merchant who
483
511
  * has just registered has no keys of their own at all, and the first one they
484
512
  * do have is one they ask for. No row travels beside the secret for the same
@@ -497,5 +525,5 @@ export const RegisteredMerchantSchema = z
497
525
  secret: KeySecretSchema,
498
526
  })
499
527
  .meta({
500
- description: "What registering produced: the merchant, and the key whoever registered them will call as them with. The key is readable here and nowhere afterwards, so whoever made this call is the only party that can keep it. It is a key made for a cabinet rather than one of the merchant's own: it appears in no list of their keys and the call that revokes a key refuses its kind by name, so no row for it comes back here either. A merchant who has just registered has no keys of their own until they ask for one. The merchant is listed under no name yet and this answer carries none — the name their products are sold under is chosen afterwards, and until it is, publishing a card is refused. What this answer does not carry either is any notion of an account or a session: registering makes a merchant and a key, and whatever signs a person in is on the other side of this call.",
528
+ description: "What registering produced: the merchant, and the key whoever registered them will call as them with. The key is readable here and nowhere afterwards, so whoever made this call is the only party that can keep it. It is a key made for a dashboard rather than one of the merchant's own: it appears in no list of their keys and the call that revokes a key refuses its kind by name, so no row for it comes back here either. A merchant who has just registered has no keys of their own until they ask for one. The merchant is listed under no name yet and this answer carries none — the name their products are sold under is chosen afterwards, and until it is, publishing a card is refused. What this answer does not carry either is any notion of an account or a session: registering makes a merchant and a key, and whatever signs a person in is on the other side of this call.",
501
529
  });
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Plain text: what the words on a card are written in.
3
+ *
4
+ * A card's title, its description and the title of each field it declares are
5
+ * read by a buying program exactly as they were written. Nothing between the
6
+ * merchant and the agent renders HTML, so markup reaches the agent as markup —
7
+ * `<p>Valid for twelve months.</p>` is read with its angle brackets, and
8
+ * `Coffee &#038; Brunch` with its number — and a discovery catalog that does
9
+ * render HTML would swallow some of it and show the rest. Either way the words
10
+ * an agent acts on are not the words the merchant meant.
11
+ *
12
+ * The door refuses such text rather than cleaning it, and that choice is the
13
+ * whole of this file. Cleaning would make an agent read text the merchant never
14
+ * wrote: stripping a tag can join two sentences, decoding a reference is a
15
+ * guess about which table it came from, and dropping an invisible character
16
+ * changes a string somebody may be matching on. And the merchant would never
17
+ * learn that their text was being rewritten, so the source stays broken for
18
+ * every other place it goes. A refusal that names what it found and where is
19
+ * the version of this a merchant can act on. A shop connector whose source is
20
+ * HTML turns it into text on its own side, before the door, and asks this same
21
+ * rule whether it succeeded.
22
+ *
23
+ * What is refused is what reads as markup or as a reference, and nothing that
24
+ * merely shares a character with them. An ampersand between words, a
25
+ * comparison and an arrow are text: `Tea & coffee`, `5 < 10`, `a -> b`, `AT&T`
26
+ * and `R&D;` all pass, and so does a space after a bracket, `List< String >`,
27
+ * which an HTML parser reads as text too.
28
+ *
29
+ * Three more shapes pass, and for them the rule is a choice rather than a fact
30
+ * about HTML: an address in angle brackets, `<jane@example.com>` or
31
+ * `<https://example.com>`, a bracket that is never closed, `x<y`, and a
32
+ * bracket before a name HTML would not give an element, `Map<String, Integer>`.
33
+ * A browser would swallow each of them as a tag, or drop it. They pass because
34
+ * each is how plain text has always been written, the rule is about text
35
+ * written as HTML rather than about everything a renderer might misread, and
36
+ * no reader between the merchant and an agent renders HTML.
37
+ *
38
+ * The rule is the publish door's and not the reader's. A card stored before it
39
+ * may carry anything it refuses, and every answer that carries a stored card is
40
+ * held to its contract on the way out — so a rule applied on reading would turn
41
+ * one old row into a failed catalog for every agent and a merchant who cannot
42
+ * see the card they are meant to fix.
43
+ */
44
+ /**
45
+ * Whether a piece of text is one line, as a title is, or may run to several, as
46
+ * a description may.
47
+ */
48
+ export type TextLines = "one line" | "several lines";
49
+ /**
50
+ * What in this text is not plain text, one phrase for each kind of thing found
51
+ * — markup, character references, control characters — and nothing when all of
52
+ * it is.
53
+ *
54
+ * Each phrase says what was found, how often, and where the first of it is, as
55
+ * in `HTML markup in 4 places, the first "<p>" at character 1`, and quotes
56
+ * nothing it would have to print as a control character. It is the one
57
+ * definition of plain text: the publish door refuses a card with it, and a
58
+ * connector that turns a shop's HTML into text asks it whether it succeeded.
59
+ */
60
+ export declare const notPlainTextIn: (text: string, lines: TextLines) => readonly string[];
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Plain text: what the words on a card are written in.
3
+ *
4
+ * A card's title, its description and the title of each field it declares are
5
+ * read by a buying program exactly as they were written. Nothing between the
6
+ * merchant and the agent renders HTML, so markup reaches the agent as markup —
7
+ * `<p>Valid for twelve months.</p>` is read with its angle brackets, and
8
+ * `Coffee &#038; Brunch` with its number — and a discovery catalog that does
9
+ * render HTML would swallow some of it and show the rest. Either way the words
10
+ * an agent acts on are not the words the merchant meant.
11
+ *
12
+ * The door refuses such text rather than cleaning it, and that choice is the
13
+ * whole of this file. Cleaning would make an agent read text the merchant never
14
+ * wrote: stripping a tag can join two sentences, decoding a reference is a
15
+ * guess about which table it came from, and dropping an invisible character
16
+ * changes a string somebody may be matching on. And the merchant would never
17
+ * learn that their text was being rewritten, so the source stays broken for
18
+ * every other place it goes. A refusal that names what it found and where is
19
+ * the version of this a merchant can act on. A shop connector whose source is
20
+ * HTML turns it into text on its own side, before the door, and asks this same
21
+ * rule whether it succeeded.
22
+ *
23
+ * What is refused is what reads as markup or as a reference, and nothing that
24
+ * merely shares a character with them. An ampersand between words, a
25
+ * comparison and an arrow are text: `Tea & coffee`, `5 < 10`, `a -> b`, `AT&T`
26
+ * and `R&D;` all pass, and so does a space after a bracket, `List< String >`,
27
+ * which an HTML parser reads as text too.
28
+ *
29
+ * Three more shapes pass, and for them the rule is a choice rather than a fact
30
+ * about HTML: an address in angle brackets, `<jane@example.com>` or
31
+ * `<https://example.com>`, a bracket that is never closed, `x<y`, and a
32
+ * bracket before a name HTML would not give an element, `Map<String, Integer>`.
33
+ * A browser would swallow each of them as a tag, or drop it. They pass because
34
+ * each is how plain text has always been written, the rule is about text
35
+ * written as HTML rather than about everything a renderer might misread, and
36
+ * no reader between the merchant and an agent renders HTML.
37
+ *
38
+ * The rule is the publish door's and not the reader's. A card stored before it
39
+ * may carry anything it refuses, and every answer that carries a stored card is
40
+ * held to its contract on the way out — so a rule applied on reading would turn
41
+ * one old row into a failed catalog for every agent and a merchant who cannot
42
+ * see the card they are meant to fix.
43
+ */
44
+ /**
45
+ * Markup: an HTML comment's opening, or a tag.
46
+ *
47
+ * A comment is refused on its opening alone, because nobody writing prose
48
+ * writes `<!--`, and matching on to the close would cost a scan to the end of
49
+ * the text for every opening in it.
50
+ *
51
+ * A tag is an angle bracket, an optional slash, a name, and then the bracket's
52
+ * close — at once, after a slash, or after whitespace and whatever attributes
53
+ * follow. The name begins with a letter and carries letters and digits, joined
54
+ * by a colon or a hyphen where there is one, which is how `<o:p>`, the tag a
55
+ * word processor leaves in pasted text, and a custom element are written. An
56
+ * attribute value in quotation marks may carry either bracket, as the alt text
57
+ * a block editor writes into an image does: `<img alt="Mug <3 coffee">` is one
58
+ * tag. A quotation mark that opens no value — the apostrophe in
59
+ * `<img alt=Mom's>`, or `<your friend's name>` — does not hide its tag either:
60
+ * where the tag cannot be read with its quotation marks paired, it is read to
61
+ * the first closing bracket, as it was before quoted values were known. What
62
+ * the name does not take is the rest of what can follow a bracket —
63
+ * a digit, a space, an `@`, a `:/` — which is why `<3`, `List< String >` and
64
+ * `<https://example.com>` are not tags here. The close has to be there, so
65
+ * `x<y` is not one either.
66
+ *
67
+ * The scan is linear in the length of the text however it is written: outside
68
+ * quotation marks everything stops at the next angle bracket, a quoted value
69
+ * stops at its own closing mark, and a quotation mark can be read only one
70
+ * way at any point, so no stretch of text is read over and over from one
71
+ * bracket. The tests hold it at a quarter of a megabyte, which is more than a
72
+ * publish body may carry, and on the short inputs that turn exponential the
73
+ * moment a quotation mark may be read two ways.
74
+ */
75
+ const MARKUP = /<!--|<\/?[A-Za-z][A-Za-z0-9]*(?:[:-][A-Za-z0-9]+)*(?:(?:[\t\n\f\r /](?:[^<>"']|"[^"]*"|'[^']*')*)?>|[\t\n\f\r /][^<>]*>)/g;
76
+ /**
77
+ * A character reference: an ampersand, a number or a name, and a semicolon.
78
+ *
79
+ * The semicolon, and a name of at least two characters, are what separate a
80
+ * reference from an ampersand in prose. HTML has no name of one letter, so
81
+ * `AT&T`, `A&B` and `R&D;` are text. A name of two or more is refused whether
82
+ * or not HTML's own list carries it: `&eacute;` and `&foo;` are both text
83
+ * written for an HTML reader, and a merchant who meant a character writes the
84
+ * character.
85
+ */
86
+ const REFERENCE = /&(?:#[0-9]+|#[xX][0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]+);/g;
87
+ /**
88
+ * Control characters: C0, DEL and C1. A description may carry a line feed,
89
+ * U+000A, and nothing else from this range.
90
+ *
91
+ * A line feed is the one line break every reader agrees on, and a description
92
+ * is prose of up to five hundred characters that may run to paragraphs. A
93
+ * carriage return is refused there with the rest, because `\r\n` is a second
94
+ * spelling of the same break and a carriage return on its own sends a terminal
95
+ * back to the start of the line it is printing. A title and a field's title are
96
+ * one line, so they carry no line break at all. The rest of the range shows
97
+ * nothing, and a character that shows nothing makes two strings that look
98
+ * identical and are not.
99
+ */
100
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are what it refuses
101
+ const CONTROL_IN_ONE_LINE = /[\u0000-\u001F\u007F-\u009F]/g;
102
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are what it refuses
103
+ const CONTROL_IN_LINES = /[\u0000-\u0009\u000B-\u001F\u007F-\u009F]/g;
104
+ /**
105
+ * The names a merchant knows the commonest control characters by, in text of
106
+ * each shape.
107
+ *
108
+ * A carriage return in a description is nearly always half of the `\r\n` a
109
+ * form on Windows submits between two lines, so there it is named together
110
+ * with the one line break a description does take.
111
+ */
112
+ const CONTROL_NAMES = {
113
+ "one line": { "\t": "a tab", "\n": "a line break", "\r": "a carriage return" },
114
+ "several lines": {
115
+ "\t": "a tab",
116
+ "\r": "a carriage return; a description breaks its lines with a line feed, U+000A, alone",
117
+ },
118
+ };
119
+ /**
120
+ * The longest piece of the text quoted back in a finding.
121
+ *
122
+ * A tag can carry an address of any length, and a finding is a line a person
123
+ * reads; forty characters is enough to recognise the tag by. Where one is cut,
124
+ * the finding says so.
125
+ */
126
+ const QUOTED_AT_MOST = 40;
127
+ /** A character by its code point, as `U+0007`. */
128
+ const codeOf = (character) => `U+${(character.codePointAt(0) ?? 0).toString(16).toUpperCase().padStart(4, "0")}`;
129
+ /**
130
+ * DEL and C1, which a quotation leaves as they are.
131
+ *
132
+ * `JSON.stringify` escapes C0 and a lone surrogate and nothing else, so a
133
+ * control character inside a quoted tag would otherwise reach the merchant's
134
+ * terminal as itself — and U+009B is the start of an escape sequence there.
135
+ */
136
+ const UNESCAPED_BY_JSON = /[\u007F-\u009F]/g;
137
+ /**
138
+ * A piece of the text as a finding quotes it: in quotation marks, with every
139
+ * control character escaped, and cut where it is long.
140
+ *
141
+ * The cut is made between two characters and never inside one: a character
142
+ * outside the basic plane is two units long, and a cut through the middle of
143
+ * it would quote half a character as an escape nobody wrote.
144
+ */
145
+ const quoted = (fragment) => {
146
+ const whole = fragment.length <= QUOTED_AT_MOST;
147
+ const head = whole ? fragment : fragment.slice(0, QUOTED_AT_MOST).replace(/[\uD800-\uDBFF]$/, "");
148
+ const shown = JSON.stringify(head).replace(UNESCAPED_BY_JSON, (character) => `\\u${(character.codePointAt(0) ?? 0).toString(16).padStart(4, "0")}`);
149
+ return whole ? shown : `${shown}… (cut short)`;
150
+ };
151
+ /** A control character as a finding names it: by its code, never printed. */
152
+ const namedIn = (lines) => (character) => {
153
+ const name = CONTROL_NAMES[lines][character];
154
+ return name === undefined ? codeOf(character) : `${codeOf(character)} (${name})`;
155
+ };
156
+ /**
157
+ * One kind of thing that is not plain text, said once for all the places it
158
+ * occurs: how many there are, and the first of them, where it is.
159
+ *
160
+ * The position counts from one, in the same units a description's length is
161
+ * counted in: UTF-16 code units, so a character outside the basic plane — an
162
+ * emoji — counts as two, and the number can run ahead of what an editor shows
163
+ * by one for each such character before it. It is the same count in both
164
+ * places, which is what lets a merchant set one number against the other.
165
+ */
166
+ const found = (text, pattern, one, many, shown) => {
167
+ const matches = [...text.matchAll(pattern)];
168
+ const first = matches[0];
169
+ if (first === undefined)
170
+ return null;
171
+ const where = `${shown(first[0])} at character ${first.index + 1}`;
172
+ return matches.length === 1
173
+ ? `${one}, ${where}`
174
+ : `${many} in ${matches.length} places, the first ${where}`;
175
+ };
176
+ /**
177
+ * What in this text is not plain text, one phrase for each kind of thing found
178
+ * — markup, character references, control characters — and nothing when all of
179
+ * it is.
180
+ *
181
+ * Each phrase says what was found, how often, and where the first of it is, as
182
+ * in `HTML markup in 4 places, the first "<p>" at character 1`, and quotes
183
+ * nothing it would have to print as a control character. It is the one
184
+ * definition of plain text: the publish door refuses a card with it, and a
185
+ * connector that turns a shop's HTML into text asks it whether it succeeded.
186
+ */
187
+ export const notPlainTextIn = (text, lines) => [
188
+ found(text, MARKUP, "HTML markup", "HTML markup", quoted),
189
+ found(text, REFERENCE, "an HTML character reference", "HTML character references", quoted),
190
+ found(text, lines === "one line" ? CONTROL_IN_ONE_LINE : CONTROL_IN_LINES, "a control character", "control characters", namedIn(lines)),
191
+ ].filter((phrase) => phrase !== null);
@@ -0,0 +1,73 @@
1
+ /**
2
+ * What a price a merchant sets has to be before Agentify will sell at it.
3
+ *
4
+ * A price comes in by two doors: on a card, when it is published, and in the
5
+ * answer to a price question, when a purchase is priced live. The gateway holds
6
+ * both to the rule below, and the merchant SDK's own check of a card holds it
7
+ * to the same rule, so a card the check passes is not refused at publication
8
+ * for its price. Three parts, each standing for a payment that cannot be
9
+ * taken.
10
+ *
11
+ * A price of zero is refused, by design. A payment request for nothing asks
12
+ * for something that cannot be done, and a gateway that took free items would
13
+ * be hosting content rather than selling it. A merchant gives a free item away
14
+ * from their own site, with no payment request in front of it (ADR-0002 §2).
15
+ *
16
+ * A currency other than the dollar is refused, because there is no exchange
17
+ * rate anywhere in this system to charge it at.
18
+ *
19
+ * An amount is written in dollars with at least two digits after the dot, so
20
+ * that a merchant who counts in cents and writes "500" for five dollars is
21
+ * refused rather than listed at five hundred. Fractions of a cent stay allowed,
22
+ * because a price per call is often below one, down to the finest amount the
23
+ * token a buyer pays in can carry and no further: past that there is no exact
24
+ * amount to charge, and a rounded charge is a different charge.
25
+ *
26
+ * So a price the door took is a price the payment edge can charge. The edge
27
+ * reads the currencies from here, and the places its token carries on every
28
+ * chain it can charge on are held equal to `PAYABLE_DECIMALS` by a test beside
29
+ * it, so the door and the edge cannot become two answers.
30
+ *
31
+ * It is a rule applied at the doors rather than part of any schema, and that is
32
+ * the point of it being a function. The schemas also read back every document
33
+ * already written — every answer that carries a card is held to the contract
34
+ * on its way out — so a rule written into the card's schema would turn the
35
+ * whole list of a merchant who published such a price before the rule existed
36
+ * into a failure, for cards they could otherwise see, pause and replace.
37
+ */
38
+ import type { Money } from "./primitives.js";
39
+ import type { Problem } from "./results.js";
40
+ /**
41
+ * The currencies a price may be written in, and the one conversion Agentify
42
+ * makes.
43
+ *
44
+ * A card priced in dollars is charged in the network's own dollar-denominated
45
+ * asset, one for one. That is a decision and not the absence of one, so it is
46
+ * written here rather than left to be inferred from the fact that it works: a
47
+ * merchant who writes "USD" is charging their buyer USDC on the configured
48
+ * chain, and the two are held to be the same number of dollars.
49
+ *
50
+ * Everything else is refused. Nobody has decided where an exchange rate would
51
+ * come from, and a charge based on an invented one would be the clearest
52
+ * possible claim beyond the evidence.
53
+ */
54
+ export declare const PAYABLE_CURRENCIES: readonly string[];
55
+ /**
56
+ * How many places after the dot a price may carry: the places of USDC, which is
57
+ * what a buyer pays in. Its smallest unit is a millionth of a dollar, so
58
+ * "0.000001" is the finest price there is an exact charge for.
59
+ */
60
+ export declare const PAYABLE_DECIMALS = 6;
61
+ /**
62
+ * What stands between this price and a sale, as findings on the fields of the
63
+ * document that carried it — empty where nothing does.
64
+ *
65
+ * The paths name `price` because a card and a price answer both carry the
66
+ * price under that name, so one finding reads right in either. An amount gets
67
+ * at most one finding: zero is said before the places, because "0" written as
68
+ * "0.00" would only be refused again.
69
+ *
70
+ * The amount has already passed `AmountSchema`, so it is digits with at most
71
+ * one dot, and the places are what follows the dot.
72
+ */
73
+ export declare function priceProblemsOf(price: Money): Problem[];
@@ -0,0 +1,104 @@
1
+ /**
2
+ * What a price a merchant sets has to be before Agentify will sell at it.
3
+ *
4
+ * A price comes in by two doors: on a card, when it is published, and in the
5
+ * answer to a price question, when a purchase is priced live. The gateway holds
6
+ * both to the rule below, and the merchant SDK's own check of a card holds it
7
+ * to the same rule, so a card the check passes is not refused at publication
8
+ * for its price. Three parts, each standing for a payment that cannot be
9
+ * taken.
10
+ *
11
+ * A price of zero is refused, by design. A payment request for nothing asks
12
+ * for something that cannot be done, and a gateway that took free items would
13
+ * be hosting content rather than selling it. A merchant gives a free item away
14
+ * from their own site, with no payment request in front of it (ADR-0002 §2).
15
+ *
16
+ * A currency other than the dollar is refused, because there is no exchange
17
+ * rate anywhere in this system to charge it at.
18
+ *
19
+ * An amount is written in dollars with at least two digits after the dot, so
20
+ * that a merchant who counts in cents and writes "500" for five dollars is
21
+ * refused rather than listed at five hundred. Fractions of a cent stay allowed,
22
+ * because a price per call is often below one, down to the finest amount the
23
+ * token a buyer pays in can carry and no further: past that there is no exact
24
+ * amount to charge, and a rounded charge is a different charge.
25
+ *
26
+ * So a price the door took is a price the payment edge can charge. The edge
27
+ * reads the currencies from here, and the places its token carries on every
28
+ * chain it can charge on are held equal to `PAYABLE_DECIMALS` by a test beside
29
+ * it, so the door and the edge cannot become two answers.
30
+ *
31
+ * It is a rule applied at the doors rather than part of any schema, and that is
32
+ * the point of it being a function. The schemas also read back every document
33
+ * already written — every answer that carries a card is held to the contract
34
+ * on its way out — so a rule written into the card's schema would turn the
35
+ * whole list of a merchant who published such a price before the rule existed
36
+ * into a failure, for cards they could otherwise see, pause and replace.
37
+ */
38
+ /**
39
+ * The currencies a price may be written in, and the one conversion Agentify
40
+ * makes.
41
+ *
42
+ * A card priced in dollars is charged in the network's own dollar-denominated
43
+ * asset, one for one. That is a decision and not the absence of one, so it is
44
+ * written here rather than left to be inferred from the fact that it works: a
45
+ * merchant who writes "USD" is charging their buyer USDC on the configured
46
+ * chain, and the two are held to be the same number of dollars.
47
+ *
48
+ * Everything else is refused. Nobody has decided where an exchange rate would
49
+ * come from, and a charge based on an invented one would be the clearest
50
+ * possible claim beyond the evidence.
51
+ */
52
+ export const PAYABLE_CURRENCIES = Object.freeze(["USD", "USDC"]);
53
+ /**
54
+ * How many places after the dot a price may carry: the places of USDC, which is
55
+ * what a buyer pays in. Its smallest unit is a millionth of a dollar, so
56
+ * "0.000001" is the finest price there is an exact charge for.
57
+ */
58
+ export const PAYABLE_DECIMALS = 6;
59
+ /**
60
+ * What stands between this price and a sale, as findings on the fields of the
61
+ * document that carried it — empty where nothing does.
62
+ *
63
+ * The paths name `price` because a card and a price answer both carry the
64
+ * price under that name, so one finding reads right in either. An amount gets
65
+ * at most one finding: zero is said before the places, because "0" written as
66
+ * "0.00" would only be refused again.
67
+ *
68
+ * The amount has already passed `AmountSchema`, so it is digits with at most
69
+ * one dot, and the places are what follows the dot.
70
+ */
71
+ export function priceProblemsOf(price) {
72
+ const problems = [];
73
+ const written = JSON.stringify(price.amount);
74
+ const places = price.amount.split(".")[1]?.length ?? 0;
75
+ if (!/[1-9]/.test(price.amount)) {
76
+ problems.push({
77
+ path: ["price", "amount"],
78
+ code: "custom",
79
+ message: `the price is ${written}, and nothing is sold through Agentify at a price of zero: a free item is offered from your own site, without a payment`,
80
+ });
81
+ }
82
+ else if (places < 2) {
83
+ problems.push({
84
+ path: ["price", "amount"],
85
+ code: "custom",
86
+ message: `the price is ${written}, and a price is written in dollars with at least two digits after the dot — five dollars is "5.00" — so that an amount counted in cents is never charged as dollars`,
87
+ });
88
+ }
89
+ else if (places > PAYABLE_DECIMALS) {
90
+ problems.push({
91
+ path: ["price", "amount"],
92
+ code: "custom",
93
+ message: `the price is ${written}, and a price carries at most ${PAYABLE_DECIMALS} digits after the dot, the finest amount USDC can be paid in, so there is no exact amount to charge for it`,
94
+ });
95
+ }
96
+ if (!PAYABLE_CURRENCIES.includes(price.currency)) {
97
+ problems.push({
98
+ path: ["price", "currency"],
99
+ code: "custom",
100
+ message: `the price is in ${JSON.stringify(price.currency)}, which Agentify cannot charge: it takes USD, paid as USDC one for one, or USDC itself, and holds no exchange rate to anything else`,
101
+ });
102
+ }
103
+ return problems;
104
+ }