@elisym/commerce 0.1.0 → 0.3.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/README.md +11 -0
- package/dist/buyer.d.ts +921 -0
- package/dist/buyer.js +1799 -0
- package/dist/buyer.js.map +1 -0
- package/dist/chunk-DYXVY6VD.js +1464 -0
- package/dist/chunk-DYXVY6VD.js.map +1 -0
- package/dist/index.d.ts +39 -542
- package/dist/index.js +1 -1442
- package/dist/index.js.map +1 -1
- package/dist/verify-offer-BZ_a9x3L.d.ts +558 -0
- package/package.json +6 -2
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { EventTemplate, NostrEvent, Filter } from 'nostr-tools';
|
|
2
|
+
import { ChainConfig, Asset } from '@elisym/pay-core';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
/** Store profile (NIP-01), signed by the store key. */
|
|
6
|
+
declare const KIND_STORE_PROFILE = 0;
|
|
7
|
+
/** Payout addresses (NIP-A3 `payto` plus the elisym `accept` extension), signed by the owner key. */
|
|
8
|
+
declare const KIND_PAYTO = 10133;
|
|
9
|
+
/**
|
|
10
|
+
* "Store key S acts for owner O", signed by the owner key. Addressable, `d` = the
|
|
11
|
+
* store pubkey.
|
|
12
|
+
*
|
|
13
|
+
* PROVISIONAL: the number is not registered yet (spec, open question 1). It is
|
|
14
|
+
* named in one place so that fixing it is a one-line change.
|
|
15
|
+
*/
|
|
16
|
+
declare const KIND_STORE_AUTH = 30490;
|
|
17
|
+
/** Product listing (NIP-99, Gamma Markets), signed by the store key. */
|
|
18
|
+
declare const KIND_PRODUCT = 30402;
|
|
19
|
+
/** Inbox relays for gift-wrapped messages (NIP-17). */
|
|
20
|
+
declare const KIND_INBOX_RELAYS = 10050;
|
|
21
|
+
/** Order message inside a gift wrap: `type` 1 order, 2 payment request, 3 status (Gamma Markets). */
|
|
22
|
+
declare const KIND_ORDER_MESSAGE = 16;
|
|
23
|
+
/** Payment receipt inside a gift wrap (Gamma Markets). */
|
|
24
|
+
declare const KIND_PAYMENT_RECEIPT = 17;
|
|
25
|
+
/** NIP-09 deletion: an `a` tag on the owner's AUTH address revokes it, like `mode` revoked. */
|
|
26
|
+
declare const KIND_DELETION = 5;
|
|
27
|
+
declare const KIND_SEAL = 13;
|
|
28
|
+
declare const KIND_GIFT_WRAP = 1059;
|
|
29
|
+
/** Opt-in tag value that lists a product in the elisym aggregator. */
|
|
30
|
+
declare const ELISYM_NETWORK_TAG = "elisym";
|
|
31
|
+
/**
|
|
32
|
+
* What a payout wallet signs to prove it belongs to the owner. The owner pubkey
|
|
33
|
+
* and the CAIP-19 id are both inside, so a proof made for one owner or one asset
|
|
34
|
+
* cannot be replayed for another.
|
|
35
|
+
*/
|
|
36
|
+
declare const PAYTO_PROOF_PREFIX = "elisym-payto:v1";
|
|
37
|
+
/**
|
|
38
|
+
* The domain of an order's payment reference hash, so it can collide with no
|
|
39
|
+
* other hash the protocol makes. The store, buyer and order id follow it.
|
|
40
|
+
*/
|
|
41
|
+
declare const ORDER_PAYMENT_REFERENCE_PREFIX = "elisym-order-payment:v1";
|
|
42
|
+
declare const DELIVERY_METHODS: readonly ["download", "license", "access", "webhook", "api"];
|
|
43
|
+
type DeliveryMethod = (typeof DELIVERY_METHODS)[number];
|
|
44
|
+
declare const ORDER_STATUSES: readonly ["pending", "confirmed", "completed", "cancelled"];
|
|
45
|
+
type OrderStatus = (typeof ORDER_STATUSES)[number];
|
|
46
|
+
/** Gamma Markets visibility values under which a product can be bought. */
|
|
47
|
+
declare const PURCHASABLE_VISIBILITIES: readonly ["on-sale", "pre-order"];
|
|
48
|
+
/** A payout address younger than this is flagged: the owner key may have been taken over. */
|
|
49
|
+
declare const PAYOUT_COOLDOWN_SECS: number;
|
|
50
|
+
/** How far in the future an event's `created_at` may sit before it is ignored. */
|
|
51
|
+
declare const MAX_FUTURE_SKEW_SECS: number;
|
|
52
|
+
declare const LIMITS: {
|
|
53
|
+
readonly MAX_ORDER_ID_LENGTH: 64;
|
|
54
|
+
readonly MAX_TAG_VALUE_LENGTH: 1024;
|
|
55
|
+
readonly MAX_CONTENT_LENGTH: number;
|
|
56
|
+
readonly MAX_ITEMS_PER_ORDER: 50;
|
|
57
|
+
readonly MAX_NIP05_DOCUMENT_BYTES: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
interface Caip19 {
|
|
61
|
+
/** The full id, exactly as written. */
|
|
62
|
+
id: string;
|
|
63
|
+
caip2: string;
|
|
64
|
+
chain: ChainConfig;
|
|
65
|
+
/** The registry coin it names, on that chain's environment. */
|
|
66
|
+
asset: Asset;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parse a CAIP-19 id for a coin the payment registry knows: `solana:<ref>/token:<mint>`
|
|
70
|
+
* or `eip155:<id>/erc20:<lowercase contract>`. Anything else - an unknown chain,
|
|
71
|
+
* a namespace that does not fit the chain, a coin the registry does not hold -
|
|
72
|
+
* is `undefined`: an asset nothing can pay in is not an asset.
|
|
73
|
+
*/
|
|
74
|
+
declare function parseCaip19(id: string): Caip19 | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Whether a mixed-case EVM address carries a valid EIP-55 checksum. An address
|
|
77
|
+
* in one case has none to check and passes; a mixed-case one with a wrong
|
|
78
|
+
* checksum is a typo.
|
|
79
|
+
*/
|
|
80
|
+
declare function hasValidEvmChecksum(address: string): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The one canonical spelling of a payout address on a chain, or `undefined` if it
|
|
83
|
+
* is not one. EVM addresses are lowercase on the wire, so every comparison is
|
|
84
|
+
* plain equality; a virtual (TIP-1022) address is refused, as the payment rail does.
|
|
85
|
+
*/
|
|
86
|
+
declare function canonicalPayoutAddress(chain: ChainConfig, address: string): string | undefined;
|
|
87
|
+
|
|
88
|
+
/** The keys a merchant's domain vouches for. */
|
|
89
|
+
interface DomainKeys {
|
|
90
|
+
domain: string;
|
|
91
|
+
/**
|
|
92
|
+
* The NIP-05 name that was looked up (`_` for the domain itself). The answer
|
|
93
|
+
* vouches for that name only: a profile under another name cannot borrow it.
|
|
94
|
+
*/
|
|
95
|
+
name: string;
|
|
96
|
+
storePubkey?: string;
|
|
97
|
+
ownerPubkey?: string;
|
|
98
|
+
source: 'nostr.json' | 'dns';
|
|
99
|
+
}
|
|
100
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
101
|
+
/**
|
|
102
|
+
* A public DNS name: lowercase labels, at least two, no IP literal, no port. The
|
|
103
|
+
* domain comes from a store's own profile - attacker-controlled - so nothing
|
|
104
|
+
* else is fetched.
|
|
105
|
+
*/
|
|
106
|
+
declare function isPublicHostname(hostname: string): boolean;
|
|
107
|
+
/** Split `local@domain` (NIP-05; a bare domain means `_@domain`), lowercased, or `undefined`. */
|
|
108
|
+
declare function splitNip05(identifier: string): {
|
|
109
|
+
local: string;
|
|
110
|
+
domain: string;
|
|
111
|
+
} | undefined;
|
|
112
|
+
/** The store (under `local`) and the owner (under `owner`) a `nostr.json` names. */
|
|
113
|
+
declare function readNostrJson(document: unknown, local: string): Pick<DomainKeys, 'storePubkey' | 'ownerPubkey'>;
|
|
114
|
+
/** `v=elisym1; owner=npub1...; store=npub1...` - the DNS record for sites that cannot serve `/.well-known`. */
|
|
115
|
+
declare function readElisymTxt(record: string): Pick<DomainKeys, 'storePubkey' | 'ownerPubkey'>;
|
|
116
|
+
interface ResolveDomainOptions {
|
|
117
|
+
/**
|
|
118
|
+
* Defaults to the global `fetch`. A SERVER (resolver, merchant node) must pass
|
|
119
|
+
* a fetch that refuses private addresses: the domain comes from a store's
|
|
120
|
+
* profile, and an unguarded fetch is an SSRF.
|
|
121
|
+
*/
|
|
122
|
+
fetch?: FetchLike;
|
|
123
|
+
timeoutMs?: number;
|
|
124
|
+
dohEndpoint?: string;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The keys `nip05` vouches for, from `/.well-known/nostr.json` and then from the
|
|
128
|
+
* `_elisym` TXT record. `undefined` when the domain answers neither - which is
|
|
129
|
+
* not the same as vouching for OTHER keys, and the caller keeps the two apart.
|
|
130
|
+
*/
|
|
131
|
+
declare function resolveDomainKeys(nip05: string, options?: ResolveDomainOptions): Promise<DomainKeys | undefined>;
|
|
132
|
+
|
|
133
|
+
/** One payout address for one asset, as the owner published it. */
|
|
134
|
+
interface PayoutTarget {
|
|
135
|
+
caip19: Caip19;
|
|
136
|
+
/** Canonical spelling (lowercase on EVM). */
|
|
137
|
+
address: string;
|
|
138
|
+
/** The wallet signed the proof message for this owner and asset, and it checks out. */
|
|
139
|
+
walletSigned: boolean;
|
|
140
|
+
}
|
|
141
|
+
interface PaytoInput {
|
|
142
|
+
/** NIP-A3 `payto` entries, e.g. `{ type: 'solana', authority: '<address>' }`. */
|
|
143
|
+
payto?: readonly {
|
|
144
|
+
type: string;
|
|
145
|
+
authority: string;
|
|
146
|
+
}[];
|
|
147
|
+
accept: readonly {
|
|
148
|
+
caip19: string;
|
|
149
|
+
address: string;
|
|
150
|
+
signature?: string;
|
|
151
|
+
}[];
|
|
152
|
+
/**
|
|
153
|
+
* The owner key that will sign the event. Required with any wallet signature:
|
|
154
|
+
* a proof made for another owner or asset is checked here, since every reader
|
|
155
|
+
* would drop that address.
|
|
156
|
+
*/
|
|
157
|
+
ownerPubkey?: string;
|
|
158
|
+
createdAt?: number;
|
|
159
|
+
}
|
|
160
|
+
/** Build the owner's kind 10133 event. The caller signs it with the OWNER key. */
|
|
161
|
+
declare function buildPaytoEvent(input: PaytoInput): EventTemplate;
|
|
162
|
+
interface ParsedPayto {
|
|
163
|
+
targets: PayoutTarget[];
|
|
164
|
+
/** `accept` entries dropped: an unknown asset, a malformed address, or a proof that fails. */
|
|
165
|
+
rejected: {
|
|
166
|
+
tag: readonly string[];
|
|
167
|
+
reason: 'unknown_asset' | 'bad_address' | 'bad_proof';
|
|
168
|
+
}[];
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The payout targets an owner's 10133 declares, from its `accept` tags only.
|
|
172
|
+
*
|
|
173
|
+
* A bare NIP-A3 `payto` tag names no asset and no network environment, so it is
|
|
174
|
+
* shown to generic clients but never paid to by elisym: a payment always goes to
|
|
175
|
+
* an `accept` address for the exact CAIP-19 id. An `accept` tag whose wallet
|
|
176
|
+
* proof is present but wrong is DROPPED, not downgraded - a wrong proof is
|
|
177
|
+
* evidence of a mistake or of tampering, and neither is an address to pay.
|
|
178
|
+
*
|
|
179
|
+
* The caller has already checked the event's signature and that its author is
|
|
180
|
+
* the owner.
|
|
181
|
+
*/
|
|
182
|
+
declare function parsePayto(event: Pick<NostrEvent, 'pubkey' | 'tags'>): ParsedPayto;
|
|
183
|
+
|
|
184
|
+
interface StoreProfile {
|
|
185
|
+
name?: string;
|
|
186
|
+
about?: string;
|
|
187
|
+
picture?: string;
|
|
188
|
+
website?: string;
|
|
189
|
+
nip05?: string;
|
|
190
|
+
/** The owner pubkey the store points at (tag `owner`). One half of the two-way link. */
|
|
191
|
+
ownerPubkey?: string;
|
|
192
|
+
}
|
|
193
|
+
interface StoreProfileInput extends Omit<StoreProfile, 'ownerPubkey'> {
|
|
194
|
+
ownerPubkey: string;
|
|
195
|
+
createdAt?: number;
|
|
196
|
+
}
|
|
197
|
+
/** Build the store's kind 0. The caller signs it with the STORE key. */
|
|
198
|
+
declare function buildStoreProfileEvent(input: StoreProfileInput): EventTemplate;
|
|
199
|
+
/**
|
|
200
|
+
* Read a store's kind 0, or `undefined` when its content is not a JSON object.
|
|
201
|
+
* A field of the wrong type or past its limit is dropped on its own: other
|
|
202
|
+
* clients edit the same kind 0, and one odd field is not a missing profile.
|
|
203
|
+
*/
|
|
204
|
+
declare function parseStoreProfile(event: Pick<NostrEvent, 'content' | 'tags'>): StoreProfile | undefined;
|
|
205
|
+
|
|
206
|
+
declare const FREQUENCIES: readonly ["hour", "day", "week", "month", "year"];
|
|
207
|
+
declare const ENDPOINT_TYPES: readonly ["x402", "mpp"];
|
|
208
|
+
type PriceFrequency = (typeof FREQUENCIES)[number];
|
|
209
|
+
type EndpointType = (typeof ENDPOINT_TYPES)[number];
|
|
210
|
+
interface ProductPrice {
|
|
211
|
+
/** Decimal string, exactly as published: never a float. */
|
|
212
|
+
amount: string;
|
|
213
|
+
/** ISO 4217, e.g. `USD`. */
|
|
214
|
+
currency: string;
|
|
215
|
+
/** Subscriptions only (NIP-99). */
|
|
216
|
+
frequency?: PriceFrequency;
|
|
217
|
+
}
|
|
218
|
+
interface Product {
|
|
219
|
+
storePubkey: string;
|
|
220
|
+
d: string;
|
|
221
|
+
title: string;
|
|
222
|
+
summary?: string;
|
|
223
|
+
description: string;
|
|
224
|
+
price: ProductPrice;
|
|
225
|
+
images: string[];
|
|
226
|
+
topics: string[];
|
|
227
|
+
/** Gamma Markets visibility; absent means on sale. */
|
|
228
|
+
visibility: string;
|
|
229
|
+
delivery?: DeliveryMethod;
|
|
230
|
+
/** Opted into the elisym aggregator (`["network", "elisym"]`). */
|
|
231
|
+
listedOnElisym: boolean;
|
|
232
|
+
/** HTTP 402 entry points for agents. Never trusted on their own: a challenge must pay a 10133 address. */
|
|
233
|
+
endpoints: {
|
|
234
|
+
type: EndpointType;
|
|
235
|
+
url: string;
|
|
236
|
+
}[];
|
|
237
|
+
/** CAIP-19 ids of the assets the store accepts. Addresses come from the owner's 10133 only. */
|
|
238
|
+
accept: string[];
|
|
239
|
+
createdAt: number;
|
|
240
|
+
}
|
|
241
|
+
declare const ProductInputSchema: z.ZodObject<{
|
|
242
|
+
d: z.ZodString;
|
|
243
|
+
title: z.ZodString;
|
|
244
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
245
|
+
description: z.ZodString;
|
|
246
|
+
price: z.ZodObject<{
|
|
247
|
+
amount: z.ZodString;
|
|
248
|
+
currency: z.ZodString;
|
|
249
|
+
frequency: z.ZodOptional<z.ZodEnum<["hour", "day", "week", "month", "year"]>>;
|
|
250
|
+
}, "strip", z.ZodTypeAny, {
|
|
251
|
+
amount: string;
|
|
252
|
+
currency: string;
|
|
253
|
+
frequency?: "hour" | "day" | "week" | "month" | "year" | undefined;
|
|
254
|
+
}, {
|
|
255
|
+
amount: string;
|
|
256
|
+
currency: string;
|
|
257
|
+
frequency?: "hour" | "day" | "week" | "month" | "year" | undefined;
|
|
258
|
+
}>;
|
|
259
|
+
images: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
260
|
+
topics: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
261
|
+
visibility: z.ZodDefault<z.ZodString>;
|
|
262
|
+
delivery: z.ZodOptional<z.ZodEnum<["download", "license", "access", "webhook", "api"]>>;
|
|
263
|
+
listedOnElisym: z.ZodDefault<z.ZodBoolean>;
|
|
264
|
+
endpoints: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
265
|
+
type: z.ZodEnum<["x402", "mpp"]>;
|
|
266
|
+
url: z.ZodString;
|
|
267
|
+
}, "strip", z.ZodTypeAny, {
|
|
268
|
+
type: "x402" | "mpp";
|
|
269
|
+
url: string;
|
|
270
|
+
}, {
|
|
271
|
+
type: "x402" | "mpp";
|
|
272
|
+
url: string;
|
|
273
|
+
}>, "many">>;
|
|
274
|
+
accept: z.ZodArray<z.ZodString, "many">;
|
|
275
|
+
createdAt: z.ZodOptional<z.ZodNumber>;
|
|
276
|
+
}, "strip", z.ZodTypeAny, {
|
|
277
|
+
accept: string[];
|
|
278
|
+
d: string;
|
|
279
|
+
title: string;
|
|
280
|
+
description: string;
|
|
281
|
+
price: {
|
|
282
|
+
amount: string;
|
|
283
|
+
currency: string;
|
|
284
|
+
frequency?: "hour" | "day" | "week" | "month" | "year" | undefined;
|
|
285
|
+
};
|
|
286
|
+
images: string[];
|
|
287
|
+
topics: string[];
|
|
288
|
+
visibility: string;
|
|
289
|
+
listedOnElisym: boolean;
|
|
290
|
+
endpoints: {
|
|
291
|
+
type: "x402" | "mpp";
|
|
292
|
+
url: string;
|
|
293
|
+
}[];
|
|
294
|
+
createdAt?: number | undefined;
|
|
295
|
+
summary?: string | undefined;
|
|
296
|
+
delivery?: "download" | "license" | "access" | "webhook" | "api" | undefined;
|
|
297
|
+
}, {
|
|
298
|
+
accept: string[];
|
|
299
|
+
d: string;
|
|
300
|
+
title: string;
|
|
301
|
+
description: string;
|
|
302
|
+
price: {
|
|
303
|
+
amount: string;
|
|
304
|
+
currency: string;
|
|
305
|
+
frequency?: "hour" | "day" | "week" | "month" | "year" | undefined;
|
|
306
|
+
};
|
|
307
|
+
createdAt?: number | undefined;
|
|
308
|
+
summary?: string | undefined;
|
|
309
|
+
images?: string[] | undefined;
|
|
310
|
+
topics?: string[] | undefined;
|
|
311
|
+
visibility?: string | undefined;
|
|
312
|
+
delivery?: "download" | "license" | "access" | "webhook" | "api" | undefined;
|
|
313
|
+
listedOnElisym?: boolean | undefined;
|
|
314
|
+
endpoints?: {
|
|
315
|
+
type: "x402" | "mpp";
|
|
316
|
+
url: string;
|
|
317
|
+
}[] | undefined;
|
|
318
|
+
}>;
|
|
319
|
+
type ProductInput = z.input<typeof ProductInputSchema>;
|
|
320
|
+
/** Build a kind 30402 listing. The caller signs it with the STORE key. */
|
|
321
|
+
declare function buildProductEvent(input: ProductInput): EventTemplate;
|
|
322
|
+
/**
|
|
323
|
+
* Read a kind 30402 listing, or `undefined` when it lacks what a sale needs (a
|
|
324
|
+
* `d`, a title, a price). Unknown extras are ignored, so a plain NIP-99 listing
|
|
325
|
+
* from another client still reads - it just accepts nothing elisym can pay.
|
|
326
|
+
* The caller has checked the signature.
|
|
327
|
+
*/
|
|
328
|
+
declare function parseProduct(event: Pick<NostrEvent, 'kind' | 'pubkey' | 'tags' | 'content' | 'created_at'>): Product | undefined;
|
|
329
|
+
declare function isPurchasable(product: Pick<Product, 'visibility'>): boolean;
|
|
330
|
+
/** `30402:<store>:<d>`, the address an order's `item` tag names. */
|
|
331
|
+
declare function productAddress(product: Pick<Product, 'storePubkey' | 'd'>): string;
|
|
332
|
+
declare function encodeProductNaddr(product: Pick<Product, 'storePubkey' | 'd'>, relays?: string[]): string;
|
|
333
|
+
interface ProductPointer {
|
|
334
|
+
storePubkey: string;
|
|
335
|
+
d: string;
|
|
336
|
+
relays: string[];
|
|
337
|
+
}
|
|
338
|
+
/** Decode a product `naddr`, or `undefined` when it is not one. */
|
|
339
|
+
declare function decodeProductNaddr(naddr: string): ProductPointer | undefined;
|
|
340
|
+
/**
|
|
341
|
+
* The price in `asset` subunits. Only a one-off `USD` price paid in a USD-pegged
|
|
342
|
+
* coin at 1:1 has one; any other pairing needs a quote (quoted mode), so it is
|
|
343
|
+
* refused here rather than converted at a guessed rate.
|
|
344
|
+
*/
|
|
345
|
+
declare function priceInSubunits(price: ProductPrice, asset: Asset): bigint;
|
|
346
|
+
|
|
347
|
+
type Tags = readonly (readonly string[])[];
|
|
348
|
+
|
|
349
|
+
/** Whether `value` is an order id this protocol accepts: 8 to 64 of `A-Za-z0-9-`. */
|
|
350
|
+
declare function isOrderId(value: string): boolean;
|
|
351
|
+
interface OrderItem {
|
|
352
|
+
/** `30402:<store>:<d>` */
|
|
353
|
+
product: string;
|
|
354
|
+
quantity: number;
|
|
355
|
+
}
|
|
356
|
+
interface Money {
|
|
357
|
+
/** Decimal string, never a float. */
|
|
358
|
+
amount: string;
|
|
359
|
+
currency: string;
|
|
360
|
+
}
|
|
361
|
+
/** Buyer -> store: kind 16, type 1. */
|
|
362
|
+
interface OrderRequest {
|
|
363
|
+
type: 'order';
|
|
364
|
+
storePubkey: string;
|
|
365
|
+
orderId: string;
|
|
366
|
+
items: OrderItem[];
|
|
367
|
+
total: Money;
|
|
368
|
+
email?: string;
|
|
369
|
+
}
|
|
370
|
+
/** Store -> buyer: kind 16, type 2 (quoted mode). `payload` is opaque here: the payer parses it with pay-core. */
|
|
371
|
+
interface PaymentRequestMessage {
|
|
372
|
+
type: 'payment_request';
|
|
373
|
+
buyerPubkey: string;
|
|
374
|
+
orderId: string;
|
|
375
|
+
total: Money;
|
|
376
|
+
options: {
|
|
377
|
+
medium: string;
|
|
378
|
+
payload: string;
|
|
379
|
+
}[];
|
|
380
|
+
}
|
|
381
|
+
/** Store -> buyer: kind 16, type 3. Signed by the store, so it is the proof of purchase. */
|
|
382
|
+
interface OrderStatusMessage {
|
|
383
|
+
type: 'status';
|
|
384
|
+
buyerPubkey: string;
|
|
385
|
+
orderId: string;
|
|
386
|
+
status: OrderStatus;
|
|
387
|
+
/** Store-supplied and unchecked: show it as text, or open it only as an `https:` link. */
|
|
388
|
+
delivery?: {
|
|
389
|
+
method: DeliveryMethod;
|
|
390
|
+
value: string;
|
|
391
|
+
};
|
|
392
|
+
/** The payment the store credited: amounts in subunits of the paid asset. */
|
|
393
|
+
receipt?: {
|
|
394
|
+
medium: string;
|
|
395
|
+
tx: string;
|
|
396
|
+
amount: string;
|
|
397
|
+
fee: string;
|
|
398
|
+
};
|
|
399
|
+
refund?: {
|
|
400
|
+
tx: string;
|
|
401
|
+
amount: string;
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Buyer -> store: kind 17. A HINT that says where to look, never proof: the
|
|
406
|
+
* store verifies the transaction on chain before it credits anything.
|
|
407
|
+
*/
|
|
408
|
+
interface PaymentReceipt {
|
|
409
|
+
type: 'receipt';
|
|
410
|
+
storePubkey: string;
|
|
411
|
+
orderId: string;
|
|
412
|
+
payment: {
|
|
413
|
+
medium: string;
|
|
414
|
+
reference: string;
|
|
415
|
+
tx: string;
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
type OrderMessage = OrderRequest | PaymentRequestMessage | OrderStatusMessage | PaymentReceipt;
|
|
419
|
+
/**
|
|
420
|
+
* The unsigned rumor for an order message. It is never published as is: it goes
|
|
421
|
+
* through `wrapOrderMessage`, which seals it with the sender's key.
|
|
422
|
+
*/
|
|
423
|
+
declare function buildOrderMessage(message: OrderMessage, createdAt?: number): EventTemplate;
|
|
424
|
+
/**
|
|
425
|
+
* Read an unwrapped rumor as an order message, or `undefined` when it is not a
|
|
426
|
+
* well-formed one. Who SENT it is not checked here: `unwrapOrderMessage` gives
|
|
427
|
+
* the authenticated sender, and the caller matches it against the role (a status
|
|
428
|
+
* must come from the store, an order from the buyer who pays).
|
|
429
|
+
*/
|
|
430
|
+
declare function parseOrderMessage(rumor: {
|
|
431
|
+
kind: number;
|
|
432
|
+
tags: Tags;
|
|
433
|
+
}): OrderMessage | undefined;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* A: the merchant's domain names both the store and the owner.
|
|
437
|
+
* B: a hosted name (e.g. `shop@elisym.shop`) - trust in the name registrar.
|
|
438
|
+
* C: keys only - the owner is pinned on first use.
|
|
439
|
+
*/
|
|
440
|
+
type TrustLevel = 'A' | 'B' | 'C';
|
|
441
|
+
type OfferRefusal = 'bad_pointer' | 'product_missing' | 'product_not_on_sale' | 'store_profile_missing' | 'owner_unknown' | 'owner_mismatch' | 'owner_pin_mismatch' | 'domain_mismatch' | 'store_auth_missing' | 'store_auth_revoked' | 'store_auth_expired' | 'origin_mismatch' | 'payto_missing' | 'no_payout_for_accepted_assets';
|
|
442
|
+
type OfferWarning =
|
|
443
|
+
/** The profile names a domain that did not vouch for both keys; trust fell back to level C. */
|
|
444
|
+
'domain_unverified'
|
|
445
|
+
/** The page embedding the widget is not on the merchant's domain. */
|
|
446
|
+
| 'origin_mismatch'
|
|
447
|
+
/** No domain to compare the page with (levels B and C). */
|
|
448
|
+
| 'origin_unverifiable'
|
|
449
|
+
/** The newest payout event is younger than the cool-down: the owner key may be in new hands. */
|
|
450
|
+
| 'payout_recently_changed'
|
|
451
|
+
/** A payout address is not among the `knownPayouts` of earlier purchases. */
|
|
452
|
+
| 'payout_changed'
|
|
453
|
+
/** A payout address carries no wallet proof. */
|
|
454
|
+
| 'payout_unsigned'
|
|
455
|
+
/**
|
|
456
|
+
* No `pinnedOwnerPubkey` (a first purchase): nothing but this bundle names the
|
|
457
|
+
* owner, and a stolen store key can name a new one - at level A through a
|
|
458
|
+
* domain of its own. Pin the owner after paying and pass it next time.
|
|
459
|
+
*/
|
|
460
|
+
| 'owner_unpinned';
|
|
461
|
+
interface VerifiedOffer {
|
|
462
|
+
level: TrustLevel;
|
|
463
|
+
/** Levels A and B: the domain that vouches for the store. */
|
|
464
|
+
domain?: string;
|
|
465
|
+
storePubkey: string;
|
|
466
|
+
ownerPubkey: string;
|
|
467
|
+
profile: StoreProfile;
|
|
468
|
+
product: Product;
|
|
469
|
+
/** Where a payment for this offer may go: the owner's 10133 addresses for the assets the product accepts. */
|
|
470
|
+
payouts: PayoutTarget[];
|
|
471
|
+
paytoCreatedAt: number;
|
|
472
|
+
warnings: OfferWarning[];
|
|
473
|
+
}
|
|
474
|
+
type OfferVerification = {
|
|
475
|
+
ok: true;
|
|
476
|
+
offer: VerifiedOffer;
|
|
477
|
+
} | {
|
|
478
|
+
ok: false;
|
|
479
|
+
refusal: OfferRefusal;
|
|
480
|
+
message: string;
|
|
481
|
+
};
|
|
482
|
+
/**
|
|
483
|
+
* The signed events one offer rests on, as relays (or a resolver) handed them
|
|
484
|
+
* over. Nothing here is trusted: every event is checked for its signature, its
|
|
485
|
+
* author and its kind before it counts, and extras are ignored.
|
|
486
|
+
*/
|
|
487
|
+
interface OfferBundle {
|
|
488
|
+
events: readonly NostrEvent[];
|
|
489
|
+
/**
|
|
490
|
+
* What the store's `nip05` domain vouches for: keys, `'unreachable'` when it
|
|
491
|
+
* answered nothing, or absent when the profile names no domain or it was not
|
|
492
|
+
* looked up. The answer is unsigned: take it from the client's own lookup,
|
|
493
|
+
* never from a resolver.
|
|
494
|
+
*/
|
|
495
|
+
domain?: DomainKeys | 'unreachable';
|
|
496
|
+
}
|
|
497
|
+
interface EvaluateOfferOptions {
|
|
498
|
+
now?: number;
|
|
499
|
+
/** The origin of the page embedding the checkout (from `postMessage`'s `event.origin`). */
|
|
500
|
+
pageOrigin?: string;
|
|
501
|
+
/** Refuse, rather than warn, when the page is not on the merchant's domain - or its origin is not given. */
|
|
502
|
+
strictOrigin?: boolean;
|
|
503
|
+
/** Domains that issue hosted names (level B), e.g. `['elisym.shop']`. */
|
|
504
|
+
hostedDomains?: readonly string[];
|
|
505
|
+
/** The owner pinned at the first purchase from this store (TOFU). A new owner then needs a re-pin. */
|
|
506
|
+
pinnedOwnerPubkey?: string;
|
|
507
|
+
cooldownSecs?: number;
|
|
508
|
+
/**
|
|
509
|
+
* When an index (resolver, relay) first saw the newest 10133. The cool-down
|
|
510
|
+
* counts from the later of this and the event's own `created_at`, which the
|
|
511
|
+
* signer picks and can back-date.
|
|
512
|
+
*/
|
|
513
|
+
paytoFirstSeenAt?: number;
|
|
514
|
+
/**
|
|
515
|
+
* Payout addresses paid at earlier purchases from this store (TOFU). A payout
|
|
516
|
+
* outside this set is flagged `payout_changed`, however old its event claims to be.
|
|
517
|
+
* Leave it out on a first purchase: an empty list flags every address.
|
|
518
|
+
*/
|
|
519
|
+
knownPayouts?: readonly {
|
|
520
|
+
caip19: string;
|
|
521
|
+
address: string;
|
|
522
|
+
}[];
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Verify an offer from its signed events (spec section 3, steps 2-7 and 9's
|
|
526
|
+
* address set). Pure: no network, so the same code runs on relay results and
|
|
527
|
+
* on a resolver bundle, and a test feeds it exactly what it needs.
|
|
528
|
+
*
|
|
529
|
+
* The protocol fee (step 8) is not read here: the payer reads it from chain with
|
|
530
|
+
* `@elisym/pay-core` when it builds the payment.
|
|
531
|
+
*/
|
|
532
|
+
declare function evaluateOffer(pointer: {
|
|
533
|
+
storePubkey: string;
|
|
534
|
+
d: string;
|
|
535
|
+
}, bundle: OfferBundle, options?: EvaluateOfferOptions): OfferVerification;
|
|
536
|
+
/**
|
|
537
|
+
* Step 9: whether a recipient named by ANY other source - a payment request, an
|
|
538
|
+
* x402 `payTo`, an MPP challenge - is one of the owner's payout addresses for
|
|
539
|
+
* that asset. Anything else must not be paid.
|
|
540
|
+
*/
|
|
541
|
+
declare function isOfferPayout(offer: VerifiedOffer, caip19: string, recipient: string): boolean;
|
|
542
|
+
interface VerifyOfferDeps extends ResolveDomainOptions {
|
|
543
|
+
/**
|
|
544
|
+
* Query relays (or a resolver). The spec asks for at least two relays so one
|
|
545
|
+
* cannot hide the newest payout event; that is this function's job, and its
|
|
546
|
+
* results are all checked again here.
|
|
547
|
+
*/
|
|
548
|
+
fetchEvents: (filters: Filter[]) => Promise<NostrEvent[]>;
|
|
549
|
+
/**
|
|
550
|
+
* Replace the domain lookup. It must be the client's OWN nostr.json / DoH
|
|
551
|
+
* lookup: the answer is unsigned, so a resolver's copy is not evidence.
|
|
552
|
+
*/
|
|
553
|
+
resolveDomain?: (nip05: string) => Promise<DomainKeys | undefined>;
|
|
554
|
+
}
|
|
555
|
+
/** Collect an offer's events from relays and verify it (spec section 3). */
|
|
556
|
+
declare function verifyOffer(naddr: string, deps: VerifyOfferDeps, options?: EvaluateOfferOptions): Promise<OfferVerification>;
|
|
557
|
+
|
|
558
|
+
export { buildProductEvent as $, type ParsedPayto as A, type PaymentReceipt as B, type Caip19 as C, DELIVERY_METHODS as D, ELISYM_NETWORK_TAG as E, type FetchLike as F, type PaymentRequestMessage as G, type PayoutTarget as H, type PaytoInput as I, type PriceFrequency as J, KIND_DELETION as K, LIMITS as L, MAX_FUTURE_SKEW_SECS as M, type Product as N, type OrderMessage as O, PAYOUT_COOLDOWN_SECS as P, type ProductInput as Q, type ProductPointer as R, type ProductPrice as S, type ResolveDomainOptions as T, type StoreProfile as U, type StoreProfileInput as V, type TrustLevel as W, type VerifiedOffer as X, type VerifyOfferDeps as Y, buildOrderMessage as Z, buildPaytoEvent as _, type DeliveryMethod as a, buildStoreProfileEvent as a0, canonicalPayoutAddress as a1, decodeProductNaddr as a2, encodeProductNaddr as a3, evaluateOffer as a4, hasValidEvmChecksum as a5, isOfferPayout as a6, isOrderId as a7, isPublicHostname as a8, isPurchasable as a9, parseCaip19 as aa, parseOrderMessage as ab, parsePayto as ac, parseProduct as ad, parseStoreProfile as ae, priceInSubunits as af, productAddress as ag, readElisymTxt as ah, readNostrJson as ai, resolveDomainKeys as aj, splitNip05 as ak, verifyOffer as al, type DomainKeys as b, type EndpointType as c, type EvaluateOfferOptions as d, KIND_GIFT_WRAP as e, KIND_INBOX_RELAYS as f, KIND_ORDER_MESSAGE as g, KIND_PAYMENT_RECEIPT as h, KIND_PAYTO as i, KIND_PRODUCT as j, KIND_SEAL as k, KIND_STORE_AUTH as l, KIND_STORE_PROFILE as m, type Money as n, ORDER_PAYMENT_REFERENCE_PREFIX as o, ORDER_STATUSES as p, type OfferBundle as q, type OfferRefusal as r, type OfferVerification as s, type OfferWarning as t, type OrderItem as u, type OrderRequest as v, type OrderStatus as w, type OrderStatusMessage as x, PAYTO_PROOF_PREFIX as y, PURCHASABLE_VISIBILITIES as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elisym/commerce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "elisym commerce protocol: signed payout addresses, store authorization, Nostr product listings, private NIP-17 orders, and offer verification.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"commerce",
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
".": {
|
|
31
31
|
"types": "./dist/index.d.ts",
|
|
32
32
|
"default": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./buyer": {
|
|
35
|
+
"types": "./dist/buyer.d.ts",
|
|
36
|
+
"default": "./dist/buyer.js"
|
|
33
37
|
}
|
|
34
38
|
},
|
|
35
39
|
"publishConfig": {
|
|
@@ -44,7 +48,7 @@
|
|
|
44
48
|
"clean": "rm -rf dist"
|
|
45
49
|
},
|
|
46
50
|
"dependencies": {
|
|
47
|
-
"@elisym/pay-core": "~0.1.
|
|
51
|
+
"@elisym/pay-core": "~0.1.2",
|
|
48
52
|
"@noble/curves": "~2.0.1",
|
|
49
53
|
"@noble/hashes": "~2.0.1",
|
|
50
54
|
"@scure/base": "~2.0.0",
|