@elisym/commerce 0.1.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 ADDED
@@ -0,0 +1,38 @@
1
+ # @elisym/commerce
2
+
3
+ The elisym commerce protocol: sell digital goods to people and AI agents, paid directly to the merchant's wallet, with the catalog and orders on Nostr and no platform in the middle.
4
+
5
+ ```bash
6
+ npm install @elisym/commerce @elisym/pay-core nostr-tools @solana/kit @solana-program/system @solana-program/token @solana-program/memo decimal.js-light
7
+ ```
8
+
9
+ ## What is in it
10
+
11
+ | Piece | Kind | Signed by | Functions |
12
+ | ------------------- | ---------------------- | ------------- | ------------------------------------------------------------------- |
13
+ | Payout addresses | 10133 (NIP-A3) | owner | `buildPaytoEvent`, `parsePayto`, `verifyPaytoProof` |
14
+ | Store authorization | 30490 (provisional) | owner | `buildStoreAuthEvent`, `buildStoreRevocationEvent`, `readStoreAuth` |
15
+ | Store profile | 0 | store | `buildStoreProfileEvent`, `parseStoreProfile` |
16
+ | Product | 30402 (NIP-99, Gamma) | store | `buildProductEvent`, `parseProduct`, `priceInSubunits` |
17
+ | Orders, receipts | 16 / 17 in a gift wrap | buyer / store | `buildOrderMessage`, `wrapOrderMessage`, `unwrapOrderMessage` |
18
+ | Offer verification | - | - | `verifyOffer`, `evaluateOffer`, `isOfferPayout` |
19
+
20
+ ## The one rule
21
+
22
+ A payment goes only to an address from the owner's kind 10133 event. Any other source of an address (a payment request, an x402 or MPP challenge) must match it:
23
+
24
+ ```ts
25
+ import { isOfferPayout, verifyOffer } from '@elisym/commerce';
26
+
27
+ const result = await verifyOffer(naddr, { fetchEvents }, { pageOrigin });
28
+ if (!result.ok) throw new Error(result.message);
29
+ if (!isOfferPayout(result.offer, caip19, challenge.payTo)) throw new Error('Not the merchant');
30
+ ```
31
+
32
+ `fetchEvents` queries relays (at least two, so one relay cannot hide the newest payout event) or a resolver. Every event it returns is checked again for its signature, author and kind.
33
+
34
+ On a server, pass `verifyOffer` a `fetch` that refuses private addresses: the merchant's domain comes from the store's own profile.
35
+
36
+ ## License
37
+
38
+ MIT
@@ -0,0 +1,643 @@
1
+ import { ChainConfig, Asset } from '@elisym/pay-core';
2
+ import { EventTemplate, NostrEvent, Filter } from 'nostr-tools';
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
+ declare const DELIVERY_METHODS: readonly ["download", "license", "access", "webhook", "api"];
38
+ type DeliveryMethod = (typeof DELIVERY_METHODS)[number];
39
+ declare const ORDER_STATUSES: readonly ["pending", "confirmed", "completed", "cancelled"];
40
+ type OrderStatus = (typeof ORDER_STATUSES)[number];
41
+ /** Gamma Markets visibility values under which a product can be bought. */
42
+ declare const PURCHASABLE_VISIBILITIES: readonly ["on-sale", "pre-order"];
43
+ /** A payout address younger than this is flagged: the owner key may have been taken over. */
44
+ declare const PAYOUT_COOLDOWN_SECS: number;
45
+ /** How far in the future an event's `created_at` may sit before it is ignored. */
46
+ declare const MAX_FUTURE_SKEW_SECS: number;
47
+ declare const LIMITS: {
48
+ readonly MAX_ORDER_ID_LENGTH: 64;
49
+ readonly MAX_TAG_VALUE_LENGTH: 1024;
50
+ readonly MAX_CONTENT_LENGTH: number;
51
+ readonly MAX_ITEMS_PER_ORDER: 50;
52
+ readonly MAX_NIP05_DOCUMENT_BYTES: number;
53
+ };
54
+
55
+ interface Caip19 {
56
+ /** The full id, exactly as written. */
57
+ id: string;
58
+ caip2: string;
59
+ chain: ChainConfig;
60
+ /** The registry coin it names, on that chain's environment. */
61
+ asset: Asset;
62
+ }
63
+ /**
64
+ * Parse a CAIP-19 id for a coin the payment registry knows: `solana:<ref>/token:<mint>`
65
+ * or `eip155:<id>/erc20:<lowercase contract>`. Anything else - an unknown chain,
66
+ * a namespace that does not fit the chain, a coin the registry does not hold -
67
+ * is `undefined`: an asset nothing can pay in is not an asset.
68
+ */
69
+ declare function parseCaip19(id: string): Caip19 | undefined;
70
+ /**
71
+ * Whether a mixed-case EVM address carries a valid EIP-55 checksum. An address
72
+ * in one case has none to check and passes; a mixed-case one with a wrong
73
+ * checksum is a typo.
74
+ */
75
+ declare function hasValidEvmChecksum(address: string): boolean;
76
+ /**
77
+ * The one canonical spelling of a payout address on a chain, or `undefined` if it
78
+ * is not one. EVM addresses are lowercase on the wire, so every comparison is
79
+ * plain equality; a virtual (TIP-1022) address is refused, as the payment rail does.
80
+ */
81
+ declare function canonicalPayoutAddress(chain: ChainConfig, address: string): string | undefined;
82
+
83
+ /**
84
+ * The exact text a payout wallet signs for an `accept` tag. A Solana wallet signs
85
+ * its UTF-8 bytes with `signMessage`; an EVM wallet signs it with `personal_sign`
86
+ * (EIP-191).
87
+ */
88
+ declare function paytoProofMessage(ownerPubkey: string, caip19: string): string;
89
+ /**
90
+ * Whether `signature` proves that the wallet at `address` agreed to receive
91
+ * `caip19` payments for `ownerPubkey`.
92
+ *
93
+ * Encodings, one per family and nothing else: Solana - the 64-byte ed25519
94
+ * signature in base58, `address` the base58 public key; EVM - the 65-byte
95
+ * `r || s || v` signature as `0x` hex, as `personal_sign` returns it, `address`
96
+ * lowercase. Any malformed input is `false`, never a throw.
97
+ */
98
+ declare function verifyPaytoProof(params: {
99
+ chain: ChainConfig;
100
+ address: string;
101
+ ownerPubkey: string;
102
+ caip19: string;
103
+ signature: string;
104
+ }): boolean;
105
+ /** keccak256("\x19Ethereum Signed Message:\n" + len(message) + message). */
106
+ declare function eip191Hash(message: Uint8Array): Uint8Array;
107
+
108
+ /** The keys a merchant's domain vouches for. */
109
+ interface DomainKeys {
110
+ domain: string;
111
+ /**
112
+ * The NIP-05 name that was looked up (`_` for the domain itself). The answer
113
+ * vouches for that name only: a profile under another name cannot borrow it.
114
+ */
115
+ name: string;
116
+ storePubkey?: string;
117
+ ownerPubkey?: string;
118
+ source: 'nostr.json' | 'dns';
119
+ }
120
+ type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
121
+ /**
122
+ * A public DNS name: lowercase labels, at least two, no IP literal, no port. The
123
+ * domain comes from a store's own profile - attacker-controlled - so nothing
124
+ * else is fetched.
125
+ */
126
+ declare function isPublicHostname(hostname: string): boolean;
127
+ /** Split `local@domain` (NIP-05; a bare domain means `_@domain`), lowercased, or `undefined`. */
128
+ declare function splitNip05(identifier: string): {
129
+ local: string;
130
+ domain: string;
131
+ } | undefined;
132
+ /** The store (under `local`) and the owner (under `owner`) a `nostr.json` names. */
133
+ declare function readNostrJson(document: unknown, local: string): Pick<DomainKeys, 'storePubkey' | 'ownerPubkey'>;
134
+ /** `v=elisym1; owner=npub1...; store=npub1...` - the DNS record for sites that cannot serve `/.well-known`. */
135
+ declare function readElisymTxt(record: string): Pick<DomainKeys, 'storePubkey' | 'ownerPubkey'>;
136
+ interface ResolveDomainOptions {
137
+ /**
138
+ * Defaults to the global `fetch`. A SERVER (resolver, merchant node) must pass
139
+ * a fetch that refuses private addresses: the domain comes from a store's
140
+ * profile, and an unguarded fetch is an SSRF.
141
+ */
142
+ fetch?: FetchLike;
143
+ timeoutMs?: number;
144
+ dohEndpoint?: string;
145
+ }
146
+ /**
147
+ * The keys `nip05` vouches for, from `/.well-known/nostr.json` and then from the
148
+ * `_elisym` TXT record. `undefined` when the domain answers neither - which is
149
+ * not the same as vouching for OTHER keys, and the caller keeps the two apart.
150
+ */
151
+ declare function resolveDomainKeys(nip05: string, options?: ResolveDomainOptions): Promise<DomainKeys | undefined>;
152
+
153
+ /** One payout address for one asset, as the owner published it. */
154
+ interface PayoutTarget {
155
+ caip19: Caip19;
156
+ /** Canonical spelling (lowercase on EVM). */
157
+ address: string;
158
+ /** The wallet signed the proof message for this owner and asset, and it checks out. */
159
+ walletSigned: boolean;
160
+ }
161
+ interface PaytoInput {
162
+ /** NIP-A3 `payto` entries, e.g. `{ type: 'solana', authority: '<address>' }`. */
163
+ payto?: readonly {
164
+ type: string;
165
+ authority: string;
166
+ }[];
167
+ accept: readonly {
168
+ caip19: string;
169
+ address: string;
170
+ signature?: string;
171
+ }[];
172
+ /**
173
+ * The owner key that will sign the event. Required with any wallet signature:
174
+ * a proof made for another owner or asset is checked here, since every reader
175
+ * would drop that address.
176
+ */
177
+ ownerPubkey?: string;
178
+ createdAt?: number;
179
+ }
180
+ /** Build the owner's kind 10133 event. The caller signs it with the OWNER key. */
181
+ declare function buildPaytoEvent(input: PaytoInput): EventTemplate;
182
+ interface ParsedPayto {
183
+ targets: PayoutTarget[];
184
+ /** `accept` entries dropped: an unknown asset, a malformed address, or a proof that fails. */
185
+ rejected: {
186
+ tag: readonly string[];
187
+ reason: 'unknown_asset' | 'bad_address' | 'bad_proof';
188
+ }[];
189
+ }
190
+ /**
191
+ * The payout targets an owner's 10133 declares, from its `accept` tags only.
192
+ *
193
+ * A bare NIP-A3 `payto` tag names no asset and no network environment, so it is
194
+ * shown to generic clients but never paid to by elisym: a payment always goes to
195
+ * an `accept` address for the exact CAIP-19 id. An `accept` tag whose wallet
196
+ * proof is present but wrong is DROPPED, not downgraded - a wrong proof is
197
+ * evidence of a mistake or of tampering, and neither is an address to pay.
198
+ *
199
+ * The caller has already checked the event's signature and that its author is
200
+ * the owner.
201
+ */
202
+ declare function parsePayto(event: Pick<NostrEvent, 'pubkey' | 'tags'>): ParsedPayto;
203
+
204
+ type StoreAuthMode = 'self-host' | 'hosted' | 'revoked';
205
+ interface StoreAuthInput {
206
+ storePubkey: string;
207
+ mode: Exclude<StoreAuthMode, 'revoked'>;
208
+ /** Hosted mode: who runs the store, e.g. `elisym.network`. */
209
+ operator?: string;
210
+ /** Unix seconds (NIP-40). Short windows limit the damage of a stolen store key. */
211
+ expiresAt?: number;
212
+ createdAt?: number;
213
+ }
214
+ /** `<kind>:<owner>:<store>`, the address a NIP-09 deletion names to revoke an AUTH. */
215
+ declare function storeAuthAddress(ownerPubkey: string, storePubkey: string): string;
216
+ /** Build the owner's authorization of a store key. The caller signs it with the OWNER key. */
217
+ declare function buildStoreAuthEvent(input: StoreAuthInput): EventTemplate;
218
+ /** Revoke a store key: the same address (`d`), `mode` revoked. Newest wins. */
219
+ declare function buildStoreRevocationEvent(storePubkey: string, createdAt?: number): EventTemplate;
220
+ type StoreAuthState = {
221
+ status: 'active';
222
+ mode: Exclude<StoreAuthMode, 'revoked'>;
223
+ operator?: string;
224
+ } | {
225
+ status: 'revoked';
226
+ } | {
227
+ status: 'expired';
228
+ } | {
229
+ status: 'malformed';
230
+ };
231
+ /**
232
+ * What an owner's newest AUTH event for `storePubkey` says at `now`. The caller
233
+ * has checked the signature and the author, and picked the newest event for the
234
+ * address. An event whose `d` or `p` names another store is malformed, never active.
235
+ */
236
+ declare function readStoreAuth(event: Pick<NostrEvent, 'kind' | 'tags'>, storePubkey: string, now?: number): StoreAuthState;
237
+
238
+ interface StoreProfile {
239
+ name?: string;
240
+ about?: string;
241
+ picture?: string;
242
+ website?: string;
243
+ nip05?: string;
244
+ /** The owner pubkey the store points at (tag `owner`). One half of the two-way link. */
245
+ ownerPubkey?: string;
246
+ }
247
+ interface StoreProfileInput extends Omit<StoreProfile, 'ownerPubkey'> {
248
+ ownerPubkey: string;
249
+ createdAt?: number;
250
+ }
251
+ /** Build the store's kind 0. The caller signs it with the STORE key. */
252
+ declare function buildStoreProfileEvent(input: StoreProfileInput): EventTemplate;
253
+ /**
254
+ * Read a store's kind 0, or `undefined` when its content is not a JSON object.
255
+ * A field of the wrong type or past its limit is dropped on its own: other
256
+ * clients edit the same kind 0, and one odd field is not a missing profile.
257
+ */
258
+ declare function parseStoreProfile(event: Pick<NostrEvent, 'content' | 'tags'>): StoreProfile | undefined;
259
+
260
+ declare const FREQUENCIES: readonly ["hour", "day", "week", "month", "year"];
261
+ declare const ENDPOINT_TYPES: readonly ["x402", "mpp"];
262
+ type PriceFrequency = (typeof FREQUENCIES)[number];
263
+ type EndpointType = (typeof ENDPOINT_TYPES)[number];
264
+ interface ProductPrice {
265
+ /** Decimal string, exactly as published: never a float. */
266
+ amount: string;
267
+ /** ISO 4217, e.g. `USD`. */
268
+ currency: string;
269
+ /** Subscriptions only (NIP-99). */
270
+ frequency?: PriceFrequency;
271
+ }
272
+ interface Product {
273
+ storePubkey: string;
274
+ d: string;
275
+ title: string;
276
+ summary?: string;
277
+ description: string;
278
+ price: ProductPrice;
279
+ images: string[];
280
+ topics: string[];
281
+ /** Gamma Markets visibility; absent means on sale. */
282
+ visibility: string;
283
+ delivery?: DeliveryMethod;
284
+ /** Opted into the elisym aggregator (`["network", "elisym"]`). */
285
+ listedOnElisym: boolean;
286
+ /** HTTP 402 entry points for agents. Never trusted on their own: a challenge must pay a 10133 address. */
287
+ endpoints: {
288
+ type: EndpointType;
289
+ url: string;
290
+ }[];
291
+ /** CAIP-19 ids of the assets the store accepts. Addresses come from the owner's 10133 only. */
292
+ accept: string[];
293
+ createdAt: number;
294
+ }
295
+ declare const ProductInputSchema: z.ZodObject<{
296
+ d: z.ZodString;
297
+ title: z.ZodString;
298
+ summary: z.ZodOptional<z.ZodString>;
299
+ description: z.ZodString;
300
+ price: z.ZodObject<{
301
+ amount: z.ZodString;
302
+ currency: z.ZodString;
303
+ frequency: z.ZodOptional<z.ZodEnum<["hour", "day", "week", "month", "year"]>>;
304
+ }, "strip", z.ZodTypeAny, {
305
+ amount: string;
306
+ currency: string;
307
+ frequency?: "hour" | "day" | "week" | "month" | "year" | undefined;
308
+ }, {
309
+ amount: string;
310
+ currency: string;
311
+ frequency?: "hour" | "day" | "week" | "month" | "year" | undefined;
312
+ }>;
313
+ images: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
314
+ topics: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
315
+ visibility: z.ZodDefault<z.ZodString>;
316
+ delivery: z.ZodOptional<z.ZodEnum<["download", "license", "access", "webhook", "api"]>>;
317
+ listedOnElisym: z.ZodDefault<z.ZodBoolean>;
318
+ endpoints: z.ZodDefault<z.ZodArray<z.ZodObject<{
319
+ type: z.ZodEnum<["x402", "mpp"]>;
320
+ url: z.ZodString;
321
+ }, "strip", z.ZodTypeAny, {
322
+ type: "x402" | "mpp";
323
+ url: string;
324
+ }, {
325
+ type: "x402" | "mpp";
326
+ url: string;
327
+ }>, "many">>;
328
+ accept: z.ZodArray<z.ZodString, "many">;
329
+ createdAt: z.ZodOptional<z.ZodNumber>;
330
+ }, "strip", z.ZodTypeAny, {
331
+ accept: string[];
332
+ d: string;
333
+ title: string;
334
+ description: string;
335
+ price: {
336
+ amount: string;
337
+ currency: string;
338
+ frequency?: "hour" | "day" | "week" | "month" | "year" | undefined;
339
+ };
340
+ images: string[];
341
+ topics: string[];
342
+ visibility: string;
343
+ listedOnElisym: boolean;
344
+ endpoints: {
345
+ type: "x402" | "mpp";
346
+ url: string;
347
+ }[];
348
+ createdAt?: number | undefined;
349
+ summary?: string | undefined;
350
+ delivery?: "download" | "license" | "access" | "webhook" | "api" | undefined;
351
+ }, {
352
+ accept: string[];
353
+ d: string;
354
+ title: string;
355
+ description: string;
356
+ price: {
357
+ amount: string;
358
+ currency: string;
359
+ frequency?: "hour" | "day" | "week" | "month" | "year" | undefined;
360
+ };
361
+ createdAt?: number | undefined;
362
+ summary?: string | undefined;
363
+ images?: string[] | undefined;
364
+ topics?: string[] | undefined;
365
+ visibility?: string | undefined;
366
+ delivery?: "download" | "license" | "access" | "webhook" | "api" | undefined;
367
+ listedOnElisym?: boolean | undefined;
368
+ endpoints?: {
369
+ type: "x402" | "mpp";
370
+ url: string;
371
+ }[] | undefined;
372
+ }>;
373
+ type ProductInput = z.input<typeof ProductInputSchema>;
374
+ /** Build a kind 30402 listing. The caller signs it with the STORE key. */
375
+ declare function buildProductEvent(input: ProductInput): EventTemplate;
376
+ /**
377
+ * Read a kind 30402 listing, or `undefined` when it lacks what a sale needs (a
378
+ * `d`, a title, a price). Unknown extras are ignored, so a plain NIP-99 listing
379
+ * from another client still reads - it just accepts nothing elisym can pay.
380
+ * The caller has checked the signature.
381
+ */
382
+ declare function parseProduct(event: Pick<NostrEvent, 'kind' | 'pubkey' | 'tags' | 'content' | 'created_at'>): Product | undefined;
383
+ declare function isPurchasable(product: Pick<Product, 'visibility'>): boolean;
384
+ /** `30402:<store>:<d>`, the address an order's `item` tag names. */
385
+ declare function productAddress(product: Pick<Product, 'storePubkey' | 'd'>): string;
386
+ declare function encodeProductNaddr(product: Pick<Product, 'storePubkey' | 'd'>, relays?: string[]): string;
387
+ interface ProductPointer {
388
+ storePubkey: string;
389
+ d: string;
390
+ relays: string[];
391
+ }
392
+ /** Decode a product `naddr`, or `undefined` when it is not one. */
393
+ declare function decodeProductNaddr(naddr: string): ProductPointer | undefined;
394
+ /**
395
+ * The price in `asset` subunits. Only a one-off `USD` price paid in a USD-pegged
396
+ * coin at 1:1 has one; any other pairing needs a quote (quoted mode), so it is
397
+ * refused here rather than converted at a guessed rate.
398
+ */
399
+ declare function priceInSubunits(price: ProductPrice, asset: Asset): bigint;
400
+
401
+ type Tags = readonly (readonly string[])[];
402
+
403
+ interface OrderItem {
404
+ /** `30402:<store>:<d>` */
405
+ product: string;
406
+ quantity: number;
407
+ }
408
+ interface Money {
409
+ /** Decimal string, never a float. */
410
+ amount: string;
411
+ currency: string;
412
+ }
413
+ /** Buyer -> store: kind 16, type 1. */
414
+ interface OrderRequest {
415
+ type: 'order';
416
+ storePubkey: string;
417
+ orderId: string;
418
+ items: OrderItem[];
419
+ total: Money;
420
+ email?: string;
421
+ }
422
+ /** Store -> buyer: kind 16, type 2 (quoted mode). `payload` is opaque here: the payer parses it with pay-core. */
423
+ interface PaymentRequestMessage {
424
+ type: 'payment_request';
425
+ buyerPubkey: string;
426
+ orderId: string;
427
+ total: Money;
428
+ options: {
429
+ medium: string;
430
+ payload: string;
431
+ }[];
432
+ }
433
+ /** Store -> buyer: kind 16, type 3. Signed by the store, so it is the proof of purchase. */
434
+ interface OrderStatusMessage {
435
+ type: 'status';
436
+ buyerPubkey: string;
437
+ orderId: string;
438
+ status: OrderStatus;
439
+ /** Store-supplied and unchecked: show it as text, or open it only as an `https:` link. */
440
+ delivery?: {
441
+ method: DeliveryMethod;
442
+ value: string;
443
+ };
444
+ /** The payment the store credited: amounts in subunits of the paid asset. */
445
+ receipt?: {
446
+ medium: string;
447
+ tx: string;
448
+ amount: string;
449
+ fee: string;
450
+ };
451
+ refund?: {
452
+ tx: string;
453
+ amount: string;
454
+ };
455
+ }
456
+ /**
457
+ * Buyer -> store: kind 17. A HINT that says where to look, never proof: the
458
+ * store verifies the transaction on chain before it credits anything.
459
+ */
460
+ interface PaymentReceipt {
461
+ type: 'receipt';
462
+ storePubkey: string;
463
+ orderId: string;
464
+ payment: {
465
+ medium: string;
466
+ reference: string;
467
+ tx: string;
468
+ };
469
+ }
470
+ type OrderMessage = OrderRequest | PaymentRequestMessage | OrderStatusMessage | PaymentReceipt;
471
+ /**
472
+ * The unsigned rumor for an order message. It is never published as is: it goes
473
+ * through `wrapOrderMessage`, which seals it with the sender's key.
474
+ */
475
+ declare function buildOrderMessage(message: OrderMessage, createdAt?: number): EventTemplate;
476
+ /**
477
+ * Read an unwrapped rumor as an order message, or `undefined` when it is not a
478
+ * well-formed one. Who SENT it is not checked here: `unwrapOrderMessage` gives
479
+ * the authenticated sender, and the caller matches it against the role (a status
480
+ * must come from the store, an order from the buyer who pays).
481
+ */
482
+ declare function parseOrderMessage(rumor: {
483
+ kind: number;
484
+ tags: Tags;
485
+ }): OrderMessage | undefined;
486
+
487
+ interface WrappedOrderMessage {
488
+ /** Publish to the recipient's inbox relays (kind 10050). */
489
+ recipientWrap: NostrEvent;
490
+ /** The sender's own copy, for its history on another device. */
491
+ selfWrap: NostrEvent;
492
+ /** The rumor id: the same in both wraps, the key to dedupe on. */
493
+ rumorId: string;
494
+ }
495
+ /**
496
+ * Seal and gift-wrap an order message (NIP-59) for `recipientPubkey`, plus a copy
497
+ * for the sender. Relays see neither the content nor who talks to whom.
498
+ */
499
+ declare function wrapOrderMessage(rumor: EventTemplate, senderSecretKey: Uint8Array, recipientPubkey: string): WrappedOrderMessage;
500
+ interface UnwrappedOrderMessage {
501
+ rumorId: string;
502
+ /** Authenticated by the seal signature. */
503
+ senderPubkey: string;
504
+ /** The `p` tag of the rumor: who the message is addressed to. */
505
+ recipientPubkey: string;
506
+ createdAt: number;
507
+ message: OrderMessage;
508
+ }
509
+ /**
510
+ * Open a gift wrap addressed to `recipientSecretKey` and read the order message
511
+ * inside, or `undefined` for anything that is not a genuine one.
512
+ *
513
+ * This replaces `nip59.unwrapEvent`, which verifies nothing: it would take a seal
514
+ * signed by anyone and report whatever `pubkey` the rumor claims. Here the wrap
515
+ * and the seal signatures are checked, the rumor must claim the seal's signer,
516
+ * and its id must be its hash.
517
+ */
518
+ declare function unwrapOrderMessage(wrap: NostrEvent, recipientSecretKey: Uint8Array): UnwrappedOrderMessage | undefined;
519
+
520
+ /**
521
+ * A: the merchant's domain names both the store and the owner.
522
+ * B: a hosted name (e.g. `shop@elisym.shop`) - trust in the name registrar.
523
+ * C: keys only - the owner is pinned on first use.
524
+ */
525
+ type TrustLevel = 'A' | 'B' | 'C';
526
+ 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';
527
+ type OfferWarning =
528
+ /** The profile names a domain that did not vouch for both keys; trust fell back to level C. */
529
+ 'domain_unverified'
530
+ /** The page embedding the widget is not on the merchant's domain. */
531
+ | 'origin_mismatch'
532
+ /** No domain to compare the page with (levels B and C). */
533
+ | 'origin_unverifiable'
534
+ /** The newest payout event is younger than the cool-down: the owner key may be in new hands. */
535
+ | 'payout_recently_changed'
536
+ /** A payout address is not among the `knownPayouts` of earlier purchases. */
537
+ | 'payout_changed'
538
+ /** A payout address carries no wallet proof. */
539
+ | 'payout_unsigned'
540
+ /**
541
+ * No `pinnedOwnerPubkey` (a first purchase): nothing but this bundle names the
542
+ * owner, and a stolen store key can name a new one - at level A through a
543
+ * domain of its own. Pin the owner after paying and pass it next time.
544
+ */
545
+ | 'owner_unpinned';
546
+ interface VerifiedOffer {
547
+ level: TrustLevel;
548
+ /** Levels A and B: the domain that vouches for the store. */
549
+ domain?: string;
550
+ storePubkey: string;
551
+ ownerPubkey: string;
552
+ profile: StoreProfile;
553
+ product: Product;
554
+ /** Where a payment for this offer may go: the owner's 10133 addresses for the assets the product accepts. */
555
+ payouts: PayoutTarget[];
556
+ paytoCreatedAt: number;
557
+ warnings: OfferWarning[];
558
+ }
559
+ type OfferVerification = {
560
+ ok: true;
561
+ offer: VerifiedOffer;
562
+ } | {
563
+ ok: false;
564
+ refusal: OfferRefusal;
565
+ message: string;
566
+ };
567
+ /**
568
+ * The signed events one offer rests on, as relays (or a resolver) handed them
569
+ * over. Nothing here is trusted: every event is checked for its signature, its
570
+ * author and its kind before it counts, and extras are ignored.
571
+ */
572
+ interface OfferBundle {
573
+ events: readonly NostrEvent[];
574
+ /**
575
+ * What the store's `nip05` domain vouches for: keys, `'unreachable'` when it
576
+ * answered nothing, or absent when the profile names no domain or it was not
577
+ * looked up. The answer is unsigned: take it from the client's own lookup,
578
+ * never from a resolver.
579
+ */
580
+ domain?: DomainKeys | 'unreachable';
581
+ }
582
+ interface EvaluateOfferOptions {
583
+ now?: number;
584
+ /** The origin of the page embedding the checkout (from `postMessage`'s `event.origin`). */
585
+ pageOrigin?: string;
586
+ /** Refuse, rather than warn, when the page is not on the merchant's domain - or its origin is not given. */
587
+ strictOrigin?: boolean;
588
+ /** Domains that issue hosted names (level B), e.g. `['elisym.shop']`. */
589
+ hostedDomains?: readonly string[];
590
+ /** The owner pinned at the first purchase from this store (TOFU). A new owner then needs a re-pin. */
591
+ pinnedOwnerPubkey?: string;
592
+ cooldownSecs?: number;
593
+ /**
594
+ * When an index (resolver, relay) first saw the newest 10133. The cool-down
595
+ * counts from the later of this and the event's own `created_at`, which the
596
+ * signer picks and can back-date.
597
+ */
598
+ paytoFirstSeenAt?: number;
599
+ /**
600
+ * Payout addresses paid at earlier purchases from this store (TOFU). A payout
601
+ * outside this set is flagged `payout_changed`, however old its event claims to be.
602
+ * Leave it out on a first purchase: an empty list flags every address.
603
+ */
604
+ knownPayouts?: readonly {
605
+ caip19: string;
606
+ address: string;
607
+ }[];
608
+ }
609
+ /**
610
+ * Verify an offer from its signed events (spec section 3, steps 2-7 and 9's
611
+ * address set). Pure: no network, so the same code runs on relay results and
612
+ * on a resolver bundle, and a test feeds it exactly what it needs.
613
+ *
614
+ * The protocol fee (step 8) is not read here: the payer reads it from chain with
615
+ * `@elisym/pay-core` when it builds the payment.
616
+ */
617
+ declare function evaluateOffer(pointer: {
618
+ storePubkey: string;
619
+ d: string;
620
+ }, bundle: OfferBundle, options?: EvaluateOfferOptions): OfferVerification;
621
+ /**
622
+ * Step 9: whether a recipient named by ANY other source - a payment request, an
623
+ * x402 `payTo`, an MPP challenge - is one of the owner's payout addresses for
624
+ * that asset. Anything else must not be paid.
625
+ */
626
+ declare function isOfferPayout(offer: VerifiedOffer, caip19: string, recipient: string): boolean;
627
+ interface VerifyOfferDeps extends ResolveDomainOptions {
628
+ /**
629
+ * Query relays (or a resolver). The spec asks for at least two relays so one
630
+ * cannot hide the newest payout event; that is this function's job, and its
631
+ * results are all checked again here.
632
+ */
633
+ fetchEvents: (filters: Filter[]) => Promise<NostrEvent[]>;
634
+ /**
635
+ * Replace the domain lookup. It must be the client's OWN nostr.json / DoH
636
+ * lookup: the answer is unsigned, so a resolver's copy is not evidence.
637
+ */
638
+ resolveDomain?: (nip05: string) => Promise<DomainKeys | undefined>;
639
+ }
640
+ /** Collect an offer's events from relays and verify it (spec section 3). */
641
+ declare function verifyOffer(naddr: string, deps: VerifyOfferDeps, options?: EvaluateOfferOptions): Promise<OfferVerification>;
642
+
643
+ export { type Caip19, DELIVERY_METHODS, type DeliveryMethod, type DomainKeys, ELISYM_NETWORK_TAG, type EndpointType, type EvaluateOfferOptions, type FetchLike, KIND_DELETION, KIND_GIFT_WRAP, KIND_INBOX_RELAYS, KIND_ORDER_MESSAGE, KIND_PAYMENT_RECEIPT, KIND_PAYTO, KIND_PRODUCT, KIND_SEAL, KIND_STORE_AUTH, KIND_STORE_PROFILE, LIMITS, MAX_FUTURE_SKEW_SECS, type Money, ORDER_STATUSES, type OfferBundle, type OfferRefusal, type OfferVerification, type OfferWarning, type OrderItem, type OrderMessage, type OrderRequest, type OrderStatus, type OrderStatusMessage, PAYOUT_COOLDOWN_SECS, PAYTO_PROOF_PREFIX, PURCHASABLE_VISIBILITIES, type ParsedPayto, type PaymentReceipt, type PaymentRequestMessage, type PayoutTarget, type PaytoInput, type PriceFrequency, type Product, type ProductInput, type ProductPointer, type ProductPrice, type ResolveDomainOptions, type StoreAuthInput, type StoreAuthMode, type StoreAuthState, type StoreProfile, type StoreProfileInput, type TrustLevel, type UnwrappedOrderMessage, type VerifiedOffer, type VerifyOfferDeps, type WrappedOrderMessage, buildOrderMessage, buildPaytoEvent, buildProductEvent, buildStoreAuthEvent, buildStoreProfileEvent, buildStoreRevocationEvent, canonicalPayoutAddress, decodeProductNaddr, eip191Hash, encodeProductNaddr, evaluateOffer, hasValidEvmChecksum, isOfferPayout, isPublicHostname, isPurchasable, parseCaip19, parseOrderMessage, parsePayto, parseProduct, parseStoreProfile, paytoProofMessage, priceInSubunits, productAddress, readElisymTxt, readNostrJson, readStoreAuth, resolveDomainKeys, splitNip05, storeAuthAddress, unwrapOrderMessage, verifyOffer, verifyPaytoProof, wrapOrderMessage };