@nuanu-ai/agentify-contracts 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +57 -215
- package/dist/api.js +75 -31
- package/dist/card.d.ts +130 -77
- package/dist/card.js +261 -63
- package/dist/index.d.ts +48 -184
- package/dist/index.js +14 -9
- package/dist/merchant.d.ts +16 -14
- package/dist/merchant.js +55 -27
- 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 +19 -6
- package/dist/primitives.js +20 -6
- package/dist/quote.js +3 -1
- package/dist/results.d.ts +5 -4
- package/dist/results.js +6 -5
- package/package.json +1 -1
package/dist/merchant.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* wallet their sales are paid into, and the keys they open the door with.
|
|
4
4
|
*
|
|
5
5
|
* The first two belong together because registering is the act that produces
|
|
6
|
-
* both: one call makes the merchant and the key its
|
|
6
|
+
* both: one call makes the merchant and the key its dashboard will call as them
|
|
7
7
|
* with, and what comes back carries that key once. Split across two files, a
|
|
8
8
|
* reader working out what registering leaves a merchant holding would have to
|
|
9
9
|
* read both to find that it is a key of a kind no list here carries. The name is here for the same reason read the other way round — it is a
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* nothing, while a flag answers only half.
|
|
26
26
|
*/
|
|
27
27
|
import { z } from "zod";
|
|
28
|
-
import { ServiceNameSchema } from "./card.js";
|
|
28
|
+
import { SellerSchema, SellerSiteSchema, ServiceNameSchema } from "./card.js";
|
|
29
29
|
import { EvmAddressSchema } from "./evm-address.js";
|
|
30
30
|
import { IdentifierSchema, TimestampSchema } from "./primitives.js";
|
|
31
31
|
/**
|
|
@@ -162,8 +162,8 @@ export const MerchantKeySchema = z
|
|
|
162
162
|
* broken.
|
|
163
163
|
*
|
|
164
164
|
* It is not always one of the keys beside it, and that is the thing a reader is
|
|
165
|
-
* likeliest to assume and be wrong about. A
|
|
166
|
-
*
|
|
165
|
+
* likeliest to assume and be wrong about. A dashboard calls with a key made for a
|
|
166
|
+
* dashboard, and those are in nobody's list, so a client that looked this
|
|
167
167
|
* identifier up among the rows would find nothing — which is an answer rather
|
|
168
168
|
* than an error, and a screen has to be built for it.
|
|
169
169
|
*
|
|
@@ -176,7 +176,7 @@ export const MerchantKeyListSchema = z
|
|
|
176
176
|
* The keys this merchant made for their own code, the revoked ones among
|
|
177
177
|
* them.
|
|
178
178
|
*
|
|
179
|
-
* The keys a
|
|
179
|
+
* The keys a dashboard holds are not here and never will be: the merchant did
|
|
180
180
|
* not issue one and has nothing to do with one. A list is what somebody
|
|
181
181
|
* acts on, and a row nobody has any business acting on is a row that only
|
|
182
182
|
* raises the question of why it will not go away.
|
|
@@ -186,7 +186,7 @@ export const MerchantKeyListSchema = z
|
|
|
186
186
|
this_call: IdentifierSchema,
|
|
187
187
|
})
|
|
188
188
|
.meta({
|
|
189
|
-
description: "The keys one merchant made for their own code, working and revoked together, and the identifier of the key this very call was made with. That last field is here because a merchant cannot disable the key they are holding: without it a screen would offer a button the gateway refuses. It is not always among the keys listed — a
|
|
189
|
+
description: "The keys one merchant made for their own code, working and revoked together, and the identifier of the key this very call was made with. That last field is here because a merchant cannot disable the key they are holding: without it a screen would offer a button the gateway refuses. It is not always among the keys listed — a dashboard calls with a key of its own, and those are in no list here — so a client matching it against the rows has to be built for finding none. The keys a dashboard holds are left out entirely: they are not issued by the merchant and cannot be revoked by them. This document does not say whether it is the whole list either — paging is not designed, and the absence of a field about it is not a promise that there is no more.",
|
|
190
190
|
});
|
|
191
191
|
/** What a merchant sends to have a key made. */
|
|
192
192
|
export const IssueKeyRequestSchema = z
|
|
@@ -211,7 +211,7 @@ export const IssuedKeySchema = z
|
|
|
211
211
|
secret: KeySecretSchema,
|
|
212
212
|
})
|
|
213
213
|
.meta({
|
|
214
|
-
description: "A key as it comes back from being issued: the row a merchant will see in their list from now on, and the key itself. It carries the key once. Three answers in this contract carry one — this, what registering gives back, and the key a
|
|
214
|
+
description: "A key as it comes back from being issued: the row a merchant will see in their list from now on, and the key itself. It carries the key once. Three answers in this contract carry one — this, what registering gives back, and the key a dashboard asks for — and nothing else does, because what is written down on our side is a digest. A key that is lost is replaced by a new one rather than read back.",
|
|
215
215
|
});
|
|
216
216
|
/**
|
|
217
217
|
* A key that has been revoked, as it now stands.
|
|
@@ -228,10 +228,10 @@ export const DisabledKeySchema = z
|
|
|
228
228
|
description: "The key that was just revoked, with the instant it stopped working on it, so a merchant reads back what happened rather than taking the call's word for it. Revoking a key that was already revoked answers this same way and keeps the first instant, because that is the true one and a retry after a dropped connection must not rewrite it.",
|
|
229
229
|
});
|
|
230
230
|
/**
|
|
231
|
-
* A key made for a
|
|
231
|
+
* A key made for a dashboard, which is the secret and nothing else.
|
|
232
232
|
*
|
|
233
233
|
* Every other answer that makes a key carries the row beside it, and this one
|
|
234
|
-
* cannot. A key made for a
|
|
234
|
+
* cannot. A key made for a dashboard is in no merchant's list — they did not
|
|
235
235
|
* issue it and have no reason to know it exists — so an identifier here would
|
|
236
236
|
* name a row that no screen of theirs draws and no call of theirs reaches: not
|
|
237
237
|
* the list it is absent from, and not the revoking, which takes the keys a
|
|
@@ -239,13 +239,13 @@ export const DisabledKeySchema = z
|
|
|
239
239
|
* is put it on the row of whoever just signed in, and that is the whole of what
|
|
240
240
|
* it needs.
|
|
241
241
|
*/
|
|
242
|
-
export const
|
|
242
|
+
export const DashboardKeySchema = z
|
|
243
243
|
.strictObject({
|
|
244
244
|
/** The only moment this is readable. Nothing on our side keeps it. */
|
|
245
245
|
secret: KeySecretSchema,
|
|
246
246
|
})
|
|
247
247
|
.meta({
|
|
248
|
-
description: "A key made for a
|
|
248
|
+
description: "A key made for a dashboard to call as one merchant, carried once and readable nowhere afterwards. There is no row beside it and there is nothing to put one: a key made this way is in no merchant's list of keys, and the call that revokes a key refuses this kind by name — so an identifier for it would name something no answer shows and no call acts on. Whoever asked for this holds it until they ask for another.",
|
|
249
249
|
});
|
|
250
250
|
/**
|
|
251
251
|
* That the key a call was made with is gone.
|
|
@@ -257,7 +257,7 @@ export const CabinetKeySchema = z
|
|
|
257
257
|
* is done, and it is said in a field rather than left to a status code so that
|
|
258
258
|
* a client reads one document and not two kinds of evidence.
|
|
259
259
|
*/
|
|
260
|
-
export const
|
|
260
|
+
export const ForgottenDashboardKeySchema = z
|
|
261
261
|
.strictObject({
|
|
262
262
|
/** The key this call was made with no longer exists. */
|
|
263
263
|
forgotten: z.literal(true),
|
|
@@ -284,11 +284,20 @@ export const ForgottenCabinetKeySchema = z
|
|
|
284
284
|
*/
|
|
285
285
|
export const SellerNameSchema = z
|
|
286
286
|
.strictObject({
|
|
287
|
-
/**
|
|
288
|
-
|
|
287
|
+
/**
|
|
288
|
+
* What buyers read beside this merchant's products, or nothing — read back
|
|
289
|
+
* by the rule an agent's `seller` reads it by, which leaves the plain-text
|
|
290
|
+
* rule to the door that writes it.
|
|
291
|
+
*/
|
|
292
|
+
seller_name: SellerSchema.shape.name,
|
|
293
|
+
/**
|
|
294
|
+
* The https address of the merchant's own shop, where an agent takes what
|
|
295
|
+
* an order cannot answer (ADR-0034), or nothing where none was given.
|
|
296
|
+
*/
|
|
297
|
+
seller_site: SellerSiteSchema.nullable(),
|
|
289
298
|
})
|
|
290
299
|
.meta({
|
|
291
|
-
description: "The name a merchant's products are sold under: what a discovery catalog lists them under and what a buyer's agent is shown beside the price. Null means nobody has chosen one, which is where every merchant starts. The field is always present rather than left out when there is no name: an absent field would be indistinguishable from a client that dropped it. What a name may be is the catalog's rule rather than ours — at most 32 characters of printable ASCII — because a name outside it is dropped there in silence, so it is refused here where somebody is told. A merchant with no name cannot publish a card: a card published without one reaches a buyer's agent inside a payment request that names no seller at all.",
|
|
300
|
+
description: "The name a merchant's products are sold under: what a discovery catalog lists them under and what a buyer's agent is shown beside the price. Null means nobody has chosen one, which is where every merchant starts. The field is always present rather than left out when there is no name: an absent field would be indistinguishable from a client that dropped it. What a name may be is the catalog's rule rather than ours — at most 32 characters of printable ASCII — because a name outside it is dropped there in silence, so it is refused here where somebody is told. A merchant with no name cannot publish a card: a card published without one reaches a buyer's agent inside a payment request that names no seller at all. seller_site is the https address of the merchant's own shop, which every agent reads beside the name on their cards and orders as where to take what an order cannot answer; null means none was given.",
|
|
292
301
|
});
|
|
293
302
|
/**
|
|
294
303
|
* What a merchant sends to change what their products are sold under.
|
|
@@ -303,7 +312,7 @@ export const SellerNameSchema = z
|
|
|
303
312
|
* which is this same call, or an end to selling, which is the pause — and the
|
|
304
313
|
* pause leaves their cards where they can find them again.
|
|
305
314
|
*
|
|
306
|
-
* So it is two documents rather than one, and a
|
|
315
|
+
* So it is two documents rather than one, and a dashboard still reads back the
|
|
307
316
|
* shape it sent. The message on a null is written here rather than left to a
|
|
308
317
|
* type error, because "expected string, received null" describes the shape and
|
|
309
318
|
* says nothing about which act the sender was reaching for.
|
|
@@ -311,7 +320,8 @@ export const SellerNameSchema = z
|
|
|
311
320
|
export const SellerNameRequestSchema = z
|
|
312
321
|
.strictObject({
|
|
313
322
|
/**
|
|
314
|
-
* What buyers are to read beside this merchant's products
|
|
323
|
+
* What buyers are to read beside this merchant's products, where it is
|
|
324
|
+
* changing.
|
|
315
325
|
*
|
|
316
326
|
* The rule lives once, in `ServiceNameSchema`, and this reaches it through
|
|
317
327
|
* a string that carries its own words for "this is not a name at all". A
|
|
@@ -320,18 +330,36 @@ export const SellerNameRequestSchema = z
|
|
|
320
330
|
*/
|
|
321
331
|
seller_name: z
|
|
322
332
|
.string({
|
|
323
|
-
// A field
|
|
324
|
-
//
|
|
325
|
-
// sentence; the first falls through to the ordinary words about a
|
|
326
|
-
// field that is not there, which is what its author needs to read.
|
|
333
|
+
// A field holding null is a client with a misunderstanding, and only
|
|
334
|
+
// that gets this sentence.
|
|
327
335
|
error: (issue) => issue.input === undefined
|
|
328
336
|
? undefined
|
|
329
337
|
: "a seller name cannot be taken away, only changed: a merchant who wants to stop being listed pauses their selling, which leaves their cards where they can put them back on sale",
|
|
330
338
|
})
|
|
331
|
-
.pipe(ServiceNameSchema)
|
|
339
|
+
.pipe(ServiceNameSchema)
|
|
340
|
+
.optional(),
|
|
341
|
+
/**
|
|
342
|
+
* The https address of the merchant's own shop, where it is changing
|
|
343
|
+
* (ADR-0034). Like the name it is changed and never taken away: an agent
|
|
344
|
+
* holding an order that named a site has been told where to go, and a
|
|
345
|
+
* site that vanished from the same order would leave it nowhere.
|
|
346
|
+
*/
|
|
347
|
+
seller_site: z
|
|
348
|
+
.string({
|
|
349
|
+
error: (issue) => issue.input === undefined
|
|
350
|
+
? undefined
|
|
351
|
+
: "a seller's site cannot be taken away, only changed: send the address it has moved to",
|
|
352
|
+
})
|
|
353
|
+
.pipe(SellerSiteSchema)
|
|
354
|
+
.optional(),
|
|
355
|
+
})
|
|
356
|
+
.refine((asked) => asked.seller_name !== undefined || asked.seller_site !== undefined, {
|
|
357
|
+
// A client that dropped both fields has a bug, and is told what this call
|
|
358
|
+
// takes rather than anything about taking a name away.
|
|
359
|
+
message: "a request names seller_name, seller_site or both: one that names neither changes nothing",
|
|
332
360
|
})
|
|
333
361
|
.meta({
|
|
334
|
-
description: "What a merchant sends to change the name their products are sold under
|
|
362
|
+
description: "What a merchant sends to change the name their products are sold under, the address of their shop's own site, or both; a field left out stays as it was, and one of the two has to be there. The name is held to the catalog's rule rather than ours — at most 32 characters of printable ASCII — and is plain text. The site is an https origin and nothing after it, such as https://shop.example. Null is refused for either. A merchant goes from no name to a name and from one name to another, never back to none, because a payment request names the seller and there would be nobody to name: every card they have published would come off sale, which is an end to their selling arriving under the name of editing a setting. Somebody reaching for null wants one of two other things: a different name, which is this call with a different value, or an end to selling, which is the pause. A site is changed the same way and never taken away.",
|
|
335
363
|
});
|
|
336
364
|
/**
|
|
337
365
|
* A change of the wallet that has been asked for, announced, and has not taken
|
|
@@ -339,7 +367,7 @@ export const SellerNameRequestSchema = z
|
|
|
339
367
|
*
|
|
340
368
|
* It exists because a replacement does not apply at once where the money is
|
|
341
369
|
* real (ADR-0019). The address a merchant is paid at is the one setting whose
|
|
342
|
-
* change redirects money, and any key of theirs reaches it — the
|
|
370
|
+
* change redirects money, and any key of theirs reaches it — the dashboard's, or
|
|
343
371
|
* one sitting in their own server's environment — so on the live deployment a
|
|
344
372
|
* replacement is told to every account of the merchant first and takes effect
|
|
345
373
|
* forty-eight hours after that. What this document says is the two facts a
|
|
@@ -475,10 +503,10 @@ export const RegistrationRequestSchema = z
|
|
|
475
503
|
description: "What somebody sends to become a merchant: the invitation code they were given, and nothing else. The name their products are sold under is deliberately not here — it is a public answer, and asked for on the way in it is answered by somebody with no products and no idea what the name is for; it is set afterwards, and changed afterwards, through the merchant's own call for it. Nothing about an account is here either: an address and a password belong to whatever signs the person in, and are never sent to the gateway. A document carrying either is refused rather than trimmed, because a field accepted and dropped is somebody believing they said something.",
|
|
476
504
|
});
|
|
477
505
|
/**
|
|
478
|
-
* What registering answers with: a merchant and the key their
|
|
506
|
+
* What registering answers with: a merchant and the key their dashboard will call
|
|
479
507
|
* as them with.
|
|
480
508
|
*
|
|
481
|
-
* The key is made for a
|
|
509
|
+
* The key is made for a dashboard rather than for the merchant's own code, and
|
|
482
510
|
* that is what the caller of this route is. So it is in no list: a merchant who
|
|
483
511
|
* has just registered has no keys of their own at all, and the first one they
|
|
484
512
|
* do have is one they ask for. No row travels beside the secret for the same
|
|
@@ -497,5 +525,5 @@ export const RegisteredMerchantSchema = z
|
|
|
497
525
|
secret: KeySecretSchema,
|
|
498
526
|
})
|
|
499
527
|
.meta({
|
|
500
|
-
description: "What registering produced: the merchant, and the key whoever registered them will call as them with. The key is readable here and nowhere afterwards, so whoever made this call is the only party that can keep it. It is a key made for a
|
|
528
|
+
description: "What registering produced: the merchant, and the key whoever registered them will call as them with. The key is readable here and nowhere afterwards, so whoever made this call is the only party that can keep it. It is a key made for a dashboard rather than one of the merchant's own: it appears in no list of their keys and the call that revokes a key refuses its kind by name, so no row for it comes back here either. A merchant who has just registered has no keys of their own until they ask for one. The merchant is listed under no name yet and this answer carries none — the name their products are sold under is chosen afterwards, and until it is, publishing a card is refused. What this answer does not carry either is any notion of an account or a session: registering makes a merchant and a key, and whatever signs a person in is on the other side of this call.",
|
|
501
529
|
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain text: what the words on a card are written in.
|
|
3
|
+
*
|
|
4
|
+
* A card's title, its description and the title of each field it declares are
|
|
5
|
+
* read by a buying program exactly as they were written. Nothing between the
|
|
6
|
+
* merchant and the agent renders HTML, so markup reaches the agent as markup —
|
|
7
|
+
* `<p>Valid for twelve months.</p>` is read with its angle brackets, and
|
|
8
|
+
* `Coffee & 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
|
+
}
|