@nuanu-ai/agentify-contracts 0.5.0 → 0.6.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/api.js CHANGED
@@ -897,7 +897,7 @@ export const API_ROUTES = Object.freeze({
897
897
  method: "POST",
898
898
  path: "/v0/quotes/:price_id/answer",
899
899
  auth: "merchant_key",
900
- description: "The price and availability for a question that came off the worker stream, against the price_id that question carried. The acknowledgement says whether the answer arrived in time to price the purchase; when it did not, stock held against the question can be released.",
900
+ description: "The price and availability for a question that came off the worker stream, against the price_id that question carried. The acknowledgement says whether the answer arrived in time to price the purchase; when it did not, stock held against the question can be released. A price that is zero, not in USD or USDC, or written with fewer than two or more than six digits after the dot is refused as malformed_body with the reason as its message, and the question stays open for a corrected answer.",
901
901
  request: QuoteResponseSchema,
902
902
  response: { document: QuoteAnswerAckSchema },
903
903
  },
package/dist/card.d.ts CHANGED
@@ -76,11 +76,12 @@ export declare const ServiceNameSchema: z.ZodString;
76
76
  */
77
77
  export declare const TagsSchema: z.ZodArray<z.ZodString>;
78
78
  /**
79
- * Both deadlines are shown to the agent before it pays, which is why a card
80
- * may only carry the ones its own mode uses. A synchronous card advertising a
81
- * delivery deadline would be advertising a wait that never happens — and the
82
- * wait for a synchronous answer is our system-wide budget, the same for every
83
- * product, so it has no field here at all.
79
+ * A card as a merchant publishes it.
80
+ *
81
+ * The plain-text rule runs whatever else is wrong with the card, so a merchant
82
+ * hears about their markup in the same answer as about their price. The rules
83
+ * that compare one field with another run only once the shape is right, as
84
+ * they always have: they need the fields to be what they claim to be.
84
85
  */
85
86
  export declare const CardSchema: z.ZodObject<{
86
87
  merchant_item_id: z.ZodString;
package/dist/card.js CHANGED
@@ -26,6 +26,7 @@
26
26
  */
27
27
  import { z } from "zod";
28
28
  import { ParamSpecSchema, paramSpecToValidator } from "./param-spec.js";
29
+ import { notPlainTextIn } from "./plain-text.js";
29
30
  import { IdentifierSchema, MoneySchema, TimestampSchema } from "./primitives.js";
30
31
  import { SellingStateSchema } from "./selling.js";
31
32
  /**
@@ -348,8 +349,14 @@ const CardFieldsSchema = z.strictObject({
348
349
  *
349
350
  * Written as the two fields, or as the one string that becomes them. What is
350
351
  * stored and what an agent reads are the two fields either way.
352
+ *
353
+ * Publishing holds it to the price rule in `price-rule.ts`, outside this
354
+ * schema so that a card stored before the rule stays readable; the export
355
+ * says so, or a generated client would first meet the rule as a refusal.
351
356
  */
352
- price: CardPriceSchema,
357
+ price: CardPriceSchema.meta({
358
+ description: 'Publishing refuses a price that is zero, not in USD or USDC, or written with fewer than two or more than six digits after the dot ("5.00", "0.001"), so that a price the catalog shows is one a payment can be taken at.',
359
+ }),
353
360
  /** What the agent has to supply to buy. Absent when the purchase needs no input. */
354
361
  params: writtenShort(ParamSpecSchema).optional(),
355
362
  result: writtenShort(DeclaredResultSchema),
@@ -390,7 +397,7 @@ const CardFieldsSchema = z.strictObject({
390
397
  * wait for a synchronous answer is our system-wide budget, the same for every
391
398
  * product, so it has no field here at all.
392
399
  */
393
- export const CardSchema = CardFieldsSchema.superRefine((card, ctx) => {
400
+ const cardRules = (card, ctx) => {
394
401
  if (card.fulfillment === "confirm") {
395
402
  // The gate, and the reason it is a refusal rather than a note somewhere.
396
403
  // A confirmation request reaches the merchant before any money moves and
@@ -428,13 +435,106 @@ export const CardSchema = CardFieldsSchema.superRefine((card, ctx) => {
428
435
  message: 'a synchronous card delivers inside the system-wide response budget and sets no delivery deadline; "async" and "confirm" do',
429
436
  });
430
437
  }
431
- }).meta({
432
- // JSON Schema has no way to say "this field only when that one has this
433
- // value", and zod drops the rules above when it renders a document. Left at
434
- // that, an engineer generating a client from the export would build one that
435
- // sends deadlines a card cannot carry and only find out on the first publish.
436
- // Saying it in words is weaker than checking it, and better than silence.
437
- description: 'A product in the catalog, as the merchant publishes it. Three rules are enforced beyond the shape below. A card cannot be published as fulfillment "confirm" during the pilot: the confirmation request has no shape on the wire yet, so a handler could not tell one from a paid order. confirm_deadline_seconds is only allowed when fulfillment is "confirm", and fulfill_deadline_seconds only when fulfillment is "async" or "confirm" — a synchronous card delivers inside the system-wide response budget and names no deadline of its own. This document describes the card as it is stored and read back; three fields also take a shorter spelling at publication, which JSON Schema has no way to show alongside the canonical one. price may be written as one string, the amount and the currency code with a single space between them ("5.00 USD"). A field of params or result may be written as its type word alone (access_url: "string") where it carries no title and no required flag. fulfillment may be left out, and a card that leaves it out is "sync". Each of those is opened out into the form below as the card is accepted, so a card generated from this document is accepted unchanged and a card read back is always in this form.',
438
+ };
439
+ // JSON Schema has no way to say "this field only when that one has this
440
+ // value", and zod drops the rules above when it renders a document. Left at
441
+ // that, an engineer generating a client from the export would build one that
442
+ // sends deadlines a card cannot carry and only find out on the first publish.
443
+ // Saying it in words is weaker than checking it, and better than silence.
444
+ const CARD_RULES_IN_WORDS = 'A card cannot be published as fulfillment "confirm" during the pilot: the confirmation request has no shape on the wire yet, so a handler could not tell one from a paid order. confirm_deadline_seconds is only allowed when fulfillment is "confirm", and fulfill_deadline_seconds only when fulfillment is "async" or "confirm" — a synchronous card delivers inside the system-wide response budget and names no deadline of its own.';
445
+ /**
446
+ * The words on a card that an agent reads: the title, the description, and
447
+ * the title of each field the card declares.
448
+ *
449
+ * It reads whatever arrived rather than a card known to be whole, because the
450
+ * rule runs beside the shape checks and not after them — a merchant told about
451
+ * a price first and about their markup on the next attempt fixes one thing per
452
+ * publish. So anything that is not where a card keeps its words is passed over
453
+ * here and named by the shape check instead.
454
+ */
455
+ const wordsOf = (card) => {
456
+ if (!isRecord(card))
457
+ return [];
458
+ const words = [];
459
+ if (typeof card.title === "string") {
460
+ words.push({ path: ["title"], text: card.title, called: "title", lines: "one line" });
461
+ }
462
+ if (typeof card.description === "string") {
463
+ words.push({
464
+ path: ["description"],
465
+ text: card.description,
466
+ called: "description",
467
+ lines: "several lines",
468
+ });
469
+ }
470
+ for (const declaration of ["params", "result"]) {
471
+ const fields = card[declaration];
472
+ if (!isRecord(fields))
473
+ continue;
474
+ for (const [name, field] of Object.entries(fields)) {
475
+ if (isRecord(field) && typeof field.title === "string") {
476
+ words.push({
477
+ path: [declaration, name, "title"],
478
+ text: field.title,
479
+ called: "title",
480
+ lines: "one line",
481
+ });
482
+ }
483
+ }
484
+ }
485
+ return words;
486
+ };
487
+ /**
488
+ * The card's words are plain text, and a finding for each kind of thing in them
489
+ * that is not (see `plain-text.ts` for what counts and why the door refuses
490
+ * rather than cleans).
491
+ *
492
+ * Each finding names the field, what was found in it and where, and what the
493
+ * field is held to. The sentence is written for the merchant who has to find
494
+ * the characters in their own editor, which is why it quotes the fragment and
495
+ * counts the position rather than printing the rule.
496
+ */
497
+ const plainWords = (card, ctx) => {
498
+ for (const words of wordsOf(card)) {
499
+ const heldTo = words.lines === "one line"
500
+ ? `a ${words.called} is plain text on one line`
501
+ : `a ${words.called} is plain text`;
502
+ for (const phrase of notPlainTextIn(words.text, words.lines)) {
503
+ ctx.addIssue({
504
+ code: "custom",
505
+ path: [...words.path],
506
+ message: `this ${words.called} carries ${phrase}, and ${heldTo}, which an agent reads exactly as it is written`,
507
+ });
508
+ }
509
+ }
510
+ };
511
+ const PLAIN_TEXT_IN_WORDS = "The title, the description and the title of every declared field are plain text, which an agent reads exactly as it is written: a card is refused where one of them carries HTML markup (a tag, or the opening of a comment), an HTML character reference such as &amp; or &#8217;, or a control character, and a description alone may carry line feeds. An ampersand, a comparison or an arrow written as text passes.";
512
+ /**
513
+ * The card as it is stored and as its merchant reads it back: every rule a
514
+ * publish holds it to except that its words are plain text.
515
+ *
516
+ * That one rule belongs to the door and not to the reader. A card stored
517
+ * before it may carry markup, and every answer that carries a stored card is
518
+ * held to its contract on the way out — so holding a stored card to the rule
519
+ * would fail the merchant's whole list, and the page of the catalog it sits
520
+ * on, over one old row, and the merchant could not even see the card they were
521
+ * meant to republish.
522
+ */
523
+ const StoredCardSchema = CardFieldsSchema.superRefine(cardRules).meta({
524
+ description: `A product in the catalog, as it is stored and as its merchant reads it back. Three rules hold beyond the shape below. ${CARD_RULES_IN_WORDS} A publish also holds the title, the description and each declared field's title to plain text; reading a card back does not, so a card stored before that rule is read back exactly as it was stored.`,
525
+ });
526
+ /**
527
+ * A card as a merchant publishes it.
528
+ *
529
+ * The plain-text rule runs whatever else is wrong with the card, so a merchant
530
+ * hears about their markup in the same answer as about their price. The rules
531
+ * that compare one field with another run only once the shape is right, as
532
+ * they always have: they need the fields to be what they claim to be.
533
+ */
534
+ export const CardSchema = CardFieldsSchema.superRefine(cardRules)
535
+ .superRefine(plainWords, { when: () => true })
536
+ .meta({
537
+ description: `A product in the catalog, as the merchant publishes it. Four rules are enforced beyond the shape below. ${CARD_RULES_IN_WORDS} ${PLAIN_TEXT_IN_WORDS} The shape below is the canonical card, the form every card is stored and read back in; three fields also take a shorter spelling at publication, which JSON Schema has no way to show alongside the canonical one. price may be written as one string, the amount and the currency code with a single space between them ("5.00 USD"). A field of params or result may be written as its type word alone (access_url: "string") where it carries no title and no required flag. fulfillment may be left out, and a card that leaves it out is "sync". Each of those is opened out into the form below as the card is accepted, so a card generated from this document is accepted unchanged and a card read back is always in this form.`,
438
538
  });
439
539
  /**
440
540
  * The check an agent's purchase parameters are held to, for this card.
@@ -688,8 +788,11 @@ export const MerchantCardSchema = z
688
788
  id: IdentifierSchema,
689
789
  /** When this version of the card was published. */
690
790
  as_of: TimestampSchema,
691
- /** The card exactly as its merchant published it. */
692
- card: CardSchema,
791
+ /**
792
+ * The card exactly as its merchant published it, held to the rules of a
793
+ * stored card rather than to the publish door's.
794
+ */
795
+ card: StoredCardSchema,
693
796
  /** What a purchase of this card meets right now. */
694
797
  selling: SellingStateSchema,
695
798
  /** Whether the pause is this card's own, rather than the whole catalog's. */
package/dist/index.d.ts CHANGED
@@ -41,6 +41,9 @@ export type { OrderStatus } from "./order-status.js";
41
41
  export { ORDER_STATUSES, OrderStatusSchema } from "./order-status.js";
42
42
  export type { FieldSpec, FieldSpecInput, ParamSpec, ParamSpecDirection, ParamSpecInput, ParamType, } from "./param-spec.js";
43
43
  export { FieldSpecSchema, ParamNameSchema, ParamSpecSchema, ParamTypeSchema, PROTOTYPE_KEY_IS_DROPPED, paramSpecToValidator, } from "./param-spec.js";
44
+ export type { TextLines } from "./plain-text.js";
45
+ export { notPlainTextIn } from "./plain-text.js";
46
+ export { PAYABLE_CURRENCIES, PAYABLE_DECIMALS, priceProblemsOf } from "./price-rule.js";
44
47
  export type { Amount, CurrencyCode, Identifier, Money, SalePrice, Timestamp, } from "./primitives.js";
45
48
  export { AmountSchema, CurrencyCodeSchema, IdentifierSchema, MoneySchema, SalePriceSchema, TimestampSchema, } from "./primitives.js";
46
49
  export type { QuotePurpose, QuoteRequest, QuoteResponse } from "./quote.js";
package/dist/index.js CHANGED
@@ -46,6 +46,8 @@ export { CabinetKeySchema, DisabledKeySchema, ForgottenCabinetKeySchema, IssuedK
46
46
  export { OrderSchema } from "./order.js";
47
47
  export { ORDER_STATUSES, OrderStatusSchema } from "./order-status.js";
48
48
  export { FieldSpecSchema, ParamNameSchema, ParamSpecSchema, ParamTypeSchema, PROTOTYPE_KEY_IS_DROPPED, paramSpecToValidator, } from "./param-spec.js";
49
+ export { notPlainTextIn } from "./plain-text.js";
50
+ export { PAYABLE_CURRENCIES, PAYABLE_DECIMALS, priceProblemsOf } from "./price-rule.js";
49
51
  export { AmountSchema, CurrencyCodeSchema, IdentifierSchema, MoneySchema, SalePriceSchema, TimestampSchema, } from "./primitives.js";
50
52
  export { QuotePurposeSchema, QuoteRequestSchema, QuoteResponseSchema } from "./quote.js";
51
53
  export { ReceiptOutcomeSchema, ReceiptSchema } from "./receipt.js";
@@ -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
+ }
@@ -39,13 +39,13 @@ export declare const AmountSchema: z.ZodString;
39
39
  * does not yet say which of the two it carries, so the shape has to admit
40
40
  * both: a national code is three letters (`USD`), and a token ticker is longer
41
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.
42
+ * this contract has had to carry.
44
43
  *
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.
44
+ * What this does not do is check membership in any list. Which currencies a
45
+ * merchant may set a price in is answered once, by the price rule in
46
+ * `price-rule.ts`, at the moment a price comes in. This shape also reads back
47
+ * every document already written, and a list here would make one written
48
+ * before the list changed unreadable.
49
49
  */
50
50
  export declare const CurrencyCodeSchema: z.ZodString;
51
51
  /** A sum of money: how much, in what. */
@@ -41,13 +41,13 @@ export const AmountSchema = z
41
41
  * does not yet say which of the two it carries, so the shape has to admit
42
42
  * both: a national code is three letters (`USD`), and a token ticker is longer
43
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.
44
+ * this contract has had to carry.
46
45
  *
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.
46
+ * What this does not do is check membership in any list. Which currencies a
47
+ * merchant may set a price in is answered once, by the price rule in
48
+ * `price-rule.ts`, at the moment a price comes in. This shape also reads back
49
+ * every document already written, and a list here would make one written
50
+ * before the list changed unreadable.
51
51
  */
52
52
  export const CurrencyCodeSchema = z
53
53
  .string()
package/dist/quote.js CHANGED
@@ -82,7 +82,9 @@ export const QuoteRequestSchema = z.strictObject({
82
82
  export const QuoteResponseSchema = z.discriminatedUnion("available", [
83
83
  z.strictObject({
84
84
  available: z.literal(true),
85
- price: MoneySchema,
85
+ price: MoneySchema.meta({
86
+ description: 'Held to the rule a card\'s price meets at publication: above zero, in USD or USDC, with two to six digits after the dot ("5.00", "0.001"); an answer that breaks it is refused and prices nothing.',
87
+ }),
86
88
  as_of: TimestampSchema,
87
89
  }),
88
90
  z.strictObject({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuanu-ai/agentify-contracts",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "The Agentify wire contract as code: the schemas for cards, orders, price checks and receipts that the gateway and the merchant SDK both read.",
6
6
  "keywords": [