"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); // nip77.ts var nip77_exports = {}; __export(nip77_exports, { Negentropy: () => Negentropy, NegentropyStorageVector: () => NegentropyStorageVector, NegentropySync: () => NegentropySync }); module.exports = __toCommonJS(nip77_exports); var import_utils = require("@noble/ciphers/utils"); var import_sha256 = require("@noble/hashes/sha256"); var PROTOCOL_VERSION = 97; var ID_SIZE = 32; var FINGERPRINT_SIZE = 16; var Mode = { Skip: 0, Fingerprint: 1, IdList: 2 }; var WrappedBuffer = class { _raw; length; constructor(buffer) { if (typeof buffer === "number") { this._raw = new Uint8Array(buffer); this.length = 0; } else if (buffer instanceof Uint8Array) { this._raw = new Uint8Array(buffer); this.length = buffer.length; } else { this._raw = new Uint8Array(512); this.length = 0; } } unwrap() { return this._raw.subarray(0, this.length); } get capacity() { return this._raw.byteLength; } extend(buf) { if (buf instanceof WrappedBuffer) buf = buf.unwrap(); if (typeof buf.length !== "number") throw Error("bad length"); const targetSize = buf.length + this.length; if (this.capacity < targetSize) { const oldRaw = this._raw; const newCapacity = Math.max(this.capacity * 2, targetSize); this._raw = new Uint8Array(newCapacity); this._raw.set(oldRaw); } this._raw.set(buf, this.length); this.length += buf.length; } shift() { const first = this._raw[0]; this._raw = this._raw.subarray(1); this.length--; return first; } shiftN(n = 1) { const firstSubarray = this._raw.subarray(0, n); this._raw = this._raw.subarray(n); this.length -= n; return firstSubarray; } }; function decodeVarInt(buf) { let res = 0; while (1) { if (buf.length === 0) throw Error("parse ends prematurely"); let byte = buf.shift(); res = res << 7 | byte & 127; if ((byte & 128) === 0) break; } return res; } function encodeVarInt(n) { if (n === 0) return new WrappedBuffer(new Uint8Array([0])); let o = []; while (n !== 0) { o.push(n & 127); n >>>= 7; } o.reverse(); for (let i = 0; i < o.length - 1; i++) o[i] |= 128; return new WrappedBuffer(new Uint8Array(o)); } function getByte(buf) { return getBytes(buf, 1)[0]; } function getBytes(buf, n) { if (buf.length < n) throw Error("parse ends prematurely"); return buf.shiftN(n); } var Accumulator = class { buf; constructor() { this.setToZero(); } setToZero() { this.buf = new Uint8Array(ID_SIZE); } add(otherBuf) { let currCarry = 0, nextCarry = 0; let p = new DataView(this.buf.buffer); let po = new DataView(otherBuf.buffer); for (let i = 0; i < 8; i++) { let offset = i * 4; let orig = p.getUint32(offset, true); let otherV = po.getUint32(offset, true); let next = orig; next += currCarry; next += otherV; if (next > 4294967295) nextCarry = 1; p.setUint32(offset, next & 4294967295, true); currCarry = nextCarry; nextCarry = 0; } } negate() { let p = new DataView(this.buf.buffer); for (let i = 0; i < 8; i++) { let offset = i * 4; p.setUint32(offset, ~p.getUint32(offset, true)); } let one = new Uint8Array(ID_SIZE); one[0] = 1; this.add(one); } getFingerprint(n) { let input = new WrappedBuffer(); input.extend(this.buf); input.extend(encodeVarInt(n)); let hash = (0, import_sha256.sha256)(input.unwrap()); return hash.subarray(0, FINGERPRINT_SIZE); } }; var NegentropyStorageVector = class { items; sealed; constructor() { this.items = []; this.sealed = false; } insert(timestamp, id) { if (this.sealed) throw Error("already sealed"); const idb = (0, import_utils.hexToBytes)(id); if (idb.byteLength !== ID_SIZE) throw Error("bad id size for added item"); this.items.push({ timestamp, id: idb }); } seal() { if (this.sealed) throw Error("already sealed"); this.sealed = true; this.items.sort(itemCompare); for (let i = 1; i < this.items.length; i++) { if (itemCompare(this.items[i - 1], this.items[i]) === 0) throw Error("duplicate item inserted"); } } unseal() { this.sealed = false; } size() { this._checkSealed(); return this.items.length; } getItem(i) { this._checkSealed(); if (i >= this.items.length) throw Error("out of range"); return this.items[i]; } iterate(begin, end, cb) { this._checkSealed(); this._checkBounds(begin, end); for (let i = begin; i < end; ++i) { if (!cb(this.items[i], i)) break; } } findLowerBound(begin, end, bound) { this._checkSealed(); this._checkBounds(begin, end); return this._binarySearch(this.items, begin, end, (a) => itemCompare(a, bound) < 0); } fingerprint(begin, end) { let out = new Accumulator(); out.setToZero(); this.iterate(begin, end, (item) => { out.add(item.id); return true; }); return out.getFingerprint(end - begin); } _checkSealed() { if (!this.sealed) throw Error("not sealed"); } _checkBounds(begin, end) { if (begin > end || end > this.items.length) throw Error("bad range"); } _binarySearch(arr, first, last, cmp) { let count = last - first; while (count > 0) { let it = first; let step = Math.floor(count / 2); it += step; if (cmp(arr[it])) { first = ++it; count -= step + 1; } else { count = step; } } return first; } }; var Negentropy = class { storage; frameSizeLimit; lastTimestampIn; lastTimestampOut; constructor(storage, frameSizeLimit = 6e4) { if (frameSizeLimit < 4096) throw Error("frameSizeLimit too small"); this.storage = storage; this.frameSizeLimit = frameSizeLimit; this.lastTimestampIn = 0; this.lastTimestampOut = 0; } _bound(timestamp, id) { return { timestamp, id: id || new Uint8Array(0) }; } initiate() { let output = new WrappedBuffer(); output.extend(new Uint8Array([PROTOCOL_VERSION])); this.splitRange(0, this.storage.size(), this._bound(Number.MAX_VALUE), output); return (0, import_utils.bytesToHex)(output.unwrap()); } reconcile(queryMsg, onhave, onneed) { const query = new WrappedBuffer((0, import_utils.hexToBytes)(queryMsg)); this.lastTimestampIn = this.lastTimestampOut = 0; let fullOutput = new WrappedBuffer(); fullOutput.extend(new Uint8Array([PROTOCOL_VERSION])); let protocolVersion = getByte(query); if (protocolVersion < 96 || protocolVersion > 111) throw Error("invalid negentropy protocol version byte"); if (protocolVersion !== PROTOCOL_VERSION) { throw Error("unsupported negentropy protocol version requested: " + (protocolVersion - 96)); } let storageSize = this.storage.size(); let prevBound = this._bound(0); let prevIndex = 0; let skip = false; while (query.length !== 0) { let o = new WrappedBuffer(); let doSkip = () => { if (skip) { skip = false; o.extend(this.encodeBound(prevBound)); o.extend(encodeVarInt(Mode.Skip)); } }; let currBound = this.decodeBound(query); let mode = decodeVarInt(query); let lower = prevIndex; let upper = this.storage.findLowerBound(prevIndex, storageSize, currBound); if (mode === Mode.Skip) { skip = true; } else if (mode === Mode.Fingerprint) { let theirFingerprint = getBytes(query, FINGERPRINT_SIZE); let ourFingerprint = this.storage.fingerprint(lower, upper); if (compareUint8Array(theirFingerprint, ourFingerprint) !== 0) { doSkip(); this.splitRange(lower, upper, currBound, o); } else { skip = true; } } else if (mode === Mode.IdList) { let numIds = decodeVarInt(query); let theirElems = {}; for (let i = 0; i < numIds; i++) { let e = getBytes(query, ID_SIZE); theirElems[(0, import_utils.bytesToHex)(e)] = e; } skip = true; this.storage.iterate(lower, upper, (item) => { let k = item.id; const id = (0, import_utils.bytesToHex)(k); if (!theirElems[id]) { onhave?.(id); } else { delete theirElems[(0, import_utils.bytesToHex)(k)]; } return true; }); if (onneed) { for (let v of Object.values(theirElems)) { onneed((0, import_utils.bytesToHex)(v)); } } } else { throw Error("unexpected mode"); } if (this.exceededFrameSizeLimit(fullOutput.length + o.length)) { let remainingFingerprint = this.storage.fingerprint(upper, storageSize); fullOutput.extend(this.encodeBound(this._bound(Number.MAX_VALUE))); fullOutput.extend(encodeVarInt(Mode.Fingerprint)); fullOutput.extend(remainingFingerprint); break; } else { fullOutput.extend(o); } prevIndex = upper; prevBound = currBound; } return fullOutput.length === 1 ? null : (0, import_utils.bytesToHex)(fullOutput.unwrap()); } splitRange(lower, upper, upperBound, o) { let numElems = upper - lower; let buckets = 16; if (numElems < buckets * 2) { o.extend(this.encodeBound(upperBound)); o.extend(encodeVarInt(Mode.IdList)); o.extend(encodeVarInt(numElems)); this.storage.iterate(lower, upper, (item) => { o.extend(item.id); return true; }); } else { let itemsPerBucket = Math.floor(numElems / buckets); let bucketsWithExtra = numElems % buckets; let curr = lower; for (let i = 0; i < buckets; i++) { let bucketSize = itemsPerBucket + (i < bucketsWithExtra ? 1 : 0); let ourFingerprint = this.storage.fingerprint(curr, curr + bucketSize); curr += bucketSize; let nextBound; if (curr === upper) { nextBound = upperBound; } else { let prevItem; let currItem; this.storage.iterate(curr - 1, curr + 1, (item, index) => { if (index === curr - 1) prevItem = item; else currItem = item; return true; }); nextBound = this.getMinimalBound(prevItem, currItem); } o.extend(this.encodeBound(nextBound)); o.extend(encodeVarInt(Mode.Fingerprint)); o.extend(ourFingerprint); } } } exceededFrameSizeLimit(n) { return n > this.frameSizeLimit - 200; } decodeTimestampIn(encoded) { let timestamp = decodeVarInt(encoded); timestamp = timestamp === 0 ? Number.MAX_VALUE : timestamp - 1; if (this.lastTimestampIn === Number.MAX_VALUE || timestamp === Number.MAX_VALUE) { this.lastTimestampIn = Number.MAX_VALUE; return Number.MAX_VALUE; } timestamp += this.lastTimestampIn; this.lastTimestampIn = timestamp; return timestamp; } decodeBound(encoded) { let timestamp = this.decodeTimestampIn(encoded); let len = decodeVarInt(encoded); if (len > ID_SIZE) throw Error("bound key too long"); let id = getBytes(encoded, len); return { timestamp, id }; } encodeTimestampOut(timestamp) { if (timestamp === Number.MAX_VALUE) { this.lastTimestampOut = Number.MAX_VALUE; return encodeVarInt(0); } let temp = timestamp; timestamp -= this.lastTimestampOut; this.lastTimestampOut = temp; return encodeVarInt(timestamp + 1); } encodeBound(key) { let output = new WrappedBuffer(); output.extend(this.encodeTimestampOut(key.timestamp)); output.extend(encodeVarInt(key.id.length)); output.extend(key.id); return output; } getMinimalBound(prev, curr) { if (curr.timestamp !== prev.timestamp) { return this._bound(curr.timestamp); } else { let sharedPrefixBytes = 0; let currKey = curr.id; let prevKey = prev.id; for (let i = 0; i < ID_SIZE; i++) { if (currKey[i] !== prevKey[i]) break; sharedPrefixBytes++; } return this._bound(curr.timestamp, curr.id.subarray(0, sharedPrefixBytes + 1)); } } }; function compareUint8Array(a, b) { for (let i = 0; i < a.byteLength; i++) { if (a[i] < b[i]) return -1; if (a[i] > b[i]) return 1; } if (a.byteLength > b.byteLength) return 1; if (a.byteLength < b.byteLength) return -1; return 0; } function itemCompare(a, b) { if (a.timestamp === b.timestamp) { return compareUint8Array(a.id, b.id); } return a.timestamp - b.timestamp; } var NegentropySync = class { relay; storage; neg; filter; subscription; onhave; onneed; constructor(relay, storage, filter, params = {}) { this.relay = relay; this.storage = storage; this.neg = new Negentropy(storage); this.onhave = params.onhave; this.onneed = params.onneed; this.filter = filter; this.subscription = this.relay.prepareSubscription([{}], { label: params.label || "negentropy" }); this.subscription.oncustom = (data) => { switch (data[0]) { case "NEG-MSG": { if (data.length < 3) { console.warn(`got invalid NEG-MSG from ${this.relay.url}: ${data}`); } try { const response = this.neg.reconcile(data[2], this.onhave, this.onneed); if (response) { this.relay.send(`["NEG-MSG", "${this.subscription.id}", "${response}"]`); } else { this.close(); params.onclose?.(); } } catch (error) { console.error("negentropy reconcile error:", error); params?.onclose?.(`reconcile error: ${error}`); } break; } case "NEG-CLOSE": { const reason = data[2]; console.warn("negentropy error:", reason); params.onclose?.(reason); break; } case "NEG-ERR": { params.onclose?.(); } } }; } async start() { const initMsg = this.neg.initiate(); this.relay.send(`["NEG-OPEN","${this.subscription.id}",${JSON.stringify(this.filter)},"${initMsg}"]`); } close() { this.relay.send(`["NEG-CLOSE","${this.subscription.id}"]`); this.subscription.close(); } };