@qntx/nostr 0.0.1
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/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/builder-BphJa-Y-.d.mts +67 -0
- package/dist/builder-q0VsvnYE.mjs +158 -0
- package/dist/builder-q0VsvnYE.mjs.map +1 -0
- package/dist/client-Bd8B1mYm.mjs +835 -0
- package/dist/client-Bd8B1mYm.mjs.map +1 -0
- package/dist/client.d.mts +2 -0
- package/dist/client.mjs +2 -0
- package/dist/core.d.mts +8 -0
- package/dist/core.mjs +9 -0
- package/dist/error-B49rbDzT.d.mts +7 -0
- package/dist/error-CwBxle6N.mjs +13 -0
- package/dist/error-CwBxle6N.mjs.map +1 -0
- package/dist/error-DZEs5OuZ.mjs +24 -0
- package/dist/error-DZEs5OuZ.mjs.map +1 -0
- package/dist/error-WEFS-R-4.d.mts +20 -0
- package/dist/event-B5D2jxt_.d.mts +108 -0
- package/dist/event-CzoQEFMF.mjs +188 -0
- package/dist/event-CzoQEFMF.mjs.map +1 -0
- package/dist/filter-BxwjEkZp.mjs +113 -0
- package/dist/filter-BxwjEkZp.mjs.map +1 -0
- package/dist/filter-DUEKP3q1.d.mts +40 -0
- package/dist/gossip-3xkRRCm_.mjs +220 -0
- package/dist/gossip-3xkRRCm_.mjs.map +1 -0
- package/dist/gossip-C5RVB1UB.d.mts +71 -0
- package/dist/gossip.d.mts +3 -0
- package/dist/gossip.mjs +2 -0
- package/dist/http-6vevbBlu.d.mts +22 -0
- package/dist/http-D2rhokZo.mjs +24 -0
- package/dist/http-D2rhokZo.mjs.map +1 -0
- package/dist/index-BYMnhRIC.d.mts +161 -0
- package/dist/index-C0C4e-Gn.d.mts +55 -0
- package/dist/index-CaxrMD1A.d.mts +1 -0
- package/dist/index-DaRyjo7O.d.mts +77 -0
- package/dist/index-DgOugrpF.d.mts +275 -0
- package/dist/index-EwL6fHLo.d.mts +267 -0
- package/dist/index-k2YhCgsh.d.mts +57 -0
- package/dist/index.d.mts +22 -0
- package/dist/index.mjs +23 -0
- package/dist/key-BiGxGLhg.d.mts +37 -0
- package/dist/key-Dn-aX4x4.mjs +130 -0
- package/dist/key-Dn-aX4x4.mjs.map +1 -0
- package/dist/kind-BLl4cLOc.mjs +59 -0
- package/dist/kind-BLl4cLOc.mjs.map +1 -0
- package/dist/loaders-BNQszDAn.mjs +767 -0
- package/dist/loaders-BNQszDAn.mjs.map +1 -0
- package/dist/loaders.d.mts +3 -0
- package/dist/loaders.mjs +2 -0
- package/dist/memory-BCmhZPek.mjs +461 -0
- package/dist/memory-BCmhZPek.mjs.map +1 -0
- package/dist/message-C_Uj-Y-c.mjs +179 -0
- package/dist/message-C_Uj-Y-c.mjs.map +1 -0
- package/dist/message-DJC-Jz6T.d.mts +32 -0
- package/dist/nip17-BWq-Kfq5.d.mts +38 -0
- package/dist/nip19-D7905oIv.d.mts +63 -0
- package/dist/nip46-C_DoxiS_.d.mts +51 -0
- package/dist/nip59-DljW_sYh.d.mts +76 -0
- package/dist/nip65-Di5hZRuV.d.mts +21 -0
- package/dist/nip77-ByxlgoQO.d.mts +55 -0
- package/dist/nips/blossom.d.mts +93 -0
- package/dist/nips/blossom.mjs +325 -0
- package/dist/nips/blossom.mjs.map +1 -0
- package/dist/nips/nip04.d.mts +8 -0
- package/dist/nips/nip04.mjs +41 -0
- package/dist/nips/nip04.mjs.map +1 -0
- package/dist/nips/nip05.d.mts +89 -0
- package/dist/nips/nip05.mjs +147 -0
- package/dist/nips/nip05.mjs.map +1 -0
- package/dist/nips/nip10.d.mts +43 -0
- package/dist/nips/nip10.mjs +194 -0
- package/dist/nips/nip10.mjs.map +1 -0
- package/dist/nips/nip11.d.mts +43 -0
- package/dist/nips/nip11.mjs +99 -0
- package/dist/nips/nip11.mjs.map +1 -0
- package/dist/nips/nip13.d.mts +23 -0
- package/dist/nips/nip13.mjs +88 -0
- package/dist/nips/nip13.mjs.map +1 -0
- package/dist/nips/nip17.d.mts +2 -0
- package/dist/nips/nip17.mjs +138 -0
- package/dist/nips/nip17.mjs.map +1 -0
- package/dist/nips/nip19.d.mts +2 -0
- package/dist/nips/nip19.mjs +174 -0
- package/dist/nips/nip19.mjs.map +1 -0
- package/dist/nips/nip21.d.mts +17 -0
- package/dist/nips/nip21.mjs +29 -0
- package/dist/nips/nip21.mjs.map +1 -0
- package/dist/nips/nip27.d.mts +42 -0
- package/dist/nips/nip27.mjs +175 -0
- package/dist/nips/nip27.mjs.map +1 -0
- package/dist/nips/nip42.d.mts +12 -0
- package/dist/nips/nip42.mjs +22 -0
- package/dist/nips/nip42.mjs.map +1 -0
- package/dist/nips/nip44.d.mts +12 -0
- package/dist/nips/nip44.mjs +125 -0
- package/dist/nips/nip44.mjs.map +1 -0
- package/dist/nips/nip46.d.mts +2 -0
- package/dist/nips/nip46.mjs +132 -0
- package/dist/nips/nip46.mjs.map +1 -0
- package/dist/nips/nip49.d.mts +12 -0
- package/dist/nips/nip49.mjs +80 -0
- package/dist/nips/nip49.mjs.map +1 -0
- package/dist/nips/nip51.d.mts +88 -0
- package/dist/nips/nip51.mjs +246 -0
- package/dist/nips/nip51.mjs.map +1 -0
- package/dist/nips/nip57.d.mts +43 -0
- package/dist/nips/nip57.mjs +230 -0
- package/dist/nips/nip57.mjs.map +1 -0
- package/dist/nips/nip59.d.mts +2 -0
- package/dist/nips/nip59.mjs +171 -0
- package/dist/nips/nip59.mjs.map +1 -0
- package/dist/nips/nip65.d.mts +2 -0
- package/dist/nips/nip65.mjs +76 -0
- package/dist/nips/nip65.mjs.map +1 -0
- package/dist/nips/nip77.d.mts +2 -0
- package/dist/nips/nip77.mjs +421 -0
- package/dist/nips/nip77.mjs.map +1 -0
- package/dist/nips/nip96.d.mts +31 -0
- package/dist/nips/nip96.mjs +95 -0
- package/dist/nips/nip96.mjs.map +1 -0
- package/dist/nips/nip98.d.mts +28 -0
- package/dist/nips/nip98.mjs +91 -0
- package/dist/nips/nip98.mjs.map +1 -0
- package/dist/nostr_crypto_wasm_bg-hi5aXi5P.mjs +4 -0
- package/dist/nostr_crypto_wasm_bg.wasm +0 -0
- package/dist/pool-C_j1OmBB.mjs +1881 -0
- package/dist/pool-C_j1OmBB.mjs.map +1 -0
- package/dist/pool-DhRi7l-b.d.mts +297 -0
- package/dist/relay.d.mts +4 -0
- package/dist/relay.mjs +2 -0
- package/dist/signer-CZ6K6FUc.mjs +473 -0
- package/dist/signer-CZ6K6FUc.mjs.map +1 -0
- package/dist/signer.d.mts +3 -0
- package/dist/signer.mjs +2 -0
- package/dist/storage-DOy0iLRE.mjs +870 -0
- package/dist/storage-DOy0iLRE.mjs.map +1 -0
- package/dist/storage.d.mts +4 -0
- package/dist/storage.mjs +4 -0
- package/dist/types-BzyADqnw.d.mts +19 -0
- package/dist/types-Dn24TPFs.d.mts +43 -0
- package/dist/util-6tfLQ9JF.mjs +73 -0
- package/dist/util-6tfLQ9JF.mjs.map +1 -0
- package/dist/wasm.d.mts +19 -0
- package/dist/wasm.mjs +237 -0
- package/dist/wasm.mjs.map +1 -0
- package/package.json +204 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nip49.mjs","names":[],"sources":["../../src/nips/nip49.ts"],"sourcesContent":["/**\n * NIP-49: Private Key Encryption (`ncryptsec`).\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/49.md\n */\nimport { xchacha20poly1305 } from \"@noble/ciphers/chacha.js\";\nimport { scrypt } from \"@noble/hashes/scrypt.js\";\nimport { concatBytes, randomBytes } from \"@noble/hashes/utils.js\";\nimport { bech32 } from \"@scure/base\";\nimport { NostrError } from \"../core/error.ts\";\nimport { assertSecretKeyBytes } from \"../core/util.ts\";\nimport { Bech32MaxSize, encodeBytes } from \"./nip19.ts\";\n\nexport type Ncryptsec = `ncryptsec1${string}`;\nexport type KeySecurityByte = 0x00 | 0x01 | 0x02;\n\nconst VERSION = 0x02;\nconst SALT_LEN = 16;\nconst NONCE_LEN = 24;\nconst PAYLOAD_LEN = 91;\nconst LOGN_MIN = 1;\nconst LOGN_MAX = 22;\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\n\nexport class Nip49Error extends NostrError {}\n\nfunction assertLogn(logn: number): void {\n if (!Number.isInteger(logn) || logn < LOGN_MIN || logn > LOGN_MAX) {\n throw new Nip49Error(`invalid logn ${logn}, expected integer ${LOGN_MIN}..${LOGN_MAX}`);\n }\n}\n\nfunction deriveKey(password: string, salt: Uint8Array, logn: number): Uint8Array {\n assertLogn(logn);\n const N = 2 ** logn;\n return scrypt(password.normalize(\"NFKC\"), salt, {\n N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n dkLen: 32,\n // noble 2.3: V (N blocks) + p B blocks + one tmp scratch block.\n maxmem: 128 * SCRYPT_R * (N + SCRYPT_P + 1),\n });\n}\n\n/** Encrypt a 32-byte secret key to an `ncryptsec` bech32 string. */\nexport function encrypt(\n secretKey: Uint8Array,\n password: string,\n logn: number = 16,\n ksb: KeySecurityByte = 0x02,\n): Ncryptsec {\n assertSecretKeyBytes(secretKey);\n const salt = randomBytes(SALT_LEN);\n const key = deriveKey(password, salt, logn);\n const nonce = randomBytes(NONCE_LEN);\n const aad = Uint8Array.from([ksb]);\n const ciphertext = xchacha20poly1305(key, nonce, aad).encrypt(secretKey);\n const bytes = concatBytes(\n Uint8Array.from([VERSION]),\n Uint8Array.from([logn]),\n salt,\n nonce,\n aad,\n ciphertext,\n );\n return encodeBytes(\"ncryptsec\", bytes);\n}\n\n/** Decrypt an `ncryptsec` bech32 string to a 32-byte secret key. */\nexport function decrypt(ncryptsec: string, password: string): Uint8Array {\n let prefix: string;\n let b: Uint8Array;\n try {\n const decoded = bech32.decode(ncryptsec as `${string}1${string}`, Bech32MaxSize);\n prefix = decoded.prefix;\n b = new Uint8Array(bech32.fromWords(decoded.words));\n } catch (cause) {\n throw new Nip49Error(\"invalid ncryptsec\", {\n cause: cause instanceof Error ? cause : undefined,\n });\n }\n if (prefix !== \"ncryptsec\") {\n throw new Nip49Error(`invalid prefix ${prefix}, expected 'ncryptsec'`);\n }\n if (b.length !== PAYLOAD_LEN) {\n throw new Nip49Error(\"invalid ncryptsec length\");\n }\n const version = b[0]!;\n if (version !== VERSION) {\n throw new Nip49Error(`invalid version ${version}, expected 0x02`);\n }\n const logn = b[1]!;\n const salt = b.subarray(2, 2 + SALT_LEN);\n const nonce = b.subarray(2 + SALT_LEN, 2 + SALT_LEN + NONCE_LEN);\n const ksb = b[2 + SALT_LEN + NONCE_LEN]!;\n const aad = Uint8Array.from([ksb]);\n const ciphertext = b.subarray(2 + SALT_LEN + NONCE_LEN + 1);\n try {\n const key = deriveKey(password, salt, logn);\n return xchacha20poly1305(key, nonce, aad).decrypt(ciphertext);\n } catch (cause) {\n if (cause instanceof Nip49Error) throw cause;\n throw new Nip49Error(\"failed to decrypt\", {\n cause: cause instanceof Error ? cause : undefined,\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAgBA,MAAM,UAAU;AAChB,MAAM,WAAW;AACjB,MAAM,YAAY;AAClB,MAAM,cAAc;AACpB,MAAM,WAAW;AACjB,MAAM,WAAW;AACjB,MAAM,WAAW;AACjB,MAAM,WAAW;AAEjB,IAAa,aAAb,cAAgC,WAAW,CAAC;AAE5C,SAAS,WAAW,MAAoB;CACtC,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,YAAY,OAAO,UACvD,MAAM,IAAI,WAAW,gBAAgB,KAAK,qBAAqB,SAAS,IAAI,UAAU;AAE1F;AAEA,SAAS,UAAU,UAAkB,MAAkB,MAA0B;CAC/E,WAAW,IAAI;CACf,MAAM,IAAI,KAAK;CACf,OAAO,OAAO,SAAS,UAAU,MAAM,GAAG,MAAM;EAC9C;EACA,GAAG;EACH,GAAG;EACH,OAAO;EAEP,QAAQ,QAAkB,IAAI,WAAW;CAC3C,CAAC;AACH;;AAGA,SAAgB,QACd,WACA,UACA,OAAe,IACf,MAAuB,GACZ;CACX,qBAAqB,SAAS;CAC9B,MAAM,OAAO,YAAY,QAAQ;CACjC,MAAM,MAAM,UAAU,UAAU,MAAM,IAAI;CAC1C,MAAM,QAAQ,YAAY,SAAS;CACnC,MAAM,MAAM,WAAW,KAAK,CAAC,GAAG,CAAC;CACjC,MAAM,aAAa,kBAAkB,KAAK,OAAO,GAAG,CAAC,CAAC,QAAQ,SAAS;CACvE,MAAM,QAAQ,YACZ,WAAW,KAAK,CAAC,OAAO,CAAC,GACzB,WAAW,KAAK,CAAC,IAAI,CAAC,GACtB,MACA,OACA,KACA,UACF;CACA,OAAO,YAAY,aAAa,KAAK;AACvC;;AAGA,SAAgB,QAAQ,WAAmB,UAA8B;CACvE,IAAI;CACJ,IAAI;CACJ,IAAI;EACF,MAAM,UAAU,OAAO,OAAO,WAAoC,aAAa;EAC/E,SAAS,QAAQ;EACjB,IAAI,IAAI,WAAW,OAAO,UAAU,QAAQ,KAAK,CAAC;CACpD,SAAS,OAAO;EACd,MAAM,IAAI,WAAW,qBAAqB,EACxC,OAAO,iBAAiB,QAAQ,QAAQ,KAAA,EAC1C,CAAC;CACH;CACA,IAAI,WAAW,aACb,MAAM,IAAI,WAAW,kBAAkB,OAAO,uBAAuB;CAEvE,IAAI,EAAE,WAAW,aACf,MAAM,IAAI,WAAW,0BAA0B;CAEjD,MAAM,UAAU,EAAE;CAClB,IAAI,YAAY,SACd,MAAM,IAAI,WAAW,mBAAmB,QAAQ,gBAAgB;CAElE,MAAM,OAAO,EAAE;CACf,MAAM,OAAO,EAAE,SAAS,GAAG,EAAY;CACvC,MAAM,QAAQ,EAAE,SAAS,IAAc,EAAwB;CAC/D,MAAM,MAAM,EAAE;CACd,MAAM,MAAM,WAAW,KAAK,CAAC,GAAG,CAAC;CACjC,MAAM,aAAa,EAAE,SAAS,EAA4B;CAC1D,IAAI;EACF,MAAM,MAAM,UAAU,UAAU,MAAM,IAAI;EAC1C,OAAO,kBAAkB,KAAK,OAAO,GAAG,CAAC,CAAC,QAAQ,UAAU;CAC9D,SAAS,OAAO;EACd,IAAI,iBAAiB,YAAY,MAAM;EACvC,MAAM,IAAI,WAAW,qBAAqB,EACxC,OAAO,iBAAiB,QAAQ,QAAQ,KAAA,EAC1C,CAAC;CACH;AACF"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { t as Event } from "../event-B5D2jxt_.mjs";
|
|
2
|
+
import { t as EventBuilder } from "../builder-BphJa-Y-.mjs";
|
|
3
|
+
//#region src/nips/nip51.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Structural crypto used by NIP-51 private tags.
|
|
6
|
+
* Satisfied by NostrSigner when nip44Encrypt/nip44Decrypt are present.
|
|
7
|
+
* This module must not import src/signer/.
|
|
8
|
+
*/
|
|
9
|
+
type Nip51Crypto = {
|
|
10
|
+
getPublicKey(): Promise<string>;
|
|
11
|
+
nip44Encrypt(peer: string, plaintext: string): Promise<string>;
|
|
12
|
+
nip44Decrypt(peer: string, payload: string): Promise<string>;
|
|
13
|
+
};
|
|
14
|
+
type MuteItem = {
|
|
15
|
+
type: "pubkey";
|
|
16
|
+
value: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: "event";
|
|
19
|
+
value: string;
|
|
20
|
+
} | {
|
|
21
|
+
type: "hashtag";
|
|
22
|
+
value: string;
|
|
23
|
+
} | {
|
|
24
|
+
type: "word";
|
|
25
|
+
value: string;
|
|
26
|
+
};
|
|
27
|
+
/** Parse kind:10000 mute list public tags (`p` / `e` / `t` / `word`). */
|
|
28
|
+
declare function parseMuteList(event: Pick<Event, "kind" | "tags">): MuteItem[];
|
|
29
|
+
/** Build an unsigned kind:10000 EventBuilder from public mute items. */
|
|
30
|
+
declare function muteListEventBuilder(items: readonly MuteItem[]): EventBuilder;
|
|
31
|
+
/** NIP-44 ciphertext of tags, conversation peer = author's own pubkey. */
|
|
32
|
+
declare function encryptPrivateTags(crypto: Nip51Crypto, tags: Event["tags"]): Promise<string>;
|
|
33
|
+
/** Decrypt NIP-51 `.content`. Empty content is no private tags, not ciphertext. */
|
|
34
|
+
declare function decryptPrivateTags(crypto: Nip51Crypto, event: Pick<Event, "pubkey" | "content">): Promise<Event["tags"]>;
|
|
35
|
+
/** Public mute items from tags; private mute items from NIP-44 `.content`. */
|
|
36
|
+
declare function parseMuteListPrivate(crypto: Nip51Crypto, event: Pick<Event, "kind" | "tags" | "pubkey" | "content">): Promise<{
|
|
37
|
+
public: MuteItem[];
|
|
38
|
+
private: MuteItem[];
|
|
39
|
+
}>;
|
|
40
|
+
/** Parse kind:10001 pin list public `e` tags. */
|
|
41
|
+
declare function parsePinList(event: Pick<Event, "kind" | "tags">): string[];
|
|
42
|
+
/** Build an unsigned kind:10001 EventBuilder from event ids. */
|
|
43
|
+
declare function pinListEventBuilder(ids: readonly string[]): EventBuilder;
|
|
44
|
+
/** Parse kind:10003 bookmark list public `e` and `a` tags. */
|
|
45
|
+
declare function parseBookmarkList(event: Pick<Event, "kind" | "tags">): {
|
|
46
|
+
e: string[];
|
|
47
|
+
a: string[];
|
|
48
|
+
};
|
|
49
|
+
/** Build an unsigned kind:10003 EventBuilder from public bookmark pointers. */
|
|
50
|
+
declare function bookmarkListEventBuilder(items: {
|
|
51
|
+
e?: readonly string[];
|
|
52
|
+
a?: readonly string[];
|
|
53
|
+
}): EventBuilder;
|
|
54
|
+
/** Parse kind:10030 user emoji list public `emoji` and `a` tags. */
|
|
55
|
+
declare function parseUserEmojiList(event: Pick<Event, "kind" | "tags">): {
|
|
56
|
+
emoji: Array<{
|
|
57
|
+
shortcode: string;
|
|
58
|
+
url: string;
|
|
59
|
+
}>;
|
|
60
|
+
sets: string[];
|
|
61
|
+
};
|
|
62
|
+
/** Parse kind:30002 relay set public `d` and `relay` tags. */
|
|
63
|
+
declare function parseRelaySet(event: Pick<Event, "kind" | "tags">): {
|
|
64
|
+
d: string;
|
|
65
|
+
relays: string[];
|
|
66
|
+
};
|
|
67
|
+
/** Parse kind:10012 favorite relays public `relay` tags and kind:30002 `a` tags. */
|
|
68
|
+
declare function parseFavoriteRelays(event: Pick<Event, "kind" | "tags">): {
|
|
69
|
+
relays: string[];
|
|
70
|
+
sets: string[];
|
|
71
|
+
};
|
|
72
|
+
/** Parse kind:30030 emoji set public `d` / `title` / `emoji` tags. */
|
|
73
|
+
declare function parseEmojiSet(event: Pick<Event, "kind" | "tags">): {
|
|
74
|
+
d: string;
|
|
75
|
+
title?: string;
|
|
76
|
+
emoji: Array<{
|
|
77
|
+
shortcode: string;
|
|
78
|
+
url: string;
|
|
79
|
+
}>;
|
|
80
|
+
};
|
|
81
|
+
/** Parse kind:39089 starter pack (follow pack) public `d` and `p` tags. */
|
|
82
|
+
declare function parseFollowPack(event: Pick<Event, "kind" | "tags">): {
|
|
83
|
+
d: string;
|
|
84
|
+
pubkeys: string[];
|
|
85
|
+
};
|
|
86
|
+
//#endregion
|
|
87
|
+
export { MuteItem, Nip51Crypto, bookmarkListEventBuilder, decryptPrivateTags, encryptPrivateTags, muteListEventBuilder, parseBookmarkList, parseEmojiSet, parseFavoriteRelays, parseFollowPack, parseMuteList, parseMuteListPrivate, parsePinList, parseRelaySet, parseUserEmojiList, pinListEventBuilder };
|
|
88
|
+
//# sourceMappingURL=nip51.d.mts.map
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { a as isHex32, s as normalizeURL, t as assertHex32 } from "../util-6tfLQ9JF.mjs";
|
|
2
|
+
import { n as EventValidationError } from "../error-DZEs5OuZ.mjs";
|
|
3
|
+
import { t as Kind } from "../kind-BLl4cLOc.mjs";
|
|
4
|
+
import { _ as getDTag, m as Tag, v as isTag } from "../event-CzoQEFMF.mjs";
|
|
5
|
+
import { t as EventBuilder } from "../builder-q0VsvnYE.mjs";
|
|
6
|
+
//#region src/nips/nip51.ts
|
|
7
|
+
/**
|
|
8
|
+
* NIP-51: Lists
|
|
9
|
+
* Public tags plus NIP-44 private `.content` (author encrypts to self).
|
|
10
|
+
* Crypto is injected; this module does not import signer. No NIP-04 sniff.
|
|
11
|
+
* Kind 10063 Blossom servers live in blossom.ts.
|
|
12
|
+
*
|
|
13
|
+
* @see https://github.com/nostr-protocol/nips/blob/master/51.md
|
|
14
|
+
*/
|
|
15
|
+
function requireKind(event, kind) {
|
|
16
|
+
if (event.kind !== kind) throw new EventValidationError(`expected kind ${kind}, got ${event.kind}`);
|
|
17
|
+
}
|
|
18
|
+
function collectRelays(tags) {
|
|
19
|
+
const out = [];
|
|
20
|
+
const seen = /* @__PURE__ */ new Set();
|
|
21
|
+
for (const tag of tags) {
|
|
22
|
+
if (tag[0] !== "relay" || !tag[1]) continue;
|
|
23
|
+
let url;
|
|
24
|
+
try {
|
|
25
|
+
url = normalizeURL(tag[1]);
|
|
26
|
+
} catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (seen.has(url)) continue;
|
|
30
|
+
seen.add(url);
|
|
31
|
+
out.push(url);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function collectEmoji(tags) {
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const tag of tags) {
|
|
38
|
+
if (tag[0] !== "emoji") continue;
|
|
39
|
+
const shortcode = tag[1];
|
|
40
|
+
const url = tag[2];
|
|
41
|
+
if (!shortcode || !url) continue;
|
|
42
|
+
out.push({
|
|
43
|
+
shortcode,
|
|
44
|
+
url
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
function firstTagValue(tags, name) {
|
|
50
|
+
for (const tag of tags) if (tag[0] === name && tag[1]) return tag[1];
|
|
51
|
+
}
|
|
52
|
+
function identifier(tags) {
|
|
53
|
+
return getDTag(tags) ?? "";
|
|
54
|
+
}
|
|
55
|
+
/** NIP-51 a-tag: `30002:<64-hex-pubkey>:<d>` with `d` possibly containing `:`. */
|
|
56
|
+
function isRelaySetAddress(value) {
|
|
57
|
+
const kindSep = value.indexOf(":");
|
|
58
|
+
if (kindSep < 0) return false;
|
|
59
|
+
if (value.slice(0, kindSep) !== String(Kind.RelaySets)) return false;
|
|
60
|
+
const pkSep = value.indexOf(":", kindSep + 1);
|
|
61
|
+
if (pkSep < 0) return false;
|
|
62
|
+
const pubkey = value.slice(kindSep + 1, pkSep);
|
|
63
|
+
const d = value.slice(pkSep + 1);
|
|
64
|
+
return isHex32(pubkey) && d.length > 0;
|
|
65
|
+
}
|
|
66
|
+
/** Parse kind:10000 mute list public tags (`p` / `e` / `t` / `word`). */
|
|
67
|
+
function parseMuteList(event) {
|
|
68
|
+
requireKind(event, Kind.MuteList);
|
|
69
|
+
const items = [];
|
|
70
|
+
for (const tag of event.tags) {
|
|
71
|
+
const value = tag[1];
|
|
72
|
+
if (!value) continue;
|
|
73
|
+
switch (tag[0]) {
|
|
74
|
+
case "p":
|
|
75
|
+
if (isHex32(value)) items.push({
|
|
76
|
+
type: "pubkey",
|
|
77
|
+
value: value.toLowerCase()
|
|
78
|
+
});
|
|
79
|
+
break;
|
|
80
|
+
case "e":
|
|
81
|
+
if (isHex32(value)) items.push({
|
|
82
|
+
type: "event",
|
|
83
|
+
value: value.toLowerCase()
|
|
84
|
+
});
|
|
85
|
+
break;
|
|
86
|
+
case "t":
|
|
87
|
+
items.push({
|
|
88
|
+
type: "hashtag",
|
|
89
|
+
value
|
|
90
|
+
});
|
|
91
|
+
break;
|
|
92
|
+
case "word": items.push({
|
|
93
|
+
type: "word",
|
|
94
|
+
value: value.toLowerCase()
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return items;
|
|
99
|
+
}
|
|
100
|
+
/** Build an unsigned kind:10000 EventBuilder from public mute items. */
|
|
101
|
+
function muteListEventBuilder(items) {
|
|
102
|
+
const b = new EventBuilder(Kind.MuteList, "");
|
|
103
|
+
for (const item of items) switch (item.type) {
|
|
104
|
+
case "pubkey":
|
|
105
|
+
b.tag(Tag.p(assertHex32(item.value, "public key")));
|
|
106
|
+
break;
|
|
107
|
+
case "event":
|
|
108
|
+
b.tag(Tag.e(assertHex32(item.value, "event id")));
|
|
109
|
+
break;
|
|
110
|
+
case "hashtag":
|
|
111
|
+
if (item.value) b.tag(Tag.t(item.value));
|
|
112
|
+
break;
|
|
113
|
+
case "word": if (item.value) b.tag(["word", item.value.toLowerCase()]);
|
|
114
|
+
}
|
|
115
|
+
return b;
|
|
116
|
+
}
|
|
117
|
+
/** NIP-44 ciphertext of tags, conversation peer = author's own pubkey. */
|
|
118
|
+
async function encryptPrivateTags(crypto, tags) {
|
|
119
|
+
const self = await crypto.getPublicKey();
|
|
120
|
+
return crypto.nip44Encrypt(self, JSON.stringify(tags));
|
|
121
|
+
}
|
|
122
|
+
/** Decrypt NIP-51 `.content`. Empty content is no private tags, not ciphertext. */
|
|
123
|
+
async function decryptPrivateTags(crypto, event) {
|
|
124
|
+
if (event.content === "") return [];
|
|
125
|
+
const self = await crypto.getPublicKey();
|
|
126
|
+
if (self.toLowerCase() !== event.pubkey.toLowerCase()) throw new EventValidationError("NIP-51 private content is only for the author");
|
|
127
|
+
const plaintext = await crypto.nip44Decrypt(self, event.content);
|
|
128
|
+
let parsed;
|
|
129
|
+
try {
|
|
130
|
+
parsed = JSON.parse(plaintext);
|
|
131
|
+
} catch (cause) {
|
|
132
|
+
throw new EventValidationError("invalid NIP-51 private tags", { cause });
|
|
133
|
+
}
|
|
134
|
+
if (!Array.isArray(parsed) || !parsed.every(isTag)) throw new EventValidationError("invalid NIP-51 private tags");
|
|
135
|
+
return parsed;
|
|
136
|
+
}
|
|
137
|
+
/** Public mute items from tags; private mute items from NIP-44 `.content`. */
|
|
138
|
+
async function parseMuteListPrivate(crypto, event) {
|
|
139
|
+
const pub = parseMuteList(event);
|
|
140
|
+
const decrypted = await decryptPrivateTags(crypto, event);
|
|
141
|
+
return {
|
|
142
|
+
public: pub,
|
|
143
|
+
private: parseMuteList({
|
|
144
|
+
kind: event.kind,
|
|
145
|
+
tags: decrypted
|
|
146
|
+
})
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Parse kind:10001 pin list public `e` tags. */
|
|
150
|
+
function parsePinList(event) {
|
|
151
|
+
requireKind(event, Kind.PinList);
|
|
152
|
+
const ids = [];
|
|
153
|
+
for (const tag of event.tags) {
|
|
154
|
+
if (tag[0] !== "e" || !tag[1] || !isHex32(tag[1])) continue;
|
|
155
|
+
ids.push(tag[1].toLowerCase());
|
|
156
|
+
}
|
|
157
|
+
return ids;
|
|
158
|
+
}
|
|
159
|
+
/** Build an unsigned kind:10001 EventBuilder from event ids. */
|
|
160
|
+
function pinListEventBuilder(ids) {
|
|
161
|
+
const b = new EventBuilder(Kind.PinList, "");
|
|
162
|
+
for (const id of ids) b.tag(Tag.e(assertHex32(id, "event id")));
|
|
163
|
+
return b;
|
|
164
|
+
}
|
|
165
|
+
/** Parse kind:10003 bookmark list public `e` and `a` tags. */
|
|
166
|
+
function parseBookmarkList(event) {
|
|
167
|
+
requireKind(event, Kind.BookmarkList);
|
|
168
|
+
const e = [];
|
|
169
|
+
const a = [];
|
|
170
|
+
for (const tag of event.tags) {
|
|
171
|
+
if (!tag[1]) continue;
|
|
172
|
+
if (tag[0] === "e") {
|
|
173
|
+
if (isHex32(tag[1])) e.push(tag[1].toLowerCase());
|
|
174
|
+
} else if (tag[0] === "a") a.push(tag[1]);
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
e,
|
|
178
|
+
a
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** Build an unsigned kind:10003 EventBuilder from public bookmark pointers. */
|
|
182
|
+
function bookmarkListEventBuilder(items) {
|
|
183
|
+
const b = new EventBuilder(Kind.BookmarkList, "");
|
|
184
|
+
if (items.e) for (const id of items.e) b.tag(Tag.e(assertHex32(id, "event id")));
|
|
185
|
+
if (items.a) {
|
|
186
|
+
for (const coord of items.a) if (coord) b.tag(Tag.a(coord));
|
|
187
|
+
}
|
|
188
|
+
return b;
|
|
189
|
+
}
|
|
190
|
+
/** Parse kind:10030 user emoji list public `emoji` and `a` tags. */
|
|
191
|
+
function parseUserEmojiList(event) {
|
|
192
|
+
requireKind(event, Kind.UserEmojiList);
|
|
193
|
+
const sets = [];
|
|
194
|
+
for (const tag of event.tags) if (tag[0] === "a" && tag[1]) sets.push(tag[1]);
|
|
195
|
+
return {
|
|
196
|
+
emoji: collectEmoji(event.tags),
|
|
197
|
+
sets
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** Parse kind:30002 relay set public `d` and `relay` tags. */
|
|
201
|
+
function parseRelaySet(event) {
|
|
202
|
+
requireKind(event, Kind.RelaySets);
|
|
203
|
+
return {
|
|
204
|
+
d: identifier(event.tags),
|
|
205
|
+
relays: collectRelays(event.tags)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** Parse kind:10012 favorite relays public `relay` tags and kind:30002 `a` tags. */
|
|
209
|
+
function parseFavoriteRelays(event) {
|
|
210
|
+
requireKind(event, Kind.FavoriteRelays);
|
|
211
|
+
const sets = [];
|
|
212
|
+
for (const tag of event.tags) {
|
|
213
|
+
if (tag[0] !== "a" || !tag[1] || !isRelaySetAddress(tag[1])) continue;
|
|
214
|
+
sets.push(tag[1]);
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
relays: collectRelays(event.tags),
|
|
218
|
+
sets
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/** Parse kind:30030 emoji set public `d` / `title` / `emoji` tags. */
|
|
222
|
+
function parseEmojiSet(event) {
|
|
223
|
+
requireKind(event, Kind.EmojiSet);
|
|
224
|
+
return {
|
|
225
|
+
d: identifier(event.tags),
|
|
226
|
+
title: firstTagValue(event.tags, "title"),
|
|
227
|
+
emoji: collectEmoji(event.tags)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** Parse kind:39089 starter pack (follow pack) public `d` and `p` tags. */
|
|
231
|
+
function parseFollowPack(event) {
|
|
232
|
+
requireKind(event, Kind.StarterPack);
|
|
233
|
+
const pubkeys = [];
|
|
234
|
+
for (const tag of event.tags) {
|
|
235
|
+
if (tag[0] !== "p" || !tag[1] || !isHex32(tag[1])) continue;
|
|
236
|
+
pubkeys.push(tag[1].toLowerCase());
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
d: identifier(event.tags),
|
|
240
|
+
pubkeys
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
//#endregion
|
|
244
|
+
export { bookmarkListEventBuilder, decryptPrivateTags, encryptPrivateTags, muteListEventBuilder, parseBookmarkList, parseEmojiSet, parseFavoriteRelays, parseFollowPack, parseMuteList, parseMuteListPrivate, parsePinList, parseRelaySet, parseUserEmojiList, pinListEventBuilder };
|
|
245
|
+
|
|
246
|
+
//# sourceMappingURL=nip51.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nip51.mjs","names":[],"sources":["../../src/nips/nip51.ts"],"sourcesContent":["/**\n * NIP-51: Lists\n * Public tags plus NIP-44 private `.content` (author encrypts to self).\n * Crypto is injected; this module does not import signer. No NIP-04 sniff.\n * Kind 10063 Blossom servers live in blossom.ts.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/51.md\n */\nimport { EventBuilder } from \"../core/builder.ts\";\nimport { EventValidationError } from \"../core/error.ts\";\nimport type { Event } from \"../core/event.ts\";\nimport { Kind } from \"../core/kind.ts\";\nimport { getDTag, isTag, Tag } from \"../core/tag.ts\";\nimport { assertHex32, isHex32, normalizeURL } from \"../core/util.ts\";\n\n/**\n * Structural crypto used by NIP-51 private tags.\n * Satisfied by NostrSigner when nip44Encrypt/nip44Decrypt are present.\n * This module must not import src/signer/.\n */\nexport type Nip51Crypto = {\n getPublicKey(): Promise<string>;\n nip44Encrypt(peer: string, plaintext: string): Promise<string>;\n nip44Decrypt(peer: string, payload: string): Promise<string>;\n};\n\nexport type MuteItem =\n | { type: \"pubkey\"; value: string }\n | { type: \"event\"; value: string }\n | { type: \"hashtag\"; value: string }\n | { type: \"word\"; value: string };\n\nfunction requireKind(event: Pick<Event, \"kind\">, kind: number): void {\n if (event.kind !== kind) {\n throw new EventValidationError(`expected kind ${kind}, got ${event.kind}`);\n }\n}\n\nfunction collectRelays(tags: Event[\"tags\"]): string[] {\n const out: string[] = [];\n const seen = new Set<string>();\n for (const tag of tags) {\n if (tag[0] !== \"relay\" || !tag[1]) continue;\n let url: string;\n try {\n url = normalizeURL(tag[1]);\n } catch {\n continue;\n }\n if (seen.has(url)) continue;\n seen.add(url);\n out.push(url);\n }\n return out;\n}\n\nfunction collectEmoji(tags: Event[\"tags\"]): Array<{ shortcode: string; url: string }> {\n const out: Array<{ shortcode: string; url: string }> = [];\n for (const tag of tags) {\n if (tag[0] !== \"emoji\") continue;\n const shortcode = tag[1];\n const url = tag[2];\n if (!shortcode || !url) continue;\n out.push({ shortcode, url });\n }\n return out;\n}\n\nfunction firstTagValue(tags: Event[\"tags\"], name: string): string | undefined {\n for (const tag of tags) {\n if (tag[0] === name && tag[1]) return tag[1];\n }\n return undefined;\n}\n\nfunction identifier(tags: Event[\"tags\"]): string {\n return getDTag(tags) ?? \"\";\n}\n\n/** NIP-51 a-tag: `30002:<64-hex-pubkey>:<d>` with `d` possibly containing `:`. */\nfunction isRelaySetAddress(value: string): boolean {\n const kindSep = value.indexOf(\":\");\n if (kindSep < 0) return false;\n if (value.slice(0, kindSep) !== String(Kind.RelaySets)) return false;\n const pkSep = value.indexOf(\":\", kindSep + 1);\n if (pkSep < 0) return false;\n const pubkey = value.slice(kindSep + 1, pkSep);\n const d = value.slice(pkSep + 1);\n return isHex32(pubkey) && d.length > 0;\n}\n\n/** Parse kind:10000 mute list public tags (`p` / `e` / `t` / `word`). */\nexport function parseMuteList(event: Pick<Event, \"kind\" | \"tags\">): MuteItem[] {\n requireKind(event, Kind.MuteList);\n const items: MuteItem[] = [];\n for (const tag of event.tags) {\n const value = tag[1];\n if (!value) continue;\n switch (tag[0]) {\n case \"p\":\n if (isHex32(value)) items.push({ type: \"pubkey\", value: value.toLowerCase() });\n break;\n case \"e\":\n if (isHex32(value)) items.push({ type: \"event\", value: value.toLowerCase() });\n break;\n case \"t\":\n // NIP-51 does not require hashtag case-folding.\n items.push({ type: \"hashtag\", value });\n break;\n case \"word\":\n items.push({ type: \"word\", value: value.toLowerCase() });\n break;\n }\n }\n return items;\n}\n\n/** Build an unsigned kind:10000 EventBuilder from public mute items. */\nexport function muteListEventBuilder(items: readonly MuteItem[]): EventBuilder {\n const b = new EventBuilder(Kind.MuteList, \"\");\n for (const item of items) {\n switch (item.type) {\n case \"pubkey\":\n b.tag(Tag.p(assertHex32(item.value, \"public key\")));\n break;\n case \"event\":\n b.tag(Tag.e(assertHex32(item.value, \"event id\")));\n break;\n case \"hashtag\":\n if (item.value) b.tag(Tag.t(item.value));\n break;\n case \"word\":\n if (item.value) b.tag([\"word\", item.value.toLowerCase()]);\n break;\n }\n }\n return b;\n}\n\n/** NIP-44 ciphertext of tags, conversation peer = author's own pubkey. */\nexport async function encryptPrivateTags(\n crypto: Nip51Crypto,\n tags: Event[\"tags\"],\n): Promise<string> {\n const self = await crypto.getPublicKey();\n return crypto.nip44Encrypt(self, JSON.stringify(tags));\n}\n\n/** Decrypt NIP-51 `.content`. Empty content is no private tags, not ciphertext. */\nexport async function decryptPrivateTags(\n crypto: Nip51Crypto,\n event: Pick<Event, \"pubkey\" | \"content\">,\n): Promise<Event[\"tags\"]> {\n // NIP-44 rejects empty plaintext; empty `.content` means no private items.\n if (event.content === \"\") return [];\n const self = await crypto.getPublicKey();\n if (self.toLowerCase() !== event.pubkey.toLowerCase()) {\n throw new EventValidationError(\"NIP-51 private content is only for the author\");\n }\n const plaintext = await crypto.nip44Decrypt(self, event.content);\n let parsed: unknown;\n try {\n parsed = JSON.parse(plaintext);\n } catch (cause) {\n throw new EventValidationError(\"invalid NIP-51 private tags\", { cause });\n }\n if (!Array.isArray(parsed) || !parsed.every(isTag)) {\n throw new EventValidationError(\"invalid NIP-51 private tags\");\n }\n return parsed;\n}\n\n/** Public mute items from tags; private mute items from NIP-44 `.content`. */\nexport async function parseMuteListPrivate(\n crypto: Nip51Crypto,\n event: Pick<Event, \"kind\" | \"tags\" | \"pubkey\" | \"content\">,\n): Promise<{ public: MuteItem[]; private: MuteItem[] }> {\n const pub = parseMuteList(event);\n const decrypted = await decryptPrivateTags(crypto, event);\n return {\n public: pub,\n private: parseMuteList({ kind: event.kind, tags: decrypted }),\n };\n}\n\n/** Parse kind:10001 pin list public `e` tags. */\nexport function parsePinList(event: Pick<Event, \"kind\" | \"tags\">): string[] {\n requireKind(event, Kind.PinList);\n const ids: string[] = [];\n for (const tag of event.tags) {\n if (tag[0] !== \"e\" || !tag[1] || !isHex32(tag[1])) continue;\n ids.push(tag[1].toLowerCase());\n }\n return ids;\n}\n\n/** Build an unsigned kind:10001 EventBuilder from event ids. */\nexport function pinListEventBuilder(ids: readonly string[]): EventBuilder {\n const b = new EventBuilder(Kind.PinList, \"\");\n for (const id of ids) b.tag(Tag.e(assertHex32(id, \"event id\")));\n return b;\n}\n\n/** Parse kind:10003 bookmark list public `e` and `a` tags. */\nexport function parseBookmarkList(event: Pick<Event, \"kind\" | \"tags\">): {\n e: string[];\n a: string[];\n} {\n requireKind(event, Kind.BookmarkList);\n const e: string[] = [];\n const a: string[] = [];\n for (const tag of event.tags) {\n if (!tag[1]) continue;\n if (tag[0] === \"e\") {\n if (isHex32(tag[1])) e.push(tag[1].toLowerCase());\n } else if (tag[0] === \"a\") {\n a.push(tag[1]);\n }\n }\n return { e, a };\n}\n\n/** Build an unsigned kind:10003 EventBuilder from public bookmark pointers. */\nexport function bookmarkListEventBuilder(items: {\n e?: readonly string[];\n a?: readonly string[];\n}): EventBuilder {\n const b = new EventBuilder(Kind.BookmarkList, \"\");\n if (items.e) {\n for (const id of items.e) b.tag(Tag.e(assertHex32(id, \"event id\")));\n }\n if (items.a) {\n for (const coord of items.a) {\n if (coord) b.tag(Tag.a(coord));\n }\n }\n return b;\n}\n\n/** Parse kind:10030 user emoji list public `emoji` and `a` tags. */\nexport function parseUserEmojiList(event: Pick<Event, \"kind\" | \"tags\">): {\n emoji: Array<{ shortcode: string; url: string }>;\n sets: string[];\n} {\n requireKind(event, Kind.UserEmojiList);\n const sets: string[] = [];\n for (const tag of event.tags) {\n if (tag[0] === \"a\" && tag[1]) sets.push(tag[1]);\n }\n return { emoji: collectEmoji(event.tags), sets };\n}\n\n/** Parse kind:30002 relay set public `d` and `relay` tags. */\nexport function parseRelaySet(event: Pick<Event, \"kind\" | \"tags\">): {\n d: string;\n relays: string[];\n} {\n requireKind(event, Kind.RelaySets);\n return { d: identifier(event.tags), relays: collectRelays(event.tags) };\n}\n\n/** Parse kind:10012 favorite relays public `relay` tags and kind:30002 `a` tags. */\nexport function parseFavoriteRelays(event: Pick<Event, \"kind\" | \"tags\">): {\n relays: string[];\n sets: string[];\n} {\n requireKind(event, Kind.FavoriteRelays);\n const sets: string[] = [];\n for (const tag of event.tags) {\n if (tag[0] !== \"a\" || !tag[1] || !isRelaySetAddress(tag[1])) continue;\n sets.push(tag[1]);\n }\n return { relays: collectRelays(event.tags), sets };\n}\n\n/** Parse kind:30030 emoji set public `d` / `title` / `emoji` tags. */\nexport function parseEmojiSet(event: Pick<Event, \"kind\" | \"tags\">): {\n d: string;\n title?: string;\n emoji: Array<{ shortcode: string; url: string }>;\n} {\n requireKind(event, Kind.EmojiSet);\n return {\n d: identifier(event.tags),\n title: firstTagValue(event.tags, \"title\"),\n emoji: collectEmoji(event.tags),\n };\n}\n\n/** Parse kind:39089 starter pack (follow pack) public `d` and `p` tags. */\nexport function parseFollowPack(event: Pick<Event, \"kind\" | \"tags\">): {\n d: string;\n pubkeys: string[];\n} {\n requireKind(event, Kind.StarterPack);\n const pubkeys: string[] = [];\n for (const tag of event.tags) {\n if (tag[0] !== \"p\" || !tag[1] || !isHex32(tag[1])) continue;\n pubkeys.push(tag[1].toLowerCase());\n }\n return { d: identifier(event.tags), pubkeys };\n}\n"],"mappings":";;;;;;;;;;;;;;AAgCA,SAAS,YAAY,OAA4B,MAAoB;CACnE,IAAI,MAAM,SAAS,MACjB,MAAM,IAAI,qBAAqB,iBAAiB,KAAK,QAAQ,MAAM,MAAM;AAE7E;AAEA,SAAS,cAAc,MAA+B;CACpD,MAAM,MAAgB,CAAC;CACvB,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,MAAM,OAAO,MAAM;EACtB,IAAI,IAAI,OAAO,WAAW,CAAC,IAAI,IAAI;EACnC,IAAI;EACJ,IAAI;GACF,MAAM,aAAa,IAAI,EAAE;EAC3B,QAAQ;GACN;EACF;EACA,IAAI,KAAK,IAAI,GAAG,GAAG;EACnB,KAAK,IAAI,GAAG;EACZ,IAAI,KAAK,GAAG;CACd;CACA,OAAO;AACT;AAEA,SAAS,aAAa,MAAgE;CACpF,MAAM,MAAiD,CAAC;CACxD,KAAK,MAAM,OAAO,MAAM;EACtB,IAAI,IAAI,OAAO,SAAS;EACxB,MAAM,YAAY,IAAI;EACtB,MAAM,MAAM,IAAI;EAChB,IAAI,CAAC,aAAa,CAAC,KAAK;EACxB,IAAI,KAAK;GAAE;GAAW;EAAI,CAAC;CAC7B;CACA,OAAO;AACT;AAEA,SAAS,cAAc,MAAqB,MAAkC;CAC5E,KAAK,MAAM,OAAO,MAChB,IAAI,IAAI,OAAO,QAAQ,IAAI,IAAI,OAAO,IAAI;AAG9C;AAEA,SAAS,WAAW,MAA6B;CAC/C,OAAO,QAAQ,IAAI,KAAK;AAC1B;;AAGA,SAAS,kBAAkB,OAAwB;CACjD,MAAM,UAAU,MAAM,QAAQ,GAAG;CACjC,IAAI,UAAU,GAAG,OAAO;CACxB,IAAI,MAAM,MAAM,GAAG,OAAO,MAAM,OAAO,KAAK,SAAS,GAAG,OAAO;CAC/D,MAAM,QAAQ,MAAM,QAAQ,KAAK,UAAU,CAAC;CAC5C,IAAI,QAAQ,GAAG,OAAO;CACtB,MAAM,SAAS,MAAM,MAAM,UAAU,GAAG,KAAK;CAC7C,MAAM,IAAI,MAAM,MAAM,QAAQ,CAAC;CAC/B,OAAO,QAAQ,MAAM,KAAK,EAAE,SAAS;AACvC;;AAGA,SAAgB,cAAc,OAAiD;CAC7E,YAAY,OAAO,KAAK,QAAQ;CAChC,MAAM,QAAoB,CAAC;CAC3B,KAAK,MAAM,OAAO,MAAM,MAAM;EAC5B,MAAM,QAAQ,IAAI;EAClB,IAAI,CAAC,OAAO;EACZ,QAAQ,IAAI,IAAZ;GACE,KAAK;IACH,IAAI,QAAQ,KAAK,GAAG,MAAM,KAAK;KAAE,MAAM;KAAU,OAAO,MAAM,YAAY;IAAE,CAAC;IAC7E;GACF,KAAK;IACH,IAAI,QAAQ,KAAK,GAAG,MAAM,KAAK;KAAE,MAAM;KAAS,OAAO,MAAM,YAAY;IAAE,CAAC;IAC5E;GACF,KAAK;IAEH,MAAM,KAAK;KAAE,MAAM;KAAW;IAAM,CAAC;IACrC;GACF,KAAK,QACH,MAAM,KAAK;IAAE,MAAM;IAAQ,OAAO,MAAM,YAAY;GAAE,CAAC;EAE3D;CACF;CACA,OAAO;AACT;;AAGA,SAAgB,qBAAqB,OAA0C;CAC7E,MAAM,IAAI,IAAI,aAAa,KAAK,UAAU,EAAE;CAC5C,KAAK,MAAM,QAAQ,OACjB,QAAQ,KAAK,MAAb;EACE,KAAK;GACH,EAAE,IAAI,IAAI,EAAE,YAAY,KAAK,OAAO,YAAY,CAAC,CAAC;GAClD;EACF,KAAK;GACH,EAAE,IAAI,IAAI,EAAE,YAAY,KAAK,OAAO,UAAU,CAAC,CAAC;GAChD;EACF,KAAK;GACH,IAAI,KAAK,OAAO,EAAE,IAAI,IAAI,EAAE,KAAK,KAAK,CAAC;GACvC;EACF,KAAK,QACH,IAAI,KAAK,OAAO,EAAE,IAAI,CAAC,QAAQ,KAAK,MAAM,YAAY,CAAC,CAAC;CAE5D;CAEF,OAAO;AACT;;AAGA,eAAsB,mBACpB,QACA,MACiB;CACjB,MAAM,OAAO,MAAM,OAAO,aAAa;CACvC,OAAO,OAAO,aAAa,MAAM,KAAK,UAAU,IAAI,CAAC;AACvD;;AAGA,eAAsB,mBACpB,QACA,OACwB;CAExB,IAAI,MAAM,YAAY,IAAI,OAAO,CAAC;CAClC,MAAM,OAAO,MAAM,OAAO,aAAa;CACvC,IAAI,KAAK,YAAY,MAAM,MAAM,OAAO,YAAY,GAClD,MAAM,IAAI,qBAAqB,+CAA+C;CAEhF,MAAM,YAAY,MAAM,OAAO,aAAa,MAAM,MAAM,OAAO;CAC/D,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,SAAS;CAC/B,SAAS,OAAO;EACd,MAAM,IAAI,qBAAqB,+BAA+B,EAAE,MAAM,CAAC;CACzE;CACA,IAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,CAAC,OAAO,MAAM,KAAK,GAC/C,MAAM,IAAI,qBAAqB,6BAA6B;CAE9D,OAAO;AACT;;AAGA,eAAsB,qBACpB,QACA,OACsD;CACtD,MAAM,MAAM,cAAc,KAAK;CAC/B,MAAM,YAAY,MAAM,mBAAmB,QAAQ,KAAK;CACxD,OAAO;EACL,QAAQ;EACR,SAAS,cAAc;GAAE,MAAM,MAAM;GAAM,MAAM;EAAU,CAAC;CAC9D;AACF;;AAGA,SAAgB,aAAa,OAA+C;CAC1E,YAAY,OAAO,KAAK,OAAO;CAC/B,MAAM,MAAgB,CAAC;CACvB,KAAK,MAAM,OAAO,MAAM,MAAM;EAC5B,IAAI,IAAI,OAAO,OAAO,CAAC,IAAI,MAAM,CAAC,QAAQ,IAAI,EAAE,GAAG;EACnD,IAAI,KAAK,IAAI,EAAE,CAAC,YAAY,CAAC;CAC/B;CACA,OAAO;AACT;;AAGA,SAAgB,oBAAoB,KAAsC;CACxE,MAAM,IAAI,IAAI,aAAa,KAAK,SAAS,EAAE;CAC3C,KAAK,MAAM,MAAM,KAAK,EAAE,IAAI,IAAI,EAAE,YAAY,IAAI,UAAU,CAAC,CAAC;CAC9D,OAAO;AACT;;AAGA,SAAgB,kBAAkB,OAGhC;CACA,YAAY,OAAO,KAAK,YAAY;CACpC,MAAM,IAAc,CAAC;CACrB,MAAM,IAAc,CAAC;CACrB,KAAK,MAAM,OAAO,MAAM,MAAM;EAC5B,IAAI,CAAC,IAAI,IAAI;EACb,IAAI,IAAI,OAAO,KACT;OAAA,QAAQ,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,EAAE,CAAC,YAAY,CAAC;EAAA,OAC3C,IAAI,IAAI,OAAO,KACpB,EAAE,KAAK,IAAI,EAAE;CAEjB;CACA,OAAO;EAAE;EAAG;CAAE;AAChB;;AAGA,SAAgB,yBAAyB,OAGxB;CACf,MAAM,IAAI,IAAI,aAAa,KAAK,cAAc,EAAE;CAChD,IAAI,MAAM,GACR,KAAK,MAAM,MAAM,MAAM,GAAG,EAAE,IAAI,IAAI,EAAE,YAAY,IAAI,UAAU,CAAC,CAAC;CAEpE,IAAI,MAAM,GACH;OAAA,MAAM,SAAS,MAAM,GACxB,IAAI,OAAO,EAAE,IAAI,IAAI,EAAE,KAAK,CAAC;CAAA;CAGjC,OAAO;AACT;;AAGA,SAAgB,mBAAmB,OAGjC;CACA,YAAY,OAAO,KAAK,aAAa;CACrC,MAAM,OAAiB,CAAC;CACxB,KAAK,MAAM,OAAO,MAAM,MACtB,IAAI,IAAI,OAAO,OAAO,IAAI,IAAI,KAAK,KAAK,IAAI,EAAE;CAEhD,OAAO;EAAE,OAAO,aAAa,MAAM,IAAI;EAAG;CAAK;AACjD;;AAGA,SAAgB,cAAc,OAG5B;CACA,YAAY,OAAO,KAAK,SAAS;CACjC,OAAO;EAAE,GAAG,WAAW,MAAM,IAAI;EAAG,QAAQ,cAAc,MAAM,IAAI;CAAE;AACxE;;AAGA,SAAgB,oBAAoB,OAGlC;CACA,YAAY,OAAO,KAAK,cAAc;CACtC,MAAM,OAAiB,CAAC;CACxB,KAAK,MAAM,OAAO,MAAM,MAAM;EAC5B,IAAI,IAAI,OAAO,OAAO,CAAC,IAAI,MAAM,CAAC,kBAAkB,IAAI,EAAE,GAAG;EAC7D,KAAK,KAAK,IAAI,EAAE;CAClB;CACA,OAAO;EAAE,QAAQ,cAAc,MAAM,IAAI;EAAG;CAAK;AACnD;;AAGA,SAAgB,cAAc,OAI5B;CACA,YAAY,OAAO,KAAK,QAAQ;CAChC,OAAO;EACL,GAAG,WAAW,MAAM,IAAI;EACxB,OAAO,cAAc,MAAM,MAAM,OAAO;EACxC,OAAO,aAAa,MAAM,IAAI;CAChC;AACF;;AAGA,SAAgB,gBAAgB,OAG9B;CACA,YAAY,OAAO,KAAK,WAAW;CACnC,MAAM,UAAoB,CAAC;CAC3B,KAAK,MAAM,OAAO,MAAM,MAAM;EAC5B,IAAI,IAAI,OAAO,OAAO,CAAC,IAAI,MAAM,CAAC,QAAQ,IAAI,EAAE,GAAG;EACnD,QAAQ,KAAK,IAAI,EAAE,CAAC,YAAY,CAAC;CACnC;CACA,OAAO;EAAE,GAAG,WAAW,MAAM,IAAI;EAAG;CAAQ;AAC9C"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { n as EventTemplate, t as Event } from "../event-B5D2jxt_.mjs";
|
|
2
|
+
//#region src/nips/nip57.d.ts
|
|
3
|
+
type ProfileZapRequest = {
|
|
4
|
+
pubkey: string;
|
|
5
|
+
/** Amount in millisats. */
|
|
6
|
+
amount: number;
|
|
7
|
+
relays: readonly string[];
|
|
8
|
+
comment?: string;
|
|
9
|
+
lnurl?: string;
|
|
10
|
+
};
|
|
11
|
+
type EventZapRequest = {
|
|
12
|
+
event: Event;
|
|
13
|
+
/** Amount in millisats. */
|
|
14
|
+
amount: number;
|
|
15
|
+
relays: readonly string[];
|
|
16
|
+
comment?: string;
|
|
17
|
+
lnurl?: string;
|
|
18
|
+
};
|
|
19
|
+
declare function makeZapRequest(params: ProfileZapRequest | EventZapRequest): EventTemplate;
|
|
20
|
+
type ZapReceiptContext = {
|
|
21
|
+
nostrPubkey: string;
|
|
22
|
+
lnurl?: string;
|
|
23
|
+
};
|
|
24
|
+
type ZapReceiptValidation = {
|
|
25
|
+
valid: boolean;
|
|
26
|
+
reason?: string;
|
|
27
|
+
request?: Event;
|
|
28
|
+
amountMsats?: number;
|
|
29
|
+
};
|
|
30
|
+
type Bolt11Fields = {
|
|
31
|
+
amountMsats?: number;
|
|
32
|
+
descriptionHash?: Uint8Array;
|
|
33
|
+
paymentHash?: Uint8Array;
|
|
34
|
+
};
|
|
35
|
+
/** Decode a BOLT11 invoice. Never throws. Requires a 32-byte payment hash (type 1). */
|
|
36
|
+
declare function parseBolt11(pr: string): Bolt11Fields | undefined;
|
|
37
|
+
/** Parse the `description` tag of a kind 9735 receipt as a signed kind 9734. Never throws. */
|
|
38
|
+
declare function parseZapRequestFromReceipt(receipt: Event): Event | undefined;
|
|
39
|
+
/** Validate a kind 9735 zap receipt. Never throws. */
|
|
40
|
+
declare function validateZapReceipt(receipt: Event, ctx: ZapReceiptContext): ZapReceiptValidation;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { Bolt11Fields, EventZapRequest, ProfileZapRequest, ZapReceiptContext, ZapReceiptValidation, makeZapRequest, parseBolt11, parseZapRequestFromReceipt, validateZapReceipt };
|
|
43
|
+
//# sourceMappingURL=nip57.d.mts.map
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { i as hexToBytes, l as utf8Encoder } from "../util-6tfLQ9JF.mjs";
|
|
2
|
+
import { n as EventValidationError } from "../error-DZEs5OuZ.mjs";
|
|
3
|
+
import { r as isAddressableKind, t as Kind } from "../kind-BLl4cLOc.mjs";
|
|
4
|
+
import { _ as getDTag, h as eventAddress, m as Tag, p as validateSignedEvent, y as parseEventAddress } from "../event-CzoQEFMF.mjs";
|
|
5
|
+
import { s as verifyEvent } from "../key-Dn-aX4x4.mjs";
|
|
6
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
7
|
+
import { bech32 } from "@scure/base";
|
|
8
|
+
//#region src/nips/nip57.ts
|
|
9
|
+
/**
|
|
10
|
+
* NIP-57 Lightning Zaps — zap request template (kind 9734) and receipt validation (kind 9735).
|
|
11
|
+
* Does not fetch LNURL. Receipt checks never throw.
|
|
12
|
+
*
|
|
13
|
+
* @see https://github.com/nostr-protocol/nips/blob/master/57.md
|
|
14
|
+
*/
|
|
15
|
+
function makeZapRequest(params) {
|
|
16
|
+
if (params.relays.length === 0) throw new EventValidationError("relays tag requires one or more URLs");
|
|
17
|
+
const recipient = "event" in params ? params.event.pubkey : params.pubkey;
|
|
18
|
+
const tags = [
|
|
19
|
+
Tag.p(recipient),
|
|
20
|
+
["amount", params.amount.toString()],
|
|
21
|
+
["relays", ...params.relays]
|
|
22
|
+
];
|
|
23
|
+
if ("event" in params) {
|
|
24
|
+
const { event } = params;
|
|
25
|
+
tags.push(Tag.e(event.id));
|
|
26
|
+
if (isAddressableKind(event.kind)) {
|
|
27
|
+
if (getDTag(event.tags) === void 0) throw new EventValidationError("d tag not found");
|
|
28
|
+
const addr = eventAddress(event);
|
|
29
|
+
if (addr) tags.push(["a", addr]);
|
|
30
|
+
}
|
|
31
|
+
tags.push(["k", event.kind.toString()]);
|
|
32
|
+
}
|
|
33
|
+
if (params.lnurl) tags.push(["lnurl", params.lnurl]);
|
|
34
|
+
return {
|
|
35
|
+
kind: Kind.ZapRequest,
|
|
36
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
37
|
+
content: params.comment ?? "",
|
|
38
|
+
tags
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function firstTagValue(tags, name) {
|
|
42
|
+
for (const tag of tags) if (tag[0] === name && tag[1] !== void 0) return tag[1];
|
|
43
|
+
}
|
|
44
|
+
function bytesEq(a, b) {
|
|
45
|
+
if (a.length !== b.length) return false;
|
|
46
|
+
let x = 0;
|
|
47
|
+
for (let i = 0; i < a.length; i++) x |= a[i] ^ b[i];
|
|
48
|
+
return x === 0;
|
|
49
|
+
}
|
|
50
|
+
function fail(reason) {
|
|
51
|
+
return {
|
|
52
|
+
valid: false,
|
|
53
|
+
reason
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const MSATS_PER_BTC = 1e11;
|
|
57
|
+
const MSATS_PER_MILLI = 1e8;
|
|
58
|
+
const MSATS_PER_MICRO = 1e5;
|
|
59
|
+
const MSATS_PER_NANO = 100;
|
|
60
|
+
/** Amount is digits after `ln` + currency letters, optional m/u/n/p. Non-integer pico is omitted. */
|
|
61
|
+
function amountMsatsFromHrp(hrp) {
|
|
62
|
+
if (!hrp.startsWith("ln")) return void 0;
|
|
63
|
+
let i = 2;
|
|
64
|
+
while (i < hrp.length) {
|
|
65
|
+
const c = hrp.charCodeAt(i);
|
|
66
|
+
if (c < 97 || c > 122) break;
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
if (i === hrp.length) return void 0;
|
|
70
|
+
const rest = hrp.slice(i);
|
|
71
|
+
const m = /^([0-9]+)([munp])?$/.exec(rest);
|
|
72
|
+
if (!m) return void 0;
|
|
73
|
+
const n = Number(m[1]);
|
|
74
|
+
if (!Number.isSafeInteger(n)) return void 0;
|
|
75
|
+
const mul = m[2];
|
|
76
|
+
if (mul === void 0) return n * MSATS_PER_BTC;
|
|
77
|
+
if (mul === "m") return n * MSATS_PER_MILLI;
|
|
78
|
+
if (mul === "u") return n * MSATS_PER_MICRO;
|
|
79
|
+
if (mul === "n") return n * MSATS_PER_NANO;
|
|
80
|
+
if (n % 10 !== 0) return void 0;
|
|
81
|
+
return n / 10;
|
|
82
|
+
}
|
|
83
|
+
function parseMsatsTag(value) {
|
|
84
|
+
if (!/^[0-9]+$/.test(value)) return void 0;
|
|
85
|
+
const n = Number(value);
|
|
86
|
+
return Number.isSafeInteger(n) ? n : void 0;
|
|
87
|
+
}
|
|
88
|
+
function countTags(tags, name) {
|
|
89
|
+
let n = 0;
|
|
90
|
+
for (const tag of tags) if (tag[0] === name) n++;
|
|
91
|
+
return n;
|
|
92
|
+
}
|
|
93
|
+
function hasHexTagValue(tags, name, value) {
|
|
94
|
+
const needle = value.toLowerCase();
|
|
95
|
+
for (const tag of tags) if (tag[0] === name && tag[1] !== void 0 && tag[1].toLowerCase() === needle) return true;
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
function hasAddressTag(tags, value) {
|
|
99
|
+
const want = parseEventAddress(value);
|
|
100
|
+
if (!want) return false;
|
|
101
|
+
for (const tag of tags) {
|
|
102
|
+
if (tag[0] !== "a" || tag[1] === void 0) continue;
|
|
103
|
+
const got = parseEventAddress(tag[1]);
|
|
104
|
+
if (got && got.kind === want.kind && got.pubkey === want.pubkey && got.identifier === want.identifier) return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
const BOLT11_TIMESTAMP_WORDS = 7;
|
|
109
|
+
const BOLT11_SIGNATURE_WORDS = 104;
|
|
110
|
+
const BOLT11_HASH_WORDS = 52;
|
|
111
|
+
const HASH_BYTES = 32;
|
|
112
|
+
/** Decode a BOLT11 invoice. Never throws. Requires a 32-byte payment hash (type 1). */
|
|
113
|
+
function parseBolt11(pr) {
|
|
114
|
+
try {
|
|
115
|
+
const { prefix, words } = bech32.decode(pr.toLowerCase(), false);
|
|
116
|
+
if (!prefix.startsWith("ln")) return void 0;
|
|
117
|
+
if (words.length < 111) return void 0;
|
|
118
|
+
const fields = {};
|
|
119
|
+
const amountMsats = amountMsatsFromHrp(prefix);
|
|
120
|
+
if (amountMsats !== void 0) fields.amountMsats = amountMsats;
|
|
121
|
+
const tlvEnd = words.length - BOLT11_SIGNATURE_WORDS;
|
|
122
|
+
let i = BOLT11_TIMESTAMP_WORDS;
|
|
123
|
+
while (i + 3 <= tlvEnd) {
|
|
124
|
+
const type = words[i];
|
|
125
|
+
const dataLen = words[i + 1] << 5 | words[i + 2];
|
|
126
|
+
i += 3;
|
|
127
|
+
if (i + dataLen > tlvEnd) break;
|
|
128
|
+
const data = words.slice(i, i + dataLen);
|
|
129
|
+
i += dataLen;
|
|
130
|
+
if (type !== 1 && type !== 23 || dataLen !== BOLT11_HASH_WORDS) continue;
|
|
131
|
+
const bytes = bech32.fromWordsUnsafe(data);
|
|
132
|
+
if (!bytes || bytes.length !== HASH_BYTES) continue;
|
|
133
|
+
if (type === 1) fields.paymentHash ??= bytes;
|
|
134
|
+
else fields.descriptionHash ??= bytes;
|
|
135
|
+
}
|
|
136
|
+
if (!fields.paymentHash) return void 0;
|
|
137
|
+
return fields;
|
|
138
|
+
} catch {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Parse the `description` tag of a kind 9735 receipt as a signed kind 9734. Never throws. */
|
|
143
|
+
function parseZapRequestFromReceipt(receipt) {
|
|
144
|
+
try {
|
|
145
|
+
const raw = firstTagValue(receipt.tags, "description");
|
|
146
|
+
if (raw === void 0) return void 0;
|
|
147
|
+
const parsed = JSON.parse(raw);
|
|
148
|
+
if (!validateSignedEvent(parsed) || parsed.kind !== Kind.ZapRequest) return void 0;
|
|
149
|
+
if (!verifyEvent(parsed)) return void 0;
|
|
150
|
+
return parsed;
|
|
151
|
+
} catch {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Validate a kind 9735 zap receipt. Never throws. */
|
|
156
|
+
function validateZapReceipt(receipt, ctx) {
|
|
157
|
+
try {
|
|
158
|
+
if (receipt.kind !== Kind.Zap || !verifyEvent(receipt)) return fail("invalid receipt");
|
|
159
|
+
if (receipt.pubkey.toLowerCase() !== ctx.nostrPubkey.toLowerCase()) return fail("pubkey mismatch");
|
|
160
|
+
const descriptionRaw = firstTagValue(receipt.tags, "description");
|
|
161
|
+
const request = parseZapRequestFromReceipt(receipt);
|
|
162
|
+
if (descriptionRaw === void 0 || !request) return fail("invalid zap request");
|
|
163
|
+
if (countTags(request.tags, "p") !== 1) return fail("invalid p count");
|
|
164
|
+
if (countTags(request.tags, "e") > 1) return fail("too many e tags");
|
|
165
|
+
if (!firstTagValue(request.tags, "relays")) return fail("missing relays");
|
|
166
|
+
for (const tag of request.tags) {
|
|
167
|
+
if (tag[0] !== "a") continue;
|
|
168
|
+
if (tag[1] === void 0) return fail("invalid a");
|
|
169
|
+
const parsed = parseEventAddress(tag[1]);
|
|
170
|
+
if (!parsed || !isAddressableKind(parsed.kind)) return fail("invalid a");
|
|
171
|
+
}
|
|
172
|
+
const requestPCount = countTags(request.tags, "P");
|
|
173
|
+
if (requestPCount > 1) return fail("too many P tags");
|
|
174
|
+
if (requestPCount === 1) {
|
|
175
|
+
const requestP = firstTagValue(request.tags, "P");
|
|
176
|
+
if (requestP === void 0 || requestP.toLowerCase() !== receipt.pubkey.toLowerCase()) return fail("request P mismatch");
|
|
177
|
+
}
|
|
178
|
+
const bolt11Tag = firstTagValue(receipt.tags, "bolt11");
|
|
179
|
+
if (bolt11Tag === void 0) return fail("missing bolt11");
|
|
180
|
+
const bolt11 = parseBolt11(bolt11Tag);
|
|
181
|
+
if (!bolt11 || !bolt11.descriptionHash) return fail("invalid bolt11");
|
|
182
|
+
const requestAmount = firstTagValue(request.tags, "amount");
|
|
183
|
+
if (requestAmount !== void 0) {
|
|
184
|
+
const msats = parseMsatsTag(requestAmount);
|
|
185
|
+
if (msats === void 0 || bolt11.amountMsats !== msats) return fail("amount mismatch");
|
|
186
|
+
}
|
|
187
|
+
if (!bytesEq(sha256(utf8Encoder.encode(descriptionRaw)), bolt11.descriptionHash)) return fail("description hash mismatch");
|
|
188
|
+
const requestLnurl = firstTagValue(request.tags, "lnurl");
|
|
189
|
+
if (requestLnurl !== void 0 && ctx.lnurl !== void 0) {
|
|
190
|
+
if (requestLnurl.toLowerCase() !== ctx.lnurl.toLowerCase()) return fail("lnurl mismatch");
|
|
191
|
+
}
|
|
192
|
+
const preimageHex = firstTagValue(receipt.tags, "preimage");
|
|
193
|
+
if (preimageHex !== void 0 && bolt11.paymentHash) {
|
|
194
|
+
let preimage;
|
|
195
|
+
try {
|
|
196
|
+
preimage = hexToBytes(preimageHex);
|
|
197
|
+
} catch {
|
|
198
|
+
return fail("preimage mismatch");
|
|
199
|
+
}
|
|
200
|
+
if (!bytesEq(sha256(preimage), bolt11.paymentHash)) return fail("preimage mismatch");
|
|
201
|
+
}
|
|
202
|
+
const recipient = firstTagValue(request.tags, "p");
|
|
203
|
+
if (recipient === void 0 || !hasHexTagValue(receipt.tags, "p", recipient)) return fail("missing p");
|
|
204
|
+
const requestE = firstTagValue(request.tags, "e");
|
|
205
|
+
if (requestE !== void 0 && !hasHexTagValue(receipt.tags, "e", requestE)) return fail("missing e");
|
|
206
|
+
for (const tag of request.tags) {
|
|
207
|
+
if (tag[0] !== "a" || tag[1] === void 0) continue;
|
|
208
|
+
if (!hasAddressTag(receipt.tags, tag[1])) return fail("missing a");
|
|
209
|
+
}
|
|
210
|
+
for (const tag of receipt.tags) {
|
|
211
|
+
if (tag[0] !== "P") continue;
|
|
212
|
+
if (tag[1] === void 0 || tag[1].toLowerCase() !== request.pubkey.toLowerCase()) return fail("receipt P mismatch");
|
|
213
|
+
}
|
|
214
|
+
const amountMsats = bolt11.amountMsats ?? (requestAmount !== void 0 ? parseMsatsTag(requestAmount) : void 0);
|
|
215
|
+
return amountMsats === void 0 ? {
|
|
216
|
+
valid: true,
|
|
217
|
+
request
|
|
218
|
+
} : {
|
|
219
|
+
valid: true,
|
|
220
|
+
request,
|
|
221
|
+
amountMsats
|
|
222
|
+
};
|
|
223
|
+
} catch {
|
|
224
|
+
return fail("invalid receipt");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
export { makeZapRequest, parseBolt11, parseZapRequestFromReceipt, validateZapReceipt };
|
|
229
|
+
|
|
230
|
+
//# sourceMappingURL=nip57.mjs.map
|