Files
2025-09-29 07:21:46 -04:00

496 lines
15 KiB
JavaScript

"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// nip29.ts
var nip29_exports = {};
__export(nip29_exports, {
GroupAdminPermission: () => GroupAdminPermission,
encodeGroupReference: () => encodeGroupReference,
fetchGroupAdminsEvent: () => fetchGroupAdminsEvent,
fetchGroupMembersEvent: () => fetchGroupMembersEvent,
fetchGroupMetadataEvent: () => fetchGroupMetadataEvent,
fetchRelayInformationByGroupReference: () => fetchRelayInformationByGroupReference,
generateGroupAdminsEventTemplate: () => generateGroupAdminsEventTemplate,
generateGroupMembersEventTemplate: () => generateGroupMembersEventTemplate,
generateGroupMetadataEventTemplate: () => generateGroupMetadataEventTemplate,
getNormalizedRelayURLByGroupReference: () => getNormalizedRelayURLByGroupReference,
loadGroup: () => loadGroup,
loadGroupFromCode: () => loadGroupFromCode,
parseGroupAdminsEvent: () => parseGroupAdminsEvent,
parseGroupCode: () => parseGroupCode,
parseGroupMembersEvent: () => parseGroupMembersEvent,
parseGroupMetadataEvent: () => parseGroupMetadataEvent,
subscribeRelayGroupsMetadataEvents: () => subscribeRelayGroupsMetadataEvents,
validateGroupAdminsEvent: () => validateGroupAdminsEvent,
validateGroupMembersEvent: () => validateGroupMembersEvent,
validateGroupMetadataEvent: () => validateGroupMetadataEvent
});
module.exports = __toCommonJS(nip29_exports);
// nip11.ts
var _fetch;
try {
_fetch = fetch;
} catch {
}
async function fetchRelayInformation(url) {
return await (await fetch(url.replace("ws://", "http://").replace("wss://", "https://"), {
headers: { Accept: "application/nostr+json" }
})).json();
}
// nip19.ts
var import_utils2 = require("@noble/hashes/utils");
var import_base = require("@scure/base");
// utils.ts
var import_utils = require("@noble/hashes/utils");
var utf8Decoder = new TextDecoder("utf-8");
var utf8Encoder = new TextEncoder();
function normalizeURL(url) {
try {
if (url.indexOf("://") === -1)
url = "wss://" + 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();
} catch (e) {
throw new Error(`Invalid URL: ${url}`);
}
}
// nip19.ts
var NostrTypeGuard = {
isNProfile: (value) => /^nprofile1[a-z\d]+$/.test(value || ""),
isNEvent: (value) => /^nevent1[a-z\d]+$/.test(value || ""),
isNAddr: (value) => /^naddr1[a-z\d]+$/.test(value || ""),
isNSec: (value) => /^nsec1[a-z\d]{58}$/.test(value || ""),
isNPub: (value) => /^npub1[a-z\d]{58}$/.test(value || ""),
isNote: (value) => /^note1[a-z\d]+$/.test(value || ""),
isNcryptsec: (value) => /^ncryptsec1[a-z\d]+$/.test(value || "")
};
var Bech32MaxSize = 5e3;
function decode(code) {
let { prefix, words } = import_base.bech32.decode(code, Bech32MaxSize);
let data = new Uint8Array(import_base.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: (0, import_utils2.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: (0, import_utils2.bytesToHex)(tlv[0][0]),
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : [],
author: tlv[2]?.[0] ? (0, import_utils2.bytesToHex)(tlv[2][0]) : void 0,
kind: tlv[3]?.[0] ? parseInt((0, import_utils2.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: (0, import_utils2.bytesToHex)(tlv[2][0]),
kind: parseInt((0, import_utils2.bytesToHex)(tlv[3][0]), 16),
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : []
}
};
}
case "nsec":
return { type: prefix, data };
case "npub":
case "note":
return { type: prefix, data: (0, import_utils2.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];
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;
}
// nip29.ts
var GroupAdminPermission = /* @__PURE__ */ ((GroupAdminPermission2) => {
GroupAdminPermission2["AddUser"] = "add-user";
GroupAdminPermission2["EditMetadata"] = "edit-metadata";
GroupAdminPermission2["DeleteEvent"] = "delete-event";
GroupAdminPermission2["RemoveUser"] = "remove-user";
GroupAdminPermission2["AddPermission"] = "add-permission";
GroupAdminPermission2["RemovePermission"] = "remove-permission";
GroupAdminPermission2["EditGroupStatus"] = "edit-group-status";
GroupAdminPermission2["PutUser"] = "put-user";
GroupAdminPermission2["CreateGroup"] = "create-group";
GroupAdminPermission2["DeleteGroup"] = "delete-group";
GroupAdminPermission2["CreateInvite"] = "create-invite";
return GroupAdminPermission2;
})(GroupAdminPermission || {});
function generateGroupMetadataEventTemplate(group) {
const tags = [["d", group.metadata.id]];
group.metadata.name && tags.push(["name", group.metadata.name]);
group.metadata.picture && tags.push(["picture", group.metadata.picture]);
group.metadata.about && tags.push(["about", group.metadata.about]);
group.metadata.isPublic && tags.push(["public"]);
group.metadata.isOpen && tags.push(["open"]);
return {
content: "",
created_at: Math.floor(Date.now() / 1e3),
kind: 39e3,
tags
};
}
function validateGroupMetadataEvent(event) {
if (event.kind !== 39e3)
return false;
if (!event.pubkey)
return false;
const requiredTags = ["d"];
for (const tag of requiredTags) {
if (!event.tags.find(([t]) => t == tag))
return false;
}
return true;
}
function generateGroupAdminsEventTemplate(group, admins) {
const tags = [["d", group.metadata.id]];
for (const admin of admins) {
tags.push(["p", admin.pubkey, admin.label || "", ...admin.permissions]);
}
return {
content: "",
created_at: Math.floor(Date.now() / 1e3),
kind: 39001,
tags
};
}
function validateGroupAdminsEvent(event) {
if (event.kind !== 39001)
return false;
const requiredTags = ["d"];
for (const tag of requiredTags) {
if (!event.tags.find(([t]) => t == tag))
return false;
}
for (const [tag, _value, _label, ...permissions] of event.tags) {
if (tag !== "p")
continue;
for (let i = 0; i < permissions.length; i += 1) {
if (typeof permissions[i] !== "string")
return false;
if (!Object.values(GroupAdminPermission).includes(permissions[i]))
return false;
}
}
return true;
}
function generateGroupMembersEventTemplate(group, members) {
const tags = [["d", group.metadata.id]];
for (const member of members) {
tags.push(["p", member.pubkey, member.label || ""]);
}
return {
content: "",
created_at: Math.floor(Date.now() / 1e3),
kind: 39002,
tags
};
}
function validateGroupMembersEvent(event) {
if (event.kind !== 39002)
return false;
const requiredTags = ["d"];
for (const tag of requiredTags) {
if (!event.tags.find(([t]) => t == tag))
return false;
}
return true;
}
function getNormalizedRelayURLByGroupReference(groupReference) {
return normalizeURL(groupReference.host);
}
async function fetchRelayInformationByGroupReference(groupReference) {
const normalizedRelayURL = getNormalizedRelayURLByGroupReference(groupReference);
return fetchRelayInformation(normalizedRelayURL);
}
async function fetchGroupMetadataEvent({
pool,
groupReference,
relayInformation,
normalizedRelayURL
}) {
if (!normalizedRelayURL) {
normalizedRelayURL = getNormalizedRelayURLByGroupReference(groupReference);
}
if (!relayInformation) {
relayInformation = await fetchRelayInformation(normalizedRelayURL);
}
const groupMetadataEvent = await pool.get([normalizedRelayURL], {
kinds: [39e3],
authors: [relayInformation.pubkey],
"#d": [groupReference.id]
});
if (!groupMetadataEvent)
throw new Error(`group '${groupReference.id}' not found on ${normalizedRelayURL}`);
return groupMetadataEvent;
}
function parseGroupMetadataEvent(event) {
if (!validateGroupMetadataEvent(event))
throw new Error("invalid group metadata event");
const metadata = {
id: "",
pubkey: event.pubkey
};
for (const [tag, value] of event.tags) {
switch (tag) {
case "d":
metadata.id = value;
break;
case "name":
metadata.name = value;
break;
case "picture":
metadata.picture = value;
break;
case "about":
metadata.about = value;
break;
case "public":
metadata.isPublic = true;
break;
case "open":
metadata.isOpen = true;
break;
}
}
return metadata;
}
async function fetchGroupAdminsEvent({
pool,
groupReference,
relayInformation,
normalizedRelayURL
}) {
if (!normalizedRelayURL) {
normalizedRelayURL = getNormalizedRelayURLByGroupReference(groupReference);
}
if (!relayInformation) {
relayInformation = await fetchRelayInformation(normalizedRelayURL);
}
const groupAdminsEvent = await pool.get([normalizedRelayURL], {
kinds: [39001],
authors: [relayInformation.pubkey],
"#d": [groupReference.id]
});
if (!groupAdminsEvent)
throw new Error(`admins for group '${groupReference.id}' not found on ${normalizedRelayURL}`);
return groupAdminsEvent;
}
function parseGroupAdminsEvent(event) {
if (!validateGroupAdminsEvent(event))
throw new Error("invalid group admins event");
const admins = [];
for (const [tag, value, label, ...permissions] of event.tags) {
if (tag !== "p")
continue;
admins.push({
pubkey: value,
label,
permissions
});
}
return admins;
}
async function fetchGroupMembersEvent({
pool,
groupReference,
relayInformation,
normalizedRelayURL
}) {
if (!normalizedRelayURL) {
normalizedRelayURL = getNormalizedRelayURLByGroupReference(groupReference);
}
if (!relayInformation) {
relayInformation = await fetchRelayInformation(normalizedRelayURL);
}
const groupMembersEvent = await pool.get([normalizedRelayURL], {
kinds: [39002],
authors: [relayInformation.pubkey],
"#d": [groupReference.id]
});
if (!groupMembersEvent)
throw new Error(`members for group '${groupReference.id}' not found on ${normalizedRelayURL}`);
return groupMembersEvent;
}
function parseGroupMembersEvent(event) {
if (!validateGroupMembersEvent(event))
throw new Error("invalid group members event");
const members = [];
for (const [tag, value, label] of event.tags) {
if (tag !== "p")
continue;
members.push({
pubkey: value,
label
});
}
return members;
}
async function loadGroup({
pool,
groupReference,
normalizedRelayURL,
relayInformation
}) {
if (!normalizedRelayURL) {
normalizedRelayURL = getNormalizedRelayURLByGroupReference(groupReference);
}
if (!relayInformation) {
relayInformation = await fetchRelayInformation(normalizedRelayURL);
}
const metadataEvent = await fetchGroupMetadataEvent({ pool, groupReference, normalizedRelayURL, relayInformation });
const metadata = parseGroupMetadataEvent(metadataEvent);
const adminsEvent = await fetchGroupAdminsEvent({ pool, groupReference, normalizedRelayURL, relayInformation });
const admins = parseGroupAdminsEvent(adminsEvent);
const membersEvent = await fetchGroupMembersEvent({ pool, groupReference, normalizedRelayURL, relayInformation });
const members = parseGroupMembersEvent(membersEvent);
const group = {
relay: normalizedRelayURL,
metadata,
admins,
members,
reference: groupReference
};
return group;
}
async function loadGroupFromCode(pool, code) {
const groupReference = parseGroupCode(code);
if (!groupReference)
throw new Error("invalid group code");
return loadGroup({ pool, groupReference });
}
function parseGroupCode(code) {
if (NostrTypeGuard.isNAddr(code)) {
try {
let { data } = decode(code);
let { relays, identifier } = data;
if (!relays || relays.length === 0)
return null;
let host = relays[0];
if (host.startsWith("wss://")) {
host = host.slice(6);
}
return { host, id: identifier };
} catch (err) {
return null;
}
} else if (code.split("'").length === 2) {
let spl = code.split("'");
return { host: spl[0], id: spl[1] };
}
return null;
}
function encodeGroupReference(gr) {
const { host, id } = gr;
const normalizedHost = host.replace(/^(https?:\/\/|wss?:\/\/)/, "");
return `${normalizedHost}'${id}`;
}
function subscribeRelayGroupsMetadataEvents({
pool,
relayURL,
onError,
onEvent,
onConnect
}) {
let sub;
const normalizedRelayURL = normalizeURL(relayURL);
fetchRelayInformation(normalizedRelayURL).then(async (info) => {
const abstractedRelay = await pool.ensureRelay(normalizedRelayURL);
onConnect?.();
sub = abstractedRelay.prepareSubscription(
[
{
kinds: [39e3],
limit: 50,
authors: [info.pubkey]
}
],
{
onevent(event) {
onEvent(event);
}
}
);
}).catch((err) => {
sub.close();
onError(err);
});
return () => sub.close();
}