@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 +9 -1
- package/dist/api.js +12 -8
- package/dist/card.d.ts +6 -5
- package/dist/card.js +114 -11
- package/dist/index.d.ts +15 -4
- package/dist/index.js +6 -3
- package/dist/merchant.d.ts +38 -0
- package/dist/merchant.js +72 -4
- package/dist/plain-text.d.ts +60 -0
- package/dist/plain-text.js +191 -0
- package/dist/price-rule.d.ts +73 -0
- package/dist/price-rule.js +104 -0
- package/dist/primitives.d.ts +6 -6
- package/dist/primitives.js +6 -6
- package/dist/quote.js +3 -1
- package/dist/results.d.ts +24 -0
- package/dist/results.js +37 -8
- package/package.json +1 -1
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,
|
|
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,
|
|
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
|
|
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
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
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
|
-
|
|
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
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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 & or ’, 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
|
-
/**
|
|
692
|
-
|
|
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,
|
package/dist/merchant.d.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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 & 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 & 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: `é` 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
|
+
}
|
package/dist/primitives.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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. */
|
package/dist/primitives.js
CHANGED
|
@@ -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
|
|
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.
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
-
/**
|
|
171
|
-
|
|
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:
|
|
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
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
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.
|
|
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": [
|