@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/dist/index.js ADDED
@@ -0,0 +1,1444 @@
1
+ import { chainByCaip2, isEvmWireAddress, assetsFor, isVirtualEvmAddress, parseAssetAmount } from '@elisym/pay-core';
2
+ import { keccak_256 } from '@noble/hashes/sha3.js';
3
+ import { hex, base58 } from '@scure/base';
4
+ import { ed25519 } from '@noble/curves/ed25519.js';
5
+ import { secp256k1 } from '@noble/curves/secp256k1.js';
6
+ import * as nip192 from 'nostr-tools/nip19';
7
+ import Decimal from 'decimal.js-light';
8
+ import { z } from 'zod';
9
+ import * as nip44 from 'nostr-tools/nip44';
10
+ import * as nip59 from 'nostr-tools/nip59';
11
+ import { getEventHash, getPublicKey, verifyEvent } from 'nostr-tools/pure';
12
+
13
+ // src/constants.ts
14
+ var KIND_STORE_PROFILE = 0;
15
+ var KIND_PAYTO = 10133;
16
+ var KIND_STORE_AUTH = 30490;
17
+ var KIND_PRODUCT = 30402;
18
+ var KIND_INBOX_RELAYS = 10050;
19
+ var KIND_ORDER_MESSAGE = 16;
20
+ var KIND_PAYMENT_RECEIPT = 17;
21
+ var KIND_DELETION = 5;
22
+ var KIND_SEAL = 13;
23
+ var KIND_GIFT_WRAP = 1059;
24
+ var ELISYM_NETWORK_TAG = "elisym";
25
+ var PAYTO_PROOF_PREFIX = "elisym-payto:v1";
26
+ var DELIVERY_METHODS = ["download", "license", "access", "webhook", "api"];
27
+ var ORDER_STATUSES = ["pending", "confirmed", "completed", "cancelled"];
28
+ var PURCHASABLE_VISIBILITIES = ["on-sale", "pre-order"];
29
+ var PAYOUT_COOLDOWN_SECS = 72 * 60 * 60;
30
+ var MAX_FUTURE_SKEW_SECS = 15 * 60;
31
+ var LIMITS = {
32
+ MAX_ORDER_ID_LENGTH: 64,
33
+ MAX_TAG_VALUE_LENGTH: 1024,
34
+ MAX_CONTENT_LENGTH: 64 * 1024,
35
+ MAX_ITEMS_PER_ORDER: 50,
36
+ MAX_NIP05_DOCUMENT_BYTES: 64 * 1024
37
+ };
38
+ var SOLANA_ADDRESS_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
39
+ var CAIP19_RE = /^([a-z0-9-]{3,8}:[-_a-zA-Z0-9]{1,32})\/(token|erc20):([-.%a-zA-Z0-9]{1,128})$/;
40
+ var SOLANA_PUBLIC_KEY_BYTES = 32;
41
+ var SOLANA_ZERO_KEY = "1".repeat(32);
42
+ var EVM_ZERO_ADDRESS = `0x${"0".repeat(40)}`;
43
+ var UTF8 = new TextEncoder();
44
+ function isSolanaPublicKey(address) {
45
+ try {
46
+ return base58.decode(address).length === SOLANA_PUBLIC_KEY_BYTES;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+ function parseCaip19(id) {
52
+ const match = CAIP19_RE.exec(id);
53
+ const caip2 = match?.[1];
54
+ const namespace = match?.[2];
55
+ const reference = match?.[3];
56
+ if (!caip2 || !namespace || !reference) {
57
+ return void 0;
58
+ }
59
+ const chain = chainByCaip2(caip2);
60
+ if (!chain) {
61
+ return void 0;
62
+ }
63
+ const expectedNamespace = chain.family === "solana" ? "token" : "erc20";
64
+ if (namespace !== expectedNamespace) {
65
+ return void 0;
66
+ }
67
+ if (chain.family === "evm" && !isEvmWireAddress(reference)) {
68
+ return void 0;
69
+ }
70
+ const asset = assetsFor(chain.slug, chain.network).find((coin) => coin.mint === reference);
71
+ if (!asset) {
72
+ return void 0;
73
+ }
74
+ return { id, caip2, chain, asset };
75
+ }
76
+ function hasValidEvmChecksum(address) {
77
+ const body = address.slice(2);
78
+ if (body === body.toLowerCase() || body === body.toUpperCase()) {
79
+ return true;
80
+ }
81
+ const digest = hex.encode(keccak_256(UTF8.encode(body.toLowerCase())));
82
+ for (let i = 0; i < body.length; i++) {
83
+ const char = body[i] ?? "";
84
+ const nibble = Number.parseInt(digest[i] ?? "0", 16);
85
+ const expected = nibble >= 8 ? char.toUpperCase() : char.toLowerCase();
86
+ if (char !== expected) {
87
+ return false;
88
+ }
89
+ }
90
+ return true;
91
+ }
92
+ function canonicalPayoutAddress(chain, address) {
93
+ if (chain.family === "solana") {
94
+ return SOLANA_ADDRESS_RE.test(address) && isSolanaPublicKey(address) && address !== SOLANA_ZERO_KEY ? address : void 0;
95
+ }
96
+ const lowered = address.toLowerCase();
97
+ if (!isEvmWireAddress(lowered) || isVirtualEvmAddress(lowered) || lowered === EVM_ZERO_ADDRESS) {
98
+ return void 0;
99
+ }
100
+ return lowered;
101
+ }
102
+ var EVM_SIGNATURE_RE = /^0x[0-9a-fA-F]{130}$/;
103
+ var ED25519_SIGNATURE_BYTES = 64;
104
+ var ED25519_PUBLIC_KEY_BYTES = 32;
105
+ var UTF82 = new TextEncoder();
106
+ function paytoProofMessage(ownerPubkey, caip19) {
107
+ return `${PAYTO_PROOF_PREFIX}:${ownerPubkey}:${caip19}`;
108
+ }
109
+ function verifyPaytoProof(params) {
110
+ const message = UTF82.encode(paytoProofMessage(params.ownerPubkey, params.caip19));
111
+ try {
112
+ return params.chain.family === "solana" ? verifyEd25519(message, params.signature, params.address) : verifyEip191(message, params.signature, params.address);
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+ function verifyEd25519(message, signature, address) {
118
+ const signatureBytes = base58.decode(signature);
119
+ const publicKey = base58.decode(address);
120
+ if (signatureBytes.length !== ED25519_SIGNATURE_BYTES || publicKey.length !== ED25519_PUBLIC_KEY_BYTES) {
121
+ return false;
122
+ }
123
+ return ed25519.verify(signatureBytes, message, publicKey, { zip215: false });
124
+ }
125
+ function eip191Hash(message) {
126
+ const prefix = UTF82.encode(`Ethereum Signed Message:
127
+ ${message.length}`);
128
+ const payload = new Uint8Array(prefix.length + message.length);
129
+ payload.set(prefix, 0);
130
+ payload.set(message, prefix.length);
131
+ return keccak_256(payload);
132
+ }
133
+ function verifyEip191(message, signature, address) {
134
+ if (!EVM_SIGNATURE_RE.test(signature)) {
135
+ return false;
136
+ }
137
+ const bytes = hex.decode(signature.slice(2).toLowerCase());
138
+ const recoveryByte = bytes[64];
139
+ if (recoveryByte === void 0) {
140
+ return false;
141
+ }
142
+ const recovery = recoveryByte >= 27 ? recoveryByte - 27 : recoveryByte;
143
+ if (recovery !== 0 && recovery !== 1) {
144
+ return false;
145
+ }
146
+ const recovered = new Uint8Array(65);
147
+ recovered[0] = recovery;
148
+ recovered.set(bytes.subarray(0, 64), 1);
149
+ const publicKey = secp256k1.recoverPublicKey(recovered, eip191Hash(message), {
150
+ prehash: false
151
+ });
152
+ return evmAddressOf(publicKey) === address.toLowerCase();
153
+ }
154
+ function evmAddressOf(publicKey) {
155
+ const uncompressed = secp256k1.Point.fromBytes(publicKey).toBytes(false);
156
+ const digest = keccak_256(uncompressed.subarray(1));
157
+ return `0x${hex.encode(digest.subarray(12))}`;
158
+ }
159
+
160
+ // src/tags.ts
161
+ var HEX_PUBKEY_RE = /^[0-9a-f]{64}$/;
162
+ function tagValue(tags, name) {
163
+ return tags.find((tag) => tag[0] === name)?.[1];
164
+ }
165
+ function tagsNamed(tags, name) {
166
+ return tags.filter((tag) => tag[0] === name);
167
+ }
168
+ function tagValues(tags, name) {
169
+ return tagsNamed(tags, name).map((tag) => tag[1]).filter((value) => value !== void 0);
170
+ }
171
+ var EXPIRATION_RE = /^\d{1,12}$/;
172
+ function expirationState(tags, now) {
173
+ const expirationTags = tagsNamed(tags, "expiration");
174
+ if (expirationTags.length === 0) {
175
+ return "none";
176
+ }
177
+ const expiration = expirationTags[0]?.[1] ?? "";
178
+ if (expirationTags.length > 1 || !EXPIRATION_RE.test(expiration)) {
179
+ return "malformed";
180
+ }
181
+ return Number(expiration) <= now ? "expired" : "active";
182
+ }
183
+ function nowSecs() {
184
+ return Math.floor(Date.now() / 1e3);
185
+ }
186
+
187
+ // src/domain.ts
188
+ var HOSTNAME_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
189
+ var LOCAL_PART_RE = /^[a-z0-9._-]{1,64}$/;
190
+ var MAX_HOSTNAME_LENGTH = 253;
191
+ var SPECIAL_USE_TOP_LEVELS = [
192
+ "localhost",
193
+ "local",
194
+ "internal",
195
+ "arpa",
196
+ "test",
197
+ "invalid",
198
+ "onion",
199
+ "lan",
200
+ "home",
201
+ "corp",
202
+ "intranet",
203
+ "private"
204
+ ];
205
+ var OWNER_NAME = "owner";
206
+ var TXT_PREFIX = "v=elisym1";
207
+ var DEFAULT_TIMEOUT_MS = 5e3;
208
+ var DEFAULT_DOH_ENDPOINT = "https://cloudflare-dns.com/dns-query";
209
+ var DNS_TYPE_TXT = 16;
210
+ function isPublicHostname(hostname) {
211
+ if (hostname.length === 0 || hostname.length > MAX_HOSTNAME_LENGTH) {
212
+ return false;
213
+ }
214
+ const labels = hostname.split(".");
215
+ if (labels.length < 2 || !labels.every((label) => HOSTNAME_LABEL_RE.test(label))) {
216
+ return false;
217
+ }
218
+ const topLevel = labels[labels.length - 1] ?? "";
219
+ return !/^(\d+|0x[0-9a-f]*)$/.test(topLevel) && !SPECIAL_USE_TOP_LEVELS.includes(topLevel);
220
+ }
221
+ function splitNip05(identifier) {
222
+ const lowered = identifier.trim().toLowerCase();
223
+ const at = lowered.lastIndexOf("@");
224
+ const local = at === -1 ? "_" : lowered.slice(0, at);
225
+ const domain = at === -1 ? lowered : lowered.slice(at + 1);
226
+ if (!LOCAL_PART_RE.test(local) || !isPublicHostname(domain)) {
227
+ return void 0;
228
+ }
229
+ return { local, domain };
230
+ }
231
+ function readHexKey(value) {
232
+ return typeof value === "string" && HEX_PUBKEY_RE.test(value) ? value : void 0;
233
+ }
234
+ function readNostrJson(document, local) {
235
+ const names = fieldOf(document, "names");
236
+ if (typeof names !== "object" || names === null) {
237
+ return {};
238
+ }
239
+ const entries = { ...names };
240
+ const keys = {};
241
+ const storePubkey = readHexKey(entries[local]);
242
+ const ownerPubkey = readHexKey(entries[OWNER_NAME]);
243
+ if (storePubkey) {
244
+ keys.storePubkey = storePubkey;
245
+ }
246
+ if (ownerPubkey) {
247
+ keys.ownerPubkey = ownerPubkey;
248
+ }
249
+ return keys;
250
+ }
251
+ function npubToHex(value) {
252
+ if (value === void 0) {
253
+ return void 0;
254
+ }
255
+ if (HEX_PUBKEY_RE.test(value)) {
256
+ return value;
257
+ }
258
+ try {
259
+ const decoded = nip192.decode(value);
260
+ return decoded.type === "npub" ? decoded.data : void 0;
261
+ } catch {
262
+ return void 0;
263
+ }
264
+ }
265
+ function readElisymTxt(record) {
266
+ const parts = record.split(";").map((part) => part.trim());
267
+ if (parts[0] !== TXT_PREFIX) {
268
+ return {};
269
+ }
270
+ const fields = /* @__PURE__ */ new Map();
271
+ for (const part of parts.slice(1)) {
272
+ const eq = part.indexOf("=");
273
+ if (eq > 0) {
274
+ const name = part.slice(0, eq).trim();
275
+ if (fields.has(name)) {
276
+ return {};
277
+ }
278
+ fields.set(name, part.slice(eq + 1).trim());
279
+ }
280
+ }
281
+ const keys = {};
282
+ const storePubkey = npubToHex(fields.get("store"));
283
+ const ownerPubkey = npubToHex(fields.get("owner"));
284
+ if (storePubkey) {
285
+ keys.storePubkey = storePubkey;
286
+ }
287
+ if (ownerPubkey) {
288
+ keys.ownerPubkey = ownerPubkey;
289
+ }
290
+ return keys;
291
+ }
292
+ async function readCappedJson(response) {
293
+ const reader = response.body?.getReader();
294
+ if (!reader) {
295
+ return void 0;
296
+ }
297
+ const chunks = [];
298
+ let total = 0;
299
+ for (; ; ) {
300
+ const { done, value } = await reader.read();
301
+ if (done) {
302
+ break;
303
+ }
304
+ total += value.byteLength;
305
+ if (total > LIMITS.MAX_NIP05_DOCUMENT_BYTES) {
306
+ await reader.cancel();
307
+ return void 0;
308
+ }
309
+ chunks.push(value);
310
+ }
311
+ const bytes = new Uint8Array(total);
312
+ let offset = 0;
313
+ for (const chunk of chunks) {
314
+ bytes.set(chunk, offset);
315
+ offset += chunk.byteLength;
316
+ }
317
+ try {
318
+ return JSON.parse(new TextDecoder().decode(bytes));
319
+ } catch {
320
+ return void 0;
321
+ }
322
+ }
323
+ function fieldOf(value, name) {
324
+ if (typeof value !== "object" || value === null) {
325
+ return void 0;
326
+ }
327
+ const record = { ...value };
328
+ return record[name];
329
+ }
330
+ function timeoutSignal(timeoutMs) {
331
+ return AbortSignal.timeout(timeoutMs);
332
+ }
333
+ async function fetchNostrJson(name, domain, fetchImpl, timeoutMs) {
334
+ const url = `https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`;
335
+ const response = await fetchImpl(url, {
336
+ redirect: "error",
337
+ signal: timeoutSignal(timeoutMs),
338
+ headers: { accept: "application/json" }
339
+ });
340
+ if (!response.ok) {
341
+ await response.body?.cancel();
342
+ return {};
343
+ }
344
+ return readNostrJson(await readCappedJson(response), name);
345
+ }
346
+ async function fetchNostrJsonKeys(local, domain, fetchImpl, timeoutMs) {
347
+ const keys = await fetchNostrJson(local, domain, fetchImpl, timeoutMs);
348
+ if (!keys.storePubkey) {
349
+ return void 0;
350
+ }
351
+ if (!keys.ownerPubkey && local === "_") {
352
+ try {
353
+ const owner = await fetchNostrJson(OWNER_NAME, domain, fetchImpl, timeoutMs);
354
+ if (owner.ownerPubkey) {
355
+ keys.ownerPubkey = owner.ownerPubkey;
356
+ }
357
+ } catch {
358
+ }
359
+ }
360
+ return { domain, name: local, source: "nostr.json", ...keys };
361
+ }
362
+ async function fetchTxtKeys(domain, name, fetchImpl, timeoutMs, dohEndpoint) {
363
+ const url = new URL(dohEndpoint);
364
+ url.searchParams.set("name", `_elisym.${domain}`);
365
+ url.searchParams.set("type", "TXT");
366
+ const response = await fetchImpl(url.toString(), {
367
+ signal: timeoutSignal(timeoutMs),
368
+ headers: { accept: "application/dns-json" }
369
+ });
370
+ if (!response.ok) {
371
+ await response.body?.cancel();
372
+ return void 0;
373
+ }
374
+ const answers = fieldOf(await readCappedJson(response), "Answer");
375
+ if (!Array.isArray(answers)) {
376
+ return void 0;
377
+ }
378
+ let found;
379
+ for (const answer of answers) {
380
+ const type = fieldOf(answer, "type");
381
+ const data = fieldOf(answer, "data");
382
+ if (type !== DNS_TYPE_TXT || typeof data !== "string") {
383
+ continue;
384
+ }
385
+ const record = data.replace(/"\s*"/g, "").replace(/^"|"$/g, "");
386
+ const keys = readElisymTxt(record);
387
+ if (!keys.storePubkey) {
388
+ continue;
389
+ }
390
+ if (found !== void 0 && (found.storePubkey !== keys.storePubkey || found.ownerPubkey !== keys.ownerPubkey)) {
391
+ return void 0;
392
+ }
393
+ found = keys;
394
+ }
395
+ return found === void 0 ? void 0 : { domain, name, source: "dns", ...found };
396
+ }
397
+ async function resolveDomainKeys(nip05, options = {}) {
398
+ const parts = splitNip05(nip05);
399
+ if (!parts) {
400
+ return void 0;
401
+ }
402
+ const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
403
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
404
+ try {
405
+ const fromJson = await fetchNostrJsonKeys(parts.local, parts.domain, fetchImpl, timeoutMs);
406
+ if (fromJson) {
407
+ return fromJson;
408
+ }
409
+ } catch {
410
+ }
411
+ try {
412
+ return await fetchTxtKeys(
413
+ parts.domain,
414
+ parts.local,
415
+ fetchImpl,
416
+ timeoutMs,
417
+ options.dohEndpoint ?? DEFAULT_DOH_ENDPOINT
418
+ );
419
+ } catch {
420
+ return void 0;
421
+ }
422
+ }
423
+
424
+ // src/events/payto.ts
425
+ function buildPaytoEvent(input) {
426
+ const tags = [];
427
+ for (const entry of input.payto ?? []) {
428
+ tags.push(["payto", entry.type, entry.authority]);
429
+ }
430
+ for (const entry of input.accept) {
431
+ const caip19 = parseCaip19(entry.caip19);
432
+ if (!caip19) {
433
+ throw new Error(`Not a payable CAIP-19 asset: ${entry.caip19}`);
434
+ }
435
+ const address = canonicalPayoutAddress(caip19.chain, entry.address);
436
+ if (!address) {
437
+ throw new Error(`Not a ${caip19.chain.family} address: ${entry.address}`);
438
+ }
439
+ if (caip19.chain.family === "evm" && !hasValidEvmChecksum(entry.address)) {
440
+ throw new Error(`EIP-55 checksum mismatch, likely a typo: ${entry.address}`);
441
+ }
442
+ if (entry.signature !== void 0) {
443
+ if (input.ownerPubkey === void 0 || !HEX_PUBKEY_RE.test(input.ownerPubkey)) {
444
+ throw new Error("A wallet signature needs ownerPubkey as 64 lowercase hex characters");
445
+ }
446
+ const proven = verifyPaytoProof({
447
+ chain: caip19.chain,
448
+ address,
449
+ ownerPubkey: input.ownerPubkey,
450
+ caip19: caip19.id,
451
+ signature: entry.signature
452
+ });
453
+ if (!proven) {
454
+ throw new Error(`The wallet signature does not prove ${address} for this owner and asset`);
455
+ }
456
+ }
457
+ tags.push(
458
+ entry.signature === void 0 ? ["accept", entry.caip19, address] : ["accept", entry.caip19, address, entry.signature]
459
+ );
460
+ }
461
+ return { kind: KIND_PAYTO, created_at: input.createdAt ?? nowSecs(), tags, content: "" };
462
+ }
463
+ function parsePayto(event) {
464
+ const candidates = /* @__PURE__ */ new Map();
465
+ const badProof = /* @__PURE__ */ new Set();
466
+ const rejected = [];
467
+ for (const tag of tagsNamed(event.tags, "accept")) {
468
+ const [, caip19Id, rawAddress, signature] = tag;
469
+ const caip19 = caip19Id === void 0 ? void 0 : parseCaip19(caip19Id);
470
+ if (!caip19) {
471
+ rejected.push({ tag, reason: "unknown_asset" });
472
+ continue;
473
+ }
474
+ const address = rawAddress === void 0 ? void 0 : canonicalPayoutAddress(caip19.chain, rawAddress);
475
+ if (!address) {
476
+ rejected.push({ tag, reason: "bad_address" });
477
+ continue;
478
+ }
479
+ const key = `${caip19.id}|${address}`;
480
+ let walletSigned = false;
481
+ if (signature !== void 0 && signature !== "") {
482
+ walletSigned = verifyPaytoProof({
483
+ chain: caip19.chain,
484
+ address,
485
+ ownerPubkey: event.pubkey,
486
+ caip19: caip19.id,
487
+ signature
488
+ });
489
+ if (!walletSigned) {
490
+ rejected.push({ tag, reason: "bad_proof" });
491
+ badProof.add(key);
492
+ continue;
493
+ }
494
+ }
495
+ const earlier = candidates.get(key);
496
+ if (earlier === void 0) {
497
+ candidates.set(key, { caip19, address, walletSigned });
498
+ } else if (walletSigned) {
499
+ earlier.walletSigned = true;
500
+ }
501
+ }
502
+ const targets = [...candidates.entries()].filter(([key]) => !badProof.has(key)).map(([, target]) => target);
503
+ return { targets, rejected };
504
+ }
505
+
506
+ // src/events/store-auth.ts
507
+ var STORE_AUTH_MODES = ["self-host", "hosted", "revoked"];
508
+ var MAX_EXPIRATION_SECS = 1e12;
509
+ function isStoreAuthMode(value) {
510
+ return value !== void 0 && STORE_AUTH_MODES.includes(value);
511
+ }
512
+ function storeAuthAddress(ownerPubkey, storePubkey) {
513
+ return `${KIND_STORE_AUTH}:${ownerPubkey}:${storePubkey}`;
514
+ }
515
+ function buildStoreAuthEvent(input) {
516
+ if (!HEX_PUBKEY_RE.test(input.storePubkey)) {
517
+ throw new Error("storePubkey must be 64 lowercase hex characters");
518
+ }
519
+ if (input.mode !== "self-host" && input.mode !== "hosted") {
520
+ throw new Error(`Invalid store mode: ${String(input.mode)}`);
521
+ }
522
+ const tags = [
523
+ ["d", input.storePubkey],
524
+ ["p", input.storePubkey],
525
+ input.operator === void 0 ? ["mode", input.mode] : ["mode", input.mode, input.operator]
526
+ ];
527
+ if (input.expiresAt !== void 0) {
528
+ if (!Number.isSafeInteger(input.expiresAt) || input.expiresAt <= 0 || input.expiresAt >= MAX_EXPIRATION_SECS) {
529
+ throw new Error("expiresAt must be a unix time in whole seconds");
530
+ }
531
+ tags.push(["expiration", String(input.expiresAt)]);
532
+ }
533
+ return { kind: KIND_STORE_AUTH, created_at: input.createdAt ?? nowSecs(), tags, content: "" };
534
+ }
535
+ function buildStoreRevocationEvent(storePubkey, createdAt) {
536
+ if (!HEX_PUBKEY_RE.test(storePubkey)) {
537
+ throw new Error("storePubkey must be 64 lowercase hex characters");
538
+ }
539
+ return {
540
+ kind: KIND_STORE_AUTH,
541
+ created_at: createdAt ?? nowSecs(),
542
+ tags: [
543
+ ["d", storePubkey],
544
+ ["p", storePubkey],
545
+ ["mode", "revoked"]
546
+ ],
547
+ content: ""
548
+ };
549
+ }
550
+ function readStoreAuth(event, storePubkey, now = nowSecs()) {
551
+ const tags = event.tags;
552
+ if (event.kind !== KIND_STORE_AUTH || tagValue(tags, "d") !== storePubkey) {
553
+ return { status: "malformed" };
554
+ }
555
+ const modeTags = tagsNamed(tags, "mode");
556
+ if (modeTags.length !== 1) {
557
+ return { status: "malformed" };
558
+ }
559
+ const modeTag = modeTags[0];
560
+ const mode = modeTag?.[1];
561
+ if (!isStoreAuthMode(mode)) {
562
+ return { status: "malformed" };
563
+ }
564
+ if (mode === "revoked") {
565
+ return { status: "revoked" };
566
+ }
567
+ if (!tagsNamed(tags, "p").some((tag) => tag[1] === storePubkey)) {
568
+ return { status: "malformed" };
569
+ }
570
+ const expiration = expirationState(tags, now);
571
+ if (expiration === "malformed") {
572
+ return { status: "malformed" };
573
+ }
574
+ if (expiration === "expired") {
575
+ return { status: "expired" };
576
+ }
577
+ const operator = modeTag?.[2];
578
+ return operator === void 0 ? { status: "active", mode } : { status: "active", mode, operator };
579
+ }
580
+
581
+ // src/events/store-profile.ts
582
+ var PROFILE_FIELDS = ["name", "about", "picture", "website", "nip05"];
583
+ var URL_FIELDS = ["picture", "website"];
584
+ var PROFILE_FIELD_LIMITS = {
585
+ name: LIMITS.MAX_TAG_VALUE_LENGTH,
586
+ about: LIMITS.MAX_CONTENT_LENGTH,
587
+ picture: LIMITS.MAX_TAG_VALUE_LENGTH,
588
+ website: LIMITS.MAX_TAG_VALUE_LENGTH,
589
+ nip05: LIMITS.MAX_TAG_VALUE_LENGTH
590
+ };
591
+ function isReadableField(key, value) {
592
+ return value.length <= PROFILE_FIELD_LIMITS[key] && (!URL_FIELDS.includes(key) || value.startsWith("https://"));
593
+ }
594
+ function buildStoreProfileEvent(input) {
595
+ if (!HEX_PUBKEY_RE.test(input.ownerPubkey)) {
596
+ throw new Error("ownerPubkey must be 64 lowercase hex characters");
597
+ }
598
+ const content = {};
599
+ for (const key of PROFILE_FIELDS) {
600
+ const value = input[key];
601
+ if (value === void 0) {
602
+ continue;
603
+ }
604
+ if (!isReadableField(key, value)) {
605
+ throw new Error(`Profile ${key} is too long or, for a link, not https`);
606
+ }
607
+ content[key] = value;
608
+ }
609
+ return {
610
+ kind: KIND_STORE_PROFILE,
611
+ created_at: input.createdAt ?? nowSecs(),
612
+ tags: [["owner", input.ownerPubkey]],
613
+ content: JSON.stringify(content)
614
+ };
615
+ }
616
+ function parseStoreProfile(event) {
617
+ let raw;
618
+ try {
619
+ raw = JSON.parse(event.content);
620
+ } catch {
621
+ return void 0;
622
+ }
623
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
624
+ return void 0;
625
+ }
626
+ const fields = { ...raw };
627
+ const owner = tagValue(event.tags, "owner");
628
+ const profile = {};
629
+ for (const key of PROFILE_FIELDS) {
630
+ const value = Object.hasOwn(fields, key) ? fields[key] : void 0;
631
+ if (typeof value === "string" && isReadableField(key, value)) {
632
+ profile[key] = value;
633
+ }
634
+ }
635
+ if (owner !== void 0 && HEX_PUBKEY_RE.test(owner)) {
636
+ profile.ownerPubkey = owner;
637
+ }
638
+ return profile;
639
+ }
640
+ var PRICE_AMOUNT_RE = /^(0|[1-9]\d{0,11})(\.\d{1,18})?$/;
641
+ var CURRENCY_RE = /^[A-Z]{3}$/;
642
+ var D_TAG_RE = /^[A-Za-z0-9._:-]{1,128}$/;
643
+ var FREQUENCIES = ["hour", "day", "week", "month", "year"];
644
+ var ENDPOINT_TYPES = ["x402", "mpp"];
645
+ var USD_STABLE_TOKENS = ["usdc", "usdce", "pathusd"];
646
+ var ProductInputSchema = z.object({
647
+ d: z.string().regex(D_TAG_RE),
648
+ title: z.string().min(1).max(LIMITS.MAX_TAG_VALUE_LENGTH),
649
+ summary: z.string().max(LIMITS.MAX_TAG_VALUE_LENGTH).optional(),
650
+ description: z.string().max(LIMITS.MAX_CONTENT_LENGTH),
651
+ price: z.object({
652
+ amount: z.string().regex(PRICE_AMOUNT_RE),
653
+ currency: z.string().regex(CURRENCY_RE),
654
+ frequency: z.enum(FREQUENCIES).optional()
655
+ }),
656
+ images: z.array(z.string().url().startsWith("https://").max(LIMITS.MAX_TAG_VALUE_LENGTH)).default([]),
657
+ topics: z.array(z.string().min(1).max(64)).default([]),
658
+ visibility: z.string().min(1).max(32).default("on-sale"),
659
+ delivery: z.enum(DELIVERY_METHODS).optional(),
660
+ listedOnElisym: z.boolean().default(false),
661
+ endpoints: z.array(z.object({ type: z.enum(ENDPOINT_TYPES), url: z.string().url().startsWith("https://") })).default([]),
662
+ accept: z.array(z.string()).min(1),
663
+ createdAt: z.number().int().positive().optional()
664
+ });
665
+ function buildProductEvent(input) {
666
+ const product = ProductInputSchema.parse(input);
667
+ for (const caip19 of product.accept) {
668
+ if (!parseCaip19(caip19)) {
669
+ throw new Error(`Not a payable CAIP-19 asset: ${caip19}`);
670
+ }
671
+ }
672
+ const tags = [
673
+ ["d", product.d],
674
+ ["title", product.title]
675
+ ];
676
+ if (product.summary !== void 0) {
677
+ tags.push(["summary", product.summary]);
678
+ }
679
+ tags.push(
680
+ product.price.frequency === void 0 ? ["price", product.price.amount, product.price.currency] : ["price", product.price.amount, product.price.currency, product.price.frequency]
681
+ );
682
+ for (const image of product.images) {
683
+ tags.push(["image", image]);
684
+ }
685
+ for (const topic of product.topics) {
686
+ tags.push(["t", topic]);
687
+ }
688
+ tags.push(["visibility", product.visibility]);
689
+ if (product.delivery !== void 0) {
690
+ tags.push(["delivery", product.delivery]);
691
+ }
692
+ if (product.listedOnElisym) {
693
+ tags.push(["network", ELISYM_NETWORK_TAG]);
694
+ }
695
+ for (const endpoint of product.endpoints) {
696
+ tags.push(["endpoint", endpoint.type, endpoint.url]);
697
+ }
698
+ for (const caip19 of product.accept) {
699
+ tags.push(["accept", caip19]);
700
+ }
701
+ return {
702
+ kind: KIND_PRODUCT,
703
+ created_at: product.createdAt ?? nowSecs(),
704
+ tags,
705
+ content: product.description
706
+ };
707
+ }
708
+ function parsePrice(tags) {
709
+ const tag = tagsNamed(tags, "price")[0];
710
+ const amount = tag?.[1];
711
+ const currency = tag?.[2];
712
+ const frequency = tag?.[3];
713
+ if (!amount || !PRICE_AMOUNT_RE.test(amount) || !currency || !CURRENCY_RE.test(currency)) {
714
+ return void 0;
715
+ }
716
+ if (frequency === void 0 || frequency === "") {
717
+ return { amount, currency };
718
+ }
719
+ const known = FREQUENCIES.find((candidate) => candidate === frequency);
720
+ return known ? { amount, currency, frequency: known } : void 0;
721
+ }
722
+ function parseProduct(event) {
723
+ if (event.kind !== KIND_PRODUCT || event.content.length > LIMITS.MAX_CONTENT_LENGTH) {
724
+ return void 0;
725
+ }
726
+ const tags = event.tags;
727
+ const d = tagValue(tags, "d");
728
+ const title = tagValue(tags, "title");
729
+ const price = parsePrice(tags);
730
+ if (d === void 0 || !D_TAG_RE.test(d) || !title || !price) {
731
+ return void 0;
732
+ }
733
+ const deliveryValue = tagValue(tags, "delivery");
734
+ const delivery = DELIVERY_METHODS.find((method) => method === deliveryValue);
735
+ const endpoints = [];
736
+ for (const tag of tagsNamed(tags, "endpoint")) {
737
+ const type = ENDPOINT_TYPES.find((candidate) => candidate === tag[1]);
738
+ const url = tag[2];
739
+ if (type && url?.startsWith("https://")) {
740
+ endpoints.push({ type, url });
741
+ }
742
+ }
743
+ const summary = tagValue(tags, "summary");
744
+ const product = {
745
+ storePubkey: event.pubkey,
746
+ d,
747
+ title,
748
+ description: event.content,
749
+ price,
750
+ // Only `https:` images, as the builder writes: never a `javascript:` or `data:` URL to render.
751
+ images: tagValues(tags, "image").filter((image) => image.startsWith("https://")),
752
+ topics: tagValues(tags, "t"),
753
+ visibility: tagValue(tags, "visibility") ?? "on-sale",
754
+ listedOnElisym: tagValues(tags, "network").includes(ELISYM_NETWORK_TAG),
755
+ endpoints,
756
+ accept: tagValues(tags, "accept"),
757
+ createdAt: event.created_at
758
+ };
759
+ if (summary !== void 0) {
760
+ product.summary = summary;
761
+ }
762
+ if (delivery !== void 0) {
763
+ product.delivery = delivery;
764
+ }
765
+ return product;
766
+ }
767
+ function isPurchasable(product) {
768
+ return PURCHASABLE_VISIBILITIES.some((visibility) => visibility === product.visibility);
769
+ }
770
+ function productAddress(product) {
771
+ return `${KIND_PRODUCT}:${product.storePubkey}:${product.d}`;
772
+ }
773
+ function encodeProductNaddr(product, relays = []) {
774
+ return nip192.naddrEncode({
775
+ kind: KIND_PRODUCT,
776
+ pubkey: product.storePubkey,
777
+ identifier: product.d,
778
+ relays
779
+ });
780
+ }
781
+ function decodeProductNaddr(naddr) {
782
+ try {
783
+ const decoded = nip192.decode(naddr);
784
+ if (decoded.type !== "naddr" || decoded.data.kind !== KIND_PRODUCT || !HEX_PUBKEY_RE.test(decoded.data.pubkey) || !D_TAG_RE.test(decoded.data.identifier)) {
785
+ return void 0;
786
+ }
787
+ return {
788
+ storePubkey: decoded.data.pubkey,
789
+ d: decoded.data.identifier,
790
+ relays: decoded.data.relays ?? []
791
+ };
792
+ } catch {
793
+ return void 0;
794
+ }
795
+ }
796
+ function priceInSubunits(price, asset) {
797
+ if (price.frequency !== void 0) {
798
+ throw new Error("A subscription price has no one-off amount");
799
+ }
800
+ if (price.currency !== "USD" || !USD_STABLE_TOKENS.includes(asset.token)) {
801
+ throw new Error(`A ${price.currency} price cannot be paid in ${asset.symbol} without a quote`);
802
+ }
803
+ return parseAssetAmount(asset, new Decimal(price.amount).toFixed());
804
+ }
805
+
806
+ // src/orders/messages.ts
807
+ var ORDER_ID_RE = new RegExp(`^[A-Za-z0-9-]{8,${LIMITS.MAX_ORDER_ID_LENGTH}}$`);
808
+ var ITEM_ADDRESS_RE = new RegExp(`^${KIND_PRODUCT}:[0-9a-f]{64}:[A-Za-z0-9._:-]{1,128}$`);
809
+ var QUANTITY_RE = /^[1-9]\d{0,3}$/;
810
+ var AMOUNT_RE = /^(0|[1-9]\d{0,11})(\.\d{1,18})?$/;
811
+ var SUBUNITS_RE = /^(0|[1-9]\d{0,38})$/;
812
+ var CURRENCY_RE2 = /^[A-Z]{3}$/;
813
+ var MEDIUM_RE = /^[a-z0-9-]{1,32}$/;
814
+ var TX_RE = /^[A-Za-z0-9]{1,128}$/;
815
+ var EMAIL_RE = /^[^\s@]{1,64}@[^\s@]{1,255}$/;
816
+ var ORDER_MESSAGE_TYPES = ["order", "payment_request", "status", "receipt"];
817
+ var TYPE_ORDER = "1";
818
+ var TYPE_PAYMENT_REQUEST = "2";
819
+ var TYPE_STATUS = "3";
820
+ function assertPubkey(value, name) {
821
+ if (!HEX_PUBKEY_RE.test(value)) {
822
+ throw new Error(`${name} must be 64 lowercase hex characters`);
823
+ }
824
+ }
825
+ function assertMatches(value, pattern, name) {
826
+ if (!pattern.test(value)) {
827
+ throw new Error(`Invalid ${name}: ${value}`);
828
+ }
829
+ }
830
+ function assertMoney(money) {
831
+ assertMatches(money.amount, AMOUNT_RE, "amount");
832
+ assertMatches(money.currency, CURRENCY_RE2, "currency");
833
+ }
834
+ function buildOrderMessage(message, createdAt = nowSecs()) {
835
+ if (!ORDER_MESSAGE_TYPES.some((known) => known === message.type)) {
836
+ throw new Error(`Unknown order message type: ${String(message.type)}`);
837
+ }
838
+ assertMatches(message.orderId, ORDER_ID_RE, "order id");
839
+ switch (message.type) {
840
+ case "order": {
841
+ assertPubkey(message.storePubkey, "storePubkey");
842
+ assertMoney(message.total);
843
+ if (message.items.length === 0 || message.items.length > LIMITS.MAX_ITEMS_PER_ORDER) {
844
+ throw new Error(`An order holds 1 to ${LIMITS.MAX_ITEMS_PER_ORDER} items`);
845
+ }
846
+ const tags = [
847
+ ["p", message.storePubkey],
848
+ ["subject", "order"],
849
+ ["type", TYPE_ORDER],
850
+ ["order", message.orderId]
851
+ ];
852
+ for (const item of message.items) {
853
+ assertMatches(item.product, ITEM_ADDRESS_RE, "item");
854
+ assertMatches(String(item.quantity), QUANTITY_RE, "quantity");
855
+ tags.push(["item", item.product, String(item.quantity)]);
856
+ }
857
+ tags.push(["amount", message.total.amount, message.total.currency]);
858
+ if (message.email !== void 0) {
859
+ assertMatches(message.email, EMAIL_RE, "email");
860
+ tags.push(["email", message.email]);
861
+ }
862
+ return { kind: KIND_ORDER_MESSAGE, created_at: createdAt, tags, content: "" };
863
+ }
864
+ case "payment_request": {
865
+ assertPubkey(message.buyerPubkey, "buyerPubkey");
866
+ assertMoney(message.total);
867
+ if (message.options.length === 0) {
868
+ throw new Error("A payment request offers at least one way to pay");
869
+ }
870
+ const tags = [
871
+ ["p", message.buyerPubkey],
872
+ ["type", TYPE_PAYMENT_REQUEST],
873
+ ["order", message.orderId],
874
+ ["amount", message.total.amount, message.total.currency]
875
+ ];
876
+ for (const option of message.options) {
877
+ assertMatches(option.medium, MEDIUM_RE, "payment medium");
878
+ if (option.payload.length === 0 || option.payload.length > LIMITS.MAX_TAG_VALUE_LENGTH * 4) {
879
+ throw new Error("Payment payload is empty or too long");
880
+ }
881
+ tags.push(["payment", option.medium, option.payload]);
882
+ }
883
+ return { kind: KIND_ORDER_MESSAGE, created_at: createdAt, tags, content: "" };
884
+ }
885
+ case "status": {
886
+ assertPubkey(message.buyerPubkey, "buyerPubkey");
887
+ if (!ORDER_STATUSES.some((status) => status === message.status)) {
888
+ throw new Error(`Invalid order status: ${message.status}`);
889
+ }
890
+ const tags = [
891
+ ["p", message.buyerPubkey],
892
+ ["type", TYPE_STATUS],
893
+ ["order", message.orderId],
894
+ ["status", message.status]
895
+ ];
896
+ if (message.delivery !== void 0) {
897
+ const { method, value } = message.delivery;
898
+ if (!DELIVERY_METHODS.some((known) => known === method)) {
899
+ throw new Error(`Invalid delivery method: ${method}`);
900
+ }
901
+ if (value.length === 0 || value.length > LIMITS.MAX_TAG_VALUE_LENGTH) {
902
+ throw new Error("Delivery value is empty or too long");
903
+ }
904
+ tags.push(["delivery", message.delivery.method, message.delivery.value]);
905
+ }
906
+ if (message.receipt !== void 0) {
907
+ assertMatches(message.receipt.medium, MEDIUM_RE, "receipt medium");
908
+ assertMatches(message.receipt.tx, TX_RE, "receipt tx");
909
+ assertMatches(message.receipt.amount, SUBUNITS_RE, "receipt amount");
910
+ assertMatches(message.receipt.fee, SUBUNITS_RE, "receipt fee");
911
+ tags.push([
912
+ "receipt",
913
+ message.receipt.medium,
914
+ message.receipt.tx,
915
+ message.receipt.amount,
916
+ message.receipt.fee
917
+ ]);
918
+ }
919
+ if (message.refund !== void 0) {
920
+ assertMatches(message.refund.tx, TX_RE, "refund tx");
921
+ assertMatches(message.refund.amount, SUBUNITS_RE, "refund amount");
922
+ tags.push(["refund", message.refund.tx, message.refund.amount]);
923
+ }
924
+ return { kind: KIND_ORDER_MESSAGE, created_at: createdAt, tags, content: "" };
925
+ }
926
+ case "receipt": {
927
+ assertPubkey(message.storePubkey, "storePubkey");
928
+ assertMatches(message.payment.medium, MEDIUM_RE, "payment medium");
929
+ assertMatches(message.payment.reference, TX_RE, "payment reference");
930
+ assertMatches(message.payment.tx, TX_RE, "payment tx");
931
+ return {
932
+ kind: KIND_PAYMENT_RECEIPT,
933
+ created_at: createdAt,
934
+ tags: [
935
+ ["p", message.storePubkey],
936
+ ["order", message.orderId],
937
+ ["payment", message.payment.medium, message.payment.reference, message.payment.tx]
938
+ ],
939
+ content: ""
940
+ };
941
+ }
942
+ }
943
+ }
944
+ function readMoney(tags) {
945
+ const tag = tagsNamed(tags, "amount")[0];
946
+ const amount = tag?.[1];
947
+ const currency = tag?.[2];
948
+ if (!amount || !AMOUNT_RE.test(amount) || !currency || !CURRENCY_RE2.test(currency)) {
949
+ return void 0;
950
+ }
951
+ return { amount, currency };
952
+ }
953
+ function readPubkey(tags) {
954
+ const value = tagValue(tags, "p");
955
+ return value !== void 0 && HEX_PUBKEY_RE.test(value) ? value : void 0;
956
+ }
957
+ function readOrderRequest(tags, orderId) {
958
+ const storePubkey = readPubkey(tags);
959
+ const total = readMoney(tags);
960
+ const itemTags = tagsNamed(tags, "item");
961
+ if (!storePubkey || !total || itemTags.length === 0 || itemTags.length > LIMITS.MAX_ITEMS_PER_ORDER) {
962
+ return void 0;
963
+ }
964
+ const items = [];
965
+ for (const tag of itemTags) {
966
+ const product = tag[1];
967
+ const quantity = tag[2] ?? "1";
968
+ if (!product || !ITEM_ADDRESS_RE.test(product) || !QUANTITY_RE.test(quantity)) {
969
+ return void 0;
970
+ }
971
+ items.push({ product, quantity: Number(quantity) });
972
+ }
973
+ const order = { type: "order", storePubkey, orderId, items, total };
974
+ const email = tagValue(tags, "email");
975
+ if (email !== void 0 && EMAIL_RE.test(email)) {
976
+ order.email = email;
977
+ }
978
+ return order;
979
+ }
980
+ function readPaymentRequest(tags, orderId) {
981
+ const buyerPubkey = readPubkey(tags);
982
+ const total = readMoney(tags);
983
+ if (!buyerPubkey || !total) {
984
+ return void 0;
985
+ }
986
+ const options = [];
987
+ for (const tag of tagsNamed(tags, "payment")) {
988
+ const medium = tag[1];
989
+ const payload = tag[2];
990
+ if (medium && MEDIUM_RE.test(medium) && payload) {
991
+ options.push({ medium, payload });
992
+ }
993
+ }
994
+ if (options.length === 0) {
995
+ return void 0;
996
+ }
997
+ return { type: "payment_request", buyerPubkey, orderId, total, options };
998
+ }
999
+ function readStatus(tags, orderId) {
1000
+ const buyerPubkey = readPubkey(tags);
1001
+ const statusValue = tagValue(tags, "status");
1002
+ const status = ORDER_STATUSES.find((candidate) => candidate === statusValue);
1003
+ if (!buyerPubkey || !status) {
1004
+ return void 0;
1005
+ }
1006
+ const message = { type: "status", buyerPubkey, orderId, status };
1007
+ const deliveryTag = tagsNamed(tags, "delivery")[0];
1008
+ const method = DELIVERY_METHODS.find((candidate) => candidate === deliveryTag?.[1]);
1009
+ const deliveryValue = deliveryTag?.[2];
1010
+ if (method && deliveryValue && deliveryValue.length <= LIMITS.MAX_TAG_VALUE_LENGTH) {
1011
+ message.delivery = { method, value: deliveryValue };
1012
+ }
1013
+ const receiptTag = tagsNamed(tags, "receipt")[0];
1014
+ const [, medium, tx, amount, fee] = receiptTag ?? [];
1015
+ if (medium && MEDIUM_RE.test(medium) && tx && TX_RE.test(tx) && amount && SUBUNITS_RE.test(amount) && fee && SUBUNITS_RE.test(fee)) {
1016
+ message.receipt = { medium, tx, amount, fee };
1017
+ }
1018
+ const refundTag = tagsNamed(tags, "refund")[0];
1019
+ const [, refundTx, refundAmount] = refundTag ?? [];
1020
+ if (refundTx && TX_RE.test(refundTx) && refundAmount && SUBUNITS_RE.test(refundAmount)) {
1021
+ message.refund = { tx: refundTx, amount: refundAmount };
1022
+ }
1023
+ return message;
1024
+ }
1025
+ function readReceipt(tags, orderId) {
1026
+ const storePubkey = readPubkey(tags);
1027
+ const [, medium, reference, tx] = tagsNamed(tags, "payment")[0] ?? [];
1028
+ if (!storePubkey || !medium || !MEDIUM_RE.test(medium) || !reference || !TX_RE.test(reference) || !tx || !TX_RE.test(tx)) {
1029
+ return void 0;
1030
+ }
1031
+ return { type: "receipt", storePubkey, orderId, payment: { medium, reference, tx } };
1032
+ }
1033
+ function parseOrderMessage(rumor) {
1034
+ const orderId = tagValue(rumor.tags, "order");
1035
+ if (orderId === void 0 || !ORDER_ID_RE.test(orderId)) {
1036
+ return void 0;
1037
+ }
1038
+ if (rumor.kind === KIND_PAYMENT_RECEIPT) {
1039
+ return readReceipt(rumor.tags, orderId);
1040
+ }
1041
+ if (rumor.kind !== KIND_ORDER_MESSAGE) {
1042
+ return void 0;
1043
+ }
1044
+ switch (tagValue(rumor.tags, "type")) {
1045
+ case TYPE_ORDER:
1046
+ return readOrderRequest(rumor.tags, orderId);
1047
+ case TYPE_PAYMENT_REQUEST:
1048
+ return readPaymentRequest(rumor.tags, orderId);
1049
+ case TYPE_STATUS:
1050
+ return readStatus(rumor.tags, orderId);
1051
+ default:
1052
+ return void 0;
1053
+ }
1054
+ }
1055
+ function isGenuineEvent(event) {
1056
+ if (!isEventShaped(event)) {
1057
+ return false;
1058
+ }
1059
+ return verifyEvent({
1060
+ id: event.id,
1061
+ pubkey: event.pubkey,
1062
+ created_at: event.created_at,
1063
+ kind: event.kind,
1064
+ tags: event.tags,
1065
+ content: event.content,
1066
+ sig: event.sig
1067
+ });
1068
+ }
1069
+ function isEventShaped(event) {
1070
+ if (typeof event !== "object" || event === null) {
1071
+ return false;
1072
+ }
1073
+ const record = { ...event };
1074
+ const { id, pubkey, created_at: createdAt, kind, tags, content, sig } = record;
1075
+ return typeof id === "string" && typeof pubkey === "string" && typeof createdAt === "number" && Number.isInteger(createdAt) && typeof kind === "number" && Number.isInteger(kind) && typeof content === "string" && typeof sig === "string" && Array.isArray(tags) && tags.every((tag) => Array.isArray(tag) && tag.every((item) => typeof item === "string"));
1076
+ }
1077
+
1078
+ // src/orders/gift-wrap.ts
1079
+ function wrapOrderMessage(rumor, senderSecretKey, recipientPubkey) {
1080
+ if (!HEX_PUBKEY_RE.test(recipientPubkey)) {
1081
+ throw new Error("recipientPubkey must be 64 lowercase hex characters");
1082
+ }
1083
+ if (rumor.kind !== KIND_ORDER_MESSAGE && rumor.kind !== KIND_PAYMENT_RECEIPT) {
1084
+ throw new Error(`Not an order message kind: ${rumor.kind}`);
1085
+ }
1086
+ const wraps = nip59.wrapManyEvents(rumor, senderSecretKey, [recipientPubkey]);
1087
+ const selfWrap = wraps[0];
1088
+ const recipientWrap = wraps[1];
1089
+ if (!selfWrap || !recipientWrap) {
1090
+ throw new Error("Unexpected wrap count from nip59.wrapManyEvents");
1091
+ }
1092
+ const rumorId = getEventHash({ ...rumor, pubkey: getPublicKey(senderSecretKey) });
1093
+ return { recipientWrap, selfWrap, rumorId };
1094
+ }
1095
+ function isStringArrayArray(value) {
1096
+ return Array.isArray(value) && value.every((entry) => Array.isArray(entry) && entry.every((item) => typeof item === "string"));
1097
+ }
1098
+ function parseEventJson(json) {
1099
+ let shape;
1100
+ try {
1101
+ shape = JSON.parse(json);
1102
+ } catch {
1103
+ return void 0;
1104
+ }
1105
+ if (typeof shape !== "object" || shape === null) {
1106
+ return void 0;
1107
+ }
1108
+ const record = { ...shape };
1109
+ const { id, pubkey, created_at: createdAt, kind, tags, content, sig } = record;
1110
+ if (typeof id !== "string" || typeof pubkey !== "string" || typeof createdAt !== "number" || !Number.isInteger(createdAt) || createdAt < 0 || typeof kind !== "number" || typeof content !== "string" || content.length > LIMITS.MAX_CONTENT_LENGTH || !isStringArrayArray(tags)) {
1111
+ return void 0;
1112
+ }
1113
+ const parsed = { id, pubkey, created_at: createdAt, kind, tags, content };
1114
+ if (typeof sig === "string") {
1115
+ parsed.sig = sig;
1116
+ }
1117
+ return parsed;
1118
+ }
1119
+ function decrypt(payload, secretKey, senderPubkey) {
1120
+ return nip44.v2.decrypt(payload, nip44.v2.utils.getConversationKey(secretKey, senderPubkey));
1121
+ }
1122
+ function unwrapOrderMessage(wrap, recipientSecretKey) {
1123
+ try {
1124
+ if (wrap.kind !== KIND_GIFT_WRAP || !isGenuineEvent(wrap)) {
1125
+ return void 0;
1126
+ }
1127
+ const seal = parseEventJson(decrypt(wrap.content, recipientSecretKey, wrap.pubkey));
1128
+ if (!seal || seal.kind !== KIND_SEAL || seal.sig === void 0 || seal.tags.length > 0) {
1129
+ return void 0;
1130
+ }
1131
+ const signedSeal = { ...seal, sig: seal.sig };
1132
+ if (!isGenuineEvent(signedSeal)) {
1133
+ return void 0;
1134
+ }
1135
+ const rumor = parseEventJson(decrypt(seal.content, recipientSecretKey, seal.pubkey));
1136
+ if (!rumor || rumor.sig !== void 0 || rumor.pubkey !== seal.pubkey || rumor.id !== getEventHash(rumor)) {
1137
+ return void 0;
1138
+ }
1139
+ if (rumor.created_at > nowSecs() + MAX_FUTURE_SKEW_SECS) {
1140
+ return void 0;
1141
+ }
1142
+ const recipientPubkey = tagValue(rumor.tags, "p");
1143
+ const message = parseOrderMessage(rumor);
1144
+ if (recipientPubkey === void 0 || !message) {
1145
+ return void 0;
1146
+ }
1147
+ return {
1148
+ rumorId: rumor.id,
1149
+ senderPubkey: rumor.pubkey,
1150
+ recipientPubkey,
1151
+ createdAt: rumor.created_at,
1152
+ message
1153
+ };
1154
+ } catch {
1155
+ return void 0;
1156
+ }
1157
+ }
1158
+
1159
+ // src/verify-offer.ts
1160
+ function refuse(refusal, message) {
1161
+ return { ok: false, refusal, message };
1162
+ }
1163
+ function newestSigned(events, kind, author, now, d) {
1164
+ let newest;
1165
+ for (const event of events) {
1166
+ if (!isEventShaped(event) || event.kind !== kind || event.pubkey !== author || event.created_at > now + MAX_FUTURE_SKEW_SECS || d !== void 0 && tagValue(event.tags, "d") !== d) {
1167
+ continue;
1168
+ }
1169
+ if (newest && (event.created_at < newest.created_at || event.created_at === newest.created_at && event.id >= newest.id)) {
1170
+ continue;
1171
+ }
1172
+ if (isGenuineEvent(event)) {
1173
+ newest = event;
1174
+ }
1175
+ }
1176
+ return newest;
1177
+ }
1178
+ var NO_FUTURE_LIMIT = Number.POSITIVE_INFINITY;
1179
+ function newestDeletion(events, address, author, now) {
1180
+ return newestSigned(
1181
+ events.filter((event) => isEventShaped(event) && tagValues(event.tags, "a").includes(address)),
1182
+ KIND_DELETION,
1183
+ author,
1184
+ now
1185
+ );
1186
+ }
1187
+ function isLapsed(event, now) {
1188
+ const state = expirationState(event.tags, now);
1189
+ return state === "expired" || state === "malformed";
1190
+ }
1191
+ function originMatchesDomain(pageOrigin, domain) {
1192
+ let url;
1193
+ try {
1194
+ url = new URL(pageOrigin);
1195
+ } catch {
1196
+ return false;
1197
+ }
1198
+ if (url.protocol !== "https:") {
1199
+ return false;
1200
+ }
1201
+ const host = url.hostname.toLowerCase();
1202
+ return host === domain || host.endsWith(`.${domain}`);
1203
+ }
1204
+ function resolveAnchor(storePubkey, profile, bundle, options) {
1205
+ const warnings = [];
1206
+ const nip05 = profile.nip05 === void 0 ? void 0 : splitNip05(profile.nip05);
1207
+ const hosted = nip05 !== void 0 && (options.hostedDomains ?? []).includes(nip05.domain);
1208
+ const keys = typeof bundle.domain === "object" ? bundle.domain : void 0;
1209
+ let hostedDomain;
1210
+ const txtForAnotherName = nip05 !== void 0 && keys !== void 0 && keys.source === "dns" && nip05.local !== "_" && keys.storePubkey !== storePubkey;
1211
+ if (nip05 && keys && keys.domain === nip05.domain && keys.name === nip05.local && keys.storePubkey !== void 0 && !txtForAnotherName) {
1212
+ if (keys.storePubkey !== storePubkey) {
1213
+ return refuse("domain_mismatch", `${nip05.domain} does not name this store`);
1214
+ }
1215
+ const domainWide = nip05.local === "_" || keys.source === "dns";
1216
+ if (!hosted && domainWide && keys.ownerPubkey && profile.ownerPubkey !== void 0) {
1217
+ if (profile.ownerPubkey !== keys.ownerPubkey) {
1218
+ return refuse("owner_mismatch", "The store profile and its domain name different owners");
1219
+ }
1220
+ return {
1221
+ level: "A",
1222
+ domain: nip05.domain,
1223
+ ownerPubkey: keys.ownerPubkey,
1224
+ ownerFromDomain: true,
1225
+ warnings
1226
+ };
1227
+ }
1228
+ if (hosted) {
1229
+ hostedDomain = nip05.domain;
1230
+ } else {
1231
+ warnings.push("domain_unverified");
1232
+ }
1233
+ } else if (profile.nip05 !== void 0) {
1234
+ warnings.push("domain_unverified");
1235
+ }
1236
+ if (!profile.ownerPubkey) {
1237
+ return refuse("owner_unknown", "The store profile names no owner");
1238
+ }
1239
+ if (hostedDomain !== void 0) {
1240
+ return {
1241
+ level: "B",
1242
+ domain: hostedDomain,
1243
+ ownerPubkey: profile.ownerPubkey,
1244
+ ownerFromDomain: false,
1245
+ warnings
1246
+ };
1247
+ }
1248
+ return { level: "C", ownerPubkey: profile.ownerPubkey, ownerFromDomain: false, warnings };
1249
+ }
1250
+ function evaluateOffer(pointer, bundle, options = {}) {
1251
+ const now = options.now ?? nowSecs();
1252
+ const { storePubkey } = pointer;
1253
+ const productEvent = newestSigned(bundle.events, KIND_PRODUCT, storePubkey, now, pointer.d);
1254
+ const product = productEvent && !isLapsed(productEvent, now) ? parseProduct(productEvent) : void 0;
1255
+ if (!product) {
1256
+ return refuse("product_missing", "No valid, unexpired listing signed by the store");
1257
+ }
1258
+ const productDeletion = newestDeletion(
1259
+ bundle.events,
1260
+ productAddress(product),
1261
+ storePubkey,
1262
+ NO_FUTURE_LIMIT
1263
+ );
1264
+ if (productDeletion !== void 0 && productDeletion.created_at >= product.createdAt) {
1265
+ return refuse("product_missing", "The store deleted this listing");
1266
+ }
1267
+ if (!isPurchasable(product)) {
1268
+ return refuse("product_not_on_sale", `The listing is ${product.visibility}`);
1269
+ }
1270
+ const profileEvent = newestSigned(bundle.events, KIND_STORE_PROFILE, storePubkey, now);
1271
+ const profile = profileEvent && !isLapsed(profileEvent, now) ? parseStoreProfile(profileEvent) : void 0;
1272
+ if (!profile) {
1273
+ return refuse("store_profile_missing", "No valid, unexpired profile signed by the store");
1274
+ }
1275
+ const anchor = resolveAnchor(storePubkey, profile, bundle, options);
1276
+ if ("ok" in anchor) {
1277
+ return anchor;
1278
+ }
1279
+ const { ownerPubkey } = anchor;
1280
+ const warnings = [...anchor.warnings];
1281
+ if (options.pinnedOwnerPubkey !== void 0 && options.pinnedOwnerPubkey !== ownerPubkey) {
1282
+ return refuse(
1283
+ "owner_pin_mismatch",
1284
+ "The store now names a different owner than at the first purchase"
1285
+ );
1286
+ }
1287
+ if (options.pinnedOwnerPubkey === void 0) {
1288
+ warnings.push("owner_unpinned");
1289
+ }
1290
+ const authEvent = newestSigned(bundle.events, KIND_STORE_AUTH, ownerPubkey, now, storePubkey);
1291
+ const auth = authEvent ? readStoreAuth(authEvent, storePubkey, now) : void 0;
1292
+ const deletion = newestDeletion(
1293
+ bundle.events,
1294
+ storeAuthAddress(ownerPubkey, storePubkey),
1295
+ ownerPubkey,
1296
+ NO_FUTURE_LIMIT
1297
+ );
1298
+ const revocation = newestSigned(
1299
+ bundle.events.filter(
1300
+ (event) => isEventShaped(event) && readStoreAuth(event, storePubkey, now).status === "revoked"
1301
+ ),
1302
+ KIND_STORE_AUTH,
1303
+ ownerPubkey,
1304
+ NO_FUTURE_LIMIT,
1305
+ storePubkey
1306
+ );
1307
+ const revokedAfter = (event) => event !== void 0 && (!authEvent || event.created_at >= authEvent.created_at);
1308
+ if (auth?.status === "revoked" || revokedAfter(deletion) || revokedAfter(revocation)) {
1309
+ return refuse("store_auth_revoked", "The owner revoked this store key");
1310
+ }
1311
+ if (auth?.status === "expired") {
1312
+ return refuse("store_auth_expired", "The owner's authorization of this store key expired");
1313
+ }
1314
+ if (auth?.status === "malformed") {
1315
+ return refuse(
1316
+ "store_auth_missing",
1317
+ "The owner's newest authorization of this store key is malformed"
1318
+ );
1319
+ }
1320
+ if (!anchor.ownerFromDomain && auth?.status !== "active") {
1321
+ return refuse("store_auth_missing", "The owner has not authorized this store key");
1322
+ }
1323
+ if (options.strictOrigin && options.pageOrigin === void 0) {
1324
+ return refuse("origin_mismatch", "Strict mode needs the origin of the embedding page");
1325
+ }
1326
+ if (options.pageOrigin !== void 0) {
1327
+ if (anchor.level !== "A" || anchor.domain === void 0) {
1328
+ if (options.strictOrigin) {
1329
+ return refuse("origin_mismatch", "The store has no verified domain to match this page");
1330
+ }
1331
+ warnings.push("origin_unverifiable");
1332
+ } else if (!originMatchesDomain(options.pageOrigin, anchor.domain)) {
1333
+ if (options.strictOrigin) {
1334
+ return refuse("origin_mismatch", `This page is not on ${anchor.domain}`);
1335
+ }
1336
+ warnings.push("origin_mismatch");
1337
+ }
1338
+ }
1339
+ const paytoEvent = newestSigned(bundle.events, KIND_PAYTO, ownerPubkey, now);
1340
+ if (!paytoEvent || isLapsed(paytoEvent, now)) {
1341
+ return refuse("payto_missing", "The owner has no current payout addresses");
1342
+ }
1343
+ const accepted = new Set(product.accept);
1344
+ const payouts = parsePayto(paytoEvent).targets.filter((target) => accepted.has(target.caip19.id));
1345
+ if (payouts.length === 0) {
1346
+ return refuse(
1347
+ "no_payout_for_accepted_assets",
1348
+ "The owner has no payout address for any asset this product accepts"
1349
+ );
1350
+ }
1351
+ const paytoAge = now - Math.max(paytoEvent.created_at, options.paytoFirstSeenAt ?? 0);
1352
+ if (paytoAge < (options.cooldownSecs ?? PAYOUT_COOLDOWN_SECS)) {
1353
+ warnings.push("payout_recently_changed");
1354
+ }
1355
+ const knownPayouts = options.knownPayouts;
1356
+ if (knownPayouts !== void 0 && payouts.some(
1357
+ (target) => !knownPayouts.some(
1358
+ (known) => known.caip19 === target.caip19.id && canonicalPayoutAddress(target.caip19.chain, known.address) === target.address
1359
+ )
1360
+ )) {
1361
+ warnings.push("payout_changed");
1362
+ }
1363
+ if (payouts.some((target) => !target.walletSigned)) {
1364
+ warnings.push("payout_unsigned");
1365
+ }
1366
+ const offer = {
1367
+ level: anchor.level,
1368
+ storePubkey,
1369
+ ownerPubkey,
1370
+ profile,
1371
+ product,
1372
+ payouts,
1373
+ paytoCreatedAt: paytoEvent.created_at,
1374
+ warnings
1375
+ };
1376
+ if (anchor.domain !== void 0) {
1377
+ offer.domain = anchor.domain;
1378
+ }
1379
+ return { ok: true, offer };
1380
+ }
1381
+ function isOfferPayout(offer, caip19, recipient) {
1382
+ const asset = parseCaip19(caip19);
1383
+ if (!asset) {
1384
+ return false;
1385
+ }
1386
+ const canonical = canonicalPayoutAddress(asset.chain, recipient);
1387
+ return canonical !== void 0 && offer.payouts.some((target) => target.caip19.id === caip19 && target.address === canonical);
1388
+ }
1389
+ async function resolveOrUnreachable(resolve, nip05) {
1390
+ try {
1391
+ return await resolve(nip05);
1392
+ } catch {
1393
+ return void 0;
1394
+ }
1395
+ }
1396
+ async function verifyOffer(naddr, deps, options = {}) {
1397
+ const pointer = decodeProductNaddr(naddr);
1398
+ if (!pointer) {
1399
+ return refuse("bad_pointer", "Not a product naddr");
1400
+ }
1401
+ const { storePubkey, d } = pointer;
1402
+ const storeEvents = await deps.fetchEvents([
1403
+ { kinds: [KIND_PRODUCT], authors: [storePubkey], "#d": [d] },
1404
+ { kinds: [KIND_STORE_PROFILE], authors: [storePubkey] },
1405
+ { kinds: [KIND_DELETION], authors: [storePubkey], "#a": [productAddress({ storePubkey, d })] }
1406
+ ]);
1407
+ const now = options.now ?? nowSecs();
1408
+ const profileEvent = newestSigned(storeEvents, KIND_STORE_PROFILE, storePubkey, now);
1409
+ const profile = profileEvent ? parseStoreProfile(profileEvent) : void 0;
1410
+ let domain;
1411
+ if (profile?.nip05 !== void 0) {
1412
+ const resolve = deps.resolveDomain ?? ((nip05) => resolveDomainKeys(nip05, deps));
1413
+ domain = splitNip05(profile.nip05) === void 0 ? "unreachable" : await resolveOrUnreachable(resolve, profile.nip05) ?? "unreachable";
1414
+ }
1415
+ const owners = /* @__PURE__ */ new Set();
1416
+ if (profile?.ownerPubkey) {
1417
+ owners.add(profile.ownerPubkey);
1418
+ }
1419
+ if (typeof domain === "object" && domain.ownerPubkey) {
1420
+ owners.add(domain.ownerPubkey);
1421
+ }
1422
+ const ownerEvents = owners.size === 0 ? [] : await deps.fetchEvents([
1423
+ { kinds: [KIND_PAYTO], authors: [...owners] },
1424
+ { kinds: [KIND_STORE_AUTH], authors: [...owners], "#d": [storePubkey] },
1425
+ {
1426
+ kinds: [KIND_DELETION],
1427
+ authors: [...owners],
1428
+ "#a": [...owners].map((owner) => storeAuthAddress(owner, storePubkey))
1429
+ }
1430
+ ]);
1431
+ const ownerKinds = [KIND_PAYTO, KIND_STORE_AUTH, KIND_DELETION];
1432
+ const ownerOnly = ownerEvents.filter(
1433
+ (event) => isEventShaped(event) && owners.has(event.pubkey) && ownerKinds.includes(event.kind)
1434
+ );
1435
+ const bundle = { events: [...storeEvents, ...ownerOnly] };
1436
+ if (domain !== void 0) {
1437
+ bundle.domain = domain;
1438
+ }
1439
+ return evaluateOffer({ storePubkey, d }, bundle, { ...options, now });
1440
+ }
1441
+
1442
+ export { DELIVERY_METHODS, ELISYM_NETWORK_TAG, 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, ORDER_STATUSES, PAYOUT_COOLDOWN_SECS, PAYTO_PROOF_PREFIX, PURCHASABLE_VISIBILITIES, 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 };
1443
+ //# sourceMappingURL=index.js.map
1444
+ //# sourceMappingURL=index.js.map