@nuanu-ai/agentify-contracts 0.4.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.d.ts CHANGED
@@ -783,7 +783,7 @@ export type ErrorEnvelope = z.infer<typeof ErrorEnvelopeSchema>;
783
783
  * has to act on it belongs to the route that sends it and to the sentence the
784
784
  * refusal carries.
785
785
  */
786
- export declare const ERROR_CODES: readonly ["body_too_large", "body_undecodable", "call_refused", "charset_unsupported", "encoding_unsupported", "gateway_failed", "key_made_for_a_cabinet", "key_opened_this_call", "malformed_body", "malformed_query", "merchant_departed", "no_such_item", "no_such_key", "no_such_order", "no_such_route", "not_a_cabinet_key", "not_authorised", "not_invited", "not_selling", "not_this_purchase", "order_closed_before_it_was_priced", "order_not_priced_yet", "params_do_not_fit", "payment_already_spent", "payment_not_taken", "payment_not_verified"];
786
+ export declare const ERROR_CODES: readonly ["body_too_large", "body_undecodable", "call_refused", "charset_unsupported", "encoding_unsupported", "gateway_failed", "key_made_for_a_cabinet", "key_opened_this_call", "malformed_body", "malformed_query", "merchant_departed", "no_such_item", "no_such_key", "no_such_order", "no_such_route", "not_a_cabinet_key", "not_authorised", "not_invited", "not_selling", "not_this_purchase", "order_closed_before_it_was_priced", "order_not_priced_yet", "params_do_not_fit", "payment_already_spent", "payment_not_taken", "payment_not_verified", "wallet_change_not_announced", "wallet_change_nobody_to_tell", "wallet_change_raced", "wallet_change_unconfirmed"];
787
787
  /** One of the codes this gateway is known to refuse a call with. */
788
788
  export type ErrorCode = (typeof ERROR_CODES)[number];
789
789
  /**
@@ -1280,6 +1280,10 @@ export declare const API_ROUTES: Readonly<{
1280
1280
  response: {
1281
1281
  document: z.ZodObject<{
1282
1282
  payout_wallet: z.ZodNullable<z.ZodString>;
1283
+ pending: z.ZodNullable<z.ZodObject<{
1284
+ payout_wallet: z.ZodString;
1285
+ takes_effect_at: z.ZodISODateTime;
1286
+ }, z.core.$strict>>;
1283
1287
  }, z.core.$strict>;
1284
1288
  };
1285
1289
  };
@@ -1294,6 +1298,10 @@ export declare const API_ROUTES: Readonly<{
1294
1298
  response: {
1295
1299
  document: z.ZodObject<{
1296
1300
  payout_wallet: z.ZodNullable<z.ZodString>;
1301
+ pending: z.ZodNullable<z.ZodObject<{
1302
+ payout_wallet: z.ZodString;
1303
+ takes_effect_at: z.ZodISODateTime;
1304
+ }, z.core.$strict>>;
1297
1305
  }, z.core.$strict>;
1298
1306
  };
1299
1307
  };
package/dist/api.js CHANGED
@@ -105,7 +105,7 @@ export const MerchantCardListSchema = z
105
105
  cards: z.array(MerchantCardSchema),
106
106
  })
107
107
  .meta({
108
- description: "A merchant's own catalog: every card they have published, and whether they are taking new orders at all. The merchant's own selling word is here as well as on each card because the two are different facts, and neither can be worked out from the other. A card reads open only where all of it holds at once: the merchant is selling, the card is not paused in its own right, the merchant has a name for it to be sold under, and — on a deployment that settles on a real chain — a wallet for the money to be paid into. So a card can read paused while this field reads open, and this field does not say which of the reasons it was. What does say is the refusal at the publish and the merchant's own settings, each of which names the piece that is missing. This document does not say whether it is the whole catalog: paging is not designed, and when it is, this object grows the field that answers it.",
108
+ description: "A merchant's own catalog: every card they have published, and whether they are taking new orders at all. The merchant's own selling word is here as well as on each card because the two are different facts, and neither can be worked out from the other. A card reads open only where all of it holds at once: the merchant is selling, the card is not paused in its own right, the merchant has a name for it to be sold under, on a deployment that settles on a real chain a wallet for the money to be paid into, and on the live deployment the operator's approval. So a card can read paused while this field reads open, and this field does not say which of the reasons it was. What does say is the refusal at the publish, whose findings about the merchant carry the codes no_seller_name, no_payout_wallet and no_operator_approval, and the merchant's own settings. This document does not say whether it is the whole catalog: paging is not designed, and when it is, this object grows the field that answers it.",
109
109
  });
110
110
  /**
111
111
  * Every receipt this merchant has.
@@ -680,6 +680,10 @@ export const ERROR_CODES = Object.freeze([
680
680
  "payment_already_spent",
681
681
  "payment_not_taken",
682
682
  "payment_not_verified",
683
+ "wallet_change_not_announced",
684
+ "wallet_change_nobody_to_tell",
685
+ "wallet_change_raced",
686
+ "wallet_change_unconfirmed",
683
687
  ]);
684
688
  /**
685
689
  * Every call of the surface, under the name it is known by in both programs.
@@ -714,7 +718,7 @@ export const API_ROUTES = Object.freeze({
714
718
  method: "POST",
715
719
  path: "/v0/catalog/publish",
716
720
  auth: "merchant_key",
717
- description: "Publishes one product, or says what is wrong with the card. Republishing under the same merchant_item_id is how a card is changed rather than how a second one appears. The catalog identifier comes back beside ok, and from then on it is what an agent, a purchase and a receipt all use. A card that is not published comes back as ok:false under the code card_rejected, never retryable, with every finding named in the error's problems. Three findings this can come back with are not about the card at all. A merchant who has not set the name their products are sold under gets no_seller_name, and the way past it is POST /v0/seller-name: a card published without one reaches a buyer's agent inside a payment request that names no seller. On a deployment that settles on a real chain, a merchant who has set no wallet gets no_payout_wallet, with POST /v0/payout-wallet as the way past it: the money from that card's sales is paid to the merchant's own address directly, and without one there is nowhere for it to go. On the live deployment, a merchant the operator has not admitted once gets no_operator_approval. That decision has no merchant API: test publication remains available, while live publication remains closed until the operator grants it. Every applicable finding comes back in the same problems list as whatever is wrong with the card, so one answer carries everything standing between this card and the catalog.",
721
+ description: "Publishes one product, or says what is wrong with the card. Republishing under the same merchant_item_id is how a card is changed rather than how a second one appears. The catalog identifier comes back beside ok, and from then on it is what an agent, a purchase and a receipt all use. A card that is not published comes back as ok:false under the code card_rejected, never retryable, with every finding named in the error's problems. Three findings this can come back with are not about the card at all. A merchant who has not set the name their products are sold under gets no_seller_name, and the way past it is POST /v0/seller-name: a card published without one reaches a buyer's agent inside a payment request that names no seller. On a deployment that settles on a real chain, a merchant who has set no wallet gets no_payout_wallet, and the way past it is the Settings screen of the merchant's cabinet, where a person signed in sets one: the money from that card's sales is paid to the merchant's own address directly, and without one there is nowhere for it to go. On the live deployment, a merchant the operator has not admitted once gets no_operator_approval. That decision has no merchant API: test publication remains available, while live publication remains closed until the operator grants it. Every applicable finding comes back in the same problems list as whatever is wrong with the card, so one answer carries everything standing between this card and the catalog.",
718
722
  request: CardSchema,
719
723
  response: { document: PublishResultSchema },
720
724
  },
@@ -757,7 +761,7 @@ export const API_ROUTES = Object.freeze({
757
761
  method: "POST",
758
762
  path: "/v0/merchants",
759
763
  auth: "none",
760
- description: "Makes a merchant and the key whoever registered them will call as them with — both of those or neither. It takes no key because nobody registering has one yet; what stands in the door instead is an invitation code out of the gateway's own configuration, and a gateway with no code configured refuses every registration in the same words a wrong code gets, so this call is not a way of finding out whether registration is open. The key comes back once and is readable nowhere afterwards, and it is a key made for a cabinet: it is in no list of the merchant's keys and is not disabled through them, so no row for it comes back here — only the key itself. What the new merchant does not have is a name: they are listed under nothing until somebody sets one at POST /v0/seller-name, and until then publishing a card is refused, so a cabinet that registers a person and takes them straight to a publish screen has built a dead end. Nothing about an account, an address or a password reaches this call either: those belong to whatever signs a person in, on the other side of it. Unlike every other call on this surface that writes something, a repeat of this one is not safe and there is nothing here that could make it so: two calls make two merchants, and the caller holds a key to only the second. A caller whose connection drops before the answer arrives cannot find out from here whether the first landed — it has no key with which to ask — and the merchant it may have made cannot be swept away afterwards, because a merchant is what every card, order and receipt is owned by.",
764
+ description: "Makes a merchant and the key whoever registered them will call as them with — both of those or neither. This call is not on the public origin: only the cabinet makes it, from inside the stack, when a signed-in person presses its one control, and from outside the path answers as one the site does not have. It takes no key because nobody registering has one yet; what stands in the door instead is an invitation code out of the gateway's own configuration, and a gateway with no code configured refuses every registration in the same words a wrong code gets, so this call is not a way of finding out whether registration is open. The key comes back once and is readable nowhere afterwards, and it is a key made for a cabinet: it is in no list of the merchant's keys and is not disabled through them, so no row for it comes back here — only the key itself. What the new merchant does not have is a name: they are listed under nothing until somebody sets one at POST /v0/seller-name, and until then publishing a card is refused, so a cabinet that registers a person and takes them straight to a publish screen has built a dead end. Nothing about an account, an address or a password reaches this call either: those belong to whatever signs a person in, on the other side of it. Unlike every other call on this surface that writes something, a repeat of this one is not safe and there is nothing here that could make it so: two calls make two merchants, and the caller holds a key to only the second. A caller whose connection drops before the answer arrives cannot find out from here whether the first landed — it has no key with which to ask — and the merchant it may have made cannot be swept away afterwards, because a merchant is what every card, order and receipt is owned by.",
761
765
  request: RegistrationRequestSchema,
762
766
  response: { document: RegisteredMerchantSchema },
763
767
  },
@@ -780,14 +784,14 @@ export const API_ROUTES = Object.freeze({
780
784
  method: "GET",
781
785
  path: "/v0/payout-wallet",
782
786
  auth: "merchant_key",
783
- description: "The address the sales of the merchant this call's own key belongs to are paid into. Payments here are not held by anybody on the way: a buyer's agent pays this address directly and no balance of the merchant's is ever held on our side, which is why the address has to be theirs and why this call exists. Null is the ordinary answer for a merchant who has set none, and it is an answer rather than a refusal — a merchant with no wallet exists and has a settings screen to draw. The address comes back in the mixed-case spelling a wallet shows, whichever of the two accepted spellings was sent to set it, so a screen showing it shows what the merchant copied out of their wallet character for character.",
787
+ description: "The address the sales of the merchant this call's own key belongs to are paid into now, and any change of it that is waiting. Payments here are not held by anybody on the way: a buyer's agent pays this address directly and no balance of the merchant's is ever held on our side, which is why the address has to be theirs and why this call exists. Any key of the merchant's reads it, the keys made for their own code included; only the merchant's cabinet sets it. Null is the ordinary answer for a merchant who has set none, and it is an answer rather than a refusal — a merchant with no wallet exists and has a settings screen to draw. The address comes back in the mixed-case spelling a wallet shows, whichever of the two accepted spellings was sent to set it, so a screen showing it shows what the merchant copied out of their wallet character for character. On the live deployment a replacement for an address already set takes effect forty-eight hours after it was announced to the merchant, and until then this answers with the address still paid and names the waiting one, with the moment it takes effect, under pending. Null there means nothing is waiting, which is always the answer on the test channel and in a sandbox.",
784
788
  response: { document: PayoutWalletSchema },
785
789
  },
786
790
  set_payout_wallet: {
787
791
  method: "POST",
788
792
  path: "/v0/payout-wallet",
789
793
  auth: "merchant_key",
790
- description: "Sets the address the sales of the merchant this call's own key belongs to are paid into. The answer is the address as it now stands, read back from what was written rather than echoed, in the mixed-case spelling a wallet shows. Setting the same address twice changes nothing and answers the same way, so a retry after a dropped connection is safe. An address whose capital letters do not agree with the rest of it is refused and nothing is written, because those capitals are a checksum and letters that disagree mean a character is wrong — and an address that is wrong is another perfectly good address belonging to somebody else. What this call will not do is take an address away: null is refused, because without an address there is nowhere to send the money and every published card would quietly come off sale — ending the selling under the name of editing a setting; somebody reaching for that wants either a different address, which is this same call, or an end to selling, which is the pause. On a deployment that settles on a real chain, a merchant with no wallet set here cannot publish a card, and the refusal at the publish says so.",
794
+ description: "Sets the address the sales of the merchant this call's own key belongs to are paid into. Only the merchant's cabinet makes this call, with the key made for it, from inside the stack, when a person sets the wallet on its Settings screen: keys operate the shop, and where the shop's money goes is set through the cabinet. The public origin does not route this call at all, so from outside it is a path the site does not have; at the gateway a key made for the merchant's own code is refused under not_a_cabinet_key, for the first address as for any other, and nothing is written or announced. The first address a merchant sets applies at once; on the live deployment every cabinet account of the merchant is then told of it, and a message that cannot be sent refuses nothing. On the live deployment a different address after that does not: before anything is written, every account that names the merchant is sent a message saying what changes, when, and that it was asked for in the cabinet, and the change takes effect forty-eight hours after those messages were handed to the mail provider. Until then every payment request names the address that applies now, and the answer carries the waiting one under pending with the moment it takes effect. Asking again for the address already waiting changes nothing, sends no second message and restarts no clock, and answers with the same pending change, so a retry after a dropped connection is safe; a different address replaces the waiting one and starts the forty-eight hours again; asking for the address that applies now cancels the waiting change. On the test channel and in a sandbox every change applies at once and no message is sent. The answer is the wallet as it now stands, read back from what was written rather than echoed, in the mixed-case spelling a wallet shows. Setting the address that already applies, with nothing waiting, changes nothing and answers the same way. An address whose capital letters do not agree with the rest of it is refused and nothing is written, because those capitals are a checksum and letters that disagree mean a character is wrong — and an address that is wrong is another perfectly good address belonging to somebody else. What this call will not do is take an address away: null is refused, because without an address there is nowhere to send the money and every published card would quietly come off sale — ending the selling under the name of editing a setting; somebody reaching for that wants either a different address, which is this same call, or an end to selling, which is the pause. On a deployment that settles on a real chain, a merchant with no wallet set here cannot publish a card, and the refusal at the publish says so.",
791
795
  request: PayoutWalletRequestSchema,
792
796
  response: { document: PayoutWalletSchema },
793
797
  },
@@ -817,14 +821,14 @@ export const API_ROUTES = Object.freeze({
817
821
  method: "POST",
818
822
  path: "/v0/keys/cabinet",
819
823
  auth: "merchant_key",
820
- description: "Makes a key for a cabinet to call as this merchant with, and hands it back once. It is a key of a different kind from the ones at /v0/keys: the merchant did not ask for it, never sees it, and it appears in no list of theirs — so nothing comes back but the key itself. What this is for is a cabinet that replaces its own credential every time somebody signs in, which is what keeps a copy of a cabinet's database from being a set of working keys for long. The call is refused to a key made for the merchant's own code, under not_a_cabinet_key: these two calls are the cabinet's own, and a merchant asking for one would be asking for a credential to a cabinet they are not standing in.",
824
+ description: "Makes a key for a cabinet to call as this merchant with, and hands it back once. This call and the forgetting beside it are not on the public origin: only the cabinet makes them, from inside the stack, and from outside the path answers as one the site does not have. It is a key of a different kind from the ones at /v0/keys: the merchant did not ask for it, never sees it, and it appears in no list of theirs — so nothing comes back but the key itself. What this is for is a cabinet that replaces its own credential every time somebody signs in, which is what keeps a copy of a cabinet's database from being a set of working keys for long. The call is refused to a key made for the merchant's own code, under not_a_cabinet_key: these two calls are the cabinet's own, and a merchant asking for one would be asking for a credential to a cabinet they are not standing in.",
821
825
  response: { document: CabinetKeySchema },
822
826
  },
823
827
  forget_cabinet_key: {
824
828
  method: "DELETE",
825
829
  path: "/v0/keys/cabinet",
826
830
  auth: "merchant_key",
827
- description: "Removes the key this call was made with, and no other. It is removed rather than revoked: a merchant never issued one, never sees one and would never read a revoked one back, so a row kept for the history would be history for nobody. There are no parameters, and that is not a convenience — a key belonging to anybody else, this merchant included, is unreachable here by construction, because the only way to name a key is to be holding it. What a caller does with this is put a key of its own beyond use once it has stopped signing in with it: ask for a fresh key, write it down where the account is kept, then forget the one that was there. A caller that forgets first can be left naming a key that no longer exists, which is the one order that locks somebody out. Made a second time with the same key it is refused as a key that does not exist, which is safe and is the confirmation the first one landed: either way that key is gone and nothing else has moved. It is refused to a key made for the merchant's own code, under not_a_cabinet_key: those are revoked from the list they appear on, at an instant their owner can read back, and removing the row outright would take that history away.",
831
+ description: "Removes the key this call was made with, and no other. Like the call that makes such a key, it is not on the public origin: only the cabinet makes it, from inside the stack. It is removed rather than revoked: a merchant never issued one, never sees one and would never read a revoked one back, so a row kept for the history would be history for nobody. There are no parameters, and that is not a convenience — a key belonging to anybody else, this merchant included, is unreachable here by construction, because the only way to name a key is to be holding it. What a caller does with this is put a key of its own beyond use once it has stopped signing in with it: ask for a fresh key, write it down where the account is kept, then forget the one that was there. A caller that forgets first can be left naming a key that no longer exists, which is the one order that locks somebody out. Made a second time with the same key it is refused as a key that does not exist, which is safe and is the confirmation the first one landed: either way that key is gone and nothing else has moved. It is refused to a key made for the merchant's own code, under not_a_cabinet_key: those are revoked from the list they appear on, at an instant their owner can read back, and removing the row outright would take that history away.",
828
832
  response: { document: ForgottenCabinetKeySchema },
829
833
  },
830
834
  get_order: {
@@ -893,7 +897,7 @@ export const API_ROUTES = Object.freeze({
893
897
  method: "POST",
894
898
  path: "/v0/quotes/:price_id/answer",
895
899
  auth: "merchant_key",
896
- 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.",
897
901
  request: QuoteResponseSchema,
898
902
  response: { document: QuoteAnswerAckSchema },
899
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
@@ -33,22 +33,25 @@ export type { EvmAddress } from "./evm-address.js";
33
33
  export { checksummedAddressOf, EvmAddressSchema } from "./evm-address.js";
34
34
  export type { Acceptance, Delivery, HandlerAnswer, Refusal, RefusalCode } from "./handler.js";
35
35
  export { AcceptanceSchema, DeliverySchema, HandlerAnswerSchema, RECOMMENDED_REFUSAL_CODES, RefusalCodeSchema, RefusalSchema, } from "./handler.js";
36
- export type { CabinetKey, DisabledKey, ForgottenCabinetKey, IssuedKey, IssueKeyRequest, MerchantKey, MerchantKeyList, PayoutWallet, PayoutWalletRequest, RegisteredMerchant, RegistrationRequest, SellerName, SellerNameRequest, } from "./merchant.js";
37
- export { CabinetKeySchema, DisabledKeySchema, ForgottenCabinetKeySchema, IssuedKeySchema, IssueKeyRequestSchema, MerchantKeyListSchema, MerchantKeySchema, PayoutWalletRequestSchema, PayoutWalletSchema, RegisteredMerchantSchema, RegistrationRequestSchema, SellerNameRequestSchema, SellerNameSchema, } from "./merchant.js";
36
+ export type { CabinetKey, DisabledKey, ForgottenCabinetKey, IssuedKey, IssueKeyRequest, MerchantKey, MerchantKeyList, PayoutWallet, PayoutWalletRequest, PendingPayoutWallet, RegisteredMerchant, RegistrationRequest, SellerName, SellerNameRequest, } from "./merchant.js";
37
+ export { CabinetKeySchema, DisabledKeySchema, ForgottenCabinetKeySchema, IssuedKeySchema, IssueKeyRequestSchema, MerchantKeyListSchema, MerchantKeySchema, PayoutWalletRequestSchema, PayoutWalletSchema, PendingPayoutWalletSchema, RegisteredMerchantSchema, RegistrationRequestSchema, SellerNameRequestSchema, SellerNameSchema, } from "./merchant.js";
38
38
  export type { Order } from "./order.js";
39
39
  export { OrderSchema } from "./order.js";
40
40
  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";
47
50
  export { QuotePurposeSchema, QuoteRequestSchema, QuoteResponseSchema } from "./quote.js";
48
51
  export type { Receipt, ReceiptOutcome } from "./receipt.js";
49
52
  export { ReceiptOutcomeSchema, ReceiptSchema } from "./receipt.js";
50
- export type { CallError, OrderCallResult, Problem, PublishResult } from "./results.js";
51
- export { CARD_REJECTED, CallErrorSchema, ORDER_CALL_ERROR_CODES, ORDER_CALL_RESULTS, OrderCallResultSchema, ProblemSchema, PublishResultSchema, } from "./results.js";
53
+ export type { CallError, MerchantFinding, OrderCallResult, Problem, PublishResult, } from "./results.js";
54
+ export { CARD_REJECTED, CallErrorSchema, MERCHANT_FINDINGS, ORDER_CALL_ERROR_CODES, ORDER_CALL_RESULTS, OrderCallResultSchema, ProblemSchema, PublishResultSchema, } from "./results.js";
52
55
  export type { SellingState } from "./selling.js";
53
56
  export { SELLING_STATES, SellingStateSchema } from "./selling.js";
54
57
  /**
@@ -616,10 +619,18 @@ export declare const schemas: Readonly<{
616
619
  }>;
617
620
  payout_wallet: z.ZodObject<{
618
621
  payout_wallet: z.ZodNullable<z.ZodString>;
622
+ pending: z.ZodNullable<z.ZodObject<{
623
+ payout_wallet: z.ZodString;
624
+ takes_effect_at: z.ZodISODateTime;
625
+ }, z.core.$strict>>;
619
626
  }, z.core.$strict>;
620
627
  payout_wallet_request: z.ZodObject<{
621
628
  payout_wallet: z.ZodPipe<z.ZodString, z.ZodString>;
622
629
  }, z.core.$strict>;
630
+ pending_payout_wallet: z.ZodObject<{
631
+ payout_wallet: z.ZodString;
632
+ takes_effect_at: z.ZodISODateTime;
633
+ }, z.core.$strict>;
623
634
  price_check: z.ZodUnion<readonly [z.ZodLiteral<"handler">, z.ZodObject<{
624
635
  url: z.ZodURL;
625
636
  }, z.core.$strict>]>;
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import { WorkerEnvelopeSchema } from "./envelope.js";
27
27
  import { OrderEventSchema, RefundDueReasonSchema } from "./events.js";
28
28
  import { EvmAddressSchema } from "./evm-address.js";
29
29
  import { AcceptanceSchema, DeliverySchema, HandlerAnswerSchema, RefusalCodeSchema, RefusalSchema, } from "./handler.js";
30
- import { CabinetKeySchema, DisabledKeySchema, ForgottenCabinetKeySchema, IssuedKeySchema, IssueKeyRequestSchema, MerchantKeyListSchema, MerchantKeySchema, PayoutWalletRequestSchema, PayoutWalletSchema, RegisteredMerchantSchema, RegistrationRequestSchema, SellerNameRequestSchema, SellerNameSchema, } from "./merchant.js";
30
+ import { CabinetKeySchema, DisabledKeySchema, ForgottenCabinetKeySchema, IssuedKeySchema, IssueKeyRequestSchema, MerchantKeyListSchema, MerchantKeySchema, PayoutWalletRequestSchema, PayoutWalletSchema, PendingPayoutWalletSchema, RegisteredMerchantSchema, RegistrationRequestSchema, SellerNameRequestSchema, SellerNameSchema, } from "./merchant.js";
31
31
  import { OrderSchema } from "./order.js";
32
32
  import { OrderStatusSchema } from "./order-status.js";
33
33
  import { FieldSpecSchema, ParamNameSchema, ParamSpecSchema, ParamTypeSchema, } from "./param-spec.js";
@@ -42,14 +42,16 @@ export { WORKER_ENVELOPE_KINDS, WORKER_ENVELOPE_PAYLOADS, WorkerEnvelopeSchema,
42
42
  export { ORDER_EVENT_TYPES, OrderEventSchema, RefundDueReasonSchema } from "./events.js";
43
43
  export { checksummedAddressOf, EvmAddressSchema } from "./evm-address.js";
44
44
  export { AcceptanceSchema, DeliverySchema, HandlerAnswerSchema, RECOMMENDED_REFUSAL_CODES, RefusalCodeSchema, RefusalSchema, } from "./handler.js";
45
- export { CabinetKeySchema, DisabledKeySchema, ForgottenCabinetKeySchema, IssuedKeySchema, IssueKeyRequestSchema, MerchantKeyListSchema, MerchantKeySchema, PayoutWalletRequestSchema, PayoutWalletSchema, RegisteredMerchantSchema, RegistrationRequestSchema, SellerNameRequestSchema, SellerNameSchema, } from "./merchant.js";
45
+ export { CabinetKeySchema, DisabledKeySchema, ForgottenCabinetKeySchema, IssuedKeySchema, IssueKeyRequestSchema, MerchantKeyListSchema, MerchantKeySchema, PayoutWalletRequestSchema, PayoutWalletSchema, PendingPayoutWalletSchema, RegisteredMerchantSchema, RegistrationRequestSchema, SellerNameRequestSchema, SellerNameSchema, } from "./merchant.js";
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";
52
- export { CARD_REJECTED, CallErrorSchema, ORDER_CALL_ERROR_CODES, ORDER_CALL_RESULTS, OrderCallResultSchema, ProblemSchema, PublishResultSchema, } from "./results.js";
54
+ export { CARD_REJECTED, CallErrorSchema, MERCHANT_FINDINGS, ORDER_CALL_ERROR_CODES, ORDER_CALL_RESULTS, OrderCallResultSchema, ProblemSchema, PublishResultSchema, } from "./results.js";
53
55
  export { SELLING_STATES, SellingStateSchema } from "./selling.js";
54
56
  /**
55
57
  * The version of the public contract. It grows when the meaning of the fields
@@ -109,6 +111,7 @@ export const schemas = Object.freeze({
109
111
  param_type: ParamTypeSchema,
110
112
  payout_wallet: PayoutWalletSchema,
111
113
  payout_wallet_request: PayoutWalletRequestSchema,
114
+ pending_payout_wallet: PendingPayoutWalletSchema,
112
115
  price_check: PriceCheckSchema,
113
116
  problem: ProblemSchema,
114
117
  public_card: PublicCardSchema,
@@ -177,6 +177,25 @@ export declare const SellerNameSchema: z.ZodObject<{
177
177
  export declare const SellerNameRequestSchema: z.ZodObject<{
178
178
  seller_name: z.ZodPipe<z.ZodString, z.ZodString>;
179
179
  }, z.core.$strict>;
180
+ /**
181
+ * A change of the wallet that has been asked for, announced, and has not taken
182
+ * effect yet.
183
+ *
184
+ * It exists because a replacement does not apply at once where the money is
185
+ * real (ADR-0019). The address a merchant is paid at is the one setting whose
186
+ * change redirects money, and any key of theirs reaches it — the cabinet's, or
187
+ * one sitting in their own server's environment — so on the live deployment a
188
+ * replacement is told to every account of the merchant first and takes effect
189
+ * forty-eight hours after that. What this document says is the two facts a
190
+ * merchant needs in that window: what replaces the address, and from when.
191
+ *
192
+ * Both are required. An address with no moment says nothing about when the
193
+ * money moves, and a moment with no address says it moves without saying where.
194
+ */
195
+ export declare const PendingPayoutWalletSchema: z.ZodObject<{
196
+ payout_wallet: z.ZodString;
197
+ takes_effect_at: z.ZodISODateTime;
198
+ }, z.core.$strict>;
180
199
  /**
181
200
  * The wallet a merchant's sales are paid into, as the merchant reads it back.
182
201
  *
@@ -200,9 +219,27 @@ export declare const SellerNameRequestSchema: z.ZodObject<{
200
219
  * address in lower case they cannot tell it from a different address without
201
220
  * comparing character by character. On the one field money is sent to, that
202
221
  * glance is the whole of the checking anybody does.
222
+ *
223
+ * `pending` is the change waiting beside it, and it is always present for the
224
+ * same reason `payout_wallet` is: null says nothing is waiting, and an absent
225
+ * field would be a silence. It is the field that keeps a caller from taking its
226
+ * own write for a failure — a merchant who asked for a new address on the live
227
+ * deployment reads the old one back, correctly, for forty-eight hours, and
228
+ * without this they would ask again, or conclude the change was lost.
229
+ *
230
+ * It is carried without moving `CONTRACT_VERSION`, which is the one known
231
+ * exception to the rule that a new required field moves it (ADR-0006 §2): no
232
+ * worker of the SDK reads this route, so the version would stop every
233
+ * installed worker for a field none of them sees. What that costs is that a
234
+ * merchant's own code holding this schema from an older release of this
235
+ * package refuses the answer until the package is upgraded.
203
236
  */
204
237
  export declare const PayoutWalletSchema: z.ZodObject<{
205
238
  payout_wallet: z.ZodNullable<z.ZodString>;
239
+ pending: z.ZodNullable<z.ZodObject<{
240
+ payout_wallet: z.ZodString;
241
+ takes_effect_at: z.ZodISODateTime;
242
+ }, z.core.$strict>>;
206
243
  }, z.core.$strict>;
207
244
  /**
208
245
  * What a merchant sends to change where their sales are paid.
@@ -270,6 +307,7 @@ export type SellerName = z.infer<typeof SellerNameSchema>;
270
307
  export type SellerNameRequest = z.infer<typeof SellerNameRequestSchema>;
271
308
  export type PayoutWallet = z.infer<typeof PayoutWalletSchema>;
272
309
  export type PayoutWalletRequest = z.infer<typeof PayoutWalletRequestSchema>;
310
+ export type PendingPayoutWallet = z.infer<typeof PendingPayoutWalletSchema>;
273
311
  export type MerchantKey = z.infer<typeof MerchantKeySchema>;
274
312
  export type MerchantKeyList = z.infer<typeof MerchantKeyListSchema>;
275
313
  export type IssueKeyRequest = z.infer<typeof IssueKeyRequestSchema>;
package/dist/merchant.js CHANGED
@@ -51,6 +51,33 @@ import { IdentifierSchema, TimestampSchema } from "./primitives.js";
51
51
  const KeyLabelSchema = z
52
52
  .string()
53
53
  .regex(/^\S(?:[\s\S]*\S)?$/u, "a label must not be empty or padded with spaces");
54
+ /**
55
+ * The longest label a key is issued with.
56
+ *
57
+ * A label used to have no bound, on the argument that no channel outside us
58
+ * carries it. One does now: on the live deployment the message that tells a
59
+ * merchant of a new key, or of a wallet change made with one, names the key
60
+ * by its label (ADR-0019), and that message is Agentify's words in somebody's
61
+ * inbox. A hundred characters is a line on a list and in a message, and
62
+ * longer than any name a person gives a worker.
63
+ */
64
+ const LONGEST_KEY_LABEL = 100;
65
+ /**
66
+ * A label as a new key is issued with it: the rule above, on one line, and no
67
+ * longer than {@link LONGEST_KEY_LABEL}.
68
+ *
69
+ * Only the request carries the bound. The keys a merchant already holds are
70
+ * read back under whatever they were named, including a key issued at the
71
+ * server's terminal, because refusing the list over one old row would hide
72
+ * every key on it; the message that names such a key cuts it to one line of
73
+ * the same length itself.
74
+ */
75
+ const IssuedKeyLabelSchema = KeyLabelSchema.max(LONGEST_KEY_LABEL, `a label is at most ${LONGEST_KEY_LABEL} characters, the one line a key is known by`).regex(
76
+ // Written as code-point ranges rather than a Unicode property, because this
77
+ // pattern is published in the JSON Schema a validator in another language
78
+ // reads, and not every one of those knows the property escapes.
79
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are what it refuses
80
+ /^[^\u0000-\u001F\u007F-\u009F\u2028\u2029]*$/u, "a label is one line: it cannot carry a line break, a tab or another control character");
54
81
  /**
55
82
  * The key itself, in the only form its owner will ever see it.
56
83
  *
@@ -164,10 +191,10 @@ export const MerchantKeyListSchema = z
164
191
  /** What a merchant sends to have a key made. */
165
192
  export const IssueKeyRequestSchema = z
166
193
  .strictObject({
167
- label: KeyLabelSchema,
194
+ label: IssuedKeyLabelSchema,
168
195
  })
169
196
  .meta({
170
- description: "What a merchant asks for when they want another key: the name they will know it by, and nothing else. There is nowhere here to put a secret, because a key is generated rather than chosen — one somebody picks is one somebody reuses somewhere else.",
197
+ description: "What a merchant asks for when they want another key: the name they will know it by, and nothing else. The name is one line of at most 100 characters, not empty and not padded with spaces; on the live deployment it is also how the message telling the merchant of the new key names it. There is nowhere here to put a secret, because a key is generated rather than chosen — one somebody picks is one somebody reuses somewhere else.",
171
198
  });
172
199
  /**
173
200
  * A key that has just been made: the row, and the secret, once.
@@ -306,6 +333,31 @@ export const SellerNameRequestSchema = z
306
333
  .meta({
307
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.",
308
335
  });
336
+ /**
337
+ * A change of the wallet that has been asked for, announced, and has not taken
338
+ * effect yet.
339
+ *
340
+ * It exists because a replacement does not apply at once where the money is
341
+ * 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
343
+ * one sitting in their own server's environment — so on the live deployment a
344
+ * replacement is told to every account of the merchant first and takes effect
345
+ * forty-eight hours after that. What this document says is the two facts a
346
+ * merchant needs in that window: what replaces the address, and from when.
347
+ *
348
+ * Both are required. An address with no moment says nothing about when the
349
+ * money moves, and a moment with no address says it moves without saying where.
350
+ */
351
+ export const PendingPayoutWalletSchema = z
352
+ .strictObject({
353
+ /** The address the merchant will be paid at once the wait is over. */
354
+ payout_wallet: EvmAddressSchema,
355
+ /** The moment it replaces the address paid now. */
356
+ takes_effect_at: TimestampSchema,
357
+ })
358
+ .meta({
359
+ description: "A replacement wallet that has been asked for and announced and has not taken effect yet. payout_wallet is the address sales will be paid into from takes_effect_at on, in the mixed-case spelling a wallet shows; until that moment every payment request still names the address paid now. The change takes effect then only if it is still the one waiting: asking for the address paid now cancels it, and asking for a different address replaces it and starts the wait again.",
360
+ });
309
361
  /**
310
362
  * The wallet a merchant's sales are paid into, as the merchant reads it back.
311
363
  *
@@ -329,14 +381,30 @@ export const SellerNameRequestSchema = z
329
381
  * address in lower case they cannot tell it from a different address without
330
382
  * comparing character by character. On the one field money is sent to, that
331
383
  * glance is the whole of the checking anybody does.
384
+ *
385
+ * `pending` is the change waiting beside it, and it is always present for the
386
+ * same reason `payout_wallet` is: null says nothing is waiting, and an absent
387
+ * field would be a silence. It is the field that keeps a caller from taking its
388
+ * own write for a failure — a merchant who asked for a new address on the live
389
+ * deployment reads the old one back, correctly, for forty-eight hours, and
390
+ * without this they would ask again, or conclude the change was lost.
391
+ *
392
+ * It is carried without moving `CONTRACT_VERSION`, which is the one known
393
+ * exception to the rule that a new required field moves it (ADR-0006 §2): no
394
+ * worker of the SDK reads this route, so the version would stop every
395
+ * installed worker for a field none of them sees. What that costs is that a
396
+ * merchant's own code holding this schema from an older release of this
397
+ * package refuses the answer until the package is upgraded.
332
398
  */
333
399
  export const PayoutWalletSchema = z
334
400
  .strictObject({
335
- /** Where this merchant's sales are paid, or nothing at all. */
401
+ /** Where this merchant's sales are paid now, or nothing at all. */
336
402
  payout_wallet: EvmAddressSchema.nullable(),
403
+ /** A replacement that has been announced and is waiting, or nothing. */
404
+ pending: PendingPayoutWalletSchema.nullable(),
337
405
  })
338
406
  .meta({
339
- description: "The address a merchant's sales are paid into. Payments are not held by anybody on the way: a buyer's agent pays this address directly, and it is the payTo of every payment request made for this merchant's products. Null means nobody has set one, which is where every merchant starts; the field is always present rather than left out, because an absent field is indistinguishable from a client that dropped it. The address comes back in the mixed-case spelling a wallet shows, whichever of the two accepted spellings was sent — so what a merchant reads back on a screen is character for character what they copied out of their wallet. On a deployment that settles on a real chain a merchant with no wallet here cannot publish a card, because the money from that card's sales would have nowhere to go.",
407
+ description: "The address a merchant's sales are paid into, and any change of it that is waiting. Payments are not held by anybody on the way: a buyer's agent pays payout_wallet directly, and it is the payTo of every payment request made for this merchant's products now. Null means nobody has set one, which is where every merchant starts; the field is always present rather than left out, because an absent field is indistinguishable from a client that dropped it. The address comes back in the mixed-case spelling a wallet shows, whichever of the two accepted spellings was sent — so what a merchant reads back on a screen is character for character what they copied out of their wallet. On a deployment that settles on a real chain a merchant with no wallet here cannot publish a card, because the money from that card's sales would have nowhere to go. pending is a replacement that has been asked for and has not taken effect: on the live deployment a merchant who already has a wallet and asks for a different one is told of it by message, and the new address takes effect forty-eight hours later, so until takes_effect_at this answer names the address still paid and the waiting one beside it. A caller reading its old address back beside a pending change has not failed to write; the change is waiting. Null means nothing is waiting, which is every answer on the test channel and in a sandbox, where a change applies at once.",
340
408
  });
341
409
  /**
342
410
  * What a merchant sends to change where their sales are paid.
@@ -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/dist/results.d.ts CHANGED
@@ -194,6 +194,30 @@ export declare const CallErrorSchema: z.ZodObject<{
194
194
  * writes the branch that reads it.
195
195
  */
196
196
  export declare const CARD_REJECTED = "card_rejected";
197
+ /**
198
+ * The findings of a refused publish that are about the merchant rather than
199
+ * the card.
200
+ *
201
+ * Each arrives in the refusal's `problems` with an empty path, beside whatever
202
+ * is wrong with the card, and none of them is cleared by editing the card: the
203
+ * merchant has no name set for buyers to read, no wallet set for their sales to
204
+ * be paid into, or no approval from the operator for the live catalog. The
205
+ * name is set with a call of the merchant's own (`POST /v0/seller-name`) or in
206
+ * the cabinet; the wallet in the cabinet's Settings alone, since no key of the
207
+ * merchant's code may say where the money goes; and the approval is the
208
+ * operator's decision, with no call. The name is asked for everywhere, the
209
+ * wallet wherever a payment settles and the approval on the live deployment
210
+ * alone, as the publish route's description says; a program needs no copy of
211
+ * that rule, because it learns what is missing where it is refused, at the
212
+ * publish.
213
+ */
214
+ export declare const MERCHANT_FINDINGS: Readonly<{
215
+ readonly NO_SELLER_NAME: "no_seller_name";
216
+ readonly NO_PAYOUT_WALLET: "no_payout_wallet";
217
+ readonly NO_OPERATOR_APPROVAL: "no_operator_approval";
218
+ }>;
219
+ /** One of the findings about the merchant, as its code travels on the wire. */
220
+ export type MerchantFinding = (typeof MERCHANT_FINDINGS)[keyof typeof MERCHANT_FINDINGS];
197
221
  /**
198
222
  * The answer to publishing a card: the catalog id, or what stands in its way.
199
223
  *
package/dist/results.js CHANGED
@@ -167,8 +167,16 @@ export const ProblemSchema = z
167
167
  * indistinguishable from a path nobody filled in.
168
168
  */
169
169
  path: z.array(z.string()),
170
- /** What kind of finding it is, for the code that reads it. */
171
- code: z.string().regex(/\S/, "a finding carries a code"),
170
+ /**
171
+ * What kind of finding it is, for the code that reads it.
172
+ *
173
+ * Open, because a finding about a field carries whatever the check that
174
+ * found it calls it. The three about the merchant are promised, and they
175
+ * are described for the export for the reason the error's codes are.
176
+ */
177
+ code: z.string().regex(/\S/, "a finding carries a code").meta({
178
+ description: 'What kind of finding it is, for the program that reads it. The set is open: a finding about a field of what was sent carries the name the check gave it. Three are promised, always with an empty path, and each says the merchant rather than the card is missing something, so no edit to the card clears it: "no_seller_name" (no name set for buyers to read), "no_payout_wallet" (no wallet set for the sales to be paid into, asked for wherever a payment settles) and "no_operator_approval" (the operator has not admitted this merchant to the live catalog, which only the operator can change).',
179
+ }),
172
180
  /** The same finding in words, for the person who has to fix the card. */
173
181
  message: z.string().regex(/\S/, "a finding carries an explanation a person can read"),
174
182
  })
@@ -219,7 +227,7 @@ export const CallErrorSchema = z.strictObject({
219
227
  * is the field the whole shape is named after.
220
228
  */
221
229
  problems: z.array(ProblemSchema).min(1).optional().meta({
222
- description: 'What was wrong with what was sent, one finding at a time: where it is, a code for the program that reads it, and the same finding in words. Present where the call is refusing what it was handed — a card that was not published, a delivery that is not what its card declares — and absent where the refusal is about a state of the world instead: a refund already settled is about the order, not about a field of the request. Never empty where it is present, and a refused publish always carries it. This field is the complete account of what stands in the way, and it is the one to read the findings from. The error\'s "message" is a single line written to be read in a log and does not carry the list: it says how many findings there are, quotes one or a few of them, and marks the place where a long one was cut — so a reader can always tell a short refusal from a shortened account of a long one.',
230
+ description: "What was wrong with what was sent, one finding at a time: where it is, a code for the program that reads it, and the same finding in words. Present where the call is refusing what it was handed — a card that was not published, a delivery that is not what its card declares — and absent where the refusal is about a state of the world instead: a refund already settled is about the order, not about a field of the request. Never empty where it is present, and a refused publish always carries it. This field is the complete account of what stands in the way, and it is the one to read the findings from. The error's \"message\" is a single line written to be read in a log and does not carry the list. Where what stands in the way is the merchant's own — no seller name, no payout wallet, no operator approval — the message names each of those plainly; for the rest it says how many findings there are, quotes one of them, and marks the place where a long one was cut — so a reader can always tell a short refusal from a shortened account of a long one.",
223
231
  }),
224
232
  });
225
233
  /**
@@ -230,6 +238,28 @@ export const CallErrorSchema = z.strictObject({
230
238
  * writes the branch that reads it.
231
239
  */
232
240
  export const CARD_REJECTED = "card_rejected";
241
+ /**
242
+ * The findings of a refused publish that are about the merchant rather than
243
+ * the card.
244
+ *
245
+ * Each arrives in the refusal's `problems` with an empty path, beside whatever
246
+ * is wrong with the card, and none of them is cleared by editing the card: the
247
+ * merchant has no name set for buyers to read, no wallet set for their sales to
248
+ * be paid into, or no approval from the operator for the live catalog. The
249
+ * name is set with a call of the merchant's own (`POST /v0/seller-name`) or in
250
+ * the cabinet; the wallet in the cabinet's Settings alone, since no key of the
251
+ * merchant's code may say where the money goes; and the approval is the
252
+ * operator's decision, with no call. The name is asked for everywhere, the
253
+ * wallet wherever a payment settles and the approval on the live deployment
254
+ * alone, as the publish route's description says; a program needs no copy of
255
+ * that rule, because it learns what is missing where it is refused, at the
256
+ * publish.
257
+ */
258
+ export const MERCHANT_FINDINGS = Object.freeze({
259
+ NO_SELLER_NAME: "no_seller_name",
260
+ NO_PAYOUT_WALLET: "no_payout_wallet",
261
+ NO_OPERATOR_APPROVAL: "no_operator_approval",
262
+ });
233
263
  /**
234
264
  * The error a refused publish carries: the shared shape, with the findings made
235
265
  * required.
@@ -237,11 +267,10 @@ export const CARD_REJECTED = "card_rejected";
237
267
  * "Refused, and here is nothing" is the one answer a merchant cannot act on,
238
268
  * and publishing is the call where that would be easiest to send — a card is
239
269
  * refused precisely because something about it is wrong, so there is always
240
- * something to name. Not every finding is about the card. A merchant who has
241
- * set no name for buyers to read is refused here too, and so is one who has set
242
- * no wallet for their sales to be paid into; both ride in the same list, so one
243
- * answer carries everything standing between this card and the catalog rather
244
- * than handing it over one round trip at a time.
270
+ * something to name. Not every finding is about the card: the merchant's own
271
+ * missing settings (`MERCHANT_FINDINGS`) ride in the same list, so one answer
272
+ * carries everything standing between this card and the catalog rather than
273
+ * handing it over one round trip at a time.
245
274
  */
246
275
  const PublishRefusalSchema = CallErrorSchema.extend({
247
276
  problems: z.array(ProblemSchema).min(1),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuanu-ai/agentify-contracts",
3
- "version": "0.4.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": [