working rss feed to nostr publish

This commit is contained in:
2023-11-24 00:43:28 -05:00
parent 06edcb57ae
commit 88d2f9cfec
8396 changed files with 783105 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
// event.ts
import { schnorr as schnorr2 } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex as bytesToHex2 } from "@noble/hashes/utils";
// keys.ts
import { schnorr } from "@noble/curves/secp256k1";
import { bytesToHex } from "@noble/hashes/utils";
function getPublicKey(privateKey) {
return bytesToHex(schnorr.getPublicKey(privateKey));
}
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
// event.ts
var verifiedSymbol = Symbol("verified");
var Kind = /* @__PURE__ */ ((Kind2) => {
Kind2[Kind2["Metadata"] = 0] = "Metadata";
Kind2[Kind2["Text"] = 1] = "Text";
Kind2[Kind2["RecommendRelay"] = 2] = "RecommendRelay";
Kind2[Kind2["Contacts"] = 3] = "Contacts";
Kind2[Kind2["EncryptedDirectMessage"] = 4] = "EncryptedDirectMessage";
Kind2[Kind2["EventDeletion"] = 5] = "EventDeletion";
Kind2[Kind2["Repost"] = 6] = "Repost";
Kind2[Kind2["Reaction"] = 7] = "Reaction";
Kind2[Kind2["BadgeAward"] = 8] = "BadgeAward";
Kind2[Kind2["ChannelCreation"] = 40] = "ChannelCreation";
Kind2[Kind2["ChannelMetadata"] = 41] = "ChannelMetadata";
Kind2[Kind2["ChannelMessage"] = 42] = "ChannelMessage";
Kind2[Kind2["ChannelHideMessage"] = 43] = "ChannelHideMessage";
Kind2[Kind2["ChannelMuteUser"] = 44] = "ChannelMuteUser";
Kind2[Kind2["Blank"] = 255] = "Blank";
Kind2[Kind2["Report"] = 1984] = "Report";
Kind2[Kind2["ZapRequest"] = 9734] = "ZapRequest";
Kind2[Kind2["Zap"] = 9735] = "Zap";
Kind2[Kind2["RelayList"] = 10002] = "RelayList";
Kind2[Kind2["ClientAuth"] = 22242] = "ClientAuth";
Kind2[Kind2["NwcRequest"] = 23194] = "NwcRequest";
Kind2[Kind2["HttpAuth"] = 27235] = "HttpAuth";
Kind2[Kind2["ProfileBadge"] = 30008] = "ProfileBadge";
Kind2[Kind2["BadgeDefinition"] = 30009] = "BadgeDefinition";
Kind2[Kind2["Article"] = 30023] = "Article";
Kind2[Kind2["FileMetadata"] = 1063] = "FileMetadata";
return Kind2;
})(Kind || {});
function getBlankEvent(kind = 255 /* Blank */) {
return {
kind,
content: "",
tags: [],
created_at: 0
};
}
function finishEvent(t, privateKey) {
const event = t;
event.pubkey = getPublicKey(privateKey);
event.id = getEventHash(event);
event.sig = getSignature(event, privateKey);
event[verifiedSymbol] = true;
return event;
}
function serializeEvent(evt) {
if (!validateEvent(evt))
throw new Error("can't serialize event with wrong or missing properties");
return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]);
}
function getEventHash(event) {
let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)));
return bytesToHex2(eventHash);
}
var isRecord = (obj) => obj instanceof Object;
function validateEvent(event) {
if (!isRecord(event))
return false;
if (typeof event.kind !== "number")
return false;
if (typeof event.content !== "string")
return false;
if (typeof event.created_at !== "number")
return false;
if (typeof event.pubkey !== "string")
return false;
if (!event.pubkey.match(/^[a-f0-9]{64}$/))
return false;
if (!Array.isArray(event.tags))
return false;
for (let i = 0; i < event.tags.length; i++) {
let tag = event.tags[i];
if (!Array.isArray(tag))
return false;
for (let j = 0; j < tag.length; j++) {
if (typeof tag[j] === "object")
return false;
}
}
return true;
}
function verifySignature(event) {
if (typeof event[verifiedSymbol] === "boolean")
return event[verifiedSymbol];
const hash = getEventHash(event);
if (hash !== event.id) {
return event[verifiedSymbol] = false;
}
try {
return event[verifiedSymbol] = schnorr2.verify(event.sig, hash, event.pubkey);
} catch (err) {
return event[verifiedSymbol] = false;
}
}
function signEvent(event, key) {
console.warn(
"nostr-tools: `signEvent` is deprecated and will be removed or changed in the future. Please use `getSignature` instead."
);
return getSignature(event, key);
}
function getSignature(event, key) {
return bytesToHex2(schnorr2.sign(getEventHash(event), key));
}
export {
Kind,
finishEvent,
getBlankEvent,
getEventHash,
getSignature,
serializeEvent,
signEvent,
validateEvent,
verifiedSymbol,
verifySignature
};
File diff suppressed because one or more lines are too long
+45
View File
@@ -0,0 +1,45 @@
// fakejson.ts
function getHex64(json, field) {
let len = field.length + 3;
let idx = json.indexOf(`"${field}":`) + len;
let s = json.slice(idx).indexOf(`"`) + idx + 1;
return json.slice(s, s + 64);
}
function getInt(json, field) {
let len = field.length;
let idx = json.indexOf(`"${field}":`) + len + 3;
let sliced = json.slice(idx);
let end = Math.min(sliced.indexOf(","), sliced.indexOf("}"));
return parseInt(sliced.slice(0, end), 10);
}
function getSubscriptionId(json) {
let idx = json.slice(0, 22).indexOf(`"EVENT"`);
if (idx === -1)
return null;
let pstart = json.slice(idx + 7 + 1).indexOf(`"`);
if (pstart === -1)
return null;
let start = idx + 7 + 1 + pstart;
let pend = json.slice(start + 1, 80).indexOf(`"`);
if (pend === -1)
return null;
let end = start + 1 + pend;
return json.slice(start + 1, end);
}
function matchEventId(json, id) {
return id === getHex64(json, "id");
}
function matchEventPubkey(json, pubkey) {
return pubkey === getHex64(json, "pubkey");
}
function matchEventKind(json, kind) {
return kind === getInt(json, "kind");
}
export {
getHex64,
getInt,
getSubscriptionId,
matchEventId,
matchEventKind,
matchEventPubkey
};
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../fakejson.ts"],
"sourcesContent": ["export function getHex64(json: string, field: string): string {\n let len = field.length + 3\n let idx = json.indexOf(`\"${field}\":`) + len\n let s = json.slice(idx).indexOf(`\"`) + idx + 1\n return json.slice(s, s + 64)\n}\n\nexport function getInt(json: string, field: string): number {\n let len = field.length\n let idx = json.indexOf(`\"${field}\":`) + len + 3\n let sliced = json.slice(idx)\n let end = Math.min(sliced.indexOf(','), sliced.indexOf('}'))\n return parseInt(sliced.slice(0, end), 10)\n}\n\nexport function getSubscriptionId(json: string): string | null {\n let idx = json.slice(0, 22).indexOf(`\"EVENT\"`)\n if (idx === -1) return null\n\n let pstart = json.slice(idx + 7 + 1).indexOf(`\"`)\n if (pstart === -1) return null\n let start = idx + 7 + 1 + pstart\n\n let pend = json.slice(start + 1, 80).indexOf(`\"`)\n if (pend === -1) return null\n let end = start + 1 + pend\n\n return json.slice(start + 1, end)\n}\n\nexport function matchEventId(json: string, id: string): boolean {\n return id === getHex64(json, 'id')\n}\n\nexport function matchEventPubkey(json: string, pubkey: string): boolean {\n return pubkey === getHex64(json, 'pubkey')\n}\n\nexport function matchEventKind(json: string, kind: number): boolean {\n return kind === getInt(json, 'kind')\n}\n"],
"mappings": ";AAAO,SAAS,SAAS,MAAc,OAAuB;AAC5D,MAAI,MAAM,MAAM,SAAS;AACzB,MAAI,MAAM,KAAK,QAAQ,IAAI,SAAS,IAAI;AACxC,MAAI,IAAI,KAAK,MAAM,GAAG,EAAE,QAAQ,GAAG,IAAI,MAAM;AAC7C,SAAO,KAAK,MAAM,GAAG,IAAI,EAAE;AAC7B;AAEO,SAAS,OAAO,MAAc,OAAuB;AAC1D,MAAI,MAAM,MAAM;AAChB,MAAI,MAAM,KAAK,QAAQ,IAAI,SAAS,IAAI,MAAM;AAC9C,MAAI,SAAS,KAAK,MAAM,GAAG;AAC3B,MAAI,MAAM,KAAK,IAAI,OAAO,QAAQ,GAAG,GAAG,OAAO,QAAQ,GAAG,CAAC;AAC3D,SAAO,SAAS,OAAO,MAAM,GAAG,GAAG,GAAG,EAAE;AAC1C;AAEO,SAAS,kBAAkB,MAA6B;AAC7D,MAAI,MAAM,KAAK,MAAM,GAAG,EAAE,EAAE,QAAQ,SAAS;AAC7C,MAAI,QAAQ;AAAI,WAAO;AAEvB,MAAI,SAAS,KAAK,MAAM,MAAM,IAAI,CAAC,EAAE,QAAQ,GAAG;AAChD,MAAI,WAAW;AAAI,WAAO;AAC1B,MAAI,QAAQ,MAAM,IAAI,IAAI;AAE1B,MAAI,OAAO,KAAK,MAAM,QAAQ,GAAG,EAAE,EAAE,QAAQ,GAAG;AAChD,MAAI,SAAS;AAAI,WAAO;AACxB,MAAI,MAAM,QAAQ,IAAI;AAEtB,SAAO,KAAK,MAAM,QAAQ,GAAG,GAAG;AAClC;AAEO,SAAS,aAAa,MAAc,IAAqB;AAC9D,SAAO,OAAO,SAAS,MAAM,IAAI;AACnC;AAEO,SAAS,iBAAiB,MAAc,QAAyB;AACtE,SAAO,WAAW,SAAS,MAAM,QAAQ;AAC3C;AAEO,SAAS,eAAe,MAAc,MAAuB;AAClE,SAAO,SAAS,OAAO,MAAM,MAAM;AACrC;",
"names": []
}
+63
View File
@@ -0,0 +1,63 @@
// filter.ts
function matchFilter(filter, event) {
if (filter.ids && filter.ids.indexOf(event.id) === -1) {
if (!filter.ids.some((prefix) => event.id.startsWith(prefix))) {
return false;
}
}
if (filter.kinds && filter.kinds.indexOf(event.kind) === -1)
return false;
if (filter.authors && filter.authors.indexOf(event.pubkey) === -1) {
if (!filter.authors.some((prefix) => event.pubkey.startsWith(prefix))) {
return false;
}
}
for (let f in filter) {
if (f[0] === "#") {
let tagName = f.slice(1);
let values = filter[`#${tagName}`];
if (values && !event.tags.find(([t, v]) => t === f.slice(1) && values.indexOf(v) !== -1))
return false;
}
}
if (filter.since && event.created_at < filter.since)
return false;
if (filter.until && event.created_at > filter.until)
return false;
return true;
}
function matchFilters(filters, event) {
for (let i = 0; i < filters.length; i++) {
if (matchFilter(filters[i], event))
return true;
}
return false;
}
function mergeFilters(...filters) {
let result = {};
for (let i = 0; i < filters.length; i++) {
let filter = filters[i];
Object.entries(filter).forEach(([property, values]) => {
if (property === "kinds" || property === "ids" || property === "authors" || property[0] === "#") {
result[property] = result[property] || [];
for (let v = 0; v < values.length; v++) {
let value = values[v];
if (!result[property].includes(value))
result[property].push(value);
}
}
});
if (filter.limit && (!result.limit || filter.limit > result.limit))
result.limit = filter.limit;
if (filter.until && (!result.until || filter.until > result.until))
result.until = filter.until;
if (filter.since && (!result.since || filter.since < result.since))
result.since = filter.since;
}
return result;
}
export {
matchFilter,
matchFilters,
mergeFilters
};
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../filter.ts"],
"sourcesContent": ["import { Event } from './event.ts'\n\nexport type Filter<K extends number = number> = {\n ids?: string[]\n kinds?: K[]\n authors?: string[]\n since?: number\n until?: number\n limit?: number\n search?: string\n [key: `#${string}`]: string[] | undefined\n}\n\nexport function matchFilter(filter: Filter<number>, event: Event<number>): boolean {\n if (filter.ids && filter.ids.indexOf(event.id) === -1) {\n if (!filter.ids.some(prefix => event.id.startsWith(prefix))) {\n return false\n }\n }\n if (filter.kinds && filter.kinds.indexOf(event.kind) === -1) return false\n if (filter.authors && filter.authors.indexOf(event.pubkey) === -1) {\n if (!filter.authors.some(prefix => event.pubkey.startsWith(prefix))) {\n return false\n }\n }\n\n for (let f in filter) {\n if (f[0] === '#') {\n let tagName = f.slice(1)\n let values = filter[`#${tagName}`]\n if (values && !event.tags.find(([t, v]) => t === f.slice(1) && values!.indexOf(v) !== -1)) return false\n }\n }\n\n if (filter.since && event.created_at < filter.since) return false\n if (filter.until && event.created_at > filter.until) return false\n\n return true\n}\n\nexport function matchFilters(filters: Filter<number>[], event: Event<number>): boolean {\n for (let i = 0; i < filters.length; i++) {\n if (matchFilter(filters[i], event)) return true\n }\n return false\n}\n\nexport function mergeFilters(...filters: Filter<number>[]): Filter<number> {\n let result: Filter<number> = {}\n for (let i = 0; i < filters.length; i++) {\n let filter = filters[i]\n Object.entries(filter).forEach(([property, values]) => {\n if (property === 'kinds' || property === 'ids' || property === 'authors' || property[0] === '#') {\n // @ts-ignore\n result[property] = result[property] || []\n // @ts-ignore\n for (let v = 0; v < values.length; v++) {\n // @ts-ignore\n let value = values[v]\n // @ts-ignore\n if (!result[property].includes(value)) result[property].push(value)\n }\n }\n })\n\n if (filter.limit && (!result.limit || filter.limit > result.limit)) result.limit = filter.limit\n if (filter.until && (!result.until || filter.until > result.until)) result.until = filter.until\n if (filter.since && (!result.since || filter.since < result.since)) result.since = filter.since\n }\n\n return result\n}\n"],
"mappings": ";AAaO,SAAS,YAAY,QAAwB,OAA+B;AACjF,MAAI,OAAO,OAAO,OAAO,IAAI,QAAQ,MAAM,EAAE,MAAM,IAAI;AACrD,QAAI,CAAC,OAAO,IAAI,KAAK,YAAU,MAAM,GAAG,WAAW,MAAM,CAAC,GAAG;AAC3D,aAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI,OAAO,SAAS,OAAO,MAAM,QAAQ,MAAM,IAAI,MAAM;AAAI,WAAO;AACpE,MAAI,OAAO,WAAW,OAAO,QAAQ,QAAQ,MAAM,MAAM,MAAM,IAAI;AACjE,QAAI,CAAC,OAAO,QAAQ,KAAK,YAAU,MAAM,OAAO,WAAW,MAAM,CAAC,GAAG;AACnE,aAAO;AAAA,IACT;AAAA,EACF;AAEA,WAAS,KAAK,QAAQ;AACpB,QAAI,EAAE,OAAO,KAAK;AAChB,UAAI,UAAU,EAAE,MAAM,CAAC;AACvB,UAAI,SAAS,OAAO,IAAI;AACxB,UAAI,UAAU,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,GAAG,CAAC,MAAM,MAAM,EAAE,MAAM,CAAC,KAAK,OAAQ,QAAQ,CAAC,MAAM,EAAE;AAAG,eAAO;AAAA,IACpG;AAAA,EACF;AAEA,MAAI,OAAO,SAAS,MAAM,aAAa,OAAO;AAAO,WAAO;AAC5D,MAAI,OAAO,SAAS,MAAM,aAAa,OAAO;AAAO,WAAO;AAE5D,SAAO;AACT;AAEO,SAAS,aAAa,SAA2B,OAA+B;AACrF,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,QAAI,YAAY,QAAQ,IAAI,KAAK;AAAG,aAAO;AAAA,EAC7C;AACA,SAAO;AACT;AAEO,SAAS,gBAAgB,SAA2C;AACzE,MAAI,SAAyB,CAAC;AAC9B,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,QAAI,SAAS,QAAQ;AACrB,WAAO,QAAQ,MAAM,EAAE,QAAQ,CAAC,CAAC,UAAU,MAAM,MAAM;AACrD,UAAI,aAAa,WAAW,aAAa,SAAS,aAAa,aAAa,SAAS,OAAO,KAAK;AAE/F,eAAO,YAAY,OAAO,aAAa,CAAC;AAExC,iBAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AAEtC,cAAI,QAAQ,OAAO;AAEnB,cAAI,CAAC,OAAO,UAAU,SAAS,KAAK;AAAG,mBAAO,UAAU,KAAK,KAAK;AAAA,QACpE;AAAA,MACF;AAAA,IACF,CAAC;AAED,QAAI,OAAO,UAAU,CAAC,OAAO,SAAS,OAAO,QAAQ,OAAO;AAAQ,aAAO,QAAQ,OAAO;AAC1F,QAAI,OAAO,UAAU,CAAC,OAAO,SAAS,OAAO,QAAQ,OAAO;AAAQ,aAAO,QAAQ,OAAO;AAC1F,QAAI,OAAO,UAAU,CAAC,OAAO,SAAS,OAAO,QAAQ,OAAO;AAAQ,aAAO,QAAQ,OAAO;AAAA,EAC5F;AAEA,SAAO;AACT;",
"names": []
}
+2109
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+13
View File
@@ -0,0 +1,13 @@
// keys.ts
import { schnorr } from "@noble/curves/secp256k1";
import { bytesToHex } from "@noble/hashes/utils";
function generatePrivateKey() {
return bytesToHex(schnorr.utils.randomPrivateKey());
}
function getPublicKey(privateKey) {
return bytesToHex(schnorr.getPublicKey(privateKey));
}
export {
generatePrivateKey,
getPublicKey
};
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../keys.ts"],
"sourcesContent": ["import { schnorr } from '@noble/curves/secp256k1'\nimport { bytesToHex } from '@noble/hashes/utils'\n\nexport function generatePrivateKey(): string {\n return bytesToHex(schnorr.utils.randomPrivateKey())\n}\n\nexport function getPublicKey(privateKey: string): string {\n return bytesToHex(schnorr.getPublicKey(privateKey))\n}\n"],
"mappings": ";AAAA,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAEpB,SAAS,qBAA6B;AAC3C,SAAO,WAAW,QAAQ,MAAM,iBAAiB,CAAC;AACpD;AAEO,SAAS,aAAa,YAA4B;AACvD,SAAO,WAAW,QAAQ,aAAa,UAAU,CAAC;AACpD;",
"names": []
}
+31
View File
@@ -0,0 +1,31 @@
// kinds.ts
function isRegularKind(kind) {
return 1e3 <= kind && kind < 1e4 || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind);
}
function isReplaceableKind(kind) {
return 1e4 <= kind && kind < 2e4 || [0, 3].includes(kind);
}
function isEphemeralKind(kind) {
return 2e4 <= kind && kind < 3e4;
}
function isParameterizedReplaceableKind(kind) {
return 3e4 <= kind && kind < 4e4;
}
function classifyKind(kind) {
if (isRegularKind(kind))
return "regular";
if (isReplaceableKind(kind))
return "replaceable";
if (isEphemeralKind(kind))
return "ephemeral";
if (isParameterizedReplaceableKind(kind))
return "parameterized";
return "unknown";
}
export {
classifyKind,
isEphemeralKind,
isParameterizedReplaceableKind,
isRegularKind,
isReplaceableKind
};
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../kinds.ts"],
"sourcesContent": ["/** Events are **regular**, which means they're all expected to be stored by relays. */\nfunction isRegularKind(kind: number) {\n return (1000 <= kind && kind < 10000) || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind)\n}\n\n/** Events are **replaceable**, which means that, for each combination of `pubkey` and `kind`, only the latest event is expected to (SHOULD) be stored by relays, older versions are expected to be discarded. */\nfunction isReplaceableKind(kind: number) {\n return (10000 <= kind && kind < 20000) || [0, 3].includes(kind)\n}\n\n/** Events are **ephemeral**, which means they are not expected to be stored by relays. */\nfunction isEphemeralKind(kind: number) {\n return 20000 <= kind && kind < 30000\n}\n\n/** Events are **parameterized replaceable**, which means that, for each combination of `pubkey`, `kind` and the `d` tag, only the latest event is expected to be stored by relays, older versions are expected to be discarded. */\nfunction isParameterizedReplaceableKind(kind: number) {\n return 30000 <= kind && kind < 40000\n}\n\n/** Classification of the event kind. */\ntype KindClassification = 'regular' | 'replaceable' | 'ephemeral' | 'parameterized' | 'unknown'\n\n/** Determine the classification of this kind of event if known, or `unknown`. */\nfunction classifyKind(kind: number): KindClassification {\n if (isRegularKind(kind)) return 'regular'\n if (isReplaceableKind(kind)) return 'replaceable'\n if (isEphemeralKind(kind)) return 'ephemeral'\n if (isParameterizedReplaceableKind(kind)) return 'parameterized'\n return 'unknown'\n}\n\nexport {\n classifyKind,\n isEphemeralKind,\n isParameterizedReplaceableKind,\n isRegularKind,\n isReplaceableKind,\n type KindClassification,\n}\n"],
"mappings": ";AACA,SAAS,cAAc,MAAc;AACnC,SAAQ,OAAQ,QAAQ,OAAO,OAAU,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,IAAI,IAAI,IAAI,IAAI,EAAE,EAAE,SAAS,IAAI;AACtG;AAGA,SAAS,kBAAkB,MAAc;AACvC,SAAQ,OAAS,QAAQ,OAAO,OAAU,CAAC,GAAG,CAAC,EAAE,SAAS,IAAI;AAChE;AAGA,SAAS,gBAAgB,MAAc;AACrC,SAAO,OAAS,QAAQ,OAAO;AACjC;AAGA,SAAS,+BAA+B,MAAc;AACpD,SAAO,OAAS,QAAQ,OAAO;AACjC;AAMA,SAAS,aAAa,MAAkC;AACtD,MAAI,cAAc,IAAI;AAAG,WAAO;AAChC,MAAI,kBAAkB,IAAI;AAAG,WAAO;AACpC,MAAI,gBAAgB,IAAI;AAAG,WAAO;AAClC,MAAI,+BAA+B,IAAI;AAAG,WAAO;AACjD,SAAO;AACT;",
"names": []
}
+42
View File
@@ -0,0 +1,42 @@
// nip04.ts
import { randomBytes } from "@noble/hashes/utils";
import { secp256k1 } from "@noble/curves/secp256k1";
import { base64 } from "@scure/base";
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
// nip04.ts
if (typeof crypto !== "undefined" && !crypto.subtle && crypto.webcrypto) {
crypto.subtle = crypto.webcrypto.subtle;
}
async function encrypt(privkey, pubkey, text) {
const key = secp256k1.getSharedSecret(privkey, "02" + pubkey);
const normalizedKey = getNormalizedX(key);
let iv = Uint8Array.from(randomBytes(16));
let plaintext = utf8Encoder.encode(text);
let cryptoKey = await crypto.subtle.importKey("raw", normalizedKey, { name: "AES-CBC" }, false, ["encrypt"]);
let ciphertext = await crypto.subtle.encrypt({ name: "AES-CBC", iv }, cryptoKey, plaintext);
let ctb64 = base64.encode(new Uint8Array(ciphertext));
let ivb64 = base64.encode(new Uint8Array(iv.buffer));
return `${ctb64}?iv=${ivb64}`;
}
async function decrypt(privkey, pubkey, data) {
let [ctb64, ivb64] = data.split("?iv=");
let key = secp256k1.getSharedSecret(privkey, "02" + pubkey);
let normalizedKey = getNormalizedX(key);
let cryptoKey = await crypto.subtle.importKey("raw", normalizedKey, { name: "AES-CBC" }, false, ["decrypt"]);
let ciphertext = base64.decode(ctb64);
let iv = base64.decode(ivb64);
let plaintext = await crypto.subtle.decrypt({ name: "AES-CBC", iv }, cryptoKey, ciphertext);
let text = utf8Decoder.decode(plaintext);
return text;
}
function getNormalizedX(key) {
return key.slice(1, 33);
}
export {
decrypt,
encrypt
};
File diff suppressed because one or more lines are too long
+57
View File
@@ -0,0 +1,57 @@
// nip05.ts
var NIP05_REGEX = /^(?:([\w.+-]+)@)?([\w.-]+)$/;
var _fetch;
try {
_fetch = fetch;
} catch {
}
function useFetchImplementation(fetchImplementation) {
_fetch = fetchImplementation;
}
async function searchDomain(domain, query = "") {
try {
let res = await (await _fetch(`https://${domain}/.well-known/nostr.json?name=${query}`)).json();
return res.names;
} catch (_) {
return {};
}
}
async function queryProfile(fullname) {
const match = fullname.match(NIP05_REGEX);
if (!match)
return null;
const [_, name = "_", domain] = match;
try {
const res = await _fetch(`https://${domain}/.well-known/nostr.json?name=${name}`);
const { names, relays } = parseNIP05Result(await res.json());
const pubkey = names[name];
return pubkey ? { pubkey, relays: relays?.[pubkey] } : null;
} catch (_e) {
return null;
}
}
function parseNIP05Result(json) {
const result = {
names: {}
};
for (const [name, pubkey] of Object.entries(json.names)) {
if (typeof name === "string" && typeof pubkey === "string") {
result.names[name] = pubkey;
}
}
if (json.relays) {
result.relays = {};
for (const [pubkey, relays] of Object.entries(json.relays)) {
if (typeof pubkey === "string" && Array.isArray(relays)) {
result.relays[pubkey] = relays.filter((relay) => typeof relay === "string");
}
}
}
return result;
}
export {
NIP05_REGEX,
queryProfile,
searchDomain,
useFetchImplementation
};
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../nip05.ts"],
"sourcesContent": ["import { ProfilePointer } from './nip19.ts'\n\n/**\n * NIP-05 regex. The localpart is optional, and should be assumed to be `_` otherwise.\n *\n * - 0: full match\n * - 1: name (optional)\n * - 2: domain\n */\nexport const NIP05_REGEX = /^(?:([\\w.+-]+)@)?([\\w.-]+)$/\n\nvar _fetch: any\n\ntry {\n _fetch = fetch\n} catch {}\n\nexport function useFetchImplementation(fetchImplementation: any) {\n _fetch = fetchImplementation\n}\n\nexport async function searchDomain(domain: string, query = ''): Promise<{ [name: string]: string }> {\n try {\n let res = await (await _fetch(`https://${domain}/.well-known/nostr.json?name=${query}`)).json()\n\n return res.names\n } catch (_) {\n return {}\n }\n}\n\nexport async function queryProfile(fullname: string): Promise<ProfilePointer | null> {\n const match = fullname.match(NIP05_REGEX)\n if (!match) return null\n\n const [_, name = '_', domain] = match\n\n try {\n const res = await _fetch(`https://${domain}/.well-known/nostr.json?name=${name}`)\n const { names, relays } = parseNIP05Result(await res.json())\n\n const pubkey = names[name]\n return pubkey ? { pubkey, relays: relays?.[pubkey] } : null\n } catch (_e) {\n return null\n }\n}\n\n/** nostr.json result. */\nexport interface NIP05Result {\n names: {\n [name: string]: string\n }\n relays?: {\n [pubkey: string]: string[]\n }\n}\n\n/** Parse the nostr.json and throw if it's not valid. */\nfunction parseNIP05Result(json: any): NIP05Result {\n const result: NIP05Result = {\n names: {},\n }\n\n for (const [name, pubkey] of Object.entries(json.names)) {\n if (typeof name === 'string' && typeof pubkey === 'string') {\n result.names[name] = pubkey\n }\n }\n\n if (json.relays) {\n result.relays = {}\n for (const [pubkey, relays] of Object.entries(json.relays)) {\n if (typeof pubkey === 'string' && Array.isArray(relays)) {\n result.relays[pubkey] = relays.filter((relay: unknown) => typeof relay === 'string')\n }\n }\n }\n\n return result\n}\n"],
"mappings": ";AASO,IAAM,cAAc;AAE3B,IAAI;AAEJ,IAAI;AACF,WAAS;AACX,QAAE;AAAO;AAEF,SAAS,uBAAuB,qBAA0B;AAC/D,WAAS;AACX;AAEA,eAAsB,aAAa,QAAgB,QAAQ,IAAyC;AAClG,MAAI;AACF,QAAI,MAAM,OAAO,MAAM,OAAO,WAAW,sCAAsC,OAAO,GAAG,KAAK;AAE9F,WAAO,IAAI;AAAA,EACb,SAAS,GAAP;AACA,WAAO,CAAC;AAAA,EACV;AACF;AAEA,eAAsB,aAAa,UAAkD;AACnF,QAAM,QAAQ,SAAS,MAAM,WAAW;AACxC,MAAI,CAAC;AAAO,WAAO;AAEnB,QAAM,CAAC,GAAG,OAAO,KAAK,MAAM,IAAI;AAEhC,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,WAAW,sCAAsC,MAAM;AAChF,UAAM,EAAE,OAAO,OAAO,IAAI,iBAAiB,MAAM,IAAI,KAAK,CAAC;AAE3D,UAAM,SAAS,MAAM;AACrB,WAAO,SAAS,EAAE,QAAQ,QAAQ,SAAS,QAAQ,IAAI;AAAA,EACzD,SAAS,IAAP;AACA,WAAO;AAAA,EACT;AACF;AAaA,SAAS,iBAAiB,MAAwB;AAChD,QAAM,SAAsB;AAAA,IAC1B,OAAO,CAAC;AAAA,EACV;AAEA,aAAW,CAAC,MAAM,MAAM,KAAK,OAAO,QAAQ,KAAK,KAAK,GAAG;AACvD,QAAI,OAAO,SAAS,YAAY,OAAO,WAAW,UAAU;AAC1D,aAAO,MAAM,QAAQ;AAAA,IACvB;AAAA,EACF;AAEA,MAAI,KAAK,QAAQ;AACf,WAAO,SAAS,CAAC;AACjB,eAAW,CAAC,QAAQ,MAAM,KAAK,OAAO,QAAQ,KAAK,MAAM,GAAG;AAC1D,UAAI,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AACvD,eAAO,OAAO,UAAU,OAAO,OAAO,CAAC,UAAmB,OAAO,UAAU,QAAQ;AAAA,MACrF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;",
"names": []
}
+23
View File
@@ -0,0 +1,23 @@
// nip06.ts
import { bytesToHex } from "@noble/hashes/utils";
import { wordlist } from "@scure/bip39/wordlists/english";
import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from "@scure/bip39";
import { HDKey } from "@scure/bip32";
function privateKeyFromSeedWords(mnemonic, passphrase) {
let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase));
let privateKey = root.derive(`m/44'/1237'/0'/0/0`).privateKey;
if (!privateKey)
throw new Error("could not derive private key");
return bytesToHex(privateKey);
}
function generateSeedWords() {
return generateMnemonic(wordlist);
}
function validateWords(words) {
return validateMnemonic(words, wordlist);
}
export {
generateSeedWords,
privateKeyFromSeedWords,
validateWords
};
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../nip06.ts"],
"sourcesContent": ["import { bytesToHex } from '@noble/hashes/utils'\nimport { wordlist } from '@scure/bip39/wordlists/english'\nimport { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from '@scure/bip39'\nimport { HDKey } from '@scure/bip32'\n\nexport function privateKeyFromSeedWords(mnemonic: string, passphrase?: string): string {\n let root = HDKey.fromMasterSeed(mnemonicToSeedSync(mnemonic, passphrase))\n let privateKey = root.derive(`m/44'/1237'/0'/0/0`).privateKey\n if (!privateKey) throw new Error('could not derive private key')\n return bytesToHex(privateKey)\n}\n\nexport function generateSeedWords(): string {\n return generateMnemonic(wordlist)\n}\n\nexport function validateWords(words: string): boolean {\n return validateMnemonic(words, wordlist)\n}\n"],
"mappings": ";AAAA,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AACzB,SAAS,kBAAkB,oBAAoB,wBAAwB;AACvE,SAAS,aAAa;AAEf,SAAS,wBAAwB,UAAkB,YAA6B;AACrF,MAAI,OAAO,MAAM,eAAe,mBAAmB,UAAU,UAAU,CAAC;AACxE,MAAI,aAAa,KAAK,OAAO,oBAAoB,EAAE;AACnD,MAAI,CAAC;AAAY,UAAM,IAAI,MAAM,8BAA8B;AAC/D,SAAO,WAAW,UAAU;AAC9B;AAEO,SAAS,oBAA4B;AAC1C,SAAO,iBAAiB,QAAQ;AAClC;AAEO,SAAS,cAAc,OAAwB;AACpD,SAAO,iBAAiB,OAAO,QAAQ;AACzC;",
"names": []
}
+56
View File
@@ -0,0 +1,56 @@
// nip10.ts
function parse(event) {
const result = {
reply: void 0,
root: void 0,
mentions: [],
profiles: []
};
const eTags = [];
for (const tag of event.tags) {
if (tag[0] === "e" && tag[1]) {
eTags.push(tag);
}
if (tag[0] === "p" && tag[1]) {
result.profiles.push({
pubkey: tag[1],
relays: tag[2] ? [tag[2]] : []
});
}
}
for (let eTagIndex = 0; eTagIndex < eTags.length; eTagIndex++) {
const eTag = eTags[eTagIndex];
const [_, eTagEventId, eTagRelayUrl, eTagMarker] = eTag;
const eventPointer = {
id: eTagEventId,
relays: eTagRelayUrl ? [eTagRelayUrl] : []
};
const isFirstETag = eTagIndex === 0;
const isLastETag = eTagIndex === eTags.length - 1;
if (eTagMarker === "root") {
result.root = eventPointer;
continue;
}
if (eTagMarker === "reply") {
result.reply = eventPointer;
continue;
}
if (eTagMarker === "mention") {
result.mentions.push(eventPointer);
continue;
}
if (isFirstETag) {
result.root = eventPointer;
continue;
}
if (isLastETag) {
result.reply = eventPointer;
continue;
}
result.mentions.push(eventPointer);
}
return result;
}
export {
parse
};
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../nip10.ts"],
"sourcesContent": ["import type { Event } from './event.ts'\nimport type { EventPointer, ProfilePointer } from './nip19.ts'\n\nexport type NIP10Result = {\n /**\n * Pointer to the root of the thread.\n */\n root: EventPointer | undefined\n\n /**\n * Pointer to a \"parent\" event that parsed event replies to (responded to).\n */\n reply: EventPointer | undefined\n\n /**\n * Pointers to events which may or may not be in the reply chain.\n */\n mentions: EventPointer[]\n\n /**\n * List of pubkeys that are involved in the thread in no particular order.\n */\n profiles: ProfilePointer[]\n}\n\nexport function parse(event: Pick<Event, 'tags'>): NIP10Result {\n const result: NIP10Result = {\n reply: undefined,\n root: undefined,\n mentions: [],\n profiles: [],\n }\n\n const eTags: string[][] = []\n\n for (const tag of event.tags) {\n if (tag[0] === 'e' && tag[1]) {\n eTags.push(tag)\n }\n\n if (tag[0] === 'p' && tag[1]) {\n result.profiles.push({\n pubkey: tag[1],\n relays: tag[2] ? [tag[2]] : [],\n })\n }\n }\n\n for (let eTagIndex = 0; eTagIndex < eTags.length; eTagIndex++) {\n const eTag = eTags[eTagIndex]\n\n const [_, eTagEventId, eTagRelayUrl, eTagMarker] = eTag as [string, string, undefined | string, undefined | string]\n\n const eventPointer: EventPointer = {\n id: eTagEventId,\n relays: eTagRelayUrl ? [eTagRelayUrl] : [],\n }\n\n const isFirstETag = eTagIndex === 0\n const isLastETag = eTagIndex === eTags.length - 1\n\n if (eTagMarker === 'root') {\n result.root = eventPointer\n continue\n }\n\n if (eTagMarker === 'reply') {\n result.reply = eventPointer\n continue\n }\n\n if (eTagMarker === 'mention') {\n result.mentions.push(eventPointer)\n continue\n }\n\n if (isFirstETag) {\n result.root = eventPointer\n continue\n }\n\n if (isLastETag) {\n result.reply = eventPointer\n continue\n }\n\n result.mentions.push(eventPointer)\n }\n\n return result\n}\n"],
"mappings": ";AAyBO,SAAS,MAAM,OAAyC;AAC7D,QAAM,SAAsB;AAAA,IAC1B,OAAO;AAAA,IACP,MAAM;AAAA,IACN,UAAU,CAAC;AAAA,IACX,UAAU,CAAC;AAAA,EACb;AAEA,QAAM,QAAoB,CAAC;AAE3B,aAAW,OAAO,MAAM,MAAM;AAC5B,QAAI,IAAI,OAAO,OAAO,IAAI,IAAI;AAC5B,YAAM,KAAK,GAAG;AAAA,IAChB;AAEA,QAAI,IAAI,OAAO,OAAO,IAAI,IAAI;AAC5B,aAAO,SAAS,KAAK;AAAA,QACnB,QAAQ,IAAI;AAAA,QACZ,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC;AAAA,MAC/B,CAAC;AAAA,IACH;AAAA,EACF;AAEA,WAAS,YAAY,GAAG,YAAY,MAAM,QAAQ,aAAa;AAC7D,UAAM,OAAO,MAAM;AAEnB,UAAM,CAAC,GAAG,aAAa,cAAc,UAAU,IAAI;AAEnD,UAAM,eAA6B;AAAA,MACjC,IAAI;AAAA,MACJ,QAAQ,eAAe,CAAC,YAAY,IAAI,CAAC;AAAA,IAC3C;AAEA,UAAM,cAAc,cAAc;AAClC,UAAM,aAAa,cAAc,MAAM,SAAS;AAEhD,QAAI,eAAe,QAAQ;AACzB,aAAO,OAAO;AACd;AAAA,IACF;AAEA,QAAI,eAAe,SAAS;AAC1B,aAAO,QAAQ;AACf;AAAA,IACF;AAEA,QAAI,eAAe,WAAW;AAC5B,aAAO,SAAS,KAAK,YAAY;AACjC;AAAA,IACF;AAEA,QAAI,aAAa;AACf,aAAO,OAAO;AACd;AAAA,IACF;AAEA,QAAI,YAAY;AACd,aAAO,QAAQ;AACf;AAAA,IACF;AAEA,WAAO,SAAS,KAAK,YAAY;AAAA,EACnC;AAEA,SAAO;AACT;",
"names": []
}
+85
View File
@@ -0,0 +1,85 @@
// event.ts
import { schnorr } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex } from "@noble/hashes/utils";
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
// event.ts
var verifiedSymbol = Symbol("verified");
function serializeEvent(evt) {
if (!validateEvent(evt))
throw new Error("can't serialize event with wrong or missing properties");
return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]);
}
function getEventHash(event) {
let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)));
return bytesToHex(eventHash);
}
var isRecord = (obj) => obj instanceof Object;
function validateEvent(event) {
if (!isRecord(event))
return false;
if (typeof event.kind !== "number")
return false;
if (typeof event.content !== "string")
return false;
if (typeof event.created_at !== "number")
return false;
if (typeof event.pubkey !== "string")
return false;
if (!event.pubkey.match(/^[a-f0-9]{64}$/))
return false;
if (!Array.isArray(event.tags))
return false;
for (let i = 0; i < event.tags.length; i++) {
let tag = event.tags[i];
if (!Array.isArray(tag))
return false;
for (let j = 0; j < tag.length; j++) {
if (typeof tag[j] === "object")
return false;
}
}
return true;
}
// nip13.ts
function getPow(hex) {
let count = 0;
for (let i = 0; i < hex.length; i++) {
const nibble = parseInt(hex[i], 16);
if (nibble === 0) {
count += 4;
} else {
count += Math.clz32(nibble) - 28;
break;
}
}
return count;
}
function minePow(unsigned, difficulty) {
let count = 0;
const event = unsigned;
const tag = ["nonce", count.toString(), difficulty.toString()];
event.tags.push(tag);
while (true) {
const now = Math.floor(new Date().getTime() / 1e3);
if (now !== event.created_at) {
count = 0;
event.created_at = now;
}
tag[1] = (++count).toString();
event.id = getEventHash(event);
if (getPow(event.id) >= difficulty) {
break;
}
}
return event;
}
export {
getPow,
minePow
};
File diff suppressed because one or more lines are too long
+140
View File
@@ -0,0 +1,140 @@
// event.ts
import { schnorr as schnorr2 } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex as bytesToHex2 } from "@noble/hashes/utils";
// keys.ts
import { schnorr } from "@noble/curves/secp256k1";
import { bytesToHex } from "@noble/hashes/utils";
function getPublicKey(privateKey) {
return bytesToHex(schnorr.getPublicKey(privateKey));
}
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
// event.ts
var verifiedSymbol = Symbol("verified");
function finishEvent(t, privateKey) {
const event = t;
event.pubkey = getPublicKey(privateKey);
event.id = getEventHash(event);
event.sig = getSignature(event, privateKey);
event[verifiedSymbol] = true;
return event;
}
function serializeEvent(evt) {
if (!validateEvent(evt))
throw new Error("can't serialize event with wrong or missing properties");
return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]);
}
function getEventHash(event) {
let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)));
return bytesToHex2(eventHash);
}
var isRecord = (obj) => obj instanceof Object;
function validateEvent(event) {
if (!isRecord(event))
return false;
if (typeof event.kind !== "number")
return false;
if (typeof event.content !== "string")
return false;
if (typeof event.created_at !== "number")
return false;
if (typeof event.pubkey !== "string")
return false;
if (!event.pubkey.match(/^[a-f0-9]{64}$/))
return false;
if (!Array.isArray(event.tags))
return false;
for (let i = 0; i < event.tags.length; i++) {
let tag = event.tags[i];
if (!Array.isArray(tag))
return false;
for (let j = 0; j < tag.length; j++) {
if (typeof tag[j] === "object")
return false;
}
}
return true;
}
function verifySignature(event) {
if (typeof event[verifiedSymbol] === "boolean")
return event[verifiedSymbol];
const hash = getEventHash(event);
if (hash !== event.id) {
return event[verifiedSymbol] = false;
}
try {
return event[verifiedSymbol] = schnorr2.verify(event.sig, hash, event.pubkey);
} catch (err) {
return event[verifiedSymbol] = false;
}
}
function getSignature(event, key) {
return bytesToHex2(schnorr2.sign(getEventHash(event), key));
}
// nip18.ts
function finishRepostEvent(t, reposted, relayUrl, privateKey) {
return finishEvent(
{
kind: 6 /* Repost */,
tags: [...t.tags ?? [], ["e", reposted.id, relayUrl], ["p", reposted.pubkey]],
content: t.content === "" ? "" : JSON.stringify(reposted),
created_at: t.created_at
},
privateKey
);
}
function getRepostedEventPointer(event) {
if (event.kind !== 6 /* Repost */) {
return void 0;
}
let lastETag;
let lastPTag;
for (let i = event.tags.length - 1; i >= 0 && (lastETag === void 0 || lastPTag === void 0); i--) {
const tag = event.tags[i];
if (tag.length >= 2) {
if (tag[0] === "e" && lastETag === void 0) {
lastETag = tag;
} else if (tag[0] === "p" && lastPTag === void 0) {
lastPTag = tag;
}
}
}
if (lastETag === void 0) {
return void 0;
}
return {
id: lastETag[1],
relays: [lastETag[2], lastPTag?.[2]].filter((x) => typeof x === "string"),
author: lastPTag?.[1]
};
}
function getRepostedEvent(event, { skipVerification } = {}) {
const pointer = getRepostedEventPointer(event);
if (pointer === void 0 || event.content === "") {
return void 0;
}
let repostedEvent;
try {
repostedEvent = JSON.parse(event.content);
} catch (error) {
return void 0;
}
if (repostedEvent.id !== pointer.id) {
return void 0;
}
if (!skipVerification && !verifySignature(repostedEvent)) {
return void 0;
}
return repostedEvent;
}
export {
finishRepostEvent,
getRepostedEvent,
getRepostedEventPointer
};
File diff suppressed because one or more lines are too long
+191
View File
@@ -0,0 +1,191 @@
// nip19.ts
import { bytesToHex, concatBytes, hexToBytes } from "@noble/hashes/utils";
import { bech32 } from "@scure/base";
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
// nip19.ts
var Bech32MaxSize = 5e3;
var BECH32_REGEX = /[\x21-\x7E]{1,83}1[023456789acdefghjklmnpqrstuvwxyz]{6,}/;
function integerToUint8Array(number) {
const uint8Array = new Uint8Array(4);
uint8Array[0] = number >> 24 & 255;
uint8Array[1] = number >> 16 & 255;
uint8Array[2] = number >> 8 & 255;
uint8Array[3] = number & 255;
return uint8Array;
}
function decode(nip19) {
let { prefix, words } = bech32.decode(nip19, Bech32MaxSize);
let data = new Uint8Array(bech32.fromWords(words));
switch (prefix) {
case "nprofile": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for nprofile");
if (tlv[0][0].length !== 32)
throw new Error("TLV 0 should be 32 bytes");
return {
type: "nprofile",
data: {
pubkey: bytesToHex(tlv[0][0]),
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : []
}
};
}
case "nevent": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for nevent");
if (tlv[0][0].length !== 32)
throw new Error("TLV 0 should be 32 bytes");
if (tlv[2] && tlv[2][0].length !== 32)
throw new Error("TLV 2 should be 32 bytes");
if (tlv[3] && tlv[3][0].length !== 4)
throw new Error("TLV 3 should be 4 bytes");
return {
type: "nevent",
data: {
id: bytesToHex(tlv[0][0]),
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : [],
author: tlv[2]?.[0] ? bytesToHex(tlv[2][0]) : void 0,
kind: tlv[3]?.[0] ? parseInt(bytesToHex(tlv[3][0]), 16) : void 0
}
};
}
case "naddr": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for naddr");
if (!tlv[2]?.[0])
throw new Error("missing TLV 2 for naddr");
if (tlv[2][0].length !== 32)
throw new Error("TLV 2 should be 32 bytes");
if (!tlv[3]?.[0])
throw new Error("missing TLV 3 for naddr");
if (tlv[3][0].length !== 4)
throw new Error("TLV 3 should be 4 bytes");
return {
type: "naddr",
data: {
identifier: utf8Decoder.decode(tlv[0][0]),
pubkey: bytesToHex(tlv[2][0]),
kind: parseInt(bytesToHex(tlv[3][0]), 16),
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : []
}
};
}
case "nrelay": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for nrelay");
return {
type: "nrelay",
data: utf8Decoder.decode(tlv[0][0])
};
}
case "nsec":
case "npub":
case "note":
return { type: prefix, data: bytesToHex(data) };
default:
throw new Error(`unknown prefix ${prefix}`);
}
}
function parseTLV(data) {
let result = {};
let rest = data;
while (rest.length > 0) {
let t = rest[0];
let l = rest[1];
if (!l)
throw new Error(`malformed TLV ${t}`);
let v = rest.slice(2, 2 + l);
rest = rest.slice(2 + l);
if (v.length < l)
throw new Error(`not enough data to read on TLV ${t}`);
result[t] = result[t] || [];
result[t].push(v);
}
return result;
}
function nsecEncode(hex) {
return encodeBytes("nsec", hex);
}
function npubEncode(hex) {
return encodeBytes("npub", hex);
}
function noteEncode(hex) {
return encodeBytes("note", hex);
}
function encodeBech32(prefix, data) {
let words = bech32.toWords(data);
return bech32.encode(prefix, words, Bech32MaxSize);
}
function encodeBytes(prefix, hex) {
let data = hexToBytes(hex);
return encodeBech32(prefix, data);
}
function nprofileEncode(profile) {
let data = encodeTLV({
0: [hexToBytes(profile.pubkey)],
1: (profile.relays || []).map((url) => utf8Encoder.encode(url))
});
return encodeBech32("nprofile", data);
}
function neventEncode(event) {
let kindArray;
if (event.kind != void 0) {
kindArray = integerToUint8Array(event.kind);
}
let data = encodeTLV({
0: [hexToBytes(event.id)],
1: (event.relays || []).map((url) => utf8Encoder.encode(url)),
2: event.author ? [hexToBytes(event.author)] : [],
3: kindArray ? [new Uint8Array(kindArray)] : []
});
return encodeBech32("nevent", data);
}
function naddrEncode(addr) {
let kind = new ArrayBuffer(4);
new DataView(kind).setUint32(0, addr.kind, false);
let data = encodeTLV({
0: [utf8Encoder.encode(addr.identifier)],
1: (addr.relays || []).map((url) => utf8Encoder.encode(url)),
2: [hexToBytes(addr.pubkey)],
3: [new Uint8Array(kind)]
});
return encodeBech32("naddr", data);
}
function nrelayEncode(url) {
let data = encodeTLV({
0: [utf8Encoder.encode(url)]
});
return encodeBech32("nrelay", data);
}
function encodeTLV(tlv) {
let entries = [];
Object.entries(tlv).forEach(([t, vs]) => {
vs.forEach((v) => {
let entry = new Uint8Array(v.length + 2);
entry.set([parseInt(t)], 0);
entry.set([v.length], 1);
entry.set(v, 2);
entries.push(entry);
});
});
return concatBytes(...entries);
}
export {
BECH32_REGEX,
decode,
naddrEncode,
neventEncode,
noteEncode,
nprofileEncode,
npubEncode,
nrelayEncode,
nsecEncode
};
File diff suppressed because one or more lines are too long
+126
View File
@@ -0,0 +1,126 @@
// nip19.ts
import { bytesToHex, concatBytes, hexToBytes } from "@noble/hashes/utils";
import { bech32 } from "@scure/base";
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
// nip19.ts
var Bech32MaxSize = 5e3;
var BECH32_REGEX = /[\x21-\x7E]{1,83}1[023456789acdefghjklmnpqrstuvwxyz]{6,}/;
function decode(nip19) {
let { prefix, words } = bech32.decode(nip19, Bech32MaxSize);
let data = new Uint8Array(bech32.fromWords(words));
switch (prefix) {
case "nprofile": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for nprofile");
if (tlv[0][0].length !== 32)
throw new Error("TLV 0 should be 32 bytes");
return {
type: "nprofile",
data: {
pubkey: bytesToHex(tlv[0][0]),
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : []
}
};
}
case "nevent": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for nevent");
if (tlv[0][0].length !== 32)
throw new Error("TLV 0 should be 32 bytes");
if (tlv[2] && tlv[2][0].length !== 32)
throw new Error("TLV 2 should be 32 bytes");
if (tlv[3] && tlv[3][0].length !== 4)
throw new Error("TLV 3 should be 4 bytes");
return {
type: "nevent",
data: {
id: bytesToHex(tlv[0][0]),
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : [],
author: tlv[2]?.[0] ? bytesToHex(tlv[2][0]) : void 0,
kind: tlv[3]?.[0] ? parseInt(bytesToHex(tlv[3][0]), 16) : void 0
}
};
}
case "naddr": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for naddr");
if (!tlv[2]?.[0])
throw new Error("missing TLV 2 for naddr");
if (tlv[2][0].length !== 32)
throw new Error("TLV 2 should be 32 bytes");
if (!tlv[3]?.[0])
throw new Error("missing TLV 3 for naddr");
if (tlv[3][0].length !== 4)
throw new Error("TLV 3 should be 4 bytes");
return {
type: "naddr",
data: {
identifier: utf8Decoder.decode(tlv[0][0]),
pubkey: bytesToHex(tlv[2][0]),
kind: parseInt(bytesToHex(tlv[3][0]), 16),
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : []
}
};
}
case "nrelay": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for nrelay");
return {
type: "nrelay",
data: utf8Decoder.decode(tlv[0][0])
};
}
case "nsec":
case "npub":
case "note":
return { type: prefix, data: bytesToHex(data) };
default:
throw new Error(`unknown prefix ${prefix}`);
}
}
function parseTLV(data) {
let result = {};
let rest = data;
while (rest.length > 0) {
let t = rest[0];
let l = rest[1];
if (!l)
throw new Error(`malformed TLV ${t}`);
let v = rest.slice(2, 2 + l);
rest = rest.slice(2 + l);
if (v.length < l)
throw new Error(`not enough data to read on TLV ${t}`);
result[t] = result[t] || [];
result[t].push(v);
}
return result;
}
// nip21.ts
var NOSTR_URI_REGEX = new RegExp(`nostr:(${BECH32_REGEX.source})`);
function test(value) {
return typeof value === "string" && new RegExp(`^${NOSTR_URI_REGEX.source}$`).test(value);
}
function parse(uri) {
const match = uri.match(new RegExp(`^${NOSTR_URI_REGEX.source}$`));
if (!match)
throw new Error(`Invalid Nostr URI: ${uri}`);
return {
uri: match[0],
value: match[1],
decoded: decode(match[1])
};
}
export {
NOSTR_URI_REGEX,
parse,
test
};
File diff suppressed because one or more lines are too long
+108
View File
@@ -0,0 +1,108 @@
// event.ts
import { schnorr as schnorr2 } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex as bytesToHex2 } from "@noble/hashes/utils";
// keys.ts
import { schnorr } from "@noble/curves/secp256k1";
import { bytesToHex } from "@noble/hashes/utils";
function getPublicKey(privateKey) {
return bytesToHex(schnorr.getPublicKey(privateKey));
}
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
// event.ts
var verifiedSymbol = Symbol("verified");
function finishEvent(t, privateKey) {
const event = t;
event.pubkey = getPublicKey(privateKey);
event.id = getEventHash(event);
event.sig = getSignature(event, privateKey);
event[verifiedSymbol] = true;
return event;
}
function serializeEvent(evt) {
if (!validateEvent(evt))
throw new Error("can't serialize event with wrong or missing properties");
return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]);
}
function getEventHash(event) {
let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)));
return bytesToHex2(eventHash);
}
var isRecord = (obj) => obj instanceof Object;
function validateEvent(event) {
if (!isRecord(event))
return false;
if (typeof event.kind !== "number")
return false;
if (typeof event.content !== "string")
return false;
if (typeof event.created_at !== "number")
return false;
if (typeof event.pubkey !== "string")
return false;
if (!event.pubkey.match(/^[a-f0-9]{64}$/))
return false;
if (!Array.isArray(event.tags))
return false;
for (let i = 0; i < event.tags.length; i++) {
let tag = event.tags[i];
if (!Array.isArray(tag))
return false;
for (let j = 0; j < tag.length; j++) {
if (typeof tag[j] === "object")
return false;
}
}
return true;
}
function getSignature(event, key) {
return bytesToHex2(schnorr2.sign(getEventHash(event), key));
}
// nip25.ts
function finishReactionEvent(t, reacted, privateKey) {
const inheritedTags = reacted.tags.filter((tag) => tag.length >= 2 && (tag[0] === "e" || tag[0] === "p"));
return finishEvent(
{
...t,
kind: 7 /* Reaction */,
tags: [...t.tags ?? [], ...inheritedTags, ["e", reacted.id], ["p", reacted.pubkey]],
content: t.content ?? "+"
},
privateKey
);
}
function getReactedEventPointer(event) {
if (event.kind !== 7 /* Reaction */) {
return void 0;
}
let lastETag;
let lastPTag;
for (let i = event.tags.length - 1; i >= 0 && (lastETag === void 0 || lastPTag === void 0); i--) {
const tag = event.tags[i];
if (tag.length >= 2) {
if (tag[0] === "e" && lastETag === void 0) {
lastETag = tag;
} else if (tag[0] === "p" && lastPTag === void 0) {
lastPTag = tag;
}
}
}
if (lastETag === void 0 || lastPTag === void 0) {
return void 0;
}
return {
id: lastETag[1],
relays: [lastETag[2], lastPTag[2]].filter((x) => x !== void 0),
author: lastPTag[1]
};
}
export {
finishReactionEvent,
getReactedEventPointer
};
File diff suppressed because one or more lines are too long
+65
View File
@@ -0,0 +1,65 @@
// nip26.ts
import { schnorr as schnorr2 } from "@noble/curves/secp256k1";
import { bytesToHex as bytesToHex2 } from "@noble/hashes/utils";
import { sha256 } from "@noble/hashes/sha256";
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
// keys.ts
import { schnorr } from "@noble/curves/secp256k1";
import { bytesToHex } from "@noble/hashes/utils";
function getPublicKey(privateKey) {
return bytesToHex(schnorr.getPublicKey(privateKey));
}
// nip26.ts
function createDelegation(privateKey, parameters) {
let conditions = [];
if ((parameters.kind || -1) >= 0)
conditions.push(`kind=${parameters.kind}`);
if (parameters.until)
conditions.push(`created_at<${parameters.until}`);
if (parameters.since)
conditions.push(`created_at>${parameters.since}`);
let cond = conditions.join("&");
if (cond === "")
throw new Error("refusing to create a delegation without any conditions");
let sighash = sha256(utf8Encoder.encode(`nostr:delegation:${parameters.pubkey}:${cond}`));
let sig = bytesToHex2(schnorr2.sign(sighash, privateKey));
return {
from: getPublicKey(privateKey),
to: parameters.pubkey,
cond,
sig
};
}
function getDelegator(event) {
let tag = event.tags.find((tag2) => tag2[0] === "delegation" && tag2.length >= 4);
if (!tag)
return null;
let pubkey = tag[1];
let cond = tag[2];
let sig = tag[3];
let conditions = cond.split("&");
for (let i = 0; i < conditions.length; i++) {
let [key, operator, value] = conditions[i].split(/\b/);
if (key === "kind" && operator === "=" && event.kind === parseInt(value))
continue;
else if (key === "created_at" && operator === "<" && event.created_at < parseInt(value))
continue;
else if (key === "created_at" && operator === ">" && event.created_at > parseInt(value))
continue;
else
return null;
}
let sighash = sha256(utf8Encoder.encode(`nostr:delegation:${event.pubkey}:${cond}`));
if (!schnorr2.verify(sig, sighash, pubkey))
return null;
return pubkey;
}
export {
createDelegation,
getDelegator
};
File diff suppressed because one or more lines are too long
+141
View File
@@ -0,0 +1,141 @@
// nip19.ts
import { bytesToHex, concatBytes, hexToBytes } from "@noble/hashes/utils";
import { bech32 } from "@scure/base";
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
// nip19.ts
var Bech32MaxSize = 5e3;
var BECH32_REGEX = /[\x21-\x7E]{1,83}1[023456789acdefghjklmnpqrstuvwxyz]{6,}/;
function decode(nip19) {
let { prefix, words } = bech32.decode(nip19, Bech32MaxSize);
let data = new Uint8Array(bech32.fromWords(words));
switch (prefix) {
case "nprofile": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for nprofile");
if (tlv[0][0].length !== 32)
throw new Error("TLV 0 should be 32 bytes");
return {
type: "nprofile",
data: {
pubkey: bytesToHex(tlv[0][0]),
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : []
}
};
}
case "nevent": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for nevent");
if (tlv[0][0].length !== 32)
throw new Error("TLV 0 should be 32 bytes");
if (tlv[2] && tlv[2][0].length !== 32)
throw new Error("TLV 2 should be 32 bytes");
if (tlv[3] && tlv[3][0].length !== 4)
throw new Error("TLV 3 should be 4 bytes");
return {
type: "nevent",
data: {
id: bytesToHex(tlv[0][0]),
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : [],
author: tlv[2]?.[0] ? bytesToHex(tlv[2][0]) : void 0,
kind: tlv[3]?.[0] ? parseInt(bytesToHex(tlv[3][0]), 16) : void 0
}
};
}
case "naddr": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for naddr");
if (!tlv[2]?.[0])
throw new Error("missing TLV 2 for naddr");
if (tlv[2][0].length !== 32)
throw new Error("TLV 2 should be 32 bytes");
if (!tlv[3]?.[0])
throw new Error("missing TLV 3 for naddr");
if (tlv[3][0].length !== 4)
throw new Error("TLV 3 should be 4 bytes");
return {
type: "naddr",
data: {
identifier: utf8Decoder.decode(tlv[0][0]),
pubkey: bytesToHex(tlv[2][0]),
kind: parseInt(bytesToHex(tlv[3][0]), 16),
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : []
}
};
}
case "nrelay": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for nrelay");
return {
type: "nrelay",
data: utf8Decoder.decode(tlv[0][0])
};
}
case "nsec":
case "npub":
case "note":
return { type: prefix, data: bytesToHex(data) };
default:
throw new Error(`unknown prefix ${prefix}`);
}
}
function parseTLV(data) {
let result = {};
let rest = data;
while (rest.length > 0) {
let t = rest[0];
let l = rest[1];
if (!l)
throw new Error(`malformed TLV ${t}`);
let v = rest.slice(2, 2 + l);
rest = rest.slice(2 + l);
if (v.length < l)
throw new Error(`not enough data to read on TLV ${t}`);
result[t] = result[t] || [];
result[t].push(v);
}
return result;
}
// nip21.ts
var NOSTR_URI_REGEX = new RegExp(`nostr:(${BECH32_REGEX.source})`);
// nip27.ts
var regex = () => new RegExp(`\\b${NOSTR_URI_REGEX.source}\\b`, "g");
function* matchAll(content) {
const matches = content.matchAll(regex());
for (const match of matches) {
try {
const [uri, value] = match;
yield {
uri,
value,
decoded: decode(value),
start: match.index,
end: match.index + uri.length
};
} catch (_e) {
}
}
}
function replaceAll(content, replacer) {
return content.replaceAll(regex(), (uri, value) => {
return replacer({
uri,
value,
decoded: decode(value)
});
});
}
export {
matchAll,
regex,
replaceAll
};
File diff suppressed because one or more lines are too long
+165
View File
@@ -0,0 +1,165 @@
// event.ts
import { schnorr as schnorr2 } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex as bytesToHex2 } from "@noble/hashes/utils";
// keys.ts
import { schnorr } from "@noble/curves/secp256k1";
import { bytesToHex } from "@noble/hashes/utils";
function getPublicKey(privateKey) {
return bytesToHex(schnorr.getPublicKey(privateKey));
}
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
// event.ts
var verifiedSymbol = Symbol("verified");
function finishEvent(t, privateKey) {
const event = t;
event.pubkey = getPublicKey(privateKey);
event.id = getEventHash(event);
event.sig = getSignature(event, privateKey);
event[verifiedSymbol] = true;
return event;
}
function serializeEvent(evt) {
if (!validateEvent(evt))
throw new Error("can't serialize event with wrong or missing properties");
return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]);
}
function getEventHash(event) {
let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)));
return bytesToHex2(eventHash);
}
var isRecord = (obj) => obj instanceof Object;
function validateEvent(event) {
if (!isRecord(event))
return false;
if (typeof event.kind !== "number")
return false;
if (typeof event.content !== "string")
return false;
if (typeof event.created_at !== "number")
return false;
if (typeof event.pubkey !== "string")
return false;
if (!event.pubkey.match(/^[a-f0-9]{64}$/))
return false;
if (!Array.isArray(event.tags))
return false;
for (let i = 0; i < event.tags.length; i++) {
let tag = event.tags[i];
if (!Array.isArray(tag))
return false;
for (let j = 0; j < tag.length; j++) {
if (typeof tag[j] === "object")
return false;
}
}
return true;
}
function getSignature(event, key) {
return bytesToHex2(schnorr2.sign(getEventHash(event), key));
}
// nip28.ts
var channelCreateEvent = (t, privateKey) => {
let content;
if (typeof t.content === "object") {
content = JSON.stringify(t.content);
} else if (typeof t.content === "string") {
content = t.content;
} else {
return void 0;
}
return finishEvent(
{
kind: 40 /* ChannelCreation */,
tags: [...t.tags ?? []],
content,
created_at: t.created_at
},
privateKey
);
};
var channelMetadataEvent = (t, privateKey) => {
let content;
if (typeof t.content === "object") {
content = JSON.stringify(t.content);
} else if (typeof t.content === "string") {
content = t.content;
} else {
return void 0;
}
return finishEvent(
{
kind: 41 /* ChannelMetadata */,
tags: [["e", t.channel_create_event_id], ...t.tags ?? []],
content,
created_at: t.created_at
},
privateKey
);
};
var channelMessageEvent = (t, privateKey) => {
const tags = [["e", t.channel_create_event_id, t.relay_url, "root"]];
if (t.reply_to_channel_message_event_id) {
tags.push(["e", t.reply_to_channel_message_event_id, t.relay_url, "reply"]);
}
return finishEvent(
{
kind: 42 /* ChannelMessage */,
tags: [...tags, ...t.tags ?? []],
content: t.content,
created_at: t.created_at
},
privateKey
);
};
var channelHideMessageEvent = (t, privateKey) => {
let content;
if (typeof t.content === "object") {
content = JSON.stringify(t.content);
} else if (typeof t.content === "string") {
content = t.content;
} else {
return void 0;
}
return finishEvent(
{
kind: 43 /* ChannelHideMessage */,
tags: [["e", t.channel_message_event_id], ...t.tags ?? []],
content,
created_at: t.created_at
},
privateKey
);
};
var channelMuteUserEvent = (t, privateKey) => {
let content;
if (typeof t.content === "object") {
content = JSON.stringify(t.content);
} else if (typeof t.content === "string") {
content = t.content;
} else {
return void 0;
}
return finishEvent(
{
kind: 44 /* ChannelMuteUser */,
tags: [["p", t.pubkey_to_mute], ...t.tags ?? []],
content,
created_at: t.created_at
},
privateKey
);
};
export {
channelCreateEvent,
channelHideMessageEvent,
channelMessageEvent,
channelMetadataEvent,
channelMuteUserEvent
};
File diff suppressed because one or more lines are too long
+21
View File
@@ -0,0 +1,21 @@
// nip39.ts
var _fetch;
try {
_fetch = fetch;
} catch {
}
function useFetchImplementation(fetchImplementation) {
_fetch = fetchImplementation;
}
async function validateGithub(pubkey, username, proof) {
try {
let res = await (await _fetch(`https://gist.github.com/${username}/${proof}/raw`)).text();
return res === `Verifying that I control the following Nostr public key: ${pubkey}`;
} catch (_) {
return false;
}
}
export {
useFetchImplementation,
validateGithub
};
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../nip39.ts"],
"sourcesContent": ["var _fetch: any\n\ntry {\n _fetch = fetch\n} catch {}\n\nexport function useFetchImplementation(fetchImplementation: any) {\n _fetch = fetchImplementation\n}\n\nexport async function validateGithub(pubkey: string, username: string, proof: string): Promise<boolean> {\n try {\n let res = await (await _fetch(`https://gist.github.com/${username}/${proof}/raw`)).text()\n return res === `Verifying that I control the following Nostr public key: ${pubkey}`\n } catch (_) {\n return false\n }\n}\n"],
"mappings": ";AAAA,IAAI;AAEJ,IAAI;AACF,WAAS;AACX,QAAE;AAAO;AAEF,SAAS,uBAAuB,qBAA0B;AAC/D,WAAS;AACX;AAEA,eAAsB,eAAe,QAAgB,UAAkB,OAAiC;AACtG,MAAI;AACF,QAAI,MAAM,OAAO,MAAM,OAAO,2BAA2B,YAAY,WAAW,GAAG,KAAK;AACxF,WAAO,QAAQ,4DAA4D;AAAA,EAC7E,SAAS,GAAP;AACA,WAAO;AAAA,EACT;AACF;",
"names": []
}
+20
View File
@@ -0,0 +1,20 @@
// nip42.ts
var authenticate = async ({
challenge,
relay,
sign
}) => {
const e = {
kind: 22242 /* ClientAuth */,
created_at: Math.floor(Date.now() / 1e3),
tags: [
["relay", relay.url],
["challenge", challenge]
],
content: ""
};
return relay.auth(await sign(e));
};
export {
authenticate
};
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../nip42.ts"],
"sourcesContent": ["import { Kind, type EventTemplate, type Event } from './event.ts'\nimport { Relay } from './relay.ts'\n\n/**\n * Authenticate via NIP-42 flow.\n *\n * @example\n * const sign = window.nostr.signEvent\n * relay.on('auth', challenge =>\n * authenticate({ relay, sign, challenge })\n * )\n */\nexport const authenticate = async ({\n challenge,\n relay,\n sign,\n}: {\n challenge: string\n relay: Relay\n sign: <K extends number = number>(e: EventTemplate<K>) => Promise<Event<K>> | Event<K>\n}): Promise<void> => {\n const e: EventTemplate = {\n kind: Kind.ClientAuth,\n created_at: Math.floor(Date.now() / 1000),\n tags: [\n ['relay', relay.url],\n ['challenge', challenge],\n ],\n content: '',\n }\n return relay.auth(await sign(e))\n}\n"],
"mappings": ";AAYO,IAAM,eAAe,OAAO;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AACF,MAIqB;AACnB,QAAM,IAAmB;AAAA,IACvB;AAAA,IACA,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,IACxC,MAAM;AAAA,MACJ,CAAC,SAAS,MAAM,GAAG;AAAA,MACnB,CAAC,aAAa,SAAS;AAAA,IACzB;AAAA,IACA,SAAS;AAAA,EACX;AACA,SAAO,MAAM,KAAK,MAAM,KAAK,CAAC,CAAC;AACjC;",
"names": []
}
+105
View File
@@ -0,0 +1,105 @@
// nip44.ts
import { chacha20 } from "@noble/ciphers/chacha";
import { ensureBytes, equalBytes } from "@noble/ciphers/utils";
import { secp256k1 } from "@noble/curves/secp256k1";
import { hkdf } from "@noble/hashes/hkdf";
import { hmac } from "@noble/hashes/hmac";
import { sha256 } from "@noble/hashes/sha256";
import { concatBytes, randomBytes } from "@noble/hashes/utils";
import { base64 } from "@scure/base";
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
// nip44.ts
var utils = {
v2: {
maxPlaintextSize: 65536 - 128,
minCiphertextSize: 100,
maxCiphertextSize: 102400,
getConversationKey(privkeyA, pubkeyB) {
const key = secp256k1.getSharedSecret(privkeyA, "02" + pubkeyB);
return key.subarray(1, 33);
},
getMessageKeys(conversationKey, salt) {
const keys = hkdf(sha256, conversationKey, salt, "nip44-v2", 76);
return {
encryption: keys.subarray(0, 32),
nonce: keys.subarray(32, 44),
auth: keys.subarray(44, 76)
};
},
calcPadding(len) {
if (!Number.isSafeInteger(len) || len < 0)
throw new Error("expected positive integer");
if (len <= 32)
return 32;
const nextpower = 1 << Math.floor(Math.log2(len - 1)) + 1;
const chunk = nextpower <= 256 ? 32 : nextpower / 8;
return chunk * (Math.floor((len - 1) / chunk) + 1);
},
pad(unpadded) {
const unpaddedB = utf8Encoder.encode(unpadded);
const len = unpaddedB.length;
if (len < 1 || len >= utils.v2.maxPlaintextSize)
throw new Error("invalid plaintext length: must be between 1b and 64KB");
const paddedLen = utils.v2.calcPadding(len);
const zeros = new Uint8Array(paddedLen - len);
const lenBuf = new Uint8Array(2);
new DataView(lenBuf.buffer).setUint16(0, len);
return concatBytes(lenBuf, unpaddedB, zeros);
},
unpad(padded) {
const unpaddedLen = new DataView(padded.buffer).getUint16(0);
const unpadded = padded.subarray(2, 2 + unpaddedLen);
if (unpaddedLen === 0 || unpadded.length !== unpaddedLen || padded.length !== 2 + utils.v2.calcPadding(unpaddedLen))
throw new Error("invalid padding");
return utf8Decoder.decode(unpadded);
}
}
};
function encrypt(key, plaintext, options = {}) {
const version = options.version ?? 2;
if (version !== 2)
throw new Error("unknown encryption version " + version);
const salt = options.salt ?? randomBytes(32);
ensureBytes(salt, 32);
const keys = utils.v2.getMessageKeys(key, salt);
const padded = utils.v2.pad(plaintext);
const ciphertext = chacha20(keys.encryption, keys.nonce, padded);
const mac = hmac(sha256, keys.auth, ciphertext);
return base64.encode(concatBytes(new Uint8Array([version]), salt, ciphertext, mac));
}
function decrypt(key, ciphertext) {
const u = utils.v2;
ensureBytes(key, 32);
const clen = ciphertext.length;
if (clen < u.minCiphertextSize || clen >= u.maxCiphertextSize)
throw new Error("invalid ciphertext length: " + clen);
if (ciphertext[0] === "#")
throw new Error("unknown encryption version");
let data;
try {
data = base64.decode(ciphertext);
} catch (error) {
throw new Error("invalid base64: " + error.message);
}
const vers = data.subarray(0, 1)[0];
if (vers !== 2)
throw new Error("unknown encryption version " + vers);
const salt = data.subarray(1, 33);
const ciphertext_ = data.subarray(33, -32);
const mac = data.subarray(-32);
const keys = u.getMessageKeys(key, salt);
const calculatedMac = hmac(sha256, keys.auth, ciphertext_);
if (!equalBytes(calculatedMac, mac))
throw new Error("invalid MAC");
const padded = chacha20(keys.encryption, keys.nonce, ciphertext_);
return u.unpad(padded);
}
export {
decrypt,
encrypt,
utils
};
File diff suppressed because one or more lines are too long
+123
View File
@@ -0,0 +1,123 @@
// event.ts
import { schnorr as schnorr2 } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex as bytesToHex2 } from "@noble/hashes/utils";
// keys.ts
import { schnorr } from "@noble/curves/secp256k1";
import { bytesToHex } from "@noble/hashes/utils";
function getPublicKey(privateKey) {
return bytesToHex(schnorr.getPublicKey(privateKey));
}
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
// event.ts
var verifiedSymbol = Symbol("verified");
function finishEvent(t, privateKey) {
const event = t;
event.pubkey = getPublicKey(privateKey);
event.id = getEventHash(event);
event.sig = getSignature(event, privateKey);
event[verifiedSymbol] = true;
return event;
}
function serializeEvent(evt) {
if (!validateEvent(evt))
throw new Error("can't serialize event with wrong or missing properties");
return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]);
}
function getEventHash(event) {
let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)));
return bytesToHex2(eventHash);
}
var isRecord = (obj) => obj instanceof Object;
function validateEvent(event) {
if (!isRecord(event))
return false;
if (typeof event.kind !== "number")
return false;
if (typeof event.content !== "string")
return false;
if (typeof event.created_at !== "number")
return false;
if (typeof event.pubkey !== "string")
return false;
if (!event.pubkey.match(/^[a-f0-9]{64}$/))
return false;
if (!Array.isArray(event.tags))
return false;
for (let i = 0; i < event.tags.length; i++) {
let tag = event.tags[i];
if (!Array.isArray(tag))
return false;
for (let j = 0; j < tag.length; j++) {
if (typeof tag[j] === "object")
return false;
}
}
return true;
}
function getSignature(event, key) {
return bytesToHex2(schnorr2.sign(getEventHash(event), key));
}
// nip04.ts
import { randomBytes } from "@noble/hashes/utils";
import { secp256k1 } from "@noble/curves/secp256k1";
import { base64 } from "@scure/base";
if (typeof crypto !== "undefined" && !crypto.subtle && crypto.webcrypto) {
crypto.subtle = crypto.webcrypto.subtle;
}
async function encrypt(privkey, pubkey, text) {
const key = secp256k1.getSharedSecret(privkey, "02" + pubkey);
const normalizedKey = getNormalizedX(key);
let iv = Uint8Array.from(randomBytes(16));
let plaintext = utf8Encoder.encode(text);
let cryptoKey = await crypto.subtle.importKey("raw", normalizedKey, { name: "AES-CBC" }, false, ["encrypt"]);
let ciphertext = await crypto.subtle.encrypt({ name: "AES-CBC", iv }, cryptoKey, plaintext);
let ctb64 = base64.encode(new Uint8Array(ciphertext));
let ivb64 = base64.encode(new Uint8Array(iv.buffer));
return `${ctb64}?iv=${ivb64}`;
}
function getNormalizedX(key) {
return key.slice(1, 33);
}
// nip47.ts
function parseConnectionString(connectionString) {
const { pathname, searchParams } = new URL(connectionString);
const pubkey = pathname;
const relay = searchParams.get("relay");
const secret = searchParams.get("secret");
if (!pubkey || !relay || !secret) {
throw new Error("invalid connection string");
}
return { pubkey, relay, secret };
}
async function makeNwcRequestEvent({
pubkey,
secret,
invoice
}) {
const content = {
method: "pay_invoice",
params: {
invoice
}
};
const encryptedContent = await encrypt(secret, pubkey, JSON.stringify(content));
const eventTemplate = {
kind: 23194 /* NwcRequest */,
created_at: Math.round(Date.now() / 1e3),
content: encryptedContent,
tags: [["p", pubkey]]
};
return finishEvent(eventTemplate, secret);
}
export {
makeNwcRequestEvent,
parseConnectionString
};
File diff suppressed because one or more lines are too long
+172
View File
@@ -0,0 +1,172 @@
// nip57.ts
import { bech32 } from "@scure/base";
// event.ts
import { schnorr } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex } from "@noble/hashes/utils";
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
// event.ts
var verifiedSymbol = Symbol("verified");
function serializeEvent(evt) {
if (!validateEvent(evt))
throw new Error("can't serialize event with wrong or missing properties");
return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]);
}
function getEventHash(event) {
let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)));
return bytesToHex(eventHash);
}
var isRecord = (obj) => obj instanceof Object;
function validateEvent(event) {
if (!isRecord(event))
return false;
if (typeof event.kind !== "number")
return false;
if (typeof event.content !== "string")
return false;
if (typeof event.created_at !== "number")
return false;
if (typeof event.pubkey !== "string")
return false;
if (!event.pubkey.match(/^[a-f0-9]{64}$/))
return false;
if (!Array.isArray(event.tags))
return false;
for (let i = 0; i < event.tags.length; i++) {
let tag = event.tags[i];
if (!Array.isArray(tag))
return false;
for (let j = 0; j < tag.length; j++) {
if (typeof tag[j] === "object")
return false;
}
}
return true;
}
function verifySignature(event) {
if (typeof event[verifiedSymbol] === "boolean")
return event[verifiedSymbol];
const hash = getEventHash(event);
if (hash !== event.id) {
return event[verifiedSymbol] = false;
}
try {
return event[verifiedSymbol] = schnorr.verify(event.sig, hash, event.pubkey);
} catch (err) {
return event[verifiedSymbol] = false;
}
}
// nip57.ts
var _fetch;
try {
_fetch = fetch;
} catch {
}
function useFetchImplementation(fetchImplementation) {
_fetch = fetchImplementation;
}
async function getZapEndpoint(metadata) {
try {
let lnurl = "";
let { lud06, lud16 } = JSON.parse(metadata.content);
if (lud06) {
let { words } = bech32.decode(lud06, 1e3);
let data = bech32.fromWords(words);
lnurl = utf8Decoder.decode(data);
} else if (lud16) {
let [name, domain] = lud16.split("@");
lnurl = `https://${domain}/.well-known/lnurlp/${name}`;
} else {
return null;
}
let res = await _fetch(lnurl);
let body = await res.json();
if (body.allowsNostr && body.nostrPubkey) {
return body.callback;
}
} catch (err) {
}
return null;
}
function makeZapRequest({
profile,
event,
amount,
relays,
comment = ""
}) {
if (!amount)
throw new Error("amount not given");
if (!profile)
throw new Error("profile not given");
let zr = {
kind: 9734,
created_at: Math.round(Date.now() / 1e3),
content: comment,
tags: [
["p", profile],
["amount", amount.toString()],
["relays", ...relays]
]
};
if (event) {
zr.tags.push(["e", event]);
}
return zr;
}
function validateZapRequest(zapRequestString) {
let zapRequest;
try {
zapRequest = JSON.parse(zapRequestString);
} catch (err) {
return "Invalid zap request JSON.";
}
if (!validateEvent(zapRequest))
return "Zap request is not a valid Nostr event.";
if (!verifySignature(zapRequest))
return "Invalid signature on zap request.";
let p = zapRequest.tags.find(([t, v]) => t === "p" && v);
if (!p)
return "Zap request doesn't have a 'p' tag.";
if (!p[1].match(/^[a-f0-9]{64}$/))
return "Zap request 'p' tag is not valid hex.";
let e = zapRequest.tags.find(([t, v]) => t === "e" && v);
if (e && !e[1].match(/^[a-f0-9]{64}$/))
return "Zap request 'e' tag is not valid hex.";
let relays = zapRequest.tags.find(([t, v]) => t === "relays" && v);
if (!relays)
return "Zap request doesn't have a 'relays' tag.";
return null;
}
function makeZapReceipt({
zapRequest,
preimage,
bolt11,
paidAt
}) {
let zr = JSON.parse(zapRequest);
let tagsFromZapRequest = zr.tags.filter(([t]) => t === "e" || t === "p" || t === "a");
let zap = {
kind: 9735,
created_at: Math.round(paidAt.getTime() / 1e3),
content: "",
tags: [...tagsFromZapRequest, ["bolt11", bolt11], ["description", zapRequest]]
};
if (preimage) {
zap.tags.push(["preimage", preimage]);
}
return zap;
}
export {
getZapEndpoint,
makeZapReceipt,
makeZapRequest,
useFetchImplementation,
validateZapRequest
};
File diff suppressed because one or more lines are too long
+140
View File
@@ -0,0 +1,140 @@
// nip98.ts
import { base64 } from "@scure/base";
// event.ts
import { schnorr } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex } from "@noble/hashes/utils";
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
// event.ts
var verifiedSymbol = Symbol("verified");
function getBlankEvent(kind = 255 /* Blank */) {
return {
kind,
content: "",
tags: [],
created_at: 0
};
}
function serializeEvent(evt) {
if (!validateEvent(evt))
throw new Error("can't serialize event with wrong or missing properties");
return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]);
}
function getEventHash(event) {
let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)));
return bytesToHex(eventHash);
}
var isRecord = (obj) => obj instanceof Object;
function validateEvent(event) {
if (!isRecord(event))
return false;
if (typeof event.kind !== "number")
return false;
if (typeof event.content !== "string")
return false;
if (typeof event.created_at !== "number")
return false;
if (typeof event.pubkey !== "string")
return false;
if (!event.pubkey.match(/^[a-f0-9]{64}$/))
return false;
if (!Array.isArray(event.tags))
return false;
for (let i = 0; i < event.tags.length; i++) {
let tag = event.tags[i];
if (!Array.isArray(tag))
return false;
for (let j = 0; j < tag.length; j++) {
if (typeof tag[j] === "object")
return false;
}
}
return true;
}
function verifySignature(event) {
if (typeof event[verifiedSymbol] === "boolean")
return event[verifiedSymbol];
const hash = getEventHash(event);
if (hash !== event.id) {
return event[verifiedSymbol] = false;
}
try {
return event[verifiedSymbol] = schnorr.verify(event.sig, hash, event.pubkey);
} catch (err) {
return event[verifiedSymbol] = false;
}
}
// nip98.ts
var _authorizationScheme = "Nostr ";
async function getToken(loginUrl, httpMethod, sign, includeAuthorizationScheme = false) {
if (!loginUrl || !httpMethod)
throw new Error("Missing loginUrl or httpMethod");
const event = getBlankEvent(27235 /* HttpAuth */);
event.tags = [
["u", loginUrl],
["method", httpMethod]
];
event.created_at = Math.round(new Date().getTime() / 1e3);
const signedEvent = await sign(event);
const authorizationScheme = includeAuthorizationScheme ? _authorizationScheme : "";
return authorizationScheme + base64.encode(utf8Encoder.encode(JSON.stringify(signedEvent)));
}
async function validateToken(token, url, method) {
const event = await unpackEventFromToken(token).catch((error) => {
throw error;
});
const valid = await validateEvent2(event, url, method).catch((error) => {
throw error;
});
return valid;
}
async function unpackEventFromToken(token) {
if (!token) {
throw new Error("Missing token");
}
token = token.replace(_authorizationScheme, "");
const eventB64 = utf8Decoder.decode(base64.decode(token));
if (!eventB64 || eventB64.length === 0 || !eventB64.startsWith("{")) {
throw new Error("Invalid token");
}
const event = JSON.parse(eventB64);
return event;
}
async function validateEvent2(event, url, method) {
if (!event) {
throw new Error("Invalid nostr event");
}
if (!verifySignature(event)) {
throw new Error("Invalid nostr event, signature invalid");
}
if (event.kind !== 27235 /* HttpAuth */) {
throw new Error("Invalid nostr event, kind invalid");
}
if (!event.created_at) {
throw new Error("Invalid nostr event, created_at invalid");
}
if (Math.round(new Date().getTime() / 1e3) - event.created_at > 60) {
throw new Error("Invalid nostr event, expired");
}
const urlTag = event.tags.find((t) => t[0] === "u");
if (urlTag?.length !== 1 && urlTag?.[1] !== url) {
throw new Error("Invalid nostr event, url tag invalid");
}
const methodTag = event.tags.find((t) => t[0] === "method");
if (methodTag?.length !== 1 && methodTag?.[1].toLowerCase() !== method.toLowerCase()) {
throw new Error("Invalid nostr event, method tag invalid");
}
return true;
}
export {
getToken,
unpackEventFromToken,
validateEvent2 as validateEvent,
validateToken
};
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
{"type":"module"}
+724
View File
@@ -0,0 +1,724 @@
// event.ts
import { schnorr } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex } from "@noble/hashes/utils";
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
function normalizeURL(url) {
let p = new URL(url);
p.pathname = p.pathname.replace(/\/+/g, "/");
if (p.pathname.endsWith("/"))
p.pathname = p.pathname.slice(0, -1);
if (p.port === "80" && p.protocol === "ws:" || p.port === "443" && p.protocol === "wss:")
p.port = "";
p.searchParams.sort();
p.hash = "";
return p.toString();
}
var MessageNode = class {
_value;
_next;
get value() {
return this._value;
}
set value(message) {
this._value = message;
}
get next() {
return this._next;
}
set next(node) {
this._next = node;
}
constructor(message) {
this._value = message;
this._next = null;
}
};
var MessageQueue = class {
_first;
_last;
get first() {
return this._first;
}
set first(messageNode) {
this._first = messageNode;
}
get last() {
return this._last;
}
set last(messageNode) {
this._last = messageNode;
}
_size;
get size() {
return this._size;
}
set size(v) {
this._size = v;
}
constructor() {
this._first = null;
this._last = null;
this._size = 0;
}
enqueue(message) {
const newNode = new MessageNode(message);
if (this._size === 0 || !this._last) {
this._first = newNode;
this._last = newNode;
} else {
this._last.next = newNode;
this._last = newNode;
}
this._size++;
return true;
}
dequeue() {
if (this._size === 0 || !this._first)
return null;
let prev = this._first;
this._first = prev.next;
prev.next = null;
this._size--;
return prev.value;
}
};
// event.ts
var verifiedSymbol = Symbol("verified");
function serializeEvent(evt) {
if (!validateEvent(evt))
throw new Error("can't serialize event with wrong or missing properties");
return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]);
}
function getEventHash(event) {
let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)));
return bytesToHex(eventHash);
}
var isRecord = (obj) => obj instanceof Object;
function validateEvent(event) {
if (!isRecord(event))
return false;
if (typeof event.kind !== "number")
return false;
if (typeof event.content !== "string")
return false;
if (typeof event.created_at !== "number")
return false;
if (typeof event.pubkey !== "string")
return false;
if (!event.pubkey.match(/^[a-f0-9]{64}$/))
return false;
if (!Array.isArray(event.tags))
return false;
for (let i = 0; i < event.tags.length; i++) {
let tag = event.tags[i];
if (!Array.isArray(tag))
return false;
for (let j = 0; j < tag.length; j++) {
if (typeof tag[j] === "object")
return false;
}
}
return true;
}
function verifySignature(event) {
if (typeof event[verifiedSymbol] === "boolean")
return event[verifiedSymbol];
const hash = getEventHash(event);
if (hash !== event.id) {
return event[verifiedSymbol] = false;
}
try {
return event[verifiedSymbol] = schnorr.verify(event.sig, hash, event.pubkey);
} catch (err) {
return event[verifiedSymbol] = false;
}
}
// filter.ts
function matchFilter(filter, event) {
if (filter.ids && filter.ids.indexOf(event.id) === -1) {
if (!filter.ids.some((prefix) => event.id.startsWith(prefix))) {
return false;
}
}
if (filter.kinds && filter.kinds.indexOf(event.kind) === -1)
return false;
if (filter.authors && filter.authors.indexOf(event.pubkey) === -1) {
if (!filter.authors.some((prefix) => event.pubkey.startsWith(prefix))) {
return false;
}
}
for (let f in filter) {
if (f[0] === "#") {
let tagName = f.slice(1);
let values = filter[`#${tagName}`];
if (values && !event.tags.find(([t, v]) => t === f.slice(1) && values.indexOf(v) !== -1))
return false;
}
}
if (filter.since && event.created_at < filter.since)
return false;
if (filter.until && event.created_at > filter.until)
return false;
return true;
}
function matchFilters(filters, event) {
for (let i = 0; i < filters.length; i++) {
if (matchFilter(filters[i], event))
return true;
}
return false;
}
function mergeFilters(...filters) {
let result = {};
for (let i = 0; i < filters.length; i++) {
let filter = filters[i];
Object.entries(filter).forEach(([property, values]) => {
if (property === "kinds" || property === "ids" || property === "authors" || property[0] === "#") {
result[property] = result[property] || [];
for (let v = 0; v < values.length; v++) {
let value = values[v];
if (!result[property].includes(value))
result[property].push(value);
}
}
});
if (filter.limit && (!result.limit || filter.limit > result.limit))
result.limit = filter.limit;
if (filter.until && (!result.until || filter.until > result.until))
result.until = filter.until;
if (filter.since && (!result.since || filter.since < result.since))
result.since = filter.since;
}
return result;
}
// fakejson.ts
function getHex64(json, field) {
let len = field.length + 3;
let idx = json.indexOf(`"${field}":`) + len;
let s = json.slice(idx).indexOf(`"`) + idx + 1;
return json.slice(s, s + 64);
}
function getSubscriptionId(json) {
let idx = json.slice(0, 22).indexOf(`"EVENT"`);
if (idx === -1)
return null;
let pstart = json.slice(idx + 7 + 1).indexOf(`"`);
if (pstart === -1)
return null;
let start = idx + 7 + 1 + pstart;
let pend = json.slice(start + 1, 80).indexOf(`"`);
if (pend === -1)
return null;
let end = start + 1 + pend;
return json.slice(start + 1, end);
}
// relay.ts
var newListeners = () => ({
connect: [],
disconnect: [],
error: [],
notice: [],
auth: []
});
function relayInit(url, options = {}) {
let { listTimeout = 3e3, getTimeout = 3e3, countTimeout = 3e3 } = options;
var ws;
var openSubs = {};
var listeners = newListeners();
var subListeners = {};
var pubListeners = {};
var connectionPromise;
async function connectRelay() {
if (connectionPromise)
return connectionPromise;
connectionPromise = new Promise((resolve, reject) => {
try {
ws = new WebSocket(url);
} catch (err) {
reject(err);
}
ws.onopen = () => {
listeners.connect.forEach((cb) => cb());
resolve();
};
ws.onerror = () => {
connectionPromise = void 0;
listeners.error.forEach((cb) => cb());
reject();
};
ws.onclose = async () => {
connectionPromise = void 0;
listeners.disconnect.forEach((cb) => cb());
};
let incomingMessageQueue = new MessageQueue();
let handleNextInterval;
ws.onmessage = (e) => {
incomingMessageQueue.enqueue(e.data);
if (!handleNextInterval) {
handleNextInterval = setInterval(handleNext, 0);
}
};
function handleNext() {
if (incomingMessageQueue.size === 0) {
clearInterval(handleNextInterval);
handleNextInterval = null;
return;
}
var json = incomingMessageQueue.dequeue();
if (!json)
return;
let subid = getSubscriptionId(json);
if (subid) {
let so = openSubs[subid];
if (so && so.alreadyHaveEvent && so.alreadyHaveEvent(getHex64(json, "id"), url)) {
return;
}
}
try {
let data = JSON.parse(json);
switch (data[0]) {
case "EVENT": {
let id2 = data[1];
let event = data[2];
if (validateEvent(event) && openSubs[id2] && (openSubs[id2].skipVerification || verifySignature(event)) && matchFilters(openSubs[id2].filters, event)) {
openSubs[id2];
(subListeners[id2]?.event || []).forEach((cb) => cb(event));
}
return;
}
case "COUNT":
let id = data[1];
let payload = data[2];
if (openSubs[id]) {
;
(subListeners[id]?.count || []).forEach((cb) => cb(payload));
}
return;
case "EOSE": {
let id2 = data[1];
if (id2 in subListeners) {
subListeners[id2].eose.forEach((cb) => cb());
subListeners[id2].eose = [];
}
return;
}
case "OK": {
let id2 = data[1];
let ok = data[2];
let reason = data[3] || "";
if (id2 in pubListeners) {
let { resolve: resolve2, reject: reject2 } = pubListeners[id2];
if (ok)
resolve2(null);
else
reject2(new Error(reason));
}
return;
}
case "NOTICE":
let notice = data[1];
listeners.notice.forEach((cb) => cb(notice));
return;
case "AUTH": {
let challenge = data[1];
listeners.auth?.forEach((cb) => cb(challenge));
return;
}
}
} catch (err) {
return;
}
}
});
return connectionPromise;
}
function connected() {
return ws?.readyState === 1;
}
async function connect() {
if (connected())
return;
await connectRelay();
}
async function trySend(params) {
let msg = JSON.stringify(params);
if (!connected()) {
await new Promise((resolve) => setTimeout(resolve, 1e3));
if (!connected()) {
return;
}
}
try {
ws.send(msg);
} catch (err) {
console.log(err);
}
}
const sub = (filters, {
verb = "REQ",
skipVerification = false,
alreadyHaveEvent = null,
id = Math.random().toString().slice(2)
} = {}) => {
let subid = id;
openSubs[subid] = {
id: subid,
filters,
skipVerification,
alreadyHaveEvent
};
trySend([verb, subid, ...filters]);
let subscription = {
sub: (newFilters, newOpts = {}) => sub(newFilters || filters, {
skipVerification: newOpts.skipVerification || skipVerification,
alreadyHaveEvent: newOpts.alreadyHaveEvent || alreadyHaveEvent,
id: subid
}),
unsub: () => {
delete openSubs[subid];
delete subListeners[subid];
trySend(["CLOSE", subid]);
},
on: (type, cb) => {
subListeners[subid] = subListeners[subid] || {
event: [],
count: [],
eose: []
};
subListeners[subid][type].push(cb);
},
off: (type, cb) => {
let listeners2 = subListeners[subid];
let idx = listeners2[type].indexOf(cb);
if (idx >= 0)
listeners2[type].splice(idx, 1);
},
get events() {
return eventsGenerator(subscription);
}
};
return subscription;
};
function _publishEvent(event, type) {
return new Promise((resolve, reject) => {
if (!event.id) {
reject(new Error(`event ${event} has no id`));
return;
}
let id = event.id;
trySend([type, event]);
pubListeners[id] = { resolve, reject };
});
}
return {
url,
sub,
on: (type, cb) => {
listeners[type].push(cb);
if (type === "connect" && ws?.readyState === 1) {
;
cb();
}
},
off: (type, cb) => {
let index = listeners[type].indexOf(cb);
if (index !== -1)
listeners[type].splice(index, 1);
},
list: (filters, opts) => new Promise((resolve) => {
let s = sub(filters, opts);
let events = [];
let timeout = setTimeout(() => {
s.unsub();
resolve(events);
}, listTimeout);
s.on("eose", () => {
s.unsub();
clearTimeout(timeout);
resolve(events);
});
s.on("event", (event) => {
events.push(event);
});
}),
get: (filter, opts) => new Promise((resolve) => {
let s = sub([filter], opts);
let timeout = setTimeout(() => {
s.unsub();
resolve(null);
}, getTimeout);
s.on("event", (event) => {
s.unsub();
clearTimeout(timeout);
resolve(event);
});
}),
count: (filters) => new Promise((resolve) => {
let s = sub(filters, { ...sub, verb: "COUNT" });
let timeout = setTimeout(() => {
s.unsub();
resolve(null);
}, countTimeout);
s.on("count", (event) => {
s.unsub();
clearTimeout(timeout);
resolve(event);
});
}),
async publish(event) {
await _publishEvent(event, "EVENT");
},
async auth(event) {
await _publishEvent(event, "AUTH");
},
connect,
close() {
listeners = newListeners();
subListeners = {};
pubListeners = {};
if (ws?.readyState === WebSocket.OPEN) {
ws.close();
}
},
get status() {
return ws?.readyState ?? 3;
}
};
}
async function* eventsGenerator(sub) {
let nextResolve;
const eventQueue = [];
const pushToQueue = (event) => {
if (nextResolve) {
nextResolve(event);
nextResolve = void 0;
} else {
eventQueue.push(event);
}
};
sub.on("event", pushToQueue);
try {
while (true) {
if (eventQueue.length > 0) {
yield eventQueue.shift();
} else {
const event = await new Promise((resolve) => {
nextResolve = resolve;
});
yield event;
}
}
} finally {
sub.off("event", pushToQueue);
}
}
// pool.ts
var SimplePool = class {
_conn;
_seenOn = {};
batchedByKey = {};
eoseSubTimeout;
getTimeout;
seenOnEnabled = true;
batchInterval = 100;
constructor(options = {}) {
this._conn = {};
this.eoseSubTimeout = options.eoseSubTimeout || 3400;
this.getTimeout = options.getTimeout || 3400;
this.seenOnEnabled = options.seenOnEnabled !== false;
this.batchInterval = options.batchInterval || 100;
}
close(relays) {
relays.forEach((url) => {
let relay = this._conn[normalizeURL(url)];
if (relay)
relay.close();
});
}
async ensureRelay(url) {
const nm = normalizeURL(url);
if (!this._conn[nm]) {
this._conn[nm] = relayInit(nm, {
getTimeout: this.getTimeout * 0.9,
listTimeout: this.getTimeout * 0.9
});
}
const relay = this._conn[nm];
await relay.connect();
return relay;
}
sub(relays, filters, opts) {
let _knownIds = /* @__PURE__ */ new Set();
let modifiedOpts = { ...opts || {} };
modifiedOpts.alreadyHaveEvent = (id, url) => {
if (opts?.alreadyHaveEvent?.(id, url)) {
return true;
}
if (this.seenOnEnabled) {
let set = this._seenOn[id] || /* @__PURE__ */ new Set();
set.add(url);
this._seenOn[id] = set;
}
return _knownIds.has(id);
};
let subs = [];
let eventListeners = /* @__PURE__ */ new Set();
let eoseListeners = /* @__PURE__ */ new Set();
let eosesMissing = relays.length;
let eoseSent = false;
let eoseTimeout = setTimeout(
() => {
eoseSent = true;
for (let cb of eoseListeners.values())
cb();
},
opts?.eoseSubTimeout || this.eoseSubTimeout
);
relays.filter((r, i, a) => a.indexOf(r) === i).forEach(async (relay) => {
let r;
try {
r = await this.ensureRelay(relay);
} catch (err) {
handleEose();
return;
}
if (!r)
return;
let s = r.sub(filters, modifiedOpts);
s.on("event", (event) => {
_knownIds.add(event.id);
for (let cb of eventListeners.values())
cb(event);
});
s.on("eose", () => {
if (eoseSent)
return;
handleEose();
});
subs.push(s);
function handleEose() {
eosesMissing--;
if (eosesMissing === 0) {
clearTimeout(eoseTimeout);
for (let cb of eoseListeners.values())
cb();
}
}
});
let greaterSub = {
sub(filters2, opts2) {
subs.forEach((sub) => sub.sub(filters2, opts2));
return greaterSub;
},
unsub() {
subs.forEach((sub) => sub.unsub());
},
on(type, cb) {
if (type === "event") {
eventListeners.add(cb);
} else if (type === "eose") {
eoseListeners.add(cb);
}
},
off(type, cb) {
if (type === "event") {
eventListeners.delete(cb);
} else if (type === "eose")
eoseListeners.delete(cb);
},
get events() {
return eventsGenerator(greaterSub);
}
};
return greaterSub;
}
get(relays, filter, opts) {
return new Promise((resolve) => {
let sub = this.sub(relays, [filter], opts);
let timeout = setTimeout(() => {
sub.unsub();
resolve(null);
}, this.getTimeout);
sub.on("event", (event) => {
resolve(event);
clearTimeout(timeout);
sub.unsub();
});
});
}
list(relays, filters, opts) {
return new Promise((resolve) => {
let events = [];
let sub = this.sub(relays, filters, opts);
sub.on("event", (event) => {
events.push(event);
});
sub.on("eose", () => {
sub.unsub();
resolve(events);
});
});
}
batchedList(batchKey, relays, filters) {
return new Promise((resolve) => {
if (!this.batchedByKey[batchKey]) {
this.batchedByKey[batchKey] = [
{
filters,
relays,
resolve,
events: []
}
];
setTimeout(() => {
Object.keys(this.batchedByKey).forEach(async (batchKey2) => {
const batchedRequests = this.batchedByKey[batchKey2];
const filters2 = [];
const relays2 = [];
batchedRequests.forEach((br) => {
filters2.push(...br.filters);
relays2.push(...br.relays);
});
const sub = this.sub(relays2, [mergeFilters(...filters2)]);
sub.on("event", (event) => {
batchedRequests.forEach((br) => matchFilters(br.filters, event) && br.events.push(event));
});
sub.on("eose", () => {
sub.unsub();
batchedRequests.forEach((br) => br.resolve(br.events));
});
delete this.batchedByKey[batchKey2];
});
}, this.batchInterval);
} else {
this.batchedByKey[batchKey].push({
filters,
relays,
resolve,
events: []
});
}
});
}
publish(relays, event) {
return relays.map(async (relay) => {
let r = await this.ensureRelay(relay);
return r.publish(event);
});
}
seenOn(id) {
return Array.from(this._seenOn[id]?.values?.() || []);
}
};
export {
SimplePool
};
+7
View File
File diff suppressed because one or more lines are too long
+196
View File
@@ -0,0 +1,196 @@
// nip19.ts
import { bytesToHex, concatBytes, hexToBytes } from "@noble/hashes/utils";
import { bech32 } from "@scure/base";
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
// nip19.ts
var Bech32MaxSize = 5e3;
function decode(nip19) {
let { prefix, words } = bech32.decode(nip19, Bech32MaxSize);
let data = new Uint8Array(bech32.fromWords(words));
switch (prefix) {
case "nprofile": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for nprofile");
if (tlv[0][0].length !== 32)
throw new Error("TLV 0 should be 32 bytes");
return {
type: "nprofile",
data: {
pubkey: bytesToHex(tlv[0][0]),
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : []
}
};
}
case "nevent": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for nevent");
if (tlv[0][0].length !== 32)
throw new Error("TLV 0 should be 32 bytes");
if (tlv[2] && tlv[2][0].length !== 32)
throw new Error("TLV 2 should be 32 bytes");
if (tlv[3] && tlv[3][0].length !== 4)
throw new Error("TLV 3 should be 4 bytes");
return {
type: "nevent",
data: {
id: bytesToHex(tlv[0][0]),
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : [],
author: tlv[2]?.[0] ? bytesToHex(tlv[2][0]) : void 0,
kind: tlv[3]?.[0] ? parseInt(bytesToHex(tlv[3][0]), 16) : void 0
}
};
}
case "naddr": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for naddr");
if (!tlv[2]?.[0])
throw new Error("missing TLV 2 for naddr");
if (tlv[2][0].length !== 32)
throw new Error("TLV 2 should be 32 bytes");
if (!tlv[3]?.[0])
throw new Error("missing TLV 3 for naddr");
if (tlv[3][0].length !== 4)
throw new Error("TLV 3 should be 4 bytes");
return {
type: "naddr",
data: {
identifier: utf8Decoder.decode(tlv[0][0]),
pubkey: bytesToHex(tlv[2][0]),
kind: parseInt(bytesToHex(tlv[3][0]), 16),
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : []
}
};
}
case "nrelay": {
let tlv = parseTLV(data);
if (!tlv[0]?.[0])
throw new Error("missing TLV 0 for nrelay");
return {
type: "nrelay",
data: utf8Decoder.decode(tlv[0][0])
};
}
case "nsec":
case "npub":
case "note":
return { type: prefix, data: bytesToHex(data) };
default:
throw new Error(`unknown prefix ${prefix}`);
}
}
function parseTLV(data) {
let result = {};
let rest = data;
while (rest.length > 0) {
let t = rest[0];
let l = rest[1];
if (!l)
throw new Error(`malformed TLV ${t}`);
let v = rest.slice(2, 2 + l);
rest = rest.slice(2 + l);
if (v.length < l)
throw new Error(`not enough data to read on TLV ${t}`);
result[t] = result[t] || [];
result[t].push(v);
}
return result;
}
// references.ts
var mentionRegex = /\bnostr:((note|npub|naddr|nevent|nprofile)1\w+)\b|#\[(\d+)\]/g;
function parseReferences(evt) {
let references = [];
for (let ref of evt.content.matchAll(mentionRegex)) {
if (ref[2]) {
try {
let { type, data } = decode(ref[1]);
switch (type) {
case "npub": {
references.push({
text: ref[0],
profile: { pubkey: data, relays: [] }
});
break;
}
case "nprofile": {
references.push({
text: ref[0],
profile: data
});
break;
}
case "note": {
references.push({
text: ref[0],
event: { id: data, relays: [] }
});
break;
}
case "nevent": {
references.push({
text: ref[0],
event: data
});
break;
}
case "naddr": {
references.push({
text: ref[0],
address: data
});
break;
}
}
} catch (err) {
}
} else if (ref[3]) {
let idx = parseInt(ref[3], 10);
let tag = evt.tags[idx];
if (!tag)
continue;
switch (tag[0]) {
case "p": {
references.push({
text: ref[0],
profile: { pubkey: tag[1], relays: tag[2] ? [tag[2]] : [] }
});
break;
}
case "e": {
references.push({
text: ref[0],
event: { id: tag[1], relays: tag[2] ? [tag[2]] : [] }
});
break;
}
case "a": {
try {
let [kind, pubkey, identifier] = tag[1].split(":");
references.push({
text: ref[0],
address: {
identifier,
pubkey,
kind: parseInt(kind, 10),
relays: tag[2] ? [tag[2]] : []
}
});
} catch (err) {
}
break;
}
}
}
}
return references;
}
export {
parseReferences
};
File diff suppressed because one or more lines are too long
+492
View File
@@ -0,0 +1,492 @@
// event.ts
import { schnorr } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha256";
import { bytesToHex } from "@noble/hashes/utils";
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
var MessageNode = class {
_value;
_next;
get value() {
return this._value;
}
set value(message) {
this._value = message;
}
get next() {
return this._next;
}
set next(node) {
this._next = node;
}
constructor(message) {
this._value = message;
this._next = null;
}
};
var MessageQueue = class {
_first;
_last;
get first() {
return this._first;
}
set first(messageNode) {
this._first = messageNode;
}
get last() {
return this._last;
}
set last(messageNode) {
this._last = messageNode;
}
_size;
get size() {
return this._size;
}
set size(v) {
this._size = v;
}
constructor() {
this._first = null;
this._last = null;
this._size = 0;
}
enqueue(message) {
const newNode = new MessageNode(message);
if (this._size === 0 || !this._last) {
this._first = newNode;
this._last = newNode;
} else {
this._last.next = newNode;
this._last = newNode;
}
this._size++;
return true;
}
dequeue() {
if (this._size === 0 || !this._first)
return null;
let prev = this._first;
this._first = prev.next;
prev.next = null;
this._size--;
return prev.value;
}
};
// event.ts
var verifiedSymbol = Symbol("verified");
function serializeEvent(evt) {
if (!validateEvent(evt))
throw new Error("can't serialize event with wrong or missing properties");
return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]);
}
function getEventHash(event) {
let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)));
return bytesToHex(eventHash);
}
var isRecord = (obj) => obj instanceof Object;
function validateEvent(event) {
if (!isRecord(event))
return false;
if (typeof event.kind !== "number")
return false;
if (typeof event.content !== "string")
return false;
if (typeof event.created_at !== "number")
return false;
if (typeof event.pubkey !== "string")
return false;
if (!event.pubkey.match(/^[a-f0-9]{64}$/))
return false;
if (!Array.isArray(event.tags))
return false;
for (let i = 0; i < event.tags.length; i++) {
let tag = event.tags[i];
if (!Array.isArray(tag))
return false;
for (let j = 0; j < tag.length; j++) {
if (typeof tag[j] === "object")
return false;
}
}
return true;
}
function verifySignature(event) {
if (typeof event[verifiedSymbol] === "boolean")
return event[verifiedSymbol];
const hash = getEventHash(event);
if (hash !== event.id) {
return event[verifiedSymbol] = false;
}
try {
return event[verifiedSymbol] = schnorr.verify(event.sig, hash, event.pubkey);
} catch (err) {
return event[verifiedSymbol] = false;
}
}
// filter.ts
function matchFilter(filter, event) {
if (filter.ids && filter.ids.indexOf(event.id) === -1) {
if (!filter.ids.some((prefix) => event.id.startsWith(prefix))) {
return false;
}
}
if (filter.kinds && filter.kinds.indexOf(event.kind) === -1)
return false;
if (filter.authors && filter.authors.indexOf(event.pubkey) === -1) {
if (!filter.authors.some((prefix) => event.pubkey.startsWith(prefix))) {
return false;
}
}
for (let f in filter) {
if (f[0] === "#") {
let tagName = f.slice(1);
let values = filter[`#${tagName}`];
if (values && !event.tags.find(([t, v]) => t === f.slice(1) && values.indexOf(v) !== -1))
return false;
}
}
if (filter.since && event.created_at < filter.since)
return false;
if (filter.until && event.created_at > filter.until)
return false;
return true;
}
function matchFilters(filters, event) {
for (let i = 0; i < filters.length; i++) {
if (matchFilter(filters[i], event))
return true;
}
return false;
}
// fakejson.ts
function getHex64(json, field) {
let len = field.length + 3;
let idx = json.indexOf(`"${field}":`) + len;
let s = json.slice(idx).indexOf(`"`) + idx + 1;
return json.slice(s, s + 64);
}
function getSubscriptionId(json) {
let idx = json.slice(0, 22).indexOf(`"EVENT"`);
if (idx === -1)
return null;
let pstart = json.slice(idx + 7 + 1).indexOf(`"`);
if (pstart === -1)
return null;
let start = idx + 7 + 1 + pstart;
let pend = json.slice(start + 1, 80).indexOf(`"`);
if (pend === -1)
return null;
let end = start + 1 + pend;
return json.slice(start + 1, end);
}
// relay.ts
var newListeners = () => ({
connect: [],
disconnect: [],
error: [],
notice: [],
auth: []
});
function relayInit(url, options = {}) {
let { listTimeout = 3e3, getTimeout = 3e3, countTimeout = 3e3 } = options;
var ws;
var openSubs = {};
var listeners = newListeners();
var subListeners = {};
var pubListeners = {};
var connectionPromise;
async function connectRelay() {
if (connectionPromise)
return connectionPromise;
connectionPromise = new Promise((resolve, reject) => {
try {
ws = new WebSocket(url);
} catch (err) {
reject(err);
}
ws.onopen = () => {
listeners.connect.forEach((cb) => cb());
resolve();
};
ws.onerror = () => {
connectionPromise = void 0;
listeners.error.forEach((cb) => cb());
reject();
};
ws.onclose = async () => {
connectionPromise = void 0;
listeners.disconnect.forEach((cb) => cb());
};
let incomingMessageQueue = new MessageQueue();
let handleNextInterval;
ws.onmessage = (e) => {
incomingMessageQueue.enqueue(e.data);
if (!handleNextInterval) {
handleNextInterval = setInterval(handleNext, 0);
}
};
function handleNext() {
if (incomingMessageQueue.size === 0) {
clearInterval(handleNextInterval);
handleNextInterval = null;
return;
}
var json = incomingMessageQueue.dequeue();
if (!json)
return;
let subid = getSubscriptionId(json);
if (subid) {
let so = openSubs[subid];
if (so && so.alreadyHaveEvent && so.alreadyHaveEvent(getHex64(json, "id"), url)) {
return;
}
}
try {
let data = JSON.parse(json);
switch (data[0]) {
case "EVENT": {
let id2 = data[1];
let event = data[2];
if (validateEvent(event) && openSubs[id2] && (openSubs[id2].skipVerification || verifySignature(event)) && matchFilters(openSubs[id2].filters, event)) {
openSubs[id2];
(subListeners[id2]?.event || []).forEach((cb) => cb(event));
}
return;
}
case "COUNT":
let id = data[1];
let payload = data[2];
if (openSubs[id]) {
;
(subListeners[id]?.count || []).forEach((cb) => cb(payload));
}
return;
case "EOSE": {
let id2 = data[1];
if (id2 in subListeners) {
subListeners[id2].eose.forEach((cb) => cb());
subListeners[id2].eose = [];
}
return;
}
case "OK": {
let id2 = data[1];
let ok = data[2];
let reason = data[3] || "";
if (id2 in pubListeners) {
let { resolve: resolve2, reject: reject2 } = pubListeners[id2];
if (ok)
resolve2(null);
else
reject2(new Error(reason));
}
return;
}
case "NOTICE":
let notice = data[1];
listeners.notice.forEach((cb) => cb(notice));
return;
case "AUTH": {
let challenge = data[1];
listeners.auth?.forEach((cb) => cb(challenge));
return;
}
}
} catch (err) {
return;
}
}
});
return connectionPromise;
}
function connected() {
return ws?.readyState === 1;
}
async function connect() {
if (connected())
return;
await connectRelay();
}
async function trySend(params) {
let msg = JSON.stringify(params);
if (!connected()) {
await new Promise((resolve) => setTimeout(resolve, 1e3));
if (!connected()) {
return;
}
}
try {
ws.send(msg);
} catch (err) {
console.log(err);
}
}
const sub = (filters, {
verb = "REQ",
skipVerification = false,
alreadyHaveEvent = null,
id = Math.random().toString().slice(2)
} = {}) => {
let subid = id;
openSubs[subid] = {
id: subid,
filters,
skipVerification,
alreadyHaveEvent
};
trySend([verb, subid, ...filters]);
let subscription = {
sub: (newFilters, newOpts = {}) => sub(newFilters || filters, {
skipVerification: newOpts.skipVerification || skipVerification,
alreadyHaveEvent: newOpts.alreadyHaveEvent || alreadyHaveEvent,
id: subid
}),
unsub: () => {
delete openSubs[subid];
delete subListeners[subid];
trySend(["CLOSE", subid]);
},
on: (type, cb) => {
subListeners[subid] = subListeners[subid] || {
event: [],
count: [],
eose: []
};
subListeners[subid][type].push(cb);
},
off: (type, cb) => {
let listeners2 = subListeners[subid];
let idx = listeners2[type].indexOf(cb);
if (idx >= 0)
listeners2[type].splice(idx, 1);
},
get events() {
return eventsGenerator(subscription);
}
};
return subscription;
};
function _publishEvent(event, type) {
return new Promise((resolve, reject) => {
if (!event.id) {
reject(new Error(`event ${event} has no id`));
return;
}
let id = event.id;
trySend([type, event]);
pubListeners[id] = { resolve, reject };
});
}
return {
url,
sub,
on: (type, cb) => {
listeners[type].push(cb);
if (type === "connect" && ws?.readyState === 1) {
;
cb();
}
},
off: (type, cb) => {
let index = listeners[type].indexOf(cb);
if (index !== -1)
listeners[type].splice(index, 1);
},
list: (filters, opts) => new Promise((resolve) => {
let s = sub(filters, opts);
let events = [];
let timeout = setTimeout(() => {
s.unsub();
resolve(events);
}, listTimeout);
s.on("eose", () => {
s.unsub();
clearTimeout(timeout);
resolve(events);
});
s.on("event", (event) => {
events.push(event);
});
}),
get: (filter, opts) => new Promise((resolve) => {
let s = sub([filter], opts);
let timeout = setTimeout(() => {
s.unsub();
resolve(null);
}, getTimeout);
s.on("event", (event) => {
s.unsub();
clearTimeout(timeout);
resolve(event);
});
}),
count: (filters) => new Promise((resolve) => {
let s = sub(filters, { ...sub, verb: "COUNT" });
let timeout = setTimeout(() => {
s.unsub();
resolve(null);
}, countTimeout);
s.on("count", (event) => {
s.unsub();
clearTimeout(timeout);
resolve(event);
});
}),
async publish(event) {
await _publishEvent(event, "EVENT");
},
async auth(event) {
await _publishEvent(event, "AUTH");
},
connect,
close() {
listeners = newListeners();
subListeners = {};
pubListeners = {};
if (ws?.readyState === WebSocket.OPEN) {
ws.close();
}
},
get status() {
return ws?.readyState ?? 3;
}
};
}
async function* eventsGenerator(sub) {
let nextResolve;
const eventQueue = [];
const pushToQueue = (event) => {
if (nextResolve) {
nextResolve(event);
nextResolve = void 0;
} else {
eventQueue.push(event);
}
};
sub.on("event", pushToQueue);
try {
while (true) {
if (eventQueue.length > 0) {
yield eventQueue.shift();
} else {
const event = await new Promise((resolve) => {
nextResolve = resolve;
});
yield event;
}
}
} finally {
sub.off("event", pushToQueue);
}
}
export {
eventsGenerator,
relayInit
};
File diff suppressed because one or more lines are too long
+16
View File
@@ -0,0 +1,16 @@
// test-helpers.ts
function buildEvent(params) {
return {
id: "",
kind: 1,
pubkey: "",
created_at: 0,
content: "",
tags: [],
sig: "",
...params
};
}
export {
buildEvent
};
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../test-helpers.ts"],
"sourcesContent": ["import type { Event } from './event.ts'\n\ntype EventParams<K extends number> = Partial<Event<K>>\n\n/** Build an event for testing purposes. */\nexport function buildEvent<K extends number = 1>(params: EventParams<K>): Event<K> {\n return {\n id: '',\n kind: 1 as K,\n pubkey: '',\n created_at: 0,\n content: '',\n tags: [],\n sig: '',\n ...params,\n }\n}\n"],
"mappings": ";AAKO,SAAS,WAAiC,QAAkC;AACjF,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,MAAM,CAAC;AAAA,IACP,KAAK;AAAA,IACL,GAAG;AAAA,EACL;AACF;",
"names": []
}
+156
View File
@@ -0,0 +1,156 @@
// utils.ts
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
function normalizeURL(url) {
let p = new URL(url);
p.pathname = p.pathname.replace(/\/+/g, "/");
if (p.pathname.endsWith("/"))
p.pathname = p.pathname.slice(0, -1);
if (p.port === "80" && p.protocol === "ws:" || p.port === "443" && p.protocol === "wss:")
p.port = "";
p.searchParams.sort();
p.hash = "";
return p.toString();
}
function insertEventIntoDescendingList(sortedArray, event) {
let start = 0;
let end = sortedArray.length - 1;
let midPoint;
let position = start;
if (end < 0) {
position = 0;
} else if (event.created_at < sortedArray[end].created_at) {
position = end + 1;
} else if (event.created_at >= sortedArray[start].created_at) {
position = start;
} else
while (true) {
if (end <= start + 1) {
position = end;
break;
}
midPoint = Math.floor(start + (end - start) / 2);
if (sortedArray[midPoint].created_at > event.created_at) {
start = midPoint;
} else if (sortedArray[midPoint].created_at < event.created_at) {
end = midPoint;
} else {
position = midPoint;
break;
}
}
if (sortedArray[position]?.id !== event.id) {
return [...sortedArray.slice(0, position), event, ...sortedArray.slice(position)];
}
return sortedArray;
}
function insertEventIntoAscendingList(sortedArray, event) {
let start = 0;
let end = sortedArray.length - 1;
let midPoint;
let position = start;
if (end < 0) {
position = 0;
} else if (event.created_at > sortedArray[end].created_at) {
position = end + 1;
} else if (event.created_at <= sortedArray[start].created_at) {
position = start;
} else
while (true) {
if (end <= start + 1) {
position = end;
break;
}
midPoint = Math.floor(start + (end - start) / 2);
if (sortedArray[midPoint].created_at < event.created_at) {
start = midPoint;
} else if (sortedArray[midPoint].created_at > event.created_at) {
end = midPoint;
} else {
position = midPoint;
break;
}
}
if (sortedArray[position]?.id !== event.id) {
return [...sortedArray.slice(0, position), event, ...sortedArray.slice(position)];
}
return sortedArray;
}
var MessageNode = class {
_value;
_next;
get value() {
return this._value;
}
set value(message) {
this._value = message;
}
get next() {
return this._next;
}
set next(node) {
this._next = node;
}
constructor(message) {
this._value = message;
this._next = null;
}
};
var MessageQueue = class {
_first;
_last;
get first() {
return this._first;
}
set first(messageNode) {
this._first = messageNode;
}
get last() {
return this._last;
}
set last(messageNode) {
this._last = messageNode;
}
_size;
get size() {
return this._size;
}
set size(v) {
this._size = v;
}
constructor() {
this._first = null;
this._last = null;
this._size = 0;
}
enqueue(message) {
const newNode = new MessageNode(message);
if (this._size === 0 || !this._last) {
this._first = newNode;
this._last = newNode;
} else {
this._last.next = newNode;
this._last = newNode;
}
this._size++;
return true;
}
dequeue() {
if (this._size === 0 || !this._first)
return null;
let prev = this._first;
this._first = prev.next;
prev.next = null;
this._size--;
return prev.value;
}
};
export {
MessageNode,
MessageQueue,
insertEventIntoAscendingList,
insertEventIntoDescendingList,
normalizeURL,
utf8Decoder,
utf8Encoder
};
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../utils.ts"],
"sourcesContent": ["import type { Event } from './event.ts'\n\nexport const utf8Decoder = new TextDecoder('utf-8')\nexport const utf8Encoder = new TextEncoder()\n\nexport function normalizeURL(url: string): string {\n let p = new URL(url)\n p.pathname = p.pathname.replace(/\\/+/g, '/')\n if (p.pathname.endsWith('/')) p.pathname = p.pathname.slice(0, -1)\n if ((p.port === '80' && p.protocol === 'ws:') || (p.port === '443' && p.protocol === 'wss:')) p.port = ''\n p.searchParams.sort()\n p.hash = ''\n return p.toString()\n}\n\n//\n// fast insert-into-sorted-array functions adapted from https://github.com/terrymorse58/fast-sorted-array\n//\nexport function insertEventIntoDescendingList(sortedArray: Event<number>[], event: Event<number>) {\n let start = 0\n let end = sortedArray.length - 1\n let midPoint\n let position = start\n\n if (end < 0) {\n position = 0\n } else if (event.created_at < sortedArray[end].created_at) {\n position = end + 1\n } else if (event.created_at >= sortedArray[start].created_at) {\n position = start\n } else\n while (true) {\n if (end <= start + 1) {\n position = end\n break\n }\n midPoint = Math.floor(start + (end - start) / 2)\n if (sortedArray[midPoint].created_at > event.created_at) {\n start = midPoint\n } else if (sortedArray[midPoint].created_at < event.created_at) {\n end = midPoint\n } else {\n // aMidPoint === num\n position = midPoint\n break\n }\n }\n\n // insert when num is NOT already in (no duplicates)\n if (sortedArray[position]?.id !== event.id) {\n return [...sortedArray.slice(0, position), event, ...sortedArray.slice(position)]\n }\n\n return sortedArray\n}\n\nexport function insertEventIntoAscendingList(sortedArray: Event<number>[], event: Event<number>) {\n let start = 0\n let end = sortedArray.length - 1\n let midPoint\n let position = start\n\n if (end < 0) {\n position = 0\n } else if (event.created_at > sortedArray[end].created_at) {\n position = end + 1\n } else if (event.created_at <= sortedArray[start].created_at) {\n position = start\n } else\n while (true) {\n if (end <= start + 1) {\n position = end\n break\n }\n midPoint = Math.floor(start + (end - start) / 2)\n if (sortedArray[midPoint].created_at < event.created_at) {\n start = midPoint\n } else if (sortedArray[midPoint].created_at > event.created_at) {\n end = midPoint\n } else {\n // aMidPoint === num\n position = midPoint\n break\n }\n }\n\n // insert when num is NOT already in (no duplicates)\n if (sortedArray[position]?.id !== event.id) {\n return [...sortedArray.slice(0, position), event, ...sortedArray.slice(position)]\n }\n\n return sortedArray\n}\n\nexport class MessageNode {\n private _value: string\n private _next: MessageNode | null\n\n public get value(): string {\n return this._value\n }\n public set value(message: string) {\n this._value = message\n }\n public get next(): MessageNode | null {\n return this._next\n }\n public set next(node: MessageNode | null) {\n this._next = node\n }\n\n constructor(message: string) {\n this._value = message\n this._next = null\n }\n}\n\nexport class MessageQueue {\n private _first: MessageNode | null\n private _last: MessageNode | null\n\n public get first(): MessageNode | null {\n return this._first\n }\n public set first(messageNode: MessageNode | null) {\n this._first = messageNode\n }\n public get last(): MessageNode | null {\n return this._last\n }\n public set last(messageNode: MessageNode | null) {\n this._last = messageNode\n }\n private _size: number\n public get size(): number {\n return this._size\n }\n public set size(v: number) {\n this._size = v\n }\n\n constructor() {\n this._first = null\n this._last = null\n this._size = 0\n }\n enqueue(message: string): boolean {\n const newNode = new MessageNode(message)\n if (this._size === 0 || !this._last) {\n this._first = newNode\n this._last = newNode\n } else {\n this._last.next = newNode\n this._last = newNode\n }\n this._size++\n return true\n }\n dequeue(): string | null {\n if (this._size === 0 || !this._first) return null\n\n let prev = this._first\n this._first = prev.next\n prev.next = null\n\n this._size--\n return prev.value\n }\n}\n"],
"mappings": ";AAEO,IAAM,cAAc,IAAI,YAAY,OAAO;AAC3C,IAAM,cAAc,IAAI,YAAY;AAEpC,SAAS,aAAa,KAAqB;AAChD,MAAI,IAAI,IAAI,IAAI,GAAG;AACnB,IAAE,WAAW,EAAE,SAAS,QAAQ,QAAQ,GAAG;AAC3C,MAAI,EAAE,SAAS,SAAS,GAAG;AAAG,MAAE,WAAW,EAAE,SAAS,MAAM,GAAG,EAAE;AACjE,MAAK,EAAE,SAAS,QAAQ,EAAE,aAAa,SAAW,EAAE,SAAS,SAAS,EAAE,aAAa;AAAS,MAAE,OAAO;AACvG,IAAE,aAAa,KAAK;AACpB,IAAE,OAAO;AACT,SAAO,EAAE,SAAS;AACpB;AAKO,SAAS,8BAA8B,aAA8B,OAAsB;AAChG,MAAI,QAAQ;AACZ,MAAI,MAAM,YAAY,SAAS;AAC/B,MAAI;AACJ,MAAI,WAAW;AAEf,MAAI,MAAM,GAAG;AACX,eAAW;AAAA,EACb,WAAW,MAAM,aAAa,YAAY,KAAK,YAAY;AACzD,eAAW,MAAM;AAAA,EACnB,WAAW,MAAM,cAAc,YAAY,OAAO,YAAY;AAC5D,eAAW;AAAA,EACb;AACE,WAAO,MAAM;AACX,UAAI,OAAO,QAAQ,GAAG;AACpB,mBAAW;AACX;AAAA,MACF;AACA,iBAAW,KAAK,MAAM,SAAS,MAAM,SAAS,CAAC;AAC/C,UAAI,YAAY,UAAU,aAAa,MAAM,YAAY;AACvD,gBAAQ;AAAA,MACV,WAAW,YAAY,UAAU,aAAa,MAAM,YAAY;AAC9D,cAAM;AAAA,MACR,OAAO;AAEL,mBAAW;AACX;AAAA,MACF;AAAA,IACF;AAGF,MAAI,YAAY,WAAW,OAAO,MAAM,IAAI;AAC1C,WAAO,CAAC,GAAG,YAAY,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,YAAY,MAAM,QAAQ,CAAC;AAAA,EAClF;AAEA,SAAO;AACT;AAEO,SAAS,6BAA6B,aAA8B,OAAsB;AAC/F,MAAI,QAAQ;AACZ,MAAI,MAAM,YAAY,SAAS;AAC/B,MAAI;AACJ,MAAI,WAAW;AAEf,MAAI,MAAM,GAAG;AACX,eAAW;AAAA,EACb,WAAW,MAAM,aAAa,YAAY,KAAK,YAAY;AACzD,eAAW,MAAM;AAAA,EACnB,WAAW,MAAM,cAAc,YAAY,OAAO,YAAY;AAC5D,eAAW;AAAA,EACb;AACE,WAAO,MAAM;AACX,UAAI,OAAO,QAAQ,GAAG;AACpB,mBAAW;AACX;AAAA,MACF;AACA,iBAAW,KAAK,MAAM,SAAS,MAAM,SAAS,CAAC;AAC/C,UAAI,YAAY,UAAU,aAAa,MAAM,YAAY;AACvD,gBAAQ;AAAA,MACV,WAAW,YAAY,UAAU,aAAa,MAAM,YAAY;AAC9D,cAAM;AAAA,MACR,OAAO;AAEL,mBAAW;AACX;AAAA,MACF;AAAA,IACF;AAGF,MAAI,YAAY,WAAW,OAAO,MAAM,IAAI;AAC1C,WAAO,CAAC,GAAG,YAAY,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,YAAY,MAAM,QAAQ,CAAC;AAAA,EAClF;AAEA,SAAO;AACT;AAEO,IAAM,cAAN,MAAkB;AAAA,EACf;AAAA,EACA;AAAA,EAER,IAAW,QAAgB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA,EACA,IAAW,MAAM,SAAiB;AAChC,SAAK,SAAS;AAAA,EAChB;AAAA,EACA,IAAW,OAA2B;AACpC,WAAO,KAAK;AAAA,EACd;AAAA,EACA,IAAW,KAAK,MAA0B;AACxC,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,YAAY,SAAiB;AAC3B,SAAK,SAAS;AACd,SAAK,QAAQ;AAAA,EACf;AACF;AAEO,IAAM,eAAN,MAAmB;AAAA,EAChB;AAAA,EACA;AAAA,EAER,IAAW,QAA4B;AACrC,WAAO,KAAK;AAAA,EACd;AAAA,EACA,IAAW,MAAM,aAAiC;AAChD,SAAK,SAAS;AAAA,EAChB;AAAA,EACA,IAAW,OAA2B;AACpC,WAAO,KAAK;AAAA,EACd;AAAA,EACA,IAAW,KAAK,aAAiC;AAC/C,SAAK,QAAQ;AAAA,EACf;AAAA,EACQ;AAAA,EACR,IAAW,OAAe;AACxB,WAAO,KAAK;AAAA,EACd;AAAA,EACA,IAAW,KAAK,GAAW;AACzB,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,cAAc;AACZ,SAAK,SAAS;AACd,SAAK,QAAQ;AACb,SAAK,QAAQ;AAAA,EACf;AAAA,EACA,QAAQ,SAA0B;AAChC,UAAM,UAAU,IAAI,YAAY,OAAO;AACvC,QAAI,KAAK,UAAU,KAAK,CAAC,KAAK,OAAO;AACnC,WAAK,SAAS;AACd,WAAK,QAAQ;AAAA,IACf,OAAO;AACL,WAAK,MAAM,OAAO;AAClB,WAAK,QAAQ;AAAA,IACf;AACA,SAAK;AACL,WAAO;AAAA,EACT;AAAA,EACA,UAAyB;AACvB,QAAI,KAAK,UAAU,KAAK,CAAC,KAAK;AAAQ,aAAO;AAE7C,QAAI,OAAO,KAAK;AAChB,SAAK,SAAS,KAAK;AACnB,SAAK,OAAO;AAEZ,SAAK;AACL,WAAO,KAAK;AAAA,EACd;AACF;",
"names": []
}