From c12342041dcd37281d1fc5743200d0a645880410 Mon Sep 17 00:00:00 2001 From: sovITxyz Date: Thu, 16 Jul 2026 15:43:35 -0600 Subject: [PATCH 1/4] feat: client crypto vendor bundle (NbreadCrypto) for multi-signer support esbuild-bundled from the existing @noble deps into a committed plain-IIFE artifact (public/js/vendor/nostr-crypto.js): NIP-01 event id/signing byte-identical to src/nostr/event.ts, NIP-44 v2 + NIP-04, bech32 (npub/nsec) ported from src/nostr/nip19.ts, WebCrypto RNG only. Cross-checked in tests against the server modules, nostr-tools, and official NIP-44 vectors; CI rebuilds and fails on artifact drift. --- .github/workflows/ci.yml | 11 + package-lock.json | 2 + package.json | 3 + public/js/README.md | 21 +- public/js/vendor/nostr-crypto.js | 3552 ++++++++++++++++++++++++++++++ scripts/build-vendor.mjs | 58 + scripts/vendor/crypto-entry.js | 583 +++++ test/unit/vendor-crypto.spec.ts | Bin 0 -> 21897 bytes 8 files changed, 4228 insertions(+), 2 deletions(-) create mode 100644 public/js/vendor/nostr-crypto.js create mode 100644 scripts/build-vendor.mjs create mode 100644 scripts/vendor/crypto-entry.js create mode 100644 test/unit/vendor-crypto.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c19d27c..ee51a36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,17 @@ jobs: - name: Install run: npm ci + # public/js/vendor/* is generated by scripts/build-vendor.mjs but + # COMMITTED (deploy never builds). Rebuild and fail on any drift between + # the entry/deps and the committed artifact. `git diff` catches modified + # tracked files but silently ignores NEW untracked output, so also + # require `git status --porcelain` to be empty for the directory. + - name: Vendor bundle drift check + run: | + npm run build:vendor + git diff --exit-code public/js/vendor/ + test -z "$(git status --porcelain public/js/vendor/)" + - name: Typecheck run: npm run typecheck diff --git a/package-lock.json b/package-lock.json index 61fe90a..e435378 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,8 +22,10 @@ }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.18.4", + "@noble/ciphers": "2.1.1", "@types/markdown-it": "^14.1.2", "@types/node": "^26.1.1", + "esbuild": "0.28.1", "fast-xml-parser": "^5.10.0", "nostr-tools": "^2.23.9", "typescript": "^7.0.2", diff --git a/package.json b/package.json index c6fa41e..e6faa87 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "test": "vitest run", "test:watch": "vitest", "fixtures": "node --experimental-strip-types scripts/gen-fixtures.ts", + "build:vendor": "node scripts/build-vendor.mjs", "migrate:local": "wrangler d1 migrations apply nbread --local", "smoke": "bash scripts/smoke.sh local", "cf-typegen": "wrangler types" @@ -30,8 +31,10 @@ }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.18.4", + "@noble/ciphers": "2.1.1", "@types/markdown-it": "^14.1.2", "@types/node": "^26.1.1", + "esbuild": "0.28.1", "fast-xml-parser": "^5.10.0", "nostr-tools": "^2.23.9", "typescript": "^7.0.2", diff --git a/public/js/README.md b/public/js/README.md index ab5ee49..2d14aae 100644 --- a/public/js/README.md +++ b/public/js/README.md @@ -1,7 +1,7 @@ # public/js -Client-side NIP-07 glue plus the hand-rolled editor (no build step, no -dependencies — every file is a plain IIFE served as-is): +Client-side NIP-07 glue plus the hand-rolled editor (no build step at serve +time, no runtime dependencies — every file is a plain IIFE served as-is): - `login.js` (P4) — fetches a one-time challenge, signs the kind 22242 auth event via `window.nostr`, POSTs it to `/login`. Keys never leave the @@ -26,3 +26,20 @@ dependencies — every file is a plain IIFE served as-is): Load order on the editor page matters: `editor-md.js` → `editor-toolbar.js` → `editor.js`. + +## vendor/ + +- `vendor/nostr-crypto.js` — GENERATED-BUT-COMMITTED crypto bundle + (`globalThis.NbreadCrypto`): hex/utf-8/base64 utils, bech32 + npub/nsec + codecs (same BIP-173 implementation as `src/nostr/nip19.ts`), NIP-01 + event ids byte-identical to `src/nostr/event.ts`, BIP-340 schnorr + sign/verify, NIP-44 v2, and legacy NIP-04. Built from + `scripts/vendor/crypto-entry.js` by `npm run build:vendor` + (esbuild, devDependency only — deploy never builds) and committed + unminified for auditability. Do NOT edit the artifact by hand: edit the + entry, rebuild, and commit both. CI rebuilds and fails on drift + (`git diff --exit-code` plus a `git status --porcelain` check so new + untracked build output also fails). All randomness comes from + `crypto.getRandomValues`. Unit-tested against the server primitives, + nostr-tools, and the official NIP-44 v2 vectors in + `test/unit/vendor-crypto.spec.ts`. diff --git a/public/js/vendor/nostr-crypto.js b/public/js/vendor/nostr-crypto.js new file mode 100644 index 0000000..32c1c26 --- /dev/null +++ b/public/js/vendor/nostr-crypto.js @@ -0,0 +1,3552 @@ +/** + * GENERATED FILE — DO NOT EDIT BY HAND. + * + * Built from scripts/vendor/crypto-entry.js by `npm run build:vendor` + * (scripts/build-vendor.mjs). Bundles @noble/curves, @noble/hashes, and + * @noble/ciphers (devDependencies pinned in package-lock.json) into a plain + * IIFE exposing globalThis.NbreadCrypto. Committed unminified so the served + * asset is auditable; CI rebuilds and fails on drift. + */ +"use strict"; +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + + // node_modules/@noble/hashes/utils.js + function isBytes(a) { + return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1; + } + function anumber(n, title = "") { + if (typeof n !== "number") { + const prefix = title && `"${title}" `; + throw new TypeError(`${prefix}expected number, got ${typeof n}`); + } + if (!Number.isSafeInteger(n) || n < 0) { + const prefix = title && `"${title}" `; + throw new RangeError(`${prefix}expected integer >= 0, got ${n}`); + } + } + function abytes(value, length, title = "") { + const bytes = isBytes(value); + const len = value?.length; + const needsLen = length !== void 0; + if (!bytes || needsLen && len !== length) { + const prefix = title && `"${title}" `; + const ofLen = needsLen ? ` of length ${length}` : ""; + const got = bytes ? `length=${len}` : `type=${typeof value}`; + const message = prefix + "expected Uint8Array" + ofLen + ", got " + got; + if (!bytes) + throw new TypeError(message); + throw new RangeError(message); + } + return value; + } + function ahash(h) { + if (typeof h !== "function" || typeof h.create !== "function") + throw new TypeError("Hash must wrapped by utils.createHasher"); + anumber(h.outputLen); + anumber(h.blockLen); + if (h.outputLen < 1) + throw new Error('"outputLen" must be >= 1'); + if (h.blockLen < 1) + throw new Error('"blockLen" must be >= 1'); + } + function aexists(instance, checkFinished = true) { + if (instance.destroyed) + throw new Error("Hash instance has been destroyed"); + if (checkFinished && instance.finished) + throw new Error("Hash#digest() has already been called"); + } + function aoutput(out, instance) { + abytes(out, void 0, "digestInto() output"); + const min = instance.outputLen; + if (out.length < min) { + throw new RangeError('"digestInto() output" expected to be of length >=' + min); + } + } + function clean(...arrays) { + for (let i = 0; i < arrays.length; i++) { + arrays[i].fill(0); + } + } + function createView(arr) { + return new DataView(arr.buffer, arr.byteOffset, arr.byteLength); + } + function rotr(word, shift) { + return word << 32 - shift | word >>> shift; + } + var hasHexBuiltin = /* @__PURE__ */ (() => ( + // @ts-ignore + typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function" + ))(); + var hexes = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0")); + function bytesToHex(bytes) { + abytes(bytes); + if (hasHexBuiltin) + return bytes.toHex(); + let hex = ""; + for (let i = 0; i < bytes.length; i++) { + hex += hexes[bytes[i]]; + } + return hex; + } + var asciis = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 }; + function asciiToBase16(ch) { + if (ch >= asciis._0 && ch <= asciis._9) + return ch - asciis._0; + if (ch >= asciis.A && ch <= asciis.F) + return ch - (asciis.A - 10); + if (ch >= asciis.a && ch <= asciis.f) + return ch - (asciis.a - 10); + return; + } + function hexToBytes(hex) { + if (typeof hex !== "string") + throw new TypeError("hex string expected, got " + typeof hex); + if (hasHexBuiltin) { + try { + return Uint8Array.fromHex(hex); + } catch (error) { + if (error instanceof SyntaxError) + throw new RangeError(error.message); + throw error; + } + } + const hl = hex.length; + const al = hl / 2; + if (hl % 2) + throw new RangeError("hex string expected, got unpadded hex of length " + hl); + const array = new Uint8Array(al); + for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) { + const n1 = asciiToBase16(hex.charCodeAt(hi)); + const n2 = asciiToBase16(hex.charCodeAt(hi + 1)); + if (n1 === void 0 || n2 === void 0) { + const char = hex[hi] + hex[hi + 1]; + throw new RangeError('hex string expected, got non-hex character "' + char + '" at index ' + hi); + } + array[ai] = n1 * 16 + n2; + } + return array; + } + function concatBytes(...arrays) { + let sum = 0; + for (let i = 0; i < arrays.length; i++) { + const a = arrays[i]; + abytes(a); + sum += a.length; + } + const res = new Uint8Array(sum); + for (let i = 0, pad = 0; i < arrays.length; i++) { + const a = arrays[i]; + res.set(a, pad); + pad += a.length; + } + return res; + } + function createHasher(hashCons, info = {}) { + const hashC = (msg, opts) => hashCons(opts).update(msg).digest(); + const tmp = hashCons(void 0); + hashC.outputLen = tmp.outputLen; + hashC.blockLen = tmp.blockLen; + hashC.canXOF = tmp.canXOF; + hashC.create = (opts) => hashCons(opts); + Object.assign(hashC, info); + return Object.freeze(hashC); + } + function randomBytes(bytesLength = 32) { + anumber(bytesLength, "bytesLength"); + const cr = typeof globalThis === "object" ? globalThis.crypto : null; + if (typeof cr?.getRandomValues !== "function") + throw new Error("crypto.getRandomValues must be defined"); + if (bytesLength > 65536) + throw new RangeError(`"bytesLength" expected <= 65536, got ${bytesLength}`); + return cr.getRandomValues(new Uint8Array(bytesLength)); + } + var oidNist = (suffix) => ({ + // Current NIST hashAlgs suffixes used here fit in one DER subidentifier octet. + // Larger suffix values would need base-128 OID encoding and a different length byte. + oid: Uint8Array.from([6, 9, 96, 134, 72, 1, 101, 3, 4, 2, suffix]) + }); + + // node_modules/@noble/hashes/_md.js + function Chi(a, b, c) { + return a & b ^ ~a & c; + } + function Maj(a, b, c) { + return a & b ^ a & c ^ b & c; + } + var HashMD = class { + constructor(blockLen, outputLen, padOffset, isLE2) { + __publicField(this, "blockLen"); + __publicField(this, "outputLen"); + __publicField(this, "canXOF", false); + __publicField(this, "padOffset"); + __publicField(this, "isLE"); + // For partial updates less than block size + __publicField(this, "buffer"); + __publicField(this, "view"); + __publicField(this, "finished", false); + __publicField(this, "length", 0); + __publicField(this, "pos", 0); + __publicField(this, "destroyed", false); + this.blockLen = blockLen; + this.outputLen = outputLen; + this.padOffset = padOffset; + this.isLE = isLE2; + this.buffer = new Uint8Array(blockLen); + this.view = createView(this.buffer); + } + update(data) { + aexists(this); + abytes(data); + const { view, buffer, blockLen } = this; + const len = data.length; + for (let pos = 0; pos < len; ) { + const take = Math.min(blockLen - this.pos, len - pos); + if (take === blockLen) { + const dataView = createView(data); + for (; blockLen <= len - pos; pos += blockLen) + this.process(dataView, pos); + continue; + } + buffer.set(data.subarray(pos, pos + take), this.pos); + this.pos += take; + pos += take; + if (this.pos === blockLen) { + this.process(view, 0); + this.pos = 0; + } + } + this.length += data.length; + this.roundClean(); + return this; + } + digestInto(out) { + aexists(this); + aoutput(out, this); + this.finished = true; + const { buffer, view, blockLen, isLE: isLE2 } = this; + let { pos } = this; + buffer[pos++] = 128; + clean(this.buffer.subarray(pos)); + if (this.padOffset > blockLen - pos) { + this.process(view, 0); + pos = 0; + } + for (let i = pos; i < blockLen; i++) + buffer[i] = 0; + view.setBigUint64(blockLen - 8, BigInt(this.length * 8), isLE2); + this.process(view, 0); + const oview = createView(out); + const len = this.outputLen; + if (len % 4) + throw new Error("_sha2: outputLen must be aligned to 32bit"); + const outLen = len / 4; + const state = this.get(); + if (outLen > state.length) + throw new Error("_sha2: outputLen bigger than state"); + for (let i = 0; i < outLen; i++) + oview.setUint32(4 * i, state[i], isLE2); + } + digest() { + const { buffer, outputLen } = this; + this.digestInto(buffer); + const res = buffer.slice(0, outputLen); + this.destroy(); + return res; + } + _cloneInto(to) { + to || (to = new this.constructor()); + to.set(...this.get()); + const { blockLen, buffer, length, finished, destroyed, pos } = this; + to.destroyed = destroyed; + to.finished = finished; + to.length = length; + to.pos = pos; + if (length % blockLen) + to.buffer.set(buffer); + return to; + } + clone() { + return this._cloneInto(); + } + }; + var SHA256_IV = /* @__PURE__ */ Uint32Array.from([ + 1779033703, + 3144134277, + 1013904242, + 2773480762, + 1359893119, + 2600822924, + 528734635, + 1541459225 + ]); + + // node_modules/@noble/hashes/sha2.js + var SHA256_K = /* @__PURE__ */ Uint32Array.from([ + 1116352408, + 1899447441, + 3049323471, + 3921009573, + 961987163, + 1508970993, + 2453635748, + 2870763221, + 3624381080, + 310598401, + 607225278, + 1426881987, + 1925078388, + 2162078206, + 2614888103, + 3248222580, + 3835390401, + 4022224774, + 264347078, + 604807628, + 770255983, + 1249150122, + 1555081692, + 1996064986, + 2554220882, + 2821834349, + 2952996808, + 3210313671, + 3336571891, + 3584528711, + 113926993, + 338241895, + 666307205, + 773529912, + 1294757372, + 1396182291, + 1695183700, + 1986661051, + 2177026350, + 2456956037, + 2730485921, + 2820302411, + 3259730800, + 3345764771, + 3516065817, + 3600352804, + 4094571909, + 275423344, + 430227734, + 506948616, + 659060556, + 883997877, + 958139571, + 1322822218, + 1537002063, + 1747873779, + 1955562222, + 2024104815, + 2227730452, + 2361852424, + 2428436474, + 2756734187, + 3204031479, + 3329325298 + ]); + var SHA256_W = /* @__PURE__ */ new Uint32Array(64); + var SHA2_32B = class extends HashMD { + constructor(outputLen) { + super(64, outputLen, 8, false); + } + get() { + const { A, B, C, D, E, F, G, H } = this; + return [A, B, C, D, E, F, G, H]; + } + // prettier-ignore + set(A, B, C, D, E, F, G, H) { + this.A = A | 0; + this.B = B | 0; + this.C = C | 0; + this.D = D | 0; + this.E = E | 0; + this.F = F | 0; + this.G = G | 0; + this.H = H | 0; + } + process(view, offset) { + for (let i = 0; i < 16; i++, offset += 4) + SHA256_W[i] = view.getUint32(offset, false); + for (let i = 16; i < 64; i++) { + const W15 = SHA256_W[i - 15]; + const W2 = SHA256_W[i - 2]; + const s0 = rotr(W15, 7) ^ rotr(W15, 18) ^ W15 >>> 3; + const s1 = rotr(W2, 17) ^ rotr(W2, 19) ^ W2 >>> 10; + SHA256_W[i] = s1 + SHA256_W[i - 7] + s0 + SHA256_W[i - 16] | 0; + } + let { A, B, C, D, E, F, G, H } = this; + for (let i = 0; i < 64; i++) { + const sigma1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25); + const T1 = H + sigma1 + Chi(E, F, G) + SHA256_K[i] + SHA256_W[i] | 0; + const sigma0 = rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22); + const T2 = sigma0 + Maj(A, B, C) | 0; + H = G; + G = F; + F = E; + E = D + T1 | 0; + D = C; + C = B; + B = A; + A = T1 + T2 | 0; + } + A = A + this.A | 0; + B = B + this.B | 0; + C = C + this.C | 0; + D = D + this.D | 0; + E = E + this.E | 0; + F = F + this.F | 0; + G = G + this.G | 0; + H = H + this.H | 0; + this.set(A, B, C, D, E, F, G, H); + } + roundClean() { + clean(SHA256_W); + } + destroy() { + this.destroyed = true; + this.set(0, 0, 0, 0, 0, 0, 0, 0); + clean(this.buffer); + } + }; + var _SHA256 = class extends SHA2_32B { + constructor() { + super(32); + // We cannot use array here since array allows indexing by variable + // which means optimizer/compiler cannot use registers. + __publicField(this, "A", SHA256_IV[0] | 0); + __publicField(this, "B", SHA256_IV[1] | 0); + __publicField(this, "C", SHA256_IV[2] | 0); + __publicField(this, "D", SHA256_IV[3] | 0); + __publicField(this, "E", SHA256_IV[4] | 0); + __publicField(this, "F", SHA256_IV[5] | 0); + __publicField(this, "G", SHA256_IV[6] | 0); + __publicField(this, "H", SHA256_IV[7] | 0); + } + }; + var sha256 = /* @__PURE__ */ createHasher( + () => new _SHA256(), + /* @__PURE__ */ oidNist(1) + ); + + // node_modules/@noble/curves/utils.js + /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ + var abytes2 = (value, length, title) => abytes(value, length, title); + var anumber2 = anumber; + var bytesToHex2 = bytesToHex; + var concatBytes2 = (...arrays) => concatBytes(...arrays); + var hexToBytes2 = (hex) => hexToBytes(hex); + var isBytes2 = isBytes; + var randomBytes2 = (bytesLength) => randomBytes(bytesLength); + var _0n = /* @__PURE__ */ BigInt(0); + var _1n = /* @__PURE__ */ BigInt(1); + function abool(value, title = "") { + if (typeof value !== "boolean") { + const prefix = title && `"${title}" `; + throw new TypeError(prefix + "expected boolean, got type=" + typeof value); + } + return value; + } + function abignumber(n) { + if (typeof n === "bigint") { + if (!isPosBig(n)) + throw new RangeError("positive bigint expected, got " + n); + } else + anumber2(n); + return n; + } + function asafenumber(value, title = "") { + if (typeof value !== "number") { + const prefix = title && `"${title}" `; + throw new TypeError(prefix + "expected number, got type=" + typeof value); + } + if (!Number.isSafeInteger(value)) { + const prefix = title && `"${title}" `; + throw new RangeError(prefix + "expected safe integer, got " + value); + } + } + function numberToHexUnpadded(num2) { + const hex = abignumber(num2).toString(16); + return hex.length & 1 ? "0" + hex : hex; + } + function hexToNumber(hex) { + if (typeof hex !== "string") + throw new TypeError("hex string expected, got " + typeof hex); + return hex === "" ? _0n : BigInt("0x" + hex); + } + function bytesToNumberBE(bytes) { + return hexToNumber(bytesToHex(bytes)); + } + function bytesToNumberLE(bytes) { + return hexToNumber(bytesToHex(copyBytes(abytes(bytes)).reverse())); + } + function numberToBytesBE(n, len) { + anumber(len); + if (len === 0) + throw new RangeError("zero length"); + n = abignumber(n); + const hex = n.toString(16); + if (hex.length > len * 2) + throw new RangeError("number too large"); + return hexToBytes(hex.padStart(len * 2, "0")); + } + function numberToBytesLE(n, len) { + return numberToBytesBE(n, len).reverse(); + } + function copyBytes(bytes) { + return Uint8Array.from(abytes2(bytes)); + } + function asciiToBytes(ascii) { + if (typeof ascii !== "string") + throw new TypeError("ascii string expected, got " + typeof ascii); + return Uint8Array.from(ascii, (c, i) => { + const charCode = c.charCodeAt(0); + if (c.length !== 1 || charCode > 127) { + throw new RangeError(`string contains non-ASCII character "${ascii[i]}" with code ${charCode} at position ${i}`); + } + return charCode; + }); + } + var isPosBig = (n) => typeof n === "bigint" && _0n <= n; + function inRange(n, min, max) { + return isPosBig(n) && isPosBig(min) && isPosBig(max) && min <= n && n < max; + } + function aInRange(title, n, min, max) { + if (!inRange(n, min, max)) + throw new RangeError("expected valid " + title + ": " + min + " <= n < " + max + ", got " + n); + } + function bitLen(n) { + if (n < _0n) + throw new Error("expected non-negative bigint, got " + n); + let len; + for (len = 0; n > _0n; n >>= _1n, len += 1) + ; + return len; + } + var bitMask = (n) => (_1n << BigInt(n)) - _1n; + function createHmacDrbg(hashLen, qByteLen, hmacFn) { + anumber(hashLen, "hashLen"); + anumber(qByteLen, "qByteLen"); + if (typeof hmacFn !== "function") + throw new TypeError("hmacFn must be a function"); + const u8n = (len) => new Uint8Array(len); + const NULL = Uint8Array.of(); + const byte0 = Uint8Array.of(0); + const byte1 = Uint8Array.of(1); + const _maxDrbgIters = 1e3; + let v = u8n(hashLen); + let k = u8n(hashLen); + let i = 0; + const reset = () => { + v.fill(1); + k.fill(0); + i = 0; + }; + const h = (...msgs) => hmacFn(k, concatBytes2(v, ...msgs)); + const reseed = (seed = NULL) => { + k = h(byte0, seed); + v = h(); + if (seed.length === 0) + return; + k = h(byte1, seed); + v = h(); + }; + const gen = () => { + if (i++ >= _maxDrbgIters) + throw new Error("drbg: tried max amount of iterations"); + let len = 0; + const out = []; + while (len < qByteLen) { + v = h(); + const sl = v.slice(); + out.push(sl); + len += v.length; + } + return concatBytes2(...out); + }; + const genUntil = (seed, pred) => { + reset(); + reseed(seed); + let res = void 0; + while ((res = pred(gen())) === void 0) + reseed(); + reset(); + return res; + }; + return genUntil; + } + function validateObject(object, fields = {}, optFields = {}) { + if (Object.prototype.toString.call(object) !== "[object Object]") + throw new TypeError("expected valid options object"); + function checkField(fieldName, expectedType, isOpt) { + if (!isOpt && expectedType !== "function" && !Object.hasOwn(object, fieldName)) + throw new TypeError(`param "${fieldName}" is invalid: expected own property`); + const val = object[fieldName]; + if (isOpt && val === void 0) + return; + const current = typeof val; + if (current !== expectedType || val === null) + throw new TypeError(`param "${fieldName}" is invalid: expected ${expectedType}, got ${current}`); + } + const iter = (f, isOpt) => Object.entries(f).forEach(([k, v]) => checkField(k, v, isOpt)); + iter(fields, false); + iter(optFields, true); + } + + // node_modules/@noble/curves/abstract/modular.js + /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ + var _0n2 = /* @__PURE__ */ BigInt(0); + var _1n2 = /* @__PURE__ */ BigInt(1); + var _2n = /* @__PURE__ */ BigInt(2); + var _3n = /* @__PURE__ */ BigInt(3); + var _4n = /* @__PURE__ */ BigInt(4); + var _5n = /* @__PURE__ */ BigInt(5); + var _7n = /* @__PURE__ */ BigInt(7); + var _8n = /* @__PURE__ */ BigInt(8); + var _9n = /* @__PURE__ */ BigInt(9); + var _16n = /* @__PURE__ */ BigInt(16); + function mod(a, b) { + if (b <= _0n2) + throw new Error("mod: expected positive modulus, got " + b); + const result = a % b; + return result >= _0n2 ? result : b + result; + } + function pow2(x, power, modulo) { + if (power < _0n2) + throw new Error("pow2: expected non-negative exponent, got " + power); + let res = x; + while (power-- > _0n2) { + res *= res; + res %= modulo; + } + return res; + } + function invert(number, modulo) { + if (number === _0n2) + throw new Error("invert: expected non-zero number"); + if (modulo <= _0n2) + throw new Error("invert: expected positive modulus, got " + modulo); + let a = mod(number, modulo); + let b = modulo; + let x = _0n2, y = _1n2, u = _1n2, v = _0n2; + while (a !== _0n2) { + const q = b / a; + const r = b - a * q; + const m = x - u * q; + const n = y - v * q; + b = a, a = r, x = u, y = v, u = m, v = n; + } + const gcd = b; + if (gcd !== _1n2) + throw new Error("invert: does not exist"); + return mod(x, modulo); + } + function assertIsSquare(Fp, root, n) { + const F = Fp; + if (!F.eql(F.sqr(root), n)) + throw new Error("Cannot find square root"); + } + function sqrt3mod4(Fp, n) { + const F = Fp; + const p1div4 = (F.ORDER + _1n2) / _4n; + const root = F.pow(n, p1div4); + assertIsSquare(F, root, n); + return root; + } + function sqrt5mod8(Fp, n) { + const F = Fp; + const p5div8 = (F.ORDER - _5n) / _8n; + const n2 = F.mul(n, _2n); + const v = F.pow(n2, p5div8); + const nv = F.mul(n, v); + const i = F.mul(F.mul(nv, _2n), v); + const root = F.mul(nv, F.sub(i, F.ONE)); + assertIsSquare(F, root, n); + return root; + } + function sqrt9mod16(P) { + const Fp_ = Field(P); + const tn = tonelliShanks(P); + const c1 = tn(Fp_, Fp_.neg(Fp_.ONE)); + const c2 = tn(Fp_, c1); + const c3 = tn(Fp_, Fp_.neg(c1)); + const c4 = (P + _7n) / _16n; + return ((Fp, n) => { + const F = Fp; + let tv1 = F.pow(n, c4); + let tv2 = F.mul(tv1, c1); + const tv3 = F.mul(tv1, c2); + const tv4 = F.mul(tv1, c3); + const e1 = F.eql(F.sqr(tv2), n); + const e2 = F.eql(F.sqr(tv3), n); + tv1 = F.cmov(tv1, tv2, e1); + tv2 = F.cmov(tv4, tv3, e2); + const e3 = F.eql(F.sqr(tv2), n); + const root = F.cmov(tv1, tv2, e3); + assertIsSquare(F, root, n); + return root; + }); + } + function tonelliShanks(P) { + if (P < _3n) + throw new Error("sqrt is not defined for small field"); + let Q = P - _1n2; + let S = 0; + while (Q % _2n === _0n2) { + Q /= _2n; + S++; + } + let Z = _2n; + const _Fp = Field(P); + while (FpLegendre(_Fp, Z) === 1) { + if (Z++ > 1e3) + throw new Error("Cannot find square root: probably non-prime P"); + } + if (S === 1) + return sqrt3mod4; + let cc = _Fp.pow(Z, Q); + const Q1div2 = (Q + _1n2) / _2n; + return function tonelliSlow(Fp, n) { + const F = Fp; + if (F.is0(n)) + return n; + if (FpLegendre(F, n) !== 1) + throw new Error("Cannot find square root"); + let M = S; + let c = F.mul(F.ONE, cc); + let t = F.pow(n, Q); + let R = F.pow(n, Q1div2); + while (!F.eql(t, F.ONE)) { + if (F.is0(t)) + return F.ZERO; + let i = 1; + let t_tmp = F.sqr(t); + while (!F.eql(t_tmp, F.ONE)) { + i++; + t_tmp = F.sqr(t_tmp); + if (i === M) + throw new Error("Cannot find square root"); + } + const exponent = _1n2 << BigInt(M - i - 1); + const b = F.pow(c, exponent); + M = i; + c = F.sqr(b); + t = F.mul(t, c); + R = F.mul(R, b); + } + return R; + }; + } + function FpSqrt(P) { + if (P % _4n === _3n) + return sqrt3mod4; + if (P % _8n === _5n) + return sqrt5mod8; + if (P % _16n === _9n) + return sqrt9mod16(P); + return tonelliShanks(P); + } + var FIELD_FIELDS = [ + "create", + "isValid", + "is0", + "neg", + "inv", + "sqrt", + "sqr", + "eql", + "add", + "sub", + "mul", + "pow", + "div", + "addN", + "subN", + "mulN", + "sqrN" + ]; + function validateField(field) { + const initial = { + ORDER: "bigint", + BYTES: "number", + BITS: "number" + }; + const opts = FIELD_FIELDS.reduce((map, val) => { + map[val] = "function"; + return map; + }, initial); + validateObject(field, opts); + asafenumber(field.BYTES, "BYTES"); + asafenumber(field.BITS, "BITS"); + if (field.BYTES < 1 || field.BITS < 1) + throw new Error("invalid field: expected BYTES/BITS > 0"); + if (field.ORDER <= _1n2) + throw new Error("invalid field: expected ORDER > 1, got " + field.ORDER); + return field; + } + function FpPow(Fp, num2, power) { + const F = Fp; + if (power < _0n2) + throw new Error("invalid exponent, negatives unsupported"); + if (power === _0n2) + return F.ONE; + if (power === _1n2) + return num2; + let p = F.ONE; + let d = num2; + while (power > _0n2) { + if (power & _1n2) + p = F.mul(p, d); + d = F.sqr(d); + power >>= _1n2; + } + return p; + } + function FpInvertBatch(Fp, nums, passZero = false) { + const F = Fp; + const inverted = new Array(nums.length).fill(passZero ? F.ZERO : void 0); + const multipliedAcc = nums.reduce((acc, num2, i) => { + if (F.is0(num2)) + return acc; + inverted[i] = acc; + return F.mul(acc, num2); + }, F.ONE); + const invertedAcc = F.inv(multipliedAcc); + nums.reduceRight((acc, num2, i) => { + if (F.is0(num2)) + return acc; + inverted[i] = F.mul(acc, inverted[i]); + return F.mul(acc, num2); + }, invertedAcc); + return inverted; + } + function FpLegendre(Fp, n) { + const F = Fp; + const p1mod2 = (F.ORDER - _1n2) / _2n; + const powered = F.pow(n, p1mod2); + const yes = F.eql(powered, F.ONE); + const zero = F.eql(powered, F.ZERO); + const no = F.eql(powered, F.neg(F.ONE)); + if (!yes && !zero && !no) + throw new Error("invalid Legendre symbol result"); + return yes ? 1 : zero ? 0 : -1; + } + function nLength(n, nBitLength) { + if (nBitLength !== void 0) + anumber2(nBitLength); + if (n <= _0n2) + throw new Error("invalid n length: expected positive n, got " + n); + if (nBitLength !== void 0 && nBitLength < 1) + throw new Error("invalid n length: expected positive bit length, got " + nBitLength); + const bits = bitLen(n); + if (nBitLength !== void 0 && nBitLength < bits) + throw new Error(`invalid n length: expected bit length (${bits}) >= n.length (${nBitLength})`); + const _nBitLength = nBitLength !== void 0 ? nBitLength : bits; + const nByteLength = Math.ceil(_nBitLength / 8); + return { nBitLength: _nBitLength, nByteLength }; + } + var FIELD_SQRT = /* @__PURE__ */ new WeakMap(); + var _Field = class { + constructor(ORDER, opts = {}) { + __publicField(this, "ORDER"); + __publicField(this, "BITS"); + __publicField(this, "BYTES"); + __publicField(this, "isLE"); + __publicField(this, "ZERO", _0n2); + __publicField(this, "ONE", _1n2); + __publicField(this, "_lengths"); + __publicField(this, "_mod"); + if (ORDER <= _1n2) + throw new Error("invalid field: expected ORDER > 1, got " + ORDER); + let _nbitLength = void 0; + this.isLE = false; + if (opts != null && typeof opts === "object") { + if (typeof opts.BITS === "number") + _nbitLength = opts.BITS; + if (typeof opts.sqrt === "function") + Object.defineProperty(this, "sqrt", { value: opts.sqrt, enumerable: true }); + if (typeof opts.isLE === "boolean") + this.isLE = opts.isLE; + if (opts.allowedLengths) + this._lengths = Object.freeze(opts.allowedLengths.slice()); + if (typeof opts.modFromBytes === "boolean") + this._mod = opts.modFromBytes; + } + const { nBitLength, nByteLength } = nLength(ORDER, _nbitLength); + if (nByteLength > 2048) + throw new Error("invalid field: expected ORDER of <= 2048 bytes"); + this.ORDER = ORDER; + this.BITS = nBitLength; + this.BYTES = nByteLength; + Object.freeze(this); + } + create(num2) { + return mod(num2, this.ORDER); + } + isValid(num2) { + if (typeof num2 !== "bigint") + throw new TypeError("invalid field element: expected bigint, got " + typeof num2); + return _0n2 <= num2 && num2 < this.ORDER; + } + is0(num2) { + return num2 === _0n2; + } + // is valid and invertible + isValidNot0(num2) { + return !this.is0(num2) && this.isValid(num2); + } + isOdd(num2) { + return (num2 & _1n2) === _1n2; + } + neg(num2) { + return mod(-num2, this.ORDER); + } + eql(lhs, rhs) { + return lhs === rhs; + } + sqr(num2) { + return mod(num2 * num2, this.ORDER); + } + add(lhs, rhs) { + return mod(lhs + rhs, this.ORDER); + } + sub(lhs, rhs) { + return mod(lhs - rhs, this.ORDER); + } + mul(lhs, rhs) { + return mod(lhs * rhs, this.ORDER); + } + pow(num2, power) { + return FpPow(this, num2, power); + } + div(lhs, rhs) { + return mod(lhs * invert(rhs, this.ORDER), this.ORDER); + } + // Same as above, but doesn't normalize + sqrN(num2) { + return num2 * num2; + } + addN(lhs, rhs) { + return lhs + rhs; + } + subN(lhs, rhs) { + return lhs - rhs; + } + mulN(lhs, rhs) { + return lhs * rhs; + } + inv(num2) { + return invert(num2, this.ORDER); + } + sqrt(num2) { + let sqrt = FIELD_SQRT.get(this); + if (!sqrt) + FIELD_SQRT.set(this, sqrt = FpSqrt(this.ORDER)); + return sqrt(this, num2); + } + toBytes(num2) { + return this.isLE ? numberToBytesLE(num2, this.BYTES) : numberToBytesBE(num2, this.BYTES); + } + fromBytes(bytes, skipValidation = false) { + abytes2(bytes); + const { _lengths: allowedLengths, BYTES, isLE: isLE2, ORDER, _mod: modFromBytes } = this; + if (allowedLengths) { + if (bytes.length < 1 || !allowedLengths.includes(bytes.length) || bytes.length > BYTES) { + throw new Error("Field.fromBytes: expected " + allowedLengths + " bytes, got " + bytes.length); + } + const padded = new Uint8Array(BYTES); + padded.set(bytes, isLE2 ? 0 : padded.length - bytes.length); + bytes = padded; + } + if (bytes.length !== BYTES) + throw new Error("Field.fromBytes: expected " + BYTES + " bytes, got " + bytes.length); + let scalar = isLE2 ? bytesToNumberLE(bytes) : bytesToNumberBE(bytes); + if (modFromBytes) + scalar = mod(scalar, ORDER); + if (!skipValidation) { + if (!this.isValid(scalar)) + throw new Error("invalid field element: outside of range 0..ORDER"); + } + return scalar; + } + // TODO: we don't need it here, move out to separate fn + invertBatch(lst) { + return FpInvertBatch(this, lst); + } + // We can't move this out because Fp6, Fp12 implement it + // and it's unclear what to return in there. + cmov(a, b, condition) { + abool(condition, "condition"); + return condition ? b : a; + } + }; + Object.freeze(_Field.prototype); + function Field(ORDER, opts = {}) { + return new _Field(ORDER, opts); + } + function getFieldBytesLength(fieldOrder) { + if (typeof fieldOrder !== "bigint") + throw new Error("field order must be bigint"); + if (fieldOrder <= _1n2) + throw new Error("field order must be greater than 1"); + const bitLength = bitLen(fieldOrder - _1n2); + return Math.ceil(bitLength / 8); + } + function getMinHashLength(fieldOrder) { + const length = getFieldBytesLength(fieldOrder); + return length + Math.ceil(length / 2); + } + function mapHashToField(key, fieldOrder, isLE2 = false) { + abytes2(key); + const len = key.length; + const fieldLen = getFieldBytesLength(fieldOrder); + const minLen = Math.max(getMinHashLength(fieldOrder), 16); + if (len < minLen || len > 1024) + throw new Error("expected " + minLen + "-1024 bytes of input, got " + len); + const num2 = isLE2 ? bytesToNumberLE(key) : bytesToNumberBE(key); + const reduced = mod(num2, fieldOrder - _1n2) + _1n2; + return isLE2 ? numberToBytesLE(reduced, fieldLen) : numberToBytesBE(reduced, fieldLen); + } + + // node_modules/@noble/curves/abstract/curve.js + /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ + var _0n3 = /* @__PURE__ */ BigInt(0); + var _1n3 = /* @__PURE__ */ BigInt(1); + function negateCt(condition, item) { + const neg = item.negate(); + return condition ? neg : item; + } + function normalizeZ(c, points) { + const invertedZs = FpInvertBatch(c.Fp, points.map((p) => p.Z)); + return points.map((p, i) => c.fromAffine(p.toAffine(invertedZs[i]))); + } + function validateW(W, bits) { + if (!Number.isSafeInteger(W) || W <= 0 || W > bits) + throw new Error("invalid window size, expected [1.." + bits + "], got W=" + W); + } + function calcWOpts(W, scalarBits) { + validateW(W, scalarBits); + const windows = Math.ceil(scalarBits / W) + 1; + const windowSize = 2 ** (W - 1); + const maxNumber = 2 ** W; + const mask = bitMask(W); + const shiftBy = BigInt(W); + return { windows, windowSize, mask, maxNumber, shiftBy }; + } + function calcOffsets(n, window, wOpts) { + const { windowSize, mask, maxNumber, shiftBy } = wOpts; + let wbits = Number(n & mask); + let nextN = n >> shiftBy; + if (wbits > windowSize) { + wbits -= maxNumber; + nextN += _1n3; + } + const offsetStart = window * windowSize; + const offset = offsetStart + Math.abs(wbits) - 1; + const isZero = wbits === 0; + const isNeg = wbits < 0; + const isNegF = window % 2 !== 0; + const offsetF = offsetStart; + return { nextN, offset, isZero, isNeg, isNegF, offsetF }; + } + var pointPrecomputes = /* @__PURE__ */ new WeakMap(); + var pointWindowSizes = /* @__PURE__ */ new WeakMap(); + function getW(P) { + return pointWindowSizes.get(P) || 1; + } + function assert0(n) { + if (n !== _0n3) + throw new Error("invalid wNAF"); + } + var wNAF = class { + // Parametrized with a given Point class (not individual point) + constructor(Point, bits) { + __publicField(this, "BASE"); + __publicField(this, "ZERO"); + __publicField(this, "Fn"); + __publicField(this, "bits"); + this.BASE = Point.BASE; + this.ZERO = Point.ZERO; + this.Fn = Point.Fn; + this.bits = bits; + } + // non-const time multiplication ladder + _unsafeLadder(elm, n, p = this.ZERO) { + let d = elm; + while (n > _0n3) { + if (n & _1n3) + p = p.add(d); + d = d.double(); + n >>= _1n3; + } + return p; + } + /** + * Creates a wNAF precomputation window. Used for caching. + * Default window size is set by `utils.precompute()` and is equal to 8. + * Number of precomputed points depends on the curve size: + * 2^(𝑊−1) * (Math.ceil(𝑛 / 𝑊) + 1), where: + * - 𝑊 is the window size + * - 𝑛 is the bitlength of the curve order. + * For a 256-bit curve and window size 8, the number of precomputed points is 128 * 33 = 4224. + * @param point - Point instance + * @param W - window size + * @returns precomputed point tables flattened to a single array + */ + precomputeWindow(point, W) { + const { windows, windowSize } = calcWOpts(W, this.bits); + const points = []; + let p = point; + let base = p; + for (let window = 0; window < windows; window++) { + base = p; + points.push(base); + for (let i = 1; i < windowSize; i++) { + base = base.add(p); + points.push(base); + } + p = base.double(); + } + return points; + } + /** + * Implements ec multiplication using precomputed tables and w-ary non-adjacent form. + * More compact implementation: + * https://github.com/paulmillr/noble-secp256k1/blob/47cb1669b6e506ad66b35fe7d76132ae97465da2/index.ts#L502-L541 + * @returns real and fake (for const-time) points + */ + wNAF(W, precomputes, n) { + if (!this.Fn.isValid(n)) + throw new Error("invalid scalar"); + let p = this.ZERO; + let f = this.BASE; + const wo = calcWOpts(W, this.bits); + for (let window = 0; window < wo.windows; window++) { + const { nextN, offset, isZero, isNeg, isNegF, offsetF } = calcOffsets(n, window, wo); + n = nextN; + if (isZero) { + f = f.add(negateCt(isNegF, precomputes[offsetF])); + } else { + p = p.add(negateCt(isNeg, precomputes[offset])); + } + } + assert0(n); + return { p, f }; + } + /** + * Implements unsafe EC multiplication using precomputed tables + * and w-ary non-adjacent form. + * @param acc - accumulator point to add result of multiplication + * @returns point + */ + wNAFUnsafe(W, precomputes, n, acc = this.ZERO) { + const wo = calcWOpts(W, this.bits); + for (let window = 0; window < wo.windows; window++) { + if (n === _0n3) + break; + const { nextN, offset, isZero, isNeg } = calcOffsets(n, window, wo); + n = nextN; + if (isZero) { + continue; + } else { + const item = precomputes[offset]; + acc = acc.add(isNeg ? item.negate() : item); + } + } + assert0(n); + return acc; + } + getPrecomputes(W, point, transform) { + let comp = pointPrecomputes.get(point); + if (!comp) { + comp = this.precomputeWindow(point, W); + if (W !== 1) { + if (typeof transform === "function") + comp = transform(comp); + pointPrecomputes.set(point, comp); + } + } + return comp; + } + cached(point, scalar, transform) { + const W = getW(point); + return this.wNAF(W, this.getPrecomputes(W, point, transform), scalar); + } + unsafe(point, scalar, transform, prev) { + const W = getW(point); + if (W === 1) + return this._unsafeLadder(point, scalar, prev); + return this.wNAFUnsafe(W, this.getPrecomputes(W, point, transform), scalar, prev); + } + // We calculate precomputes for elliptic curve point multiplication + // using windowed method. This specifies window size and + // stores precomputed values. Usually only base point would be precomputed. + createCache(P, W) { + validateW(W, this.bits); + pointWindowSizes.set(P, W); + pointPrecomputes.delete(P); + } + hasCache(elm) { + return getW(elm) !== 1; + } + }; + function mulEndoUnsafe(Point, point, k1, k2) { + let acc = point; + let p1 = Point.ZERO; + let p2 = Point.ZERO; + while (k1 > _0n3 || k2 > _0n3) { + if (k1 & _1n3) + p1 = p1.add(acc); + if (k2 & _1n3) + p2 = p2.add(acc); + acc = acc.double(); + k1 >>= _1n3; + k2 >>= _1n3; + } + return { p1, p2 }; + } + function createField(order, field, isLE2) { + if (field) { + if (field.ORDER !== order) + throw new Error("Field.ORDER must match order: Fp == p, Fn == n"); + validateField(field); + return field; + } else { + return Field(order, { isLE: isLE2 }); + } + } + function createCurveFields(type, CURVE, curveOpts = {}, FpFnLE) { + if (FpFnLE === void 0) + FpFnLE = type === "edwards"; + if (!CURVE || typeof CURVE !== "object") + throw new Error(`expected valid ${type} CURVE object`); + for (const p of ["p", "n", "h"]) { + const val = CURVE[p]; + if (!(typeof val === "bigint" && val > _0n3)) + throw new Error(`CURVE.${p} must be positive bigint`); + } + const Fp = createField(CURVE.p, curveOpts.Fp, FpFnLE); + const Fn = createField(CURVE.n, curveOpts.Fn, FpFnLE); + const _b = type === "weierstrass" ? "b" : "d"; + const params = ["Gx", "Gy", "a", _b]; + for (const p of params) { + if (!Fp.isValid(CURVE[p])) + throw new Error(`CURVE.${p} must be valid field element of CURVE.Fp`); + } + CURVE = Object.freeze(Object.assign({}, CURVE)); + return { CURVE, Fp, Fn }; + } + function createKeygen(randomSecretKey, getPublicKey) { + return function keygen(seed) { + const secretKey = randomSecretKey(seed); + return { secretKey, publicKey: getPublicKey(secretKey) }; + }; + } + + // node_modules/@noble/hashes/hmac.js + var _HMAC = class { + constructor(hash, key) { + __publicField(this, "oHash"); + __publicField(this, "iHash"); + __publicField(this, "blockLen"); + __publicField(this, "outputLen"); + __publicField(this, "canXOF", false); + __publicField(this, "finished", false); + __publicField(this, "destroyed", false); + ahash(hash); + abytes(key, void 0, "key"); + this.iHash = hash.create(); + if (typeof this.iHash.update !== "function") + throw new Error("Expected instance of class which extends utils.Hash"); + this.blockLen = this.iHash.blockLen; + this.outputLen = this.iHash.outputLen; + const blockLen = this.blockLen; + const pad = new Uint8Array(blockLen); + pad.set(key.length > blockLen ? hash.create().update(key).digest() : key); + for (let i = 0; i < pad.length; i++) + pad[i] ^= 54; + this.iHash.update(pad); + this.oHash = hash.create(); + for (let i = 0; i < pad.length; i++) + pad[i] ^= 54 ^ 92; + this.oHash.update(pad); + clean(pad); + } + update(buf) { + aexists(this); + this.iHash.update(buf); + return this; + } + digestInto(out) { + aexists(this); + aoutput(out, this); + this.finished = true; + const buf = out.subarray(0, this.outputLen); + this.iHash.digestInto(buf); + this.oHash.update(buf); + this.oHash.digestInto(buf); + this.destroy(); + } + digest() { + const out = new Uint8Array(this.oHash.outputLen); + this.digestInto(out); + return out; + } + _cloneInto(to) { + to || (to = Object.create(Object.getPrototypeOf(this), {})); + const { oHash, iHash, finished, destroyed, blockLen, outputLen } = this; + to = to; + to.finished = finished; + to.destroyed = destroyed; + to.blockLen = blockLen; + to.outputLen = outputLen; + to.oHash = oHash._cloneInto(to.oHash); + to.iHash = iHash._cloneInto(to.iHash); + return to; + } + clone() { + return this._cloneInto(); + } + destroy() { + this.destroyed = true; + this.oHash.destroy(); + this.iHash.destroy(); + } + }; + var hmac = /* @__PURE__ */ (() => { + const hmac_ = ((hash, key, message) => new _HMAC(hash, key).update(message).digest()); + hmac_.create = (hash, key) => new _HMAC(hash, key); + return hmac_; + })(); + + // node_modules/@noble/curves/abstract/weierstrass.js + /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ + var divNearest = (num2, den) => (num2 + (num2 >= 0 ? den : -den) / _2n2) / den; + function _splitEndoScalar(k, basis, n) { + aInRange("scalar", k, _0n4, n); + const [[a1, b1], [a2, b2]] = basis; + const c1 = divNearest(b2 * k, n); + const c2 = divNearest(-b1 * k, n); + let k1 = k - c1 * a1 - c2 * a2; + let k2 = -c1 * b1 - c2 * b2; + const k1neg = k1 < _0n4; + const k2neg = k2 < _0n4; + if (k1neg) + k1 = -k1; + if (k2neg) + k2 = -k2; + const MAX_NUM = bitMask(Math.ceil(bitLen(n) / 2)) + _1n4; + if (k1 < _0n4 || k1 >= MAX_NUM || k2 < _0n4 || k2 >= MAX_NUM) { + throw new Error("splitScalar (endomorphism): failed for k"); + } + return { k1neg, k1, k2neg, k2 }; + } + function validateSigFormat(format) { + if (!["compact", "recovered", "der"].includes(format)) + throw new Error('Signature format must be "compact", "recovered", or "der"'); + return format; + } + function validateSigOpts(opts, def) { + validateObject(opts); + const optsn = {}; + for (let optName of Object.keys(def)) { + optsn[optName] = opts[optName] === void 0 ? def[optName] : opts[optName]; + } + abool(optsn.lowS, "lowS"); + abool(optsn.prehash, "prehash"); + if (optsn.format !== void 0) + validateSigFormat(optsn.format); + return optsn; + } + var DERErr = class extends Error { + constructor(m = "") { + super(m); + } + }; + var DER = { + // asn.1 DER encoding utils + Err: DERErr, + // Basic building block is TLV (Tag-Length-Value) + _tlv: { + encode: (tag, data) => { + const { Err: E } = DER; + asafenumber(tag, "tag"); + if (tag < 0 || tag > 255) + throw new E("tlv.encode: wrong tag"); + if (typeof data !== "string") + throw new TypeError('"data" expected string, got type=' + typeof data); + if (data.length & 1) + throw new E("tlv.encode: unpadded data"); + const dataLen = data.length / 2; + const len = numberToHexUnpadded(dataLen); + if (len.length / 2 & 128) + throw new E("tlv.encode: long form length too big"); + const lenLen = dataLen > 127 ? numberToHexUnpadded(len.length / 2 | 128) : ""; + const t = numberToHexUnpadded(tag); + return t + lenLen + len + data; + }, + // v - value, l - left bytes (unparsed) + decode(tag, data) { + const { Err: E } = DER; + data = abytes2(data, void 0, "DER data"); + let pos = 0; + if (tag < 0 || tag > 255) + throw new E("tlv.encode: wrong tag"); + if (data.length < 2 || data[pos++] !== tag) + throw new E("tlv.decode: wrong tlv"); + const first = data[pos++]; + const isLong = !!(first & 128); + let length = 0; + if (!isLong) + length = first; + else { + const lenLen = first & 127; + if (!lenLen) + throw new E("tlv.decode(long): indefinite length not supported"); + if (lenLen > 4) + throw new E("tlv.decode(long): byte length is too big"); + const lengthBytes = data.subarray(pos, pos + lenLen); + if (lengthBytes.length !== lenLen) + throw new E("tlv.decode: length bytes not complete"); + if (lengthBytes[0] === 0) + throw new E("tlv.decode(long): zero leftmost byte"); + for (const b of lengthBytes) + length = length << 8 | b; + pos += lenLen; + if (length < 128) + throw new E("tlv.decode(long): not minimal encoding"); + } + const v = data.subarray(pos, pos + length); + if (v.length !== length) + throw new E("tlv.decode: wrong value length"); + return { v, l: data.subarray(pos + length) }; + } + }, + // https://crypto.stackexchange.com/a/57734 Leftmost bit of first byte is 'negative' flag, + // since we always use positive integers here. It must always be empty: + // - add zero byte if exists + // - if next byte doesn't have a flag, leading zero is not allowed (minimal encoding) + _int: { + encode(num2) { + const { Err: E } = DER; + abignumber(num2); + if (num2 < _0n4) + throw new E("integer: negative integers are not allowed"); + let hex = numberToHexUnpadded(num2); + if (Number.parseInt(hex[0], 16) & 8) + hex = "00" + hex; + if (hex.length & 1) + throw new E("unexpected DER parsing assertion: unpadded hex"); + return hex; + }, + decode(data) { + const { Err: E } = DER; + if (data.length < 1) + throw new E("invalid signature integer: empty"); + if (data[0] & 128) + throw new E("invalid signature integer: negative"); + if (data.length > 1 && data[0] === 0 && !(data[1] & 128)) + throw new E("invalid signature integer: unnecessary leading zero"); + return bytesToNumberBE(data); + } + }, + toSig(bytes) { + const { Err: E, _int: int, _tlv: tlv } = DER; + const data = abytes2(bytes, void 0, "signature"); + const { v: seqBytes, l: seqLeftBytes } = tlv.decode(48, data); + if (seqLeftBytes.length) + throw new E("invalid signature: left bytes after parsing"); + const { v: rBytes, l: rLeftBytes } = tlv.decode(2, seqBytes); + const { v: sBytes, l: sLeftBytes } = tlv.decode(2, rLeftBytes); + if (sLeftBytes.length) + throw new E("invalid signature: left bytes after parsing"); + return { r: int.decode(rBytes), s: int.decode(sBytes) }; + }, + hexFromSig(sig) { + const { _tlv: tlv, _int: int } = DER; + const rs = tlv.encode(2, int.encode(sig.r)); + const ss = tlv.encode(2, int.encode(sig.s)); + const seq = rs + ss; + return tlv.encode(48, seq); + } + }; + Object.freeze(DER._tlv); + Object.freeze(DER._int); + Object.freeze(DER); + var _0n4 = /* @__PURE__ */ BigInt(0); + var _1n4 = /* @__PURE__ */ BigInt(1); + var _2n2 = /* @__PURE__ */ BigInt(2); + var _3n2 = /* @__PURE__ */ BigInt(3); + var _4n2 = /* @__PURE__ */ BigInt(4); + function weierstrass(params, extraOpts = {}) { + const validated = createCurveFields("weierstrass", params, extraOpts); + const Fp = validated.Fp; + const Fn = validated.Fn; + let CURVE = validated.CURVE; + const { h: cofactor, n: CURVE_ORDER } = CURVE; + validateObject(extraOpts, {}, { + allowInfinityPoint: "boolean", + clearCofactor: "function", + isTorsionFree: "function", + fromBytes: "function", + toBytes: "function", + endo: "object" + }); + const { endo, allowInfinityPoint } = extraOpts; + if (endo) { + if (!Fp.is0(CURVE.a) || typeof endo.beta !== "bigint" || !Array.isArray(endo.basises)) { + throw new Error('invalid endo: expected "beta": bigint and "basises": array'); + } + } + const lengths = getWLengths(Fp, Fn); + function assertCompressionIsSupported() { + if (!Fp.isOdd) + throw new Error("compression is not supported: Field does not have .isOdd()"); + } + function pointToBytes2(_c, point, isCompressed) { + if (allowInfinityPoint && point.is0()) + return Uint8Array.of(0); + const { x, y } = point.toAffine(); + const bx = Fp.toBytes(x); + abool(isCompressed, "isCompressed"); + if (isCompressed) { + assertCompressionIsSupported(); + const hasEvenY = !Fp.isOdd(y); + return concatBytes2(pprefix(hasEvenY), bx); + } else { + return concatBytes2(Uint8Array.of(4), bx, Fp.toBytes(y)); + } + } + function pointFromBytes(bytes) { + abytes2(bytes, void 0, "Point"); + const { publicKey: comp, publicKeyUncompressed: uncomp } = lengths; + const length = bytes.length; + const head = bytes[0]; + const tail = bytes.subarray(1); + if (allowInfinityPoint && length === 1 && head === 0) + return { x: Fp.ZERO, y: Fp.ZERO }; + if (length === comp && (head === 2 || head === 3)) { + const x = Fp.fromBytes(tail); + if (!Fp.isValid(x)) + throw new Error("bad point: is not on curve, wrong x"); + const y2 = weierstrassEquation(x); + let y; + try { + y = Fp.sqrt(y2); + } catch (sqrtError) { + const err = sqrtError instanceof Error ? ": " + sqrtError.message : ""; + throw new Error("bad point: is not on curve, sqrt error" + err); + } + assertCompressionIsSupported(); + const evenY = Fp.isOdd(y); + const evenH = (head & 1) === 1; + if (evenH !== evenY) + y = Fp.neg(y); + return { x, y }; + } else if (length === uncomp && head === 4) { + const L = Fp.BYTES; + const x = Fp.fromBytes(tail.subarray(0, L)); + const y = Fp.fromBytes(tail.subarray(L, L * 2)); + if (!isValidXY(x, y)) + throw new Error("bad point: is not on curve"); + return { x, y }; + } else { + throw new Error(`bad point: got length ${length}, expected compressed=${comp} or uncompressed=${uncomp}`); + } + } + const encodePoint = extraOpts.toBytes === void 0 ? pointToBytes2 : extraOpts.toBytes; + const decodePoint = extraOpts.fromBytes === void 0 ? pointFromBytes : extraOpts.fromBytes; + function weierstrassEquation(x) { + const x2 = Fp.sqr(x); + const x3 = Fp.mul(x2, x); + return Fp.add(Fp.add(x3, Fp.mul(x, CURVE.a)), CURVE.b); + } + function isValidXY(x, y) { + const left = Fp.sqr(y); + const right = weierstrassEquation(x); + return Fp.eql(left, right); + } + if (!isValidXY(CURVE.Gx, CURVE.Gy)) + throw new Error("bad curve params: generator point"); + const _4a3 = Fp.mul(Fp.pow(CURVE.a, _3n2), _4n2); + const _27b2 = Fp.mul(Fp.sqr(CURVE.b), BigInt(27)); + if (Fp.is0(Fp.add(_4a3, _27b2))) + throw new Error("bad curve params: a or b"); + function acoord(title, n, banZero = false) { + if (!Fp.isValid(n) || banZero && Fp.is0(n)) + throw new Error(`bad point coordinate ${title}`); + return n; + } + function aprjpoint(other) { + if (!(other instanceof Point)) + throw new Error("Weierstrass Point expected"); + } + function splitEndoScalarN(k) { + if (!endo || !endo.basises) + throw new Error("no endo"); + return _splitEndoScalar(k, endo.basises, Fn.ORDER); + } + function finishEndo(endoBeta, k1p, k2p, k1neg, k2neg) { + k2p = new Point(Fp.mul(k2p.X, endoBeta), k2p.Y, k2p.Z); + k1p = negateCt(k1neg, k1p); + k2p = negateCt(k2neg, k2p); + return k1p.add(k2p); + } + const _Point = class _Point { + /** Does NOT validate if the point is valid. Use `.assertValidity()`. */ + constructor(X, Y, Z) { + __publicField(this, "X"); + __publicField(this, "Y"); + __publicField(this, "Z"); + this.X = acoord("x", X); + this.Y = acoord("y", Y, true); + this.Z = acoord("z", Z); + Object.freeze(this); + } + static CURVE() { + return CURVE; + } + /** Does NOT validate if the point is valid. Use `.assertValidity()`. */ + static fromAffine(p) { + const { x, y } = p || {}; + if (!p || !Fp.isValid(x) || !Fp.isValid(y)) + throw new Error("invalid affine point"); + if (p instanceof _Point) + throw new Error("projective point not allowed"); + if (Fp.is0(x) && Fp.is0(y)) + return _Point.ZERO; + return new _Point(x, y, Fp.ONE); + } + static fromBytes(bytes) { + const P = _Point.fromAffine(decodePoint(abytes2(bytes, void 0, "point"))); + P.assertValidity(); + return P; + } + static fromHex(hex) { + return _Point.fromBytes(hexToBytes2(hex)); + } + get x() { + return this.toAffine().x; + } + get y() { + return this.toAffine().y; + } + /** + * + * @param windowSize + * @param isLazy - true will defer table computation until the first multiplication + * @returns + */ + precompute(windowSize = 8, isLazy = true) { + wnaf.createCache(this, windowSize); + if (!isLazy) + this.multiply(_3n2); + return this; + } + // TODO: return `this` + /** A point on curve is valid if it conforms to equation. */ + assertValidity() { + const p = this; + if (p.is0()) { + if (extraOpts.allowInfinityPoint && Fp.is0(p.X) && Fp.eql(p.Y, Fp.ONE) && Fp.is0(p.Z)) + return; + throw new Error("bad point: ZERO"); + } + const { x, y } = p.toAffine(); + if (!Fp.isValid(x) || !Fp.isValid(y)) + throw new Error("bad point: x or y not field elements"); + if (!isValidXY(x, y)) + throw new Error("bad point: equation left != right"); + if (!p.isTorsionFree()) + throw new Error("bad point: not in prime-order subgroup"); + } + hasEvenY() { + const { y } = this.toAffine(); + if (!Fp.isOdd) + throw new Error("Field doesn't support isOdd"); + return !Fp.isOdd(y); + } + /** Compare one point to another. */ + equals(other) { + aprjpoint(other); + const { X: X1, Y: Y1, Z: Z1 } = this; + const { X: X2, Y: Y2, Z: Z2 } = other; + const U1 = Fp.eql(Fp.mul(X1, Z2), Fp.mul(X2, Z1)); + const U2 = Fp.eql(Fp.mul(Y1, Z2), Fp.mul(Y2, Z1)); + return U1 && U2; + } + /** Flips point to one corresponding to (x, -y) in Affine coordinates. */ + negate() { + return new _Point(this.X, Fp.neg(this.Y), this.Z); + } + // Renes-Costello-Batina exception-free doubling formula. + // There is 30% faster Jacobian formula, but it is not complete. + // https://eprint.iacr.org/2015/1060, algorithm 3 + // Cost: 8M + 3S + 3*a + 2*b3 + 15add. + double() { + const { a, b } = CURVE; + const b3 = Fp.mul(b, _3n2); + const { X: X1, Y: Y1, Z: Z1 } = this; + let X3 = Fp.ZERO, Y3 = Fp.ZERO, Z3 = Fp.ZERO; + let t0 = Fp.mul(X1, X1); + let t1 = Fp.mul(Y1, Y1); + let t2 = Fp.mul(Z1, Z1); + let t3 = Fp.mul(X1, Y1); + t3 = Fp.add(t3, t3); + Z3 = Fp.mul(X1, Z1); + Z3 = Fp.add(Z3, Z3); + X3 = Fp.mul(a, Z3); + Y3 = Fp.mul(b3, t2); + Y3 = Fp.add(X3, Y3); + X3 = Fp.sub(t1, Y3); + Y3 = Fp.add(t1, Y3); + Y3 = Fp.mul(X3, Y3); + X3 = Fp.mul(t3, X3); + Z3 = Fp.mul(b3, Z3); + t2 = Fp.mul(a, t2); + t3 = Fp.sub(t0, t2); + t3 = Fp.mul(a, t3); + t3 = Fp.add(t3, Z3); + Z3 = Fp.add(t0, t0); + t0 = Fp.add(Z3, t0); + t0 = Fp.add(t0, t2); + t0 = Fp.mul(t0, t3); + Y3 = Fp.add(Y3, t0); + t2 = Fp.mul(Y1, Z1); + t2 = Fp.add(t2, t2); + t0 = Fp.mul(t2, t3); + X3 = Fp.sub(X3, t0); + Z3 = Fp.mul(t2, t1); + Z3 = Fp.add(Z3, Z3); + Z3 = Fp.add(Z3, Z3); + return new _Point(X3, Y3, Z3); + } + // Renes-Costello-Batina exception-free addition formula. + // There is 30% faster Jacobian formula, but it is not complete. + // https://eprint.iacr.org/2015/1060, algorithm 1 + // Cost: 12M + 0S + 3*a + 3*b3 + 23add. + add(other) { + aprjpoint(other); + const { X: X1, Y: Y1, Z: Z1 } = this; + const { X: X2, Y: Y2, Z: Z2 } = other; + let X3 = Fp.ZERO, Y3 = Fp.ZERO, Z3 = Fp.ZERO; + const a = CURVE.a; + const b3 = Fp.mul(CURVE.b, _3n2); + let t0 = Fp.mul(X1, X2); + let t1 = Fp.mul(Y1, Y2); + let t2 = Fp.mul(Z1, Z2); + let t3 = Fp.add(X1, Y1); + let t4 = Fp.add(X2, Y2); + t3 = Fp.mul(t3, t4); + t4 = Fp.add(t0, t1); + t3 = Fp.sub(t3, t4); + t4 = Fp.add(X1, Z1); + let t5 = Fp.add(X2, Z2); + t4 = Fp.mul(t4, t5); + t5 = Fp.add(t0, t2); + t4 = Fp.sub(t4, t5); + t5 = Fp.add(Y1, Z1); + X3 = Fp.add(Y2, Z2); + t5 = Fp.mul(t5, X3); + X3 = Fp.add(t1, t2); + t5 = Fp.sub(t5, X3); + Z3 = Fp.mul(a, t4); + X3 = Fp.mul(b3, t2); + Z3 = Fp.add(X3, Z3); + X3 = Fp.sub(t1, Z3); + Z3 = Fp.add(t1, Z3); + Y3 = Fp.mul(X3, Z3); + t1 = Fp.add(t0, t0); + t1 = Fp.add(t1, t0); + t2 = Fp.mul(a, t2); + t4 = Fp.mul(b3, t4); + t1 = Fp.add(t1, t2); + t2 = Fp.sub(t0, t2); + t2 = Fp.mul(a, t2); + t4 = Fp.add(t4, t2); + t0 = Fp.mul(t1, t4); + Y3 = Fp.add(Y3, t0); + t0 = Fp.mul(t5, t4); + X3 = Fp.mul(t3, X3); + X3 = Fp.sub(X3, t0); + t0 = Fp.mul(t3, t1); + Z3 = Fp.mul(t5, Z3); + Z3 = Fp.add(Z3, t0); + return new _Point(X3, Y3, Z3); + } + subtract(other) { + aprjpoint(other); + return this.add(other.negate()); + } + is0() { + return this.equals(_Point.ZERO); + } + /** + * Constant time multiplication. + * Uses wNAF method. Windowed method may be 10% faster, + * but takes 2x longer to generate and consumes 2x memory. + * Uses precomputes when available. + * Uses endomorphism for Koblitz curves. + * @param scalar - by which the point would be multiplied + * @returns New point + */ + multiply(scalar) { + const { endo: endo2 } = extraOpts; + if (!Fn.isValidNot0(scalar)) + throw new RangeError("invalid scalar: out of range"); + let point, fake; + const mul = (n) => wnaf.cached(this, n, (p) => normalizeZ(_Point, p)); + if (endo2) { + const { k1neg, k1, k2neg, k2 } = splitEndoScalarN(scalar); + const { p: k1p, f: k1f } = mul(k1); + const { p: k2p, f: k2f } = mul(k2); + fake = k1f.add(k2f); + point = finishEndo(endo2.beta, k1p, k2p, k1neg, k2neg); + } else { + const { p, f } = mul(scalar); + point = p; + fake = f; + } + return normalizeZ(_Point, [point, fake])[0]; + } + /** + * Non-constant-time multiplication. Uses double-and-add algorithm. + * It's faster, but should only be used when you don't care about + * an exposed secret key e.g. sig verification, which works over *public* keys. + */ + multiplyUnsafe(scalar) { + const { endo: endo2 } = extraOpts; + const p = this; + const sc = scalar; + if (!Fn.isValid(sc)) + throw new RangeError("invalid scalar: out of range"); + if (sc === _0n4 || p.is0()) + return _Point.ZERO; + if (sc === _1n4) + return p; + if (wnaf.hasCache(this)) + return this.multiply(sc); + if (endo2) { + const { k1neg, k1, k2neg, k2 } = splitEndoScalarN(sc); + const { p1, p2 } = mulEndoUnsafe(_Point, p, k1, k2); + return finishEndo(endo2.beta, p1, p2, k1neg, k2neg); + } else { + return wnaf.unsafe(p, sc); + } + } + /** + * Converts Projective point to affine (x, y) coordinates. + * (X, Y, Z) ∋ (x=X/Z, y=Y/Z). + * @param invertedZ - Z^-1 (inverted zero) - optional, precomputation is useful for invertBatch + */ + toAffine(invertedZ) { + const p = this; + let iz = invertedZ; + const { X, Y, Z } = p; + if (Fp.eql(Z, Fp.ONE)) + return { x: X, y: Y }; + const is0 = p.is0(); + if (iz == null) + iz = is0 ? Fp.ONE : Fp.inv(Z); + const x = Fp.mul(X, iz); + const y = Fp.mul(Y, iz); + const zz = Fp.mul(Z, iz); + if (is0) + return { x: Fp.ZERO, y: Fp.ZERO }; + if (!Fp.eql(zz, Fp.ONE)) + throw new Error("invZ was invalid"); + return { x, y }; + } + /** + * Checks whether Point is free of torsion elements (is in prime subgroup). + * Always torsion-free for cofactor=1 curves. + */ + isTorsionFree() { + const { isTorsionFree } = extraOpts; + if (cofactor === _1n4) + return true; + if (isTorsionFree) + return isTorsionFree(_Point, this); + return wnaf.unsafe(this, CURVE_ORDER).is0(); + } + clearCofactor() { + const { clearCofactor } = extraOpts; + if (cofactor === _1n4) + return this; + if (clearCofactor) + return clearCofactor(_Point, this); + return this.multiplyUnsafe(cofactor); + } + isSmallOrder() { + if (cofactor === _1n4) + return this.is0(); + return this.clearCofactor().is0(); + } + toBytes(isCompressed = true) { + abool(isCompressed, "isCompressed"); + this.assertValidity(); + return encodePoint(_Point, this, isCompressed); + } + toHex(isCompressed = true) { + return bytesToHex2(this.toBytes(isCompressed)); + } + toString() { + return ``; + } + }; + // base / generator point + __publicField(_Point, "BASE", new _Point(CURVE.Gx, CURVE.Gy, Fp.ONE)); + // zero / infinity / identity point + __publicField(_Point, "ZERO", new _Point(Fp.ZERO, Fp.ONE, Fp.ZERO)); + // 0, 1, 0 + // math field + __publicField(_Point, "Fp", Fp); + // scalar field + __publicField(_Point, "Fn", Fn); + let Point = _Point; + const bits = Fn.BITS; + const wnaf = new wNAF(Point, extraOpts.endo ? Math.ceil(bits / 2) : bits); + if (bits >= 8) + Point.BASE.precompute(8); + Object.freeze(Point.prototype); + Object.freeze(Point); + return Point; + } + function pprefix(hasEvenY) { + return Uint8Array.of(hasEvenY ? 2 : 3); + } + function getWLengths(Fp, Fn) { + return { + secretKey: Fn.BYTES, + publicKey: 1 + Fp.BYTES, + publicKeyUncompressed: 1 + 2 * Fp.BYTES, + publicKeyHasPrefix: true, + // Raw compact `(r || s)` signature width; DER and recovered signatures use + // different lengths outside this helper. + signature: 2 * Fn.BYTES + }; + } + function ecdh(Point, ecdhOpts = {}) { + const { Fn } = Point; + const randomBytes_ = ecdhOpts.randomBytes === void 0 ? randomBytes2 : ecdhOpts.randomBytes; + const lengths = Object.assign(getWLengths(Point.Fp, Fn), { + seed: Math.max(getMinHashLength(Fn.ORDER), 16) + }); + function isValidSecretKey(secretKey) { + try { + const num2 = Fn.fromBytes(secretKey); + return Fn.isValidNot0(num2); + } catch (error) { + return false; + } + } + function isValidPublicKey(publicKey, isCompressed) { + const { publicKey: comp, publicKeyUncompressed } = lengths; + try { + const l = publicKey.length; + if (isCompressed === true && l !== comp) + return false; + if (isCompressed === false && l !== publicKeyUncompressed) + return false; + return !!Point.fromBytes(publicKey); + } catch (error) { + return false; + } + } + function randomSecretKey(seed) { + seed = seed === void 0 ? randomBytes_(lengths.seed) : seed; + return mapHashToField(abytes2(seed, lengths.seed, "seed"), Fn.ORDER); + } + function getPublicKey(secretKey, isCompressed = true) { + return Point.BASE.multiply(Fn.fromBytes(secretKey)).toBytes(isCompressed); + } + function isProbPub(item) { + const { secretKey, publicKey, publicKeyUncompressed } = lengths; + const allowedLengths = Fn._lengths; + if (!isBytes2(item)) + return void 0; + const l = abytes2(item, void 0, "key").length; + const isPub = l === publicKey || l === publicKeyUncompressed; + const isSec = l === secretKey || !!allowedLengths?.includes(l); + if (isPub && isSec) + return void 0; + return isPub; + } + function getSharedSecret(secretKeyA, publicKeyB, isCompressed = true) { + if (isProbPub(secretKeyA) === true) + throw new Error("first arg must be private key"); + if (isProbPub(publicKeyB) === false) + throw new Error("second arg must be public key"); + const s = Fn.fromBytes(secretKeyA); + const b = Point.fromBytes(publicKeyB); + return b.multiply(s).toBytes(isCompressed); + } + const utils = { + isValidSecretKey, + isValidPublicKey, + randomSecretKey + }; + const keygen = createKeygen(randomSecretKey, getPublicKey); + Object.freeze(utils); + Object.freeze(lengths); + return Object.freeze({ getPublicKey, getSharedSecret, keygen, Point, utils, lengths }); + } + function ecdsa(Point, hash, ecdsaOpts = {}) { + const hash_ = hash; + ahash(hash_); + validateObject(ecdsaOpts, {}, { + hmac: "function", + lowS: "boolean", + randomBytes: "function", + bits2int: "function", + bits2int_modN: "function" + }); + ecdsaOpts = Object.assign({}, ecdsaOpts); + const randomBytes5 = ecdsaOpts.randomBytes === void 0 ? randomBytes2 : ecdsaOpts.randomBytes; + const hmac2 = ecdsaOpts.hmac === void 0 ? (key, msg) => hmac(hash_, key, msg) : ecdsaOpts.hmac; + const { Fp, Fn } = Point; + const { ORDER: CURVE_ORDER, BITS: fnBits } = Fn; + const { keygen, getPublicKey, getSharedSecret, utils, lengths } = ecdh(Point, ecdsaOpts); + const defaultSigOpts = { + prehash: true, + lowS: typeof ecdsaOpts.lowS === "boolean" ? ecdsaOpts.lowS : true, + format: "compact", + extraEntropy: false + }; + const hasLargeRecoveryLifts = CURVE_ORDER * _2n2 + _1n4 < Fp.ORDER; + function isBiggerThanHalfOrder(number) { + const HALF = CURVE_ORDER >> _1n4; + return number > HALF; + } + function validateRS(title, num2) { + if (!Fn.isValidNot0(num2)) + throw new Error(`invalid signature ${title}: out of range 1..Point.Fn.ORDER`); + return num2; + } + function assertRecoverableCurve() { + if (hasLargeRecoveryLifts) + throw new Error('"recovered" sig type is not supported for cofactor >2 curves'); + } + function validateSigLength(bytes, format) { + validateSigFormat(format); + const size = lengths.signature; + const sizer = format === "compact" ? size : format === "recovered" ? size + 1 : void 0; + return abytes2(bytes, sizer); + } + class Signature { + constructor(r, s, recovery) { + __publicField(this, "r"); + __publicField(this, "s"); + __publicField(this, "recovery"); + this.r = validateRS("r", r); + this.s = validateRS("s", s); + if (recovery != null) { + assertRecoverableCurve(); + if (![0, 1, 2, 3].includes(recovery)) + throw new Error("invalid recovery id"); + this.recovery = recovery; + } + Object.freeze(this); + } + static fromBytes(bytes, format = defaultSigOpts.format) { + validateSigLength(bytes, format); + let recid; + if (format === "der") { + const { r: r2, s: s2 } = DER.toSig(abytes2(bytes)); + return new Signature(r2, s2); + } + if (format === "recovered") { + recid = bytes[0]; + format = "compact"; + bytes = bytes.subarray(1); + } + const L = lengths.signature / 2; + const r = bytes.subarray(0, L); + const s = bytes.subarray(L, L * 2); + return new Signature(Fn.fromBytes(r), Fn.fromBytes(s), recid); + } + static fromHex(hex, format) { + return this.fromBytes(hexToBytes2(hex), format); + } + assertRecovery() { + const { recovery } = this; + if (recovery == null) + throw new Error("invalid recovery id: must be present"); + return recovery; + } + addRecoveryBit(recovery) { + return new Signature(this.r, this.s, recovery); + } + // Unlike the top-level helper below, this method expects a digest that has + // already been hashed to the curve's message representative. + recoverPublicKey(messageHash) { + const { r, s } = this; + const recovery = this.assertRecovery(); + const radj = recovery === 2 || recovery === 3 ? r + CURVE_ORDER : r; + if (!Fp.isValid(radj)) + throw new Error("invalid recovery id: sig.r+curve.n != R.x"); + const x = Fp.toBytes(radj); + const R = Point.fromBytes(concatBytes2(pprefix((recovery & 1) === 0), x)); + const ir = Fn.inv(radj); + const h = bits2int_modN(abytes2(messageHash, void 0, "msgHash")); + const u1 = Fn.create(-h * ir); + const u2 = Fn.create(s * ir); + const Q = Point.BASE.multiplyUnsafe(u1).add(R.multiplyUnsafe(u2)); + if (Q.is0()) + throw new Error("invalid recovery: point at infinify"); + Q.assertValidity(); + return Q; + } + // Signatures should be low-s, to prevent malleability. + hasHighS() { + return isBiggerThanHalfOrder(this.s); + } + toBytes(format = defaultSigOpts.format) { + validateSigFormat(format); + if (format === "der") + return hexToBytes2(DER.hexFromSig(this)); + const { r, s } = this; + const rb = Fn.toBytes(r); + const sb = Fn.toBytes(s); + if (format === "recovered") { + assertRecoverableCurve(); + return concatBytes2(Uint8Array.of(this.assertRecovery()), rb, sb); + } + return concatBytes2(rb, sb); + } + toHex(format) { + return bytesToHex2(this.toBytes(format)); + } + } + Object.freeze(Signature.prototype); + Object.freeze(Signature); + const bits2int = ecdsaOpts.bits2int === void 0 ? function bits2int_def(bytes) { + if (bytes.length > 8192) + throw new Error("input is too large"); + const num2 = bytesToNumberBE(bytes); + const delta = bytes.length * 8 - fnBits; + return delta > 0 ? num2 >> BigInt(delta) : num2; + } : ecdsaOpts.bits2int; + const bits2int_modN = ecdsaOpts.bits2int_modN === void 0 ? function bits2int_modN_def(bytes) { + return Fn.create(bits2int(bytes)); + } : ecdsaOpts.bits2int_modN; + const ORDER_MASK = bitMask(fnBits); + function int2octets(num2) { + aInRange("num < 2^" + fnBits, num2, _0n4, ORDER_MASK); + return Fn.toBytes(num2); + } + function validateMsgAndHash(message, prehash) { + abytes2(message, void 0, "message"); + return prehash ? abytes2(hash_(message), void 0, "prehashed message") : message; + } + function prepSig(message, secretKey, opts) { + const { lowS, prehash, extraEntropy } = validateSigOpts(opts, defaultSigOpts); + message = validateMsgAndHash(message, prehash); + const h1int = bits2int_modN(message); + const d = Fn.fromBytes(secretKey); + if (!Fn.isValidNot0(d)) + throw new Error("invalid private key"); + const seedArgs = [int2octets(d), int2octets(h1int)]; + if (extraEntropy != null && extraEntropy !== false) { + const e = extraEntropy === true ? randomBytes5(lengths.secretKey) : extraEntropy; + seedArgs.push(abytes2(e, void 0, "extraEntropy")); + } + const seed = concatBytes2(...seedArgs); + const m = h1int; + function k2sig(kBytes) { + const k = bits2int(kBytes); + if (!Fn.isValidNot0(k)) + return; + const ik = Fn.inv(k); + const q = Point.BASE.multiply(k).toAffine(); + const r = Fn.create(q.x); + if (r === _0n4) + return; + const s = Fn.create(ik * Fn.create(m + r * d)); + if (s === _0n4) + return; + let recovery = (q.x === r ? 0 : 2) | Number(q.y & _1n4); + let normS = s; + if (lowS && isBiggerThanHalfOrder(s)) { + normS = Fn.neg(s); + recovery ^= 1; + } + return new Signature(r, normS, hasLargeRecoveryLifts ? void 0 : recovery); + } + return { seed, k2sig }; + } + function sign(message, secretKey, opts = {}) { + const { seed, k2sig } = prepSig(message, secretKey, opts); + const drbg = createHmacDrbg(hash_.outputLen, Fn.BYTES, hmac2); + const sig = drbg(seed, k2sig); + return sig.toBytes(opts.format); + } + function verify(signature, message, publicKey, opts = {}) { + const { lowS, prehash, format } = validateSigOpts(opts, defaultSigOpts); + publicKey = abytes2(publicKey, void 0, "publicKey"); + message = validateMsgAndHash(message, prehash); + if (!isBytes2(signature)) { + const end = signature instanceof Signature ? ", use sig.toBytes()" : ""; + throw new Error("verify expects Uint8Array signature" + end); + } + validateSigLength(signature, format); + try { + const sig = Signature.fromBytes(signature, format); + const P = Point.fromBytes(publicKey); + if (lowS && sig.hasHighS()) + return false; + const { r, s } = sig; + const h = bits2int_modN(message); + const is = Fn.inv(s); + const u1 = Fn.create(h * is); + const u2 = Fn.create(r * is); + const R = Point.BASE.multiplyUnsafe(u1).add(P.multiplyUnsafe(u2)); + if (R.is0()) + return false; + const v = Fn.create(R.x); + return v === r; + } catch (e) { + return false; + } + } + function recoverPublicKey(signature, message, opts = {}) { + const { prehash } = validateSigOpts(opts, defaultSigOpts); + message = validateMsgAndHash(message, prehash); + return Signature.fromBytes(signature, "recovered").recoverPublicKey(message).toBytes(); + } + return Object.freeze({ + keygen, + getPublicKey, + getSharedSecret, + utils, + lengths, + Point, + sign, + verify, + recoverPublicKey, + Signature, + hash: hash_ + }); + } + + // node_modules/@noble/curves/secp256k1.js + /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ + var secp256k1_CURVE = { + p: BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"), + n: BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"), + h: BigInt(1), + a: BigInt(0), + b: BigInt(7), + Gx: BigInt("0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), + Gy: BigInt("0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8") + }; + var secp256k1_ENDO = { + beta: BigInt("0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"), + basises: [ + [BigInt("0x3086d221a7d46bcde86c90e49284eb15"), -BigInt("0xe4437ed6010e88286f547fa90abfe4c3")], + [BigInt("0x114ca50f7a8e2f3f657c1108d9d44cfd8"), BigInt("0x3086d221a7d46bcde86c90e49284eb15")] + ] + }; + var _0n5 = /* @__PURE__ */ BigInt(0); + var _2n3 = /* @__PURE__ */ BigInt(2); + function sqrtMod(y) { + const P = secp256k1_CURVE.p; + const _3n3 = BigInt(3), _6n = BigInt(6), _11n = BigInt(11), _22n = BigInt(22); + const _23n = BigInt(23), _44n = BigInt(44), _88n = BigInt(88); + const b2 = y * y * y % P; + const b3 = b2 * b2 * y % P; + const b6 = pow2(b3, _3n3, P) * b3 % P; + const b9 = pow2(b6, _3n3, P) * b3 % P; + const b11 = pow2(b9, _2n3, P) * b2 % P; + const b22 = pow2(b11, _11n, P) * b11 % P; + const b44 = pow2(b22, _22n, P) * b22 % P; + const b88 = pow2(b44, _44n, P) * b44 % P; + const b176 = pow2(b88, _88n, P) * b88 % P; + const b220 = pow2(b176, _44n, P) * b44 % P; + const b223 = pow2(b220, _3n3, P) * b3 % P; + const t1 = pow2(b223, _23n, P) * b22 % P; + const t2 = pow2(t1, _6n, P) * b2 % P; + const root = pow2(t2, _2n3, P); + if (!Fpk1.eql(Fpk1.sqr(root), y)) + throw new Error("Cannot find square root"); + return root; + } + var Fpk1 = Field(secp256k1_CURVE.p, { sqrt: sqrtMod }); + var Pointk1 = /* @__PURE__ */ weierstrass(secp256k1_CURVE, { + Fp: Fpk1, + endo: secp256k1_ENDO + }); + var secp256k1 = /* @__PURE__ */ ecdsa(Pointk1, sha256); + var TAGGED_HASH_PREFIXES = {}; + function taggedHash(tag, ...messages) { + let tagP = TAGGED_HASH_PREFIXES[tag]; + if (tagP === void 0) { + const tagH = sha256(asciiToBytes(tag)); + tagP = concatBytes2(tagH, tagH); + TAGGED_HASH_PREFIXES[tag] = tagP; + } + return sha256(concatBytes2(tagP, ...messages)); + } + var pointToBytes = (point) => point.toBytes(true).slice(1); + var hasEven = (y) => y % _2n3 === _0n5; + function schnorrGetExtPubKey(priv) { + const { Fn, BASE } = Pointk1; + const d_ = Fn.fromBytes(priv); + const p = BASE.multiply(d_); + const scalar = hasEven(p.y) ? d_ : Fn.neg(d_); + return { scalar, bytes: pointToBytes(p) }; + } + function lift_x(x) { + const Fp = Fpk1; + if (!Fp.isValidNot0(x)) + throw new Error("invalid x: Fail if x ≥ p"); + const xx = Fp.create(x * x); + const c = Fp.create(xx * x + BigInt(7)); + let y = Fp.sqrt(c); + if (!hasEven(y)) + y = Fp.neg(y); + const p = Pointk1.fromAffine({ x, y }); + p.assertValidity(); + return p; + } + var num = bytesToNumberBE; + function challenge(...args) { + return Pointk1.Fn.create(num(taggedHash("BIP0340/challenge", ...args))); + } + function schnorrGetPublicKey(secretKey) { + return schnorrGetExtPubKey(secretKey).bytes; + } + function schnorrSign(message, secretKey, auxRand = randomBytes(32)) { + const { Fn, BASE } = Pointk1; + const m = abytes2(message, void 0, "message"); + const { bytes: px, scalar: d } = schnorrGetExtPubKey(secretKey); + const a = abytes2(auxRand, 32, "auxRand"); + const t = Fn.toBytes(d ^ num(taggedHash("BIP0340/aux", a))); + const rand = taggedHash("BIP0340/nonce", t, px, m); + const k_ = Fn.create(num(rand)); + if (k_ === 0n) + throw new Error("sign failed: k is zero"); + const p = BASE.multiply(k_); + const k = hasEven(p.y) ? k_ : Fn.neg(k_); + const rx = pointToBytes(p); + const e = challenge(rx, px, m); + const sig = new Uint8Array(64); + sig.set(rx, 0); + sig.set(Fn.toBytes(Fn.create(k + e * d)), 32); + if (!schnorrVerify(sig, m, px)) + throw new Error("sign: Invalid signature produced"); + return sig; + } + function schnorrVerify(signature, message, publicKey) { + const { Fp, Fn, BASE } = Pointk1; + const sig = abytes2(signature, 64, "signature"); + const m = abytes2(message, void 0, "message"); + const pub = abytes2(publicKey, 32, "publicKey"); + try { + const P = lift_x(num(pub)); + const r = num(sig.subarray(0, 32)); + if (!Fp.isValidNot0(r)) + return false; + const s = num(sig.subarray(32, 64)); + if (!Fn.isValidNot0(s)) + return false; + const e = challenge(Fn.toBytes(r), pointToBytes(P), m); + const R = BASE.multiplyUnsafe(s).add(P.multiplyUnsafe(Fn.neg(e))); + const { x, y } = R.toAffine(); + if (R.is0() || !hasEven(y) || x !== r) + return false; + return true; + } catch (error) { + return false; + } + } + var schnorr = /* @__PURE__ */ (() => { + const size = 32; + const seedLength = 48; + const randomSecretKey = (seed) => { + seed = seed === void 0 ? randomBytes(seedLength) : seed; + return mapHashToField(seed, secp256k1_CURVE.n); + }; + return Object.freeze({ + keygen: createKeygen(randomSecretKey, schnorrGetPublicKey), + getPublicKey: schnorrGetPublicKey, + sign: schnorrSign, + verify: schnorrVerify, + Point: Pointk1, + utils: Object.freeze({ + randomSecretKey, + taggedHash, + lift_x, + pointToBytes + }), + lengths: Object.freeze({ + secretKey: size, + publicKey: size, + publicKeyHasPrefix: false, + signature: size * 2, + seed: seedLength + }) + }); + })(); + + // node_modules/@noble/hashes/hkdf.js + function extract(hash, ikm, salt) { + ahash(hash); + if (salt === void 0) + salt = new Uint8Array(hash.outputLen); + return hmac(hash, salt, ikm); + } + var HKDF_COUNTER = /* @__PURE__ */ Uint8Array.of(0); + var EMPTY_BUFFER = /* @__PURE__ */ Uint8Array.of(); + function expand(hash, prk, info, length = 32) { + ahash(hash); + anumber(length, "length"); + abytes(prk, void 0, "prk"); + const olen = hash.outputLen; + if (prk.length < olen) + throw new Error('"prk" must be at least HashLen octets'); + if (length > 255 * olen) + throw new Error("Length must be <= 255*HashLen"); + const blocks = Math.ceil(length / olen); + if (info === void 0) + info = EMPTY_BUFFER; + else + abytes(info, void 0, "info"); + const okm = new Uint8Array(blocks * olen); + const HMAC = hmac.create(hash, prk); + const HMACTmp = HMAC._cloneInto(); + const T = new Uint8Array(HMAC.outputLen); + for (let counter = 0; counter < blocks; counter++) { + HKDF_COUNTER[0] = counter + 1; + HMACTmp.update(counter === 0 ? EMPTY_BUFFER : T).update(info).update(HKDF_COUNTER).digestInto(T); + okm.set(T, olen * counter); + HMAC._cloneInto(HMACTmp); + } + HMAC.destroy(); + HMACTmp.destroy(); + clean(T, HKDF_COUNTER); + return okm.slice(0, length); + } + + // node_modules/@noble/ciphers/utils.js + /*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) */ + function isBytes3(a) { + return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array"; + } + function abool2(b) { + if (typeof b !== "boolean") + throw new Error(`boolean expected, not ${b}`); + } + function anumber3(n) { + if (!Number.isSafeInteger(n) || n < 0) + throw new Error("positive integer expected, got " + n); + } + function abytes3(value, length, title = "") { + const bytes = isBytes3(value); + const len = value?.length; + const needsLen = length !== void 0; + if (!bytes || needsLen && len !== length) { + const prefix = title && `"${title}" `; + const ofLen = needsLen ? ` of length ${length}` : ""; + const got = bytes ? `length=${len}` : `type=${typeof value}`; + throw new Error(prefix + "expected Uint8Array" + ofLen + ", got " + got); + } + return value; + } + function aexists2(instance, checkFinished = true) { + if (instance.destroyed) + throw new Error("Hash instance has been destroyed"); + if (checkFinished && instance.finished) + throw new Error("Hash#digest() has already been called"); + } + function aoutput2(out, instance) { + abytes3(out, void 0, "output"); + const min = instance.outputLen; + if (out.length < min) { + throw new Error("digestInto() expects output buffer of length at least " + min); + } + } + function u32(arr) { + return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4)); + } + function clean2(...arrays) { + for (let i = 0; i < arrays.length; i++) { + arrays[i].fill(0); + } + } + function createView2(arr) { + return new DataView(arr.buffer, arr.byteOffset, arr.byteLength); + } + var isLE = /* @__PURE__ */ (() => new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68)(); + function checkOpts(defaults, opts) { + if (opts == null || typeof opts !== "object") + throw new Error("options must be defined"); + const merged = Object.assign(defaults, opts); + return merged; + } + function equalBytes(a, b) { + if (a.length !== b.length) + return false; + let diff = 0; + for (let i = 0; i < a.length; i++) + diff |= a[i] ^ b[i]; + return diff === 0; + } + var wrapCipher = /* @__NO_SIDE_EFFECTS__ */ (params, constructor) => { + function wrappedCipher(key, ...args) { + abytes3(key, void 0, "key"); + if (!isLE) + throw new Error("Non little-endian hardware is not yet supported"); + if (params.nonceLength !== void 0) { + const nonce = args[0]; + abytes3(nonce, params.varSizeNonce ? void 0 : params.nonceLength, "nonce"); + } + const tagl = params.tagLength; + if (tagl && args[1] !== void 0) + abytes3(args[1], void 0, "AAD"); + const cipher = constructor(key, ...args); + const checkOutput = (fnLength, output) => { + if (output !== void 0) { + if (fnLength !== 2) + throw new Error("cipher output not supported"); + abytes3(output, void 0, "output"); + } + }; + let called = false; + const wrCipher = { + encrypt(data, output) { + if (called) + throw new Error("cannot encrypt() twice with same key + nonce"); + called = true; + abytes3(data); + checkOutput(cipher.encrypt.length, output); + return cipher.encrypt(data, output); + }, + decrypt(data, output) { + abytes3(data); + if (tagl && data.length < tagl) + throw new Error('"ciphertext" expected length bigger than tagLength=' + tagl); + checkOutput(cipher.decrypt.length, output); + return cipher.decrypt(data, output); + } + }; + return wrCipher; + } + Object.assign(wrappedCipher, params); + return wrappedCipher; + }; + function getOutput(expectedLength, out, onlyAligned = true) { + if (out === void 0) + return new Uint8Array(expectedLength); + if (out.length !== expectedLength) + throw new Error('"output" expected Uint8Array of length ' + expectedLength + ", got: " + out.length); + if (onlyAligned && !isAligned32(out)) + throw new Error("invalid output, must be aligned"); + return out; + } + function u64Lengths(dataLength, aadLength, isLE2) { + abool2(isLE2); + const num2 = new Uint8Array(16); + const view = createView2(num2); + view.setBigUint64(0, BigInt(aadLength), isLE2); + view.setBigUint64(8, BigInt(dataLength), isLE2); + return num2; + } + function isAligned32(bytes) { + return bytes.byteOffset % 4 === 0; + } + function copyBytes2(bytes) { + return Uint8Array.from(bytes); + } + + // node_modules/@noble/ciphers/_arx.js + var encodeStr = (str) => Uint8Array.from(str.split(""), (c) => c.charCodeAt(0)); + var sigma16 = encodeStr("expand 16-byte k"); + var sigma32 = encodeStr("expand 32-byte k"); + var sigma16_32 = u32(sigma16); + var sigma32_32 = u32(sigma32); + function rotl(a, b) { + return a << b | a >>> 32 - b; + } + function isAligned322(b) { + return b.byteOffset % 4 === 0; + } + var BLOCK_LEN = 64; + var BLOCK_LEN32 = 16; + var MAX_COUNTER = 2 ** 32 - 1; + var U32_EMPTY = Uint32Array.of(); + function runCipher(core, sigma, key, nonce, data, output, counter, rounds) { + const len = data.length; + const block = new Uint8Array(BLOCK_LEN); + const b32 = u32(block); + const isAligned = isAligned322(data) && isAligned322(output); + const d32 = isAligned ? u32(data) : U32_EMPTY; + const o32 = isAligned ? u32(output) : U32_EMPTY; + for (let pos = 0; pos < len; counter++) { + core(sigma, key, nonce, b32, counter, rounds); + if (counter >= MAX_COUNTER) + throw new Error("arx: counter overflow"); + const take = Math.min(BLOCK_LEN, len - pos); + if (isAligned && take === BLOCK_LEN) { + const pos32 = pos / 4; + if (pos % 4 !== 0) + throw new Error("arx: invalid block position"); + for (let j = 0, posj; j < BLOCK_LEN32; j++) { + posj = pos32 + j; + o32[posj] = d32[posj] ^ b32[j]; + } + pos += BLOCK_LEN; + continue; + } + for (let j = 0, posj; j < take; j++) { + posj = pos + j; + output[posj] = data[posj] ^ block[j]; + } + pos += take; + } + } + function createCipher(core, opts) { + const { allowShortKeys, extendNonceFn, counterLength, counterRight, rounds } = checkOpts({ allowShortKeys: false, counterLength: 8, counterRight: false, rounds: 20 }, opts); + if (typeof core !== "function") + throw new Error("core must be a function"); + anumber3(counterLength); + anumber3(rounds); + abool2(counterRight); + abool2(allowShortKeys); + return (key, nonce, data, output, counter = 0) => { + abytes3(key, void 0, "key"); + abytes3(nonce, void 0, "nonce"); + abytes3(data, void 0, "data"); + const len = data.length; + if (output === void 0) + output = new Uint8Array(len); + abytes3(output, void 0, "output"); + anumber3(counter); + if (counter < 0 || counter >= MAX_COUNTER) + throw new Error("arx: counter overflow"); + if (output.length < len) + throw new Error(`arx: output (${output.length}) is shorter than data (${len})`); + const toClean = []; + let l = key.length; + let k; + let sigma; + if (l === 32) { + toClean.push(k = copyBytes2(key)); + sigma = sigma32_32; + } else if (l === 16 && allowShortKeys) { + k = new Uint8Array(32); + k.set(key); + k.set(key, 16); + sigma = sigma16_32; + toClean.push(k); + } else { + abytes3(key, 32, "arx key"); + throw new Error("invalid key size"); + } + if (!isAligned322(nonce)) + toClean.push(nonce = copyBytes2(nonce)); + const k32 = u32(k); + if (extendNonceFn) { + if (nonce.length !== 24) + throw new Error(`arx: extended nonce must be 24 bytes`); + extendNonceFn(sigma, k32, u32(nonce.subarray(0, 16)), k32); + nonce = nonce.subarray(16); + } + const nonceNcLen = 16 - counterLength; + if (nonceNcLen !== nonce.length) + throw new Error(`arx: nonce must be ${nonceNcLen} or 16 bytes`); + if (nonceNcLen !== 12) { + const nc = new Uint8Array(12); + nc.set(nonce, counterRight ? 0 : 12 - nonce.length); + nonce = nc; + toClean.push(nonce); + } + const n32 = u32(nonce); + runCipher(core, sigma, k32, n32, data, output, counter, rounds); + clean2(...toClean); + return output; + }; + } + + // node_modules/@noble/ciphers/_poly1305.js + function u8to16(a, i) { + return a[i++] & 255 | (a[i++] & 255) << 8; + } + var Poly1305 = class { + // Can be speed-up using BigUint64Array, at the cost of complexity + constructor(key) { + __publicField(this, "blockLen", 16); + __publicField(this, "outputLen", 16); + __publicField(this, "buffer", new Uint8Array(16)); + __publicField(this, "r", new Uint16Array(10)); + // Allocating 1 array with .subarray() here is slower than 3 + __publicField(this, "h", new Uint16Array(10)); + __publicField(this, "pad", new Uint16Array(8)); + __publicField(this, "pos", 0); + __publicField(this, "finished", false); + key = copyBytes2(abytes3(key, 32, "key")); + const t0 = u8to16(key, 0); + const t1 = u8to16(key, 2); + const t2 = u8to16(key, 4); + const t3 = u8to16(key, 6); + const t4 = u8to16(key, 8); + const t5 = u8to16(key, 10); + const t6 = u8to16(key, 12); + const t7 = u8to16(key, 14); + this.r[0] = t0 & 8191; + this.r[1] = (t0 >>> 13 | t1 << 3) & 8191; + this.r[2] = (t1 >>> 10 | t2 << 6) & 7939; + this.r[3] = (t2 >>> 7 | t3 << 9) & 8191; + this.r[4] = (t3 >>> 4 | t4 << 12) & 255; + this.r[5] = t4 >>> 1 & 8190; + this.r[6] = (t4 >>> 14 | t5 << 2) & 8191; + this.r[7] = (t5 >>> 11 | t6 << 5) & 8065; + this.r[8] = (t6 >>> 8 | t7 << 8) & 8191; + this.r[9] = t7 >>> 5 & 127; + for (let i = 0; i < 8; i++) + this.pad[i] = u8to16(key, 16 + 2 * i); + } + process(data, offset, isLast = false) { + const hibit = isLast ? 0 : 1 << 11; + const { h, r } = this; + const r0 = r[0]; + const r1 = r[1]; + const r2 = r[2]; + const r3 = r[3]; + const r4 = r[4]; + const r5 = r[5]; + const r6 = r[6]; + const r7 = r[7]; + const r8 = r[8]; + const r9 = r[9]; + const t0 = u8to16(data, offset + 0); + const t1 = u8to16(data, offset + 2); + const t2 = u8to16(data, offset + 4); + const t3 = u8to16(data, offset + 6); + const t4 = u8to16(data, offset + 8); + const t5 = u8to16(data, offset + 10); + const t6 = u8to16(data, offset + 12); + const t7 = u8to16(data, offset + 14); + let h0 = h[0] + (t0 & 8191); + let h1 = h[1] + ((t0 >>> 13 | t1 << 3) & 8191); + let h2 = h[2] + ((t1 >>> 10 | t2 << 6) & 8191); + let h3 = h[3] + ((t2 >>> 7 | t3 << 9) & 8191); + let h4 = h[4] + ((t3 >>> 4 | t4 << 12) & 8191); + let h5 = h[5] + (t4 >>> 1 & 8191); + let h6 = h[6] + ((t4 >>> 14 | t5 << 2) & 8191); + let h7 = h[7] + ((t5 >>> 11 | t6 << 5) & 8191); + let h8 = h[8] + ((t6 >>> 8 | t7 << 8) & 8191); + let h9 = h[9] + (t7 >>> 5 | hibit); + let c = 0; + let d0 = c + h0 * r0 + h1 * (5 * r9) + h2 * (5 * r8) + h3 * (5 * r7) + h4 * (5 * r6); + c = d0 >>> 13; + d0 &= 8191; + d0 += h5 * (5 * r5) + h6 * (5 * r4) + h7 * (5 * r3) + h8 * (5 * r2) + h9 * (5 * r1); + c += d0 >>> 13; + d0 &= 8191; + let d1 = c + h0 * r1 + h1 * r0 + h2 * (5 * r9) + h3 * (5 * r8) + h4 * (5 * r7); + c = d1 >>> 13; + d1 &= 8191; + d1 += h5 * (5 * r6) + h6 * (5 * r5) + h7 * (5 * r4) + h8 * (5 * r3) + h9 * (5 * r2); + c += d1 >>> 13; + d1 &= 8191; + let d2 = c + h0 * r2 + h1 * r1 + h2 * r0 + h3 * (5 * r9) + h4 * (5 * r8); + c = d2 >>> 13; + d2 &= 8191; + d2 += h5 * (5 * r7) + h6 * (5 * r6) + h7 * (5 * r5) + h8 * (5 * r4) + h9 * (5 * r3); + c += d2 >>> 13; + d2 &= 8191; + let d3 = c + h0 * r3 + h1 * r2 + h2 * r1 + h3 * r0 + h4 * (5 * r9); + c = d3 >>> 13; + d3 &= 8191; + d3 += h5 * (5 * r8) + h6 * (5 * r7) + h7 * (5 * r6) + h8 * (5 * r5) + h9 * (5 * r4); + c += d3 >>> 13; + d3 &= 8191; + let d4 = c + h0 * r4 + h1 * r3 + h2 * r2 + h3 * r1 + h4 * r0; + c = d4 >>> 13; + d4 &= 8191; + d4 += h5 * (5 * r9) + h6 * (5 * r8) + h7 * (5 * r7) + h8 * (5 * r6) + h9 * (5 * r5); + c += d4 >>> 13; + d4 &= 8191; + let d5 = c + h0 * r5 + h1 * r4 + h2 * r3 + h3 * r2 + h4 * r1; + c = d5 >>> 13; + d5 &= 8191; + d5 += h5 * r0 + h6 * (5 * r9) + h7 * (5 * r8) + h8 * (5 * r7) + h9 * (5 * r6); + c += d5 >>> 13; + d5 &= 8191; + let d6 = c + h0 * r6 + h1 * r5 + h2 * r4 + h3 * r3 + h4 * r2; + c = d6 >>> 13; + d6 &= 8191; + d6 += h5 * r1 + h6 * r0 + h7 * (5 * r9) + h8 * (5 * r8) + h9 * (5 * r7); + c += d6 >>> 13; + d6 &= 8191; + let d7 = c + h0 * r7 + h1 * r6 + h2 * r5 + h3 * r4 + h4 * r3; + c = d7 >>> 13; + d7 &= 8191; + d7 += h5 * r2 + h6 * r1 + h7 * r0 + h8 * (5 * r9) + h9 * (5 * r8); + c += d7 >>> 13; + d7 &= 8191; + let d8 = c + h0 * r8 + h1 * r7 + h2 * r6 + h3 * r5 + h4 * r4; + c = d8 >>> 13; + d8 &= 8191; + d8 += h5 * r3 + h6 * r2 + h7 * r1 + h8 * r0 + h9 * (5 * r9); + c += d8 >>> 13; + d8 &= 8191; + let d9 = c + h0 * r9 + h1 * r8 + h2 * r7 + h3 * r6 + h4 * r5; + c = d9 >>> 13; + d9 &= 8191; + d9 += h5 * r4 + h6 * r3 + h7 * r2 + h8 * r1 + h9 * r0; + c += d9 >>> 13; + d9 &= 8191; + c = (c << 2) + c | 0; + c = c + d0 | 0; + d0 = c & 8191; + c = c >>> 13; + d1 += c; + h[0] = d0; + h[1] = d1; + h[2] = d2; + h[3] = d3; + h[4] = d4; + h[5] = d5; + h[6] = d6; + h[7] = d7; + h[8] = d8; + h[9] = d9; + } + finalize() { + const { h, pad } = this; + const g = new Uint16Array(10); + let c = h[1] >>> 13; + h[1] &= 8191; + for (let i = 2; i < 10; i++) { + h[i] += c; + c = h[i] >>> 13; + h[i] &= 8191; + } + h[0] += c * 5; + c = h[0] >>> 13; + h[0] &= 8191; + h[1] += c; + c = h[1] >>> 13; + h[1] &= 8191; + h[2] += c; + g[0] = h[0] + 5; + c = g[0] >>> 13; + g[0] &= 8191; + for (let i = 1; i < 10; i++) { + g[i] = h[i] + c; + c = g[i] >>> 13; + g[i] &= 8191; + } + g[9] -= 1 << 13; + let mask = (c ^ 1) - 1; + for (let i = 0; i < 10; i++) + g[i] &= mask; + mask = ~mask; + for (let i = 0; i < 10; i++) + h[i] = h[i] & mask | g[i]; + h[0] = (h[0] | h[1] << 13) & 65535; + h[1] = (h[1] >>> 3 | h[2] << 10) & 65535; + h[2] = (h[2] >>> 6 | h[3] << 7) & 65535; + h[3] = (h[3] >>> 9 | h[4] << 4) & 65535; + h[4] = (h[4] >>> 12 | h[5] << 1 | h[6] << 14) & 65535; + h[5] = (h[6] >>> 2 | h[7] << 11) & 65535; + h[6] = (h[7] >>> 5 | h[8] << 8) & 65535; + h[7] = (h[8] >>> 8 | h[9] << 5) & 65535; + let f = h[0] + pad[0]; + h[0] = f & 65535; + for (let i = 1; i < 8; i++) { + f = (h[i] + pad[i] | 0) + (f >>> 16) | 0; + h[i] = f & 65535; + } + clean2(g); + } + update(data) { + aexists2(this); + abytes3(data); + data = copyBytes2(data); + const { buffer, blockLen } = this; + const len = data.length; + for (let pos = 0; pos < len; ) { + const take = Math.min(blockLen - this.pos, len - pos); + if (take === blockLen) { + for (; blockLen <= len - pos; pos += blockLen) + this.process(data, pos); + continue; + } + buffer.set(data.subarray(pos, pos + take), this.pos); + this.pos += take; + pos += take; + if (this.pos === blockLen) { + this.process(buffer, 0, false); + this.pos = 0; + } + } + return this; + } + destroy() { + clean2(this.h, this.r, this.buffer, this.pad); + } + digestInto(out) { + aexists2(this); + aoutput2(out, this); + this.finished = true; + const { buffer, h } = this; + let { pos } = this; + if (pos) { + buffer[pos++] = 1; + for (; pos < 16; pos++) + buffer[pos] = 0; + this.process(buffer, 0, true); + } + this.finalize(); + let opos = 0; + for (let i = 0; i < 8; i++) { + out[opos++] = h[i] >>> 0; + out[opos++] = h[i] >>> 8; + } + return out; + } + digest() { + const { buffer, outputLen } = this; + this.digestInto(buffer); + const res = buffer.slice(0, outputLen); + this.destroy(); + return res; + } + }; + function wrapConstructorWithKey(hashCons) { + const hashC = (msg, key) => hashCons(key).update(msg).digest(); + const tmp = hashCons(new Uint8Array(32)); + hashC.outputLen = tmp.outputLen; + hashC.blockLen = tmp.blockLen; + hashC.create = (key) => hashCons(key); + return hashC; + } + var poly1305 = /* @__PURE__ */ (() => wrapConstructorWithKey((key) => new Poly1305(key)))(); + + // node_modules/@noble/ciphers/chacha.js + function chachaCore(s, k, n, out, cnt, rounds = 20) { + let y00 = s[0], y01 = s[1], y02 = s[2], y03 = s[3], y04 = k[0], y05 = k[1], y06 = k[2], y07 = k[3], y08 = k[4], y09 = k[5], y10 = k[6], y11 = k[7], y12 = cnt, y13 = n[0], y14 = n[1], y15 = n[2]; + let x00 = y00, x01 = y01, x02 = y02, x03 = y03, x04 = y04, x05 = y05, x06 = y06, x07 = y07, x08 = y08, x09 = y09, x10 = y10, x11 = y11, x12 = y12, x13 = y13, x14 = y14, x15 = y15; + for (let r = 0; r < rounds; r += 2) { + x00 = x00 + x04 | 0; + x12 = rotl(x12 ^ x00, 16); + x08 = x08 + x12 | 0; + x04 = rotl(x04 ^ x08, 12); + x00 = x00 + x04 | 0; + x12 = rotl(x12 ^ x00, 8); + x08 = x08 + x12 | 0; + x04 = rotl(x04 ^ x08, 7); + x01 = x01 + x05 | 0; + x13 = rotl(x13 ^ x01, 16); + x09 = x09 + x13 | 0; + x05 = rotl(x05 ^ x09, 12); + x01 = x01 + x05 | 0; + x13 = rotl(x13 ^ x01, 8); + x09 = x09 + x13 | 0; + x05 = rotl(x05 ^ x09, 7); + x02 = x02 + x06 | 0; + x14 = rotl(x14 ^ x02, 16); + x10 = x10 + x14 | 0; + x06 = rotl(x06 ^ x10, 12); + x02 = x02 + x06 | 0; + x14 = rotl(x14 ^ x02, 8); + x10 = x10 + x14 | 0; + x06 = rotl(x06 ^ x10, 7); + x03 = x03 + x07 | 0; + x15 = rotl(x15 ^ x03, 16); + x11 = x11 + x15 | 0; + x07 = rotl(x07 ^ x11, 12); + x03 = x03 + x07 | 0; + x15 = rotl(x15 ^ x03, 8); + x11 = x11 + x15 | 0; + x07 = rotl(x07 ^ x11, 7); + x00 = x00 + x05 | 0; + x15 = rotl(x15 ^ x00, 16); + x10 = x10 + x15 | 0; + x05 = rotl(x05 ^ x10, 12); + x00 = x00 + x05 | 0; + x15 = rotl(x15 ^ x00, 8); + x10 = x10 + x15 | 0; + x05 = rotl(x05 ^ x10, 7); + x01 = x01 + x06 | 0; + x12 = rotl(x12 ^ x01, 16); + x11 = x11 + x12 | 0; + x06 = rotl(x06 ^ x11, 12); + x01 = x01 + x06 | 0; + x12 = rotl(x12 ^ x01, 8); + x11 = x11 + x12 | 0; + x06 = rotl(x06 ^ x11, 7); + x02 = x02 + x07 | 0; + x13 = rotl(x13 ^ x02, 16); + x08 = x08 + x13 | 0; + x07 = rotl(x07 ^ x08, 12); + x02 = x02 + x07 | 0; + x13 = rotl(x13 ^ x02, 8); + x08 = x08 + x13 | 0; + x07 = rotl(x07 ^ x08, 7); + x03 = x03 + x04 | 0; + x14 = rotl(x14 ^ x03, 16); + x09 = x09 + x14 | 0; + x04 = rotl(x04 ^ x09, 12); + x03 = x03 + x04 | 0; + x14 = rotl(x14 ^ x03, 8); + x09 = x09 + x14 | 0; + x04 = rotl(x04 ^ x09, 7); + } + let oi = 0; + out[oi++] = y00 + x00 | 0; + out[oi++] = y01 + x01 | 0; + out[oi++] = y02 + x02 | 0; + out[oi++] = y03 + x03 | 0; + out[oi++] = y04 + x04 | 0; + out[oi++] = y05 + x05 | 0; + out[oi++] = y06 + x06 | 0; + out[oi++] = y07 + x07 | 0; + out[oi++] = y08 + x08 | 0; + out[oi++] = y09 + x09 | 0; + out[oi++] = y10 + x10 | 0; + out[oi++] = y11 + x11 | 0; + out[oi++] = y12 + x12 | 0; + out[oi++] = y13 + x13 | 0; + out[oi++] = y14 + x14 | 0; + out[oi++] = y15 + x15 | 0; + } + function hchacha(s, k, i, out) { + let x00 = s[0], x01 = s[1], x02 = s[2], x03 = s[3], x04 = k[0], x05 = k[1], x06 = k[2], x07 = k[3], x08 = k[4], x09 = k[5], x10 = k[6], x11 = k[7], x12 = i[0], x13 = i[1], x14 = i[2], x15 = i[3]; + for (let r = 0; r < 20; r += 2) { + x00 = x00 + x04 | 0; + x12 = rotl(x12 ^ x00, 16); + x08 = x08 + x12 | 0; + x04 = rotl(x04 ^ x08, 12); + x00 = x00 + x04 | 0; + x12 = rotl(x12 ^ x00, 8); + x08 = x08 + x12 | 0; + x04 = rotl(x04 ^ x08, 7); + x01 = x01 + x05 | 0; + x13 = rotl(x13 ^ x01, 16); + x09 = x09 + x13 | 0; + x05 = rotl(x05 ^ x09, 12); + x01 = x01 + x05 | 0; + x13 = rotl(x13 ^ x01, 8); + x09 = x09 + x13 | 0; + x05 = rotl(x05 ^ x09, 7); + x02 = x02 + x06 | 0; + x14 = rotl(x14 ^ x02, 16); + x10 = x10 + x14 | 0; + x06 = rotl(x06 ^ x10, 12); + x02 = x02 + x06 | 0; + x14 = rotl(x14 ^ x02, 8); + x10 = x10 + x14 | 0; + x06 = rotl(x06 ^ x10, 7); + x03 = x03 + x07 | 0; + x15 = rotl(x15 ^ x03, 16); + x11 = x11 + x15 | 0; + x07 = rotl(x07 ^ x11, 12); + x03 = x03 + x07 | 0; + x15 = rotl(x15 ^ x03, 8); + x11 = x11 + x15 | 0; + x07 = rotl(x07 ^ x11, 7); + x00 = x00 + x05 | 0; + x15 = rotl(x15 ^ x00, 16); + x10 = x10 + x15 | 0; + x05 = rotl(x05 ^ x10, 12); + x00 = x00 + x05 | 0; + x15 = rotl(x15 ^ x00, 8); + x10 = x10 + x15 | 0; + x05 = rotl(x05 ^ x10, 7); + x01 = x01 + x06 | 0; + x12 = rotl(x12 ^ x01, 16); + x11 = x11 + x12 | 0; + x06 = rotl(x06 ^ x11, 12); + x01 = x01 + x06 | 0; + x12 = rotl(x12 ^ x01, 8); + x11 = x11 + x12 | 0; + x06 = rotl(x06 ^ x11, 7); + x02 = x02 + x07 | 0; + x13 = rotl(x13 ^ x02, 16); + x08 = x08 + x13 | 0; + x07 = rotl(x07 ^ x08, 12); + x02 = x02 + x07 | 0; + x13 = rotl(x13 ^ x02, 8); + x08 = x08 + x13 | 0; + x07 = rotl(x07 ^ x08, 7); + x03 = x03 + x04 | 0; + x14 = rotl(x14 ^ x03, 16); + x09 = x09 + x14 | 0; + x04 = rotl(x04 ^ x09, 12); + x03 = x03 + x04 | 0; + x14 = rotl(x14 ^ x03, 8); + x09 = x09 + x14 | 0; + x04 = rotl(x04 ^ x09, 7); + } + let oi = 0; + out[oi++] = x00; + out[oi++] = x01; + out[oi++] = x02; + out[oi++] = x03; + out[oi++] = x12; + out[oi++] = x13; + out[oi++] = x14; + out[oi++] = x15; + } + var chacha20 = /* @__PURE__ */ createCipher(chachaCore, { + counterRight: false, + counterLength: 4, + allowShortKeys: false + }); + var xchacha20 = /* @__PURE__ */ createCipher(chachaCore, { + counterRight: false, + counterLength: 8, + extendNonceFn: hchacha, + allowShortKeys: false + }); + var ZEROS16 = /* @__PURE__ */ new Uint8Array(16); + var updatePadded = (h, msg) => { + h.update(msg); + const leftover = msg.length % 16; + if (leftover) + h.update(ZEROS16.subarray(leftover)); + }; + var ZEROS32 = /* @__PURE__ */ new Uint8Array(32); + function computeTag(fn, key, nonce, ciphertext, AAD) { + if (AAD !== void 0) + abytes3(AAD, void 0, "AAD"); + const authKey = fn(key, nonce, ZEROS32); + const lengths = u64Lengths(ciphertext.length, AAD ? AAD.length : 0, true); + const h = poly1305.create(authKey); + if (AAD) + updatePadded(h, AAD); + updatePadded(h, ciphertext); + h.update(lengths); + const res = h.digest(); + clean2(authKey, lengths); + return res; + } + var _poly1305_aead = (xorStream) => (key, nonce, AAD) => { + const tagLength = 16; + return { + encrypt(plaintext, output) { + const plength = plaintext.length; + output = getOutput(plength + tagLength, output, false); + output.set(plaintext); + const oPlain = output.subarray(0, -tagLength); + xorStream(key, nonce, oPlain, oPlain, 1); + const tag = computeTag(xorStream, key, nonce, oPlain, AAD); + output.set(tag, plength); + clean2(tag); + return output; + }, + decrypt(ciphertext, output) { + output = getOutput(ciphertext.length - tagLength, output, false); + const data = ciphertext.subarray(0, -tagLength); + const passedTag = ciphertext.subarray(-tagLength); + const tag = computeTag(xorStream, key, nonce, data, AAD); + if (!equalBytes(passedTag, tag)) + throw new Error("invalid tag"); + output.set(ciphertext.subarray(0, -tagLength)); + xorStream(key, nonce, output, output, 1); + clean2(tag); + return output; + } + }; + }; + var chacha20poly1305 = /* @__PURE__ */ wrapCipher({ blockSize: 64, nonceLength: 12, tagLength: 16 }, _poly1305_aead(chacha20)); + var xchacha20poly1305 = /* @__PURE__ */ wrapCipher({ blockSize: 64, nonceLength: 24, tagLength: 16 }, _poly1305_aead(xchacha20)); + + // scripts/vendor/crypto-entry.js + var HEX_64 = /^[0-9a-f]{64}$/; + function hexToBytes3(hex) { + if (typeof hex !== "string" || hex.length % 2 !== 0 || /[^0-9a-fA-F]/.test(hex)) { + throw new Error("NbreadCrypto: invalid hex string"); + } + const out = new Uint8Array(hex.length / 2); + for (let i = 0; i < out.length; i++) { + out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); + } + return out; + } + function bytesToHex4(bytes) { + let out = ""; + for (let i = 0; i < bytes.length; i++) { + out += bytes[i].toString(16).padStart(2, "0"); + } + return out; + } + var textEncoder = new TextEncoder(); + var textDecoder = new TextDecoder("utf-8"); + function utf8Encode(s) { + return textEncoder.encode(s); + } + function utf8Decode(b) { + return textDecoder.decode(b); + } + function base64Encode(bytes) { + let bin = ""; + const CHUNK = 32768; + for (let i = 0; i < bytes.length; i += CHUNK) { + bin += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK)); + } + return btoa(bin); + } + var BASE64_STRICT = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; + function base64Decode(s) { + if (typeof s !== "string" || !BASE64_STRICT.test(s)) { + throw new Error("NbreadCrypto: invalid base64"); + } + const bin = atob(s); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; + } + function randomBytes4(n) { + const out = new Uint8Array(n); + const CHUNK = 65536; + for (let i = 0; i < out.length; i += CHUNK) { + crypto.getRandomValues(out.subarray(i, i + CHUNK)); + } + return out; + } + function concatBytes4(...arrays) { + let total = 0; + for (const a of arrays) total += a.length; + const out = new Uint8Array(total); + let offset = 0; + for (const a of arrays) { + out.set(a, offset); + offset += a.length; + } + return out; + } + function equalBytesConstantTime(a, b) { + if (a.length !== b.length) return false; + let diff = 0; + for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]; + return diff === 0; + } + var CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + var CHARSET_REV = new Map([...CHARSET].map((c, i) => [c, i])); + var GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059]; + var MAX_BECH32_LENGTH = 5e3; + function polymod(values) { + let chk = 1; + for (const v of values) { + const top = chk >>> 25; + chk = (chk & 33554431) << 5 ^ v; + for (let i = 0; i < 5; i++) { + if (top >>> i & 1) chk ^= GENERATOR[i]; + } + } + return chk; + } + function hrpExpand(hrp) { + const out = []; + for (let i = 0; i < hrp.length; i++) out.push(hrp.charCodeAt(i) >>> 5); + out.push(0); + for (let i = 0; i < hrp.length; i++) out.push(hrp.charCodeAt(i) & 31); + return out; + } + function convertBits(data, fromBits, toBits, pad) { + let acc = 0; + let bits = 0; + const out = []; + const maxv = (1 << toBits) - 1; + for (let i = 0; i < data.length; i++) { + const value = data[i]; + if (value < 0 || value >>> fromBits !== 0) { + throw new Error("NbreadCrypto: invalid value in bit conversion"); + } + acc = acc << fromBits | value; + bits += fromBits; + while (bits >= toBits) { + bits -= toBits; + out.push(acc >>> bits & maxv); + } + } + if (pad) { + if (bits > 0) out.push(acc << toBits - bits & maxv); + } else if (bits >= fromBits || (acc << toBits - bits & maxv) !== 0) { + throw new Error("NbreadCrypto: invalid padding in bech32 data"); + } + return out; + } + function bech32Encode(hrp, bytes) { + const data = convertBits(bytes, 8, 5, true); + const combined = [...hrpExpand(hrp), ...data]; + const polymodTarget = polymod([...combined, 0, 0, 0, 0, 0, 0]) ^ 1; + const checksum = []; + for (let i = 0; i < 6; i++) { + checksum.push(polymodTarget >>> 5 * (5 - i) & 31); + } + const encoded = hrp + "1" + [...data, ...checksum].map((v) => CHARSET[v]).join(""); + if (encoded.length > MAX_BECH32_LENGTH) { + throw new Error("NbreadCrypto: encoded bech32 entity too long"); + } + return encoded; + } + function bech32Decode(str) { + if (typeof str !== "string" || str.length < 8 || str.length > MAX_BECH32_LENGTH) { + throw new Error("NbreadCrypto: invalid bech32 length"); + } + const lower = str.toLowerCase(); + if (str !== lower && str !== str.toUpperCase()) { + throw new Error("NbreadCrypto: mixed-case bech32 is invalid"); + } + const sep = lower.lastIndexOf("1"); + if (sep < 1 || sep + 7 > lower.length) { + throw new Error("NbreadCrypto: missing bech32 separator or checksum"); + } + const hrp = lower.slice(0, sep); + for (let i = 0; i < hrp.length; i++) { + const code = hrp.charCodeAt(i); + if (code < 33 || code > 126) { + throw new Error("NbreadCrypto: invalid bech32 hrp character"); + } + } + const data = []; + for (const ch of lower.slice(sep + 1)) { + const v = CHARSET_REV.get(ch); + if (v === void 0) throw new Error("NbreadCrypto: invalid bech32 character"); + data.push(v); + } + if (polymod([...hrpExpand(hrp), ...data]) !== 1) { + throw new Error("NbreadCrypto: bad bech32 checksum"); + } + const bytes = Uint8Array.from(convertBits(data.slice(0, -6), 5, 8, false)); + return { hrp, bytes }; + } + function hexToBytes32(hex, what) { + if (typeof hex !== "string" || !HEX_64.test(hex)) { + throw new Error(`NbreadCrypto: ${what} must be 64 lowercase hex chars`); + } + return hexToBytes3(hex); + } + function decode32(str, expectedHrp) { + const { hrp, bytes } = bech32Decode(str); + if (hrp !== expectedHrp) { + throw new Error(`NbreadCrypto: expected ${expectedHrp}, got ${hrp}`); + } + if (bytes.length !== 32) { + throw new Error(`NbreadCrypto: ${expectedHrp} payload must be 32 bytes`); + } + return bytesToHex4(bytes); + } + function npubEncode(pkHex) { + return bech32Encode("npub", hexToBytes32(pkHex, "pubkey")); + } + function npubDecode(npub) { + return decode32(npub, "npub"); + } + function nsecEncode(skHex) { + return bech32Encode("nsec", hexToBytes32(skHex, "secret key")); + } + function nsecDecode(nsec) { + return decode32(nsec, "nsec"); + } + function eventId(unsigned) { + const serialized = JSON.stringify([ + 0, + unsigned.pubkey, + unsigned.created_at, + unsigned.kind, + unsigned.tags, + unsigned.content + ]); + return bytesToHex4(sha256(utf8Encode(serialized))); + } + function getPublicKeyHex(skBytes) { + return bytesToHex4(schnorr.getPublicKey(skBytes)); + } + function schnorrSign2(msgHex, skBytes) { + return bytesToHex4(schnorr.sign(hexToBytes3(msgHex), skBytes)); + } + function schnorrVerify2(sigHex, msgHex, pubHex) { + try { + return schnorr.verify(hexToBytes3(sigHex), hexToBytes3(msgHex), hexToBytes3(pubHex)); + } catch { + return false; + } + } + function finalizeEvent(unsignedNoPubkey, skBytes) { + const pubkey = getPublicKeyHex(skBytes); + const unsigned = { + pubkey, + created_at: unsignedNoPubkey.created_at, + kind: unsignedNoPubkey.kind, + tags: unsignedNoPubkey.tags, + content: unsignedNoPubkey.content + }; + const id = eventId(unsigned); + const sig = schnorrSign2(id, skBytes); + return { + id, + pubkey, + kind: unsigned.kind, + created_at: unsigned.created_at, + tags: unsigned.tags, + content: unsigned.content, + sig + }; + } + function sharedX(skBytes, pubHex) { + const pub = hexToBytes32(pubHex, "pubkey"); + const shared = secp256k1.getSharedSecret(skBytes, concatBytes4(new Uint8Array([2]), pub)); + return shared.subarray(1, 33); + } + var NIP44_SALT = utf8Encode("nip44-v2"); + var NIP44_MIN_PLAINTEXT = 1; + var NIP44_MAX_PLAINTEXT = 65535; + function nip44ConversationKey(skBytes, pubHex) { + return extract(sha256, sharedX(skBytes, pubHex), NIP44_SALT); + } + function nip44MessageKeys(convKey, nonce) { + if (convKey.length !== 32) { + throw new Error("NbreadCrypto: nip44 conversation key must be 32 bytes"); + } + if (nonce.length !== 32) { + throw new Error("NbreadCrypto: nip44 nonce must be 32 bytes"); + } + const keys = expand(sha256, convKey, nonce, 76); + return { + chachaKey: keys.subarray(0, 32), + chachaNonce: keys.subarray(32, 44), + hmacKey: keys.subarray(44, 76) + }; + } + function nip44CalcPaddedLen(len) { + if (!Number.isSafeInteger(len) || len < 1) { + throw new Error("NbreadCrypto: expected positive integer"); + } + if (len <= 32) return 32; + const nextPower = 2 ** (Math.floor(Math.log2(len - 1)) + 1); + const chunk = nextPower <= 256 ? 32 : nextPower / 8; + return chunk * (Math.floor((len - 1) / chunk) + 1); + } + function nip44Pad(plaintext) { + const unpadded = utf8Encode(plaintext); + const len = unpadded.length; + if (len < NIP44_MIN_PLAINTEXT || len > NIP44_MAX_PLAINTEXT) { + throw new Error( + "NbreadCrypto: invalid nip44 plaintext size: must be 1..65535 bytes" + ); + } + const prefix = new Uint8Array([len >>> 8 & 255, len & 255]); + const suffix = new Uint8Array(nip44CalcPaddedLen(len) - len); + return concatBytes4(prefix, unpadded, suffix); + } + function nip44Unpad(padded) { + if (padded.length < 34) throw new Error("NbreadCrypto: invalid nip44 padding"); + const len = padded[0] << 8 | padded[1]; + if (len < NIP44_MIN_PLAINTEXT || len > NIP44_MAX_PLAINTEXT || padded.length !== 2 + nip44CalcPaddedLen(len)) { + throw new Error("NbreadCrypto: invalid nip44 padding"); + } + return utf8Decode(padded.subarray(2, 2 + len)); + } + function nip44EncryptWithNonce(convKey, plaintext, nonce) { + const { chachaKey, chachaNonce, hmacKey } = nip44MessageKeys(convKey, nonce); + const padded = nip44Pad(plaintext); + const ciphertext = chacha20(chachaKey, chachaNonce, padded); + const mac = hmac(sha256, hmacKey, concatBytes4(nonce, ciphertext)); + return base64Encode(concatBytes4(new Uint8Array([2]), nonce, ciphertext, mac)); + } + function nip44Encrypt(convKey, plaintext) { + return nip44EncryptWithNonce(convKey, plaintext, randomBytes4(32)); + } + function nip44Decrypt(convKey, payload) { + if (typeof payload !== "string" || payload.length === 0) { + throw new Error("NbreadCrypto: invalid nip44 payload"); + } + if (payload[0] === "#") { + throw new Error("NbreadCrypto: unknown nip44 encryption version"); + } + if (payload.length < 132 || payload.length > 87472) { + throw new Error("NbreadCrypto: invalid nip44 payload length"); + } + const data = base64Decode(payload); + if (data.length < 99 || data.length > 65603) { + throw new Error("NbreadCrypto: invalid nip44 data length"); + } + if (data[0] !== 2) { + throw new Error(`NbreadCrypto: unknown nip44 encryption version ${data[0]}`); + } + const nonce = data.subarray(1, 33); + const ciphertext = data.subarray(33, data.length - 32); + const mac = data.subarray(data.length - 32); + const { chachaKey, chachaNonce, hmacKey } = nip44MessageKeys(convKey, nonce); + const expectedMac = hmac(sha256, hmacKey, concatBytes4(nonce, ciphertext)); + if (!equalBytesConstantTime(mac, expectedMac)) { + throw new Error("NbreadCrypto: invalid nip44 MAC"); + } + const padded = chacha20(chachaKey, chachaNonce, ciphertext); + return nip44Unpad(padded); + } + async function nip04Key(skBytes, pubHex, usage) { + return crypto.subtle.importKey("raw", sharedX(skBytes, pubHex), { name: "AES-CBC" }, false, [ + usage + ]); + } + async function nip04Encrypt(skBytes, pubHex, text) { + const key = await nip04Key(skBytes, pubHex, "encrypt"); + const iv = randomBytes4(16); + const ciphertext = new Uint8Array( + await crypto.subtle.encrypt({ name: "AES-CBC", iv }, key, utf8Encode(text)) + ); + return `${base64Encode(ciphertext)}?iv=${base64Encode(iv)}`; + } + async function nip04Decrypt(skBytes, pubHex, payload) { + if (typeof payload !== "string") { + throw new Error("NbreadCrypto: invalid nip04 payload"); + } + const sep = payload.indexOf("?iv="); + if (sep === -1) throw new Error("NbreadCrypto: invalid nip04 payload"); + const ciphertext = base64Decode(payload.slice(0, sep)); + const iv = base64Decode(payload.slice(sep + 4)); + if (iv.length !== 16) throw new Error("NbreadCrypto: invalid nip04 iv"); + const key = await nip04Key(skBytes, pubHex, "decrypt"); + const plaintext = new Uint8Array( + await crypto.subtle.decrypt({ name: "AES-CBC", iv }, key, ciphertext) + ); + return utf8Decode(plaintext); + } + var api = { + // encoding + hexToBytes: hexToBytes3, + bytesToHex: bytesToHex4, + utf8Encode, + utf8Decode, + base64Encode, + base64Decode, + randomBytes: randomBytes4, + // bech32 / NIP-19 + bech32Encode, + bech32Decode, + npubEncode, + npubDecode, + nsecEncode, + nsecDecode, + // NIP-01 / BIP-340 + eventId, + getPublicKeyHex, + schnorrSign: schnorrSign2, + schnorrVerify: schnorrVerify2, + finalizeEvent, + // NIP-44 v2 + nip44ConversationKey, + nip44Encrypt, + nip44Decrypt, + // UNSAFE, tests only: fixed-nonce encrypt for the official NIP-44 vectors. + __nip44EncryptWithNonceForTests: nip44EncryptWithNonce, + // NIP-04 (legacy) + nip04Encrypt, + nip04Decrypt + }; + Object.freeze(api); + globalThis.NbreadCrypto = api; +})(); diff --git a/scripts/build-vendor.mjs b/scripts/build-vendor.mjs new file mode 100644 index 0000000..5d439c7 --- /dev/null +++ b/scripts/build-vendor.mjs @@ -0,0 +1,58 @@ +/** + * Build the committed client crypto vendor bundle: + * scripts/vendor/crypto-entry.js → public/js/vendor/nostr-crypto.js + * + * Run via `npm run build:vendor`. The output is a plain IIFE (globalThis. + * NbreadCrypto), unminified for auditability, and COMMITTED — deploy never + * runs this. CI rebuilds and requires `git status --porcelain + * public/js/vendor/` to be empty (tracked drift AND new untracked output), so + * any mismatch between entry/deps and the committed artifact fails the build. + * + * Determinism: esbuild output is a pure function of the entry file, the + * pinned dependency versions (package-lock.json), and the pinned esbuild + * version. Paths in the bundle's comments are relative to the repo root + * (we chdir there), so the output is machine-independent. + */ +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import process from "node:process"; +import { statSync } from "node:fs"; +import { build } from "esbuild"; + +// Always build relative to the repo root regardless of invocation cwd. +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +process.chdir(repoRoot); + +const OUTFILE = "public/js/vendor/nostr-crypto.js"; + +const BANNER = `/** + * GENERATED FILE — DO NOT EDIT BY HAND. + * + * Built from scripts/vendor/crypto-entry.js by \`npm run build:vendor\` + * (scripts/build-vendor.mjs). Bundles @noble/curves, @noble/hashes, and + * @noble/ciphers (devDependencies pinned in package-lock.json) into a plain + * IIFE exposing globalThis.NbreadCrypto. Committed unminified so the served + * asset is auditable; CI rebuilds and fails on drift. + */`; + +await build({ + entryPoints: ["scripts/vendor/crypto-entry.js"], + outfile: OUTFILE, + bundle: true, + // Plain IIFE with NO `globalName`: the entry's explicit + // `globalThis.NbreadCrypto = api` assignment is the single export surface, + // so classic + + + + diff --git a/src/views/main/login.tsx b/src/views/main/login.tsx index 760077f..bd590aa 100644 --- a/src/views/main/login.tsx +++ b/src/views/main/login.tsx @@ -2,8 +2,15 @@ import { Layout } from "../layout"; import { SiteHeader, SiteFooter } from "./chrome"; /** - * Login page: NIP-07 challenge-response. All logic lives in /js/login.js - * (fetch challenge → window.nostr.signEvent kind 22242 → POST /login). + * Login page: signer method picker + per-method panels. All logic lives in + * /js/login.js on top of the NbreadSigner dispatcher (signer.js): fetch a + * one-time challenge, sign a kind 22242 event with the chosen backend + * (NIP-07 extension, NIP-46 remote signer, NIP-55 Amber redirect, or a + * pasted local key), POST it to /login. + * + * Every panel is server-rendered statically and hidden via the `hidden` + * attribute — login.js reveals the right one. No inline executable script + * (CSP: script-src 'self'); nothing here renders user/relay-sourced values. */ export function LoginPage() { return ( @@ -12,20 +19,141 @@ export function LoginPage() {

Sign in

- Sign in with your Nostr key. You need a{" "} + Sign in with your Nostr key. Choose how you want to sign: a{" "} NIP-07 browser extension {" "} - (Alby, nos2x, …) — your key never leaves it; you only sign a - one-time login challenge. + (Alby, nos2x, …), a remote signer, Amber on Android, or a pasted + key. Whichever you pick, you only sign a one-time login challenge.

- + + + {/* Shown when a signer is already configured in this browser. */} + + + + + + + + + + + +

+ + + +
diff --git a/test/unit/signer-core.spec.ts b/test/unit/signer-core.spec.ts index 93fbe34..1373988 100644 --- a/test/unit/signer-core.spec.ts +++ b/test/unit/signer-core.spec.ts @@ -4,7 +4,7 @@ // committed crypto vendor bundle. signer.js itself is deliberately NOT // imported here — it touches window/localStorage/location and cannot load in // workerd. -import { describe, expect, it } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it } from "vitest"; // @ts-ignore — plain browser IIFE, intentionally shipped without types import "../../public/js/vendor/nostr-crypto.js"; // @ts-ignore — plain browser IIFE, intentionally shipped without types @@ -34,6 +34,10 @@ const C = (globalThis as any).NbreadCrypto as { nsecEncode: (skHex: string) => string; eventId: (unsigned: Unsigned) => string; getPublicKeyHex: (skBytes: Uint8Array) => string; + finalizeEvent: ( + unsigned: Partial, + skBytes: Uint8Array, + ) => Unsigned & { id: string; sig: string }; }; const Core = (globalThis as any).NbreadSignerCore as { @@ -195,30 +199,48 @@ describe("NbreadSignerCore.parseNip55Callback", () => { }); }); - it("tolerates generic-param variants across Amber versions", () => { - // Signature under alternate names, no nip55 marker: inferred from shape. - expect(Core.parseNip55Callback(`?sig=${SIG_128}`)).toEqual({ + it("tolerates alternate result params across Amber versions once marked", () => { + // With the nip55 marker present, the value is extracted from whichever + // param this Amber version appended. + expect(Core.parseNip55Callback(`?nip55=sign&sig=${SIG_128}`)).toEqual({ kind: "sign", value: SIG_128, }); - expect(Core.parseNip55Callback(`?signature=${SIG_128}`)).toEqual({ + expect(Core.parseNip55Callback(`?nip55=sign&signature=${SIG_128}`)).toEqual({ kind: "sign", value: SIG_128, }); - expect(Core.parseNip55Callback(`?event=${SIG_128}`)).toEqual({ - kind: "sign", - value: SIG_128, - }); - // Pubkey under alternate names. - expect(Core.parseNip55Callback(`?result=${keys.bob.pk}`)).toEqual({ + expect(Core.parseNip55Callback(`?nip55=pubkey&result=${keys.bob.pk}`)).toEqual({ kind: "pubkey", value: keys.bob.pk, }); const npub = C.npubEncode(keys.bob.pk); - expect(Core.parseNip55Callback(`?pubkey=${npub}`)).toEqual({ + expect(Core.parseNip55Callback(`?nip55=pubkey&pubkey=${npub}`)).toEqual({ kind: "pubkey", value: npub, }); + // Long-form marker values still name the flow. + expect(Core.parseNip55Callback(`?nip55=sign_event&event=${SIG_128}`)).toEqual({ + kind: "sign", + value: SIG_128, + }); + expect( + Core.parseNip55Callback(`?nip55=get_public_key&event=${keys.bob.pk}`), + ).toEqual({ kind: "pubkey", value: keys.bob.pk }); + }); + + it("ignores marker-less URLs even when they carry result-shaped params", () => { + // Regression: an innocent link with ?pubkey=/?sig=/… must NOT be treated + // as a NIP-55 callback (it would consume/strip signer state). + const none = { kind: null, value: null }; + expect(Core.parseNip55Callback(`?sig=${SIG_128}`)).toEqual(none); + expect(Core.parseNip55Callback(`?signature=${SIG_128}`)).toEqual(none); + expect(Core.parseNip55Callback(`?event=${SIG_128}`)).toEqual(none); + expect(Core.parseNip55Callback(`?result=${keys.bob.pk}`)).toEqual(none); + expect(Core.parseNip55Callback(`?pubkey=${keys.bob.pk}`)).toEqual(none); + expect(Core.parseNip55Callback(`?npub=${C.npubEncode(keys.bob.pk)}`)).toEqual(none); + // Unknown marker values do not count either. + expect(Core.parseNip55Callback(`?nip55=wat&event=${SIG_128}`)).toEqual(none); }); it("keeps the marker's kind even when a value is missing (cancel case)", () => { @@ -379,3 +401,134 @@ describe("NbreadSignerCore.completeUnsigned", () => { ).toThrow(); }); }); + +// --- NbreadSigner.resumePending: callback-type <-> pending-record binding ---- +// +// signer.js touches window/localStorage/location, so (like signer-nip46.spec) +// it is imported dynamically in beforeAll after minimal shims are installed — +// static imports hoist above module-body statements. `history` stays +// undefined; stripCallbackParams swallows that (cosmetic only). +describe("NbreadSigner.resumePending (NIP-55 callback binding)", () => { + const KEY_PENDING = "nbread:nip55:pending"; + const KEY_NIP55 = "nbread:signer:nip55"; + const KEY_METHOD = "nbread:signer:method"; + + const store = new Map(); + const loc = { + href: "https://nbread.lol/login", + origin: "https://nbread.lol", + pathname: "/login", + search: "", + }; + + type ResumeResult = { + kind: string | null; + unsigned: unknown; + signed?: Record; + pubkey?: string; + error?: string; + } | null; + + let Signer: { resumePending: () => ResumeResult }; + + beforeAll(async () => { + (globalThis as any).localStorage = { + getItem: (k: string) => (store.has(k) ? store.get(k)! : null), + setItem: (k: string, v: string) => void store.set(k, String(v)), + removeItem: (k: string) => void store.delete(k), + }; + (globalThis as any).location = loc; + // @ts-ignore — plain browser IIFE, intentionally shipped without types + await import("../../public/js/signer.js"); + Signer = (globalThis as any).NbreadSigner; + }); + + beforeEach(() => { + store.clear(); + setSearch(""); + }); + + function nowSec() { + return Math.floor(Date.now() / 1000); + } + function stash(record: unknown) { + store.set(KEY_PENDING, JSON.stringify(record)); + } + function setSearch(search: string) { + loc.search = search; + loc.href = "https://nbread.lol/login" + search; + } + function signFlowUnsigned() { + const unsigned: Unsigned = { + pubkey: keys.alice.pk, + created_at: 1_700_000_000, + kind: 1, + tags: [], + content: "hi", + }; + return { ...unsigned, id: C.eventId(unsigned) }; + } + + it("returns null and leaves all state alone on a marker-less URL", () => { + stash({ kind: "login", unsigned: null, returnTo: "/login", ts: nowSec() }); + setSearch(`?pubkey=${keys.alice.pk}`); + expect(Signer.resumePending()).toBeNull(); + expect(store.has(KEY_PENDING)).toBe(true); // NOT consumed + expect(store.has(KEY_NIP55)).toBe(false); + expect(store.has(KEY_METHOD)).toBe(false); + }); + + it("completes a legitimate get_public_key round-trip and persists the pubkey", () => { + stash({ kind: "login", unsigned: null, returnTo: "/login", ts: nowSec() }); + setSearch(`?nip55=pubkey&event=${keys.alice.pk}`); + const res = Signer.resumePending(); + expect(res).toEqual({ kind: "login", unsigned: null, pubkey: keys.alice.pk }); + expect(store.has(KEY_PENDING)).toBe(false); // one shot + expect(JSON.parse(store.get(KEY_NIP55)!)).toEqual({ pkHex: keys.alice.pk }); + expect(store.get(KEY_METHOD)).toBe("nip55"); + }); + + it("completes a legitimate sign_event round-trip (contract shape unchanged)", () => { + const full = signFlowUnsigned(); + // finalizeEvent rederives pubkey/id from the same fields, so its sig is + // exactly what Amber would append for this stashed unsigned event. + const signed = C.finalizeEvent(full, C.hexToBytes(keys.alice.sk)); + stash({ kind: "publish", unsigned: full, returnTo: "/write", ts: nowSec() }); + setSearch(`?nip55=sign&event=${signed.sig}`); + const res = Signer.resumePending(); + expect(res?.error).toBeUndefined(); + expect(res?.kind).toBe("publish"); + expect(res?.signed).toEqual({ ...full, sig: signed.sig }); + }); + + it("rejects a sign callback against a pubkey-flow pending record and consumes it", () => { + stash({ kind: "login", unsigned: null, returnTo: "/login", ts: nowSec() }); + setSearch(`?nip55=sign&event=${SIG_128}`); + const res = Signer.resumePending(); + expect(res?.error).toBe("unexpected signer callback"); + expect(res?.signed).toBeUndefined(); + expect(res?.pubkey).toBeUndefined(); + expect(store.has(KEY_PENDING)).toBe(false); // consumed even on rejection + expect(store.has(KEY_NIP55)).toBe(false); + expect(store.has(KEY_METHOD)).toBe(false); + }); + + it("rejects a pubkey callback against a sign-flow pending record — nothing persisted", () => { + stash({ kind: "publish", unsigned: signFlowUnsigned(), returnTo: "/write", ts: nowSec() }); + setSearch(`?nip55=pubkey&event=${keys.mallory.pk}`); + const res = Signer.resumePending(); + expect(res?.error).toBe("unexpected signer callback"); + expect(res?.pubkey).toBeUndefined(); + expect(store.has(KEY_PENDING)).toBe(false); + expect(store.has(KEY_NIP55)).toBe(false); // attacker pubkey never stored + expect(store.has(KEY_METHOD)).toBe(false); + }); + + it("never persists a pubkey when no pending record exists at all", () => { + setSearch(`?nip55=pubkey&event=${keys.mallory.pk}`); + const res = Signer.resumePending(); + expect(res?.error).toMatch(/expired/); + expect(store.has(KEY_NIP55)).toBe(false); + expect(store.has(KEY_METHOD)).toBe(false); + }); +}); From 3a82c03cac586c8ed2b4de456f1fc16f3a6419c1 Mon Sep 17 00:00:00 2001 From: sovITxyz Date: Thu, 16 Jul 2026 16:40:59 -0600 Subject: [PATCH 4/4] docs: signer-neutral copy + real-device signer test checklist docs/home/privacy copy now describes all four signing methods (and the localStorage caveat for pasted keys); public/js README documents the signer stack and load order; ops.md notes the vendor-bundle drift guard; docs/manual-signer-tests.md is the Amber/nsec.app/NIP-55/nsec-paste device checklist covering issue #2 acceptance. --- docs/manual-signer-tests.md | 127 ++++++++++++++++++ docs/ops.md | 8 ++ public/js/README.md | 31 ++++- src/views/main/docs.tsx | 20 ++- src/views/main/home.tsx | 7 +- src/views/main/privacy.tsx | 7 +- .../__snapshots__/discover.spec.ts.snap | 2 +- 7 files changed, 183 insertions(+), 19 deletions(-) create mode 100644 docs/manual-signer-tests.md diff --git a/docs/manual-signer-tests.md b/docs/manual-signer-tests.md new file mode 100644 index 0000000..2cd3c1a --- /dev/null +++ b/docs/manual-signer-tests.md @@ -0,0 +1,127 @@ +# Manual signer test checklist + +Real-device acceptance checks for the signer expansion (NIP-46 remote +signers, NIP-55 Amber, pasted local key) plus the NIP-07 regression. Run +against a deployed instance (workers.dev preview or nbread.lol) — the +NIP-55 flows need a real Android device with Amber installed, and the +NIP-46 flows need live wss:// relays. Automated coverage (unit + +integration) already gates CI; this list covers only what a browser and a +phone can prove. + +Conventions used below: + +- "editor" = `/dashboard/posts/new` or `/dashboard/editor?slug=…` +- Signer state lives under `localStorage` keys prefixed `nbread:` — clear + them (or use "Forget this signer" on `/login`) to reset between sections. + +## 1. Amber via bunker:// (NIP-46) — issue #2 acceptance + +End-to-end on any browser (desktop is fine; Amber runs on the phone and +talks over relays). + +- [ ] In Amber, create a bunker:// connection URI and copy it. +- [ ] On `/login`, choose "Remote signer — Amber, nsec.app (NIP-46)", + paste the `bunker://…` URI, press Connect; approve the connect + request in Amber. +- [ ] Sign-in completes: approving the kind 22242 challenge in Amber lands + you on `/dashboard` with your handle/npub shown. +- [ ] Publish: write a post in the editor, press "Sign & publish", approve + the kind 30023 in Amber → redirected to `/dashboard`, post renders at + `https://.nbread.lol/`. +- [ ] Edit: open the same post, change the body, "Sign & republish", + approve in Amber → the blog page shows the new content (same slug). +- [ ] Delete: "Delete post", confirm the dialog, approve the kind 5 in + Amber → post disappears from the blog and the dashboard list. + +## 2. nsec.app remote signer (NIP-46, incl. auth_url) + +- [ ] Pair via `bunker://` from nsec.app (or "Generate nostrconnect:// + link" and paste it into nsec.app) on `/login`. +- [ ] When nsec.app answers with an auth_url, the login page surfaces the + approval link as a link/prompt — it must NOT auto-navigate. Open it, + approve, and confirm the original request then completes. +- [ ] Sign in, publish a post, and delete it — each sign request appears + in nsec.app for approval and completes after approving. + +## 3. NIP-55 — Amber on Android Chrome + +All on the phone; the flow is full-page redirects into the Amber app and +back. + +- [ ] Sign in: `/login` → "Amber on this device (Android)" → "Sign in with + Amber" → Amber opens (get_public_key), approve → returned to + `/login`, then approve the challenge signature → session established. +- [ ] Publish: editor → "Sign & publish" → page hands off to Amber → + approve → returned to the editor, which resumes ("Resuming…"), + mirrors, broadcasts, and lands on `/dashboard`; post is live. +- [ ] Delete: open a post → "Delete post" → confirm → approve the kind 5 + in Amber → resumed on return, post gone. +- [ ] Cancel mid-sign: start a publish, but REJECT (or back out of) the + request in Amber. Back in the editor the status says signing was + cancelled, nothing was published, and the draft-notice offers to + restore your text — restoring brings the full draft back. +- [ ] Oversized post: paste content past the 256 KiB publish cap (the + counter turns red: "too large to publish") and attempt "Sign & + publish" with Amber. The flow must refuse cleanly — either the + intent hand-off fails or `/api/mirror` rejects the event on resume — + with an error message, no partial publish, and the draft intact. +- [ ] Reload safety: after any completed Amber round-trip, reloading the + page must not replay the signature (the callback params are stripped + from the URL). + +## 4. Pasted secret key (local) — desktop + Android + +- [ ] Desktop: `/login` → "Paste secret key (not recommended)" → the red + unencrypted-localStorage warning is visible → paste an `nsec1…` → + the derived npub is shown for confirmation → "Sign in as this key" + completes login. +- [ ] Publish and delete a post from the editor — signing happens in-page + with no prompts and no redirects. +- [ ] "Forget this signer" on `/login` clears the stored key + (`nbread:signer:nsec` gone from localStorage) and returns you to the + method picker. +- [ ] Re-import: paste the same nsec again and sign in — works identically + (and hex-form input works too). +- [ ] Repeat sign-in + publish on Android Chrome. + +## 5. NIP-07 regression (Alby or nos2x) + +Behavior must be identical to before the signer expansion. + +- [ ] `/login` → "Browser extension (NIP-07)" → "Sign in with extension" → + extension approval prompt → `/dashboard`. +- [ ] Publish, edit, and delete a post from the editor; each action + prompts the extension once and completes. +- [ ] With the extension disabled, the panel reports that no NIP-07 + extension (window.nostr) was found instead of failing silently. + +## 6. Negative checks (any signer unless noted) + +- [ ] Expired challenge: fetch `/login`, wait more than 5 minutes (the + nonce TTL is 300 s), then complete the sign-in — the server rejects + the stale challenge and the page shows the error; retrying fresh + succeeds. +- [ ] Dead bunker relay (NIP-46): pair using a `bunker://` URI whose relay + is unreachable — the connect times out with a clear error (60 s + request / 120 s pairing budget), no hang, no crash; the page stays + usable. +- [ ] Wrong-identity signer vs session: sign in as key A, then on `/login` + reconfigure the browser's signer to key B WITHOUT logging out. In the + editor, publishing must refuse with "This browser's signer is a + different Nostr identity than the one signed in…" and publish + nothing. +- [ ] NIP-55 expired pending record: start an Amber sign, leave the phone + idle past the pending TTL, then complete the callback — the editor + reports the request expired, nothing publishes, the draft survives. + +## 7. Devtools network audit (secret never leaves the page) + +With the pasted-key (local) signer configured: + +- [ ] Open devtools → Network, sign in, publish, and delete a post. +- [ ] Inspect EVERY request (fetches, WebSocket frames, and navigations, + including `intent:`/callback URLs on Android): no request body, URL, + or header ever contains the nsec or the 64-hex secret key — only + signed events (pubkey + sig) and the public key ever leave the page. +- [ ] Repeat the sweep once for a NIP-46 session: WebSocket frames to the + bunker relay carry only encrypted kind 24133 envelopes. diff --git a/docs/ops.md b/docs/ops.md index 2c13b81..ef748fc 100644 --- a/docs/ops.md +++ b/docs/ops.md @@ -320,3 +320,11 @@ see the manual-check notes at the bottom of `scripts/smoke.sh`). Rollback: `wrangler deployments list` + `wrangler rollback` restore the previous Worker version; DNS records can stay (the guard 404s anything it does not recognize). + +## 7. Vendor crypto bundle + +`public/js/vendor/nostr-crypto.js` is generated from +`scripts/vendor/crypto-entry.js` by `npm run build:vendor` (esbuild, +devDependency only — deploy never builds) and committed unminified. Never +edit the artifact by hand: edit the entry, rebuild, commit both. CI rebuilds +and fails on any drift between the entry and the committed bundle. diff --git a/public/js/README.md b/public/js/README.md index 2d14aae..4325bff 100644 --- a/public/js/README.md +++ b/public/js/README.md @@ -1,13 +1,28 @@ # public/js -Client-side NIP-07 glue plus the hand-rolled editor (no build step at serve +Client-side signer glue plus the hand-rolled editor (no build step at serve time, no runtime dependencies — every file is a plain IIFE served as-is): +- `signer-core.js` — DOM-free pure helpers (`globalThis.NbreadSignerCore`): + nsec decoding, pubkey normalization, the NIP-55 (Amber) intent URL + builder + callback parser, and the pending-record make/validate pair the + redirect flow persists across page unloads. Unit-tested directly. +- `signer.js` — the dispatcher (`globalThis.NbreadSigner`): one signing + seam (`ready`/`getPublicKey`/`signEvent`) over four backends — NIP-07 + extension, pasted local key (signs in-page, key never leaves the browser), + NIP-55 Amber redirects (incl. `resumePending()` after the callback), and + a `register()`ed NIP-46 backend. Owns the `nbread:signer:*` localStorage + keys and `forget()`. +- `signer-nip46.js` — NIP-46 remote-signer client + (`globalThis.NbreadNip46`): bunker:// / nostrconnect:// pairing, the + NIP-44 (with legacy NIP-04 fallback) request envelope, auth_url + surfacing, and schnorr verification of every returned event; registers + itself as the dispatcher's "nip46" backend on load. - `login.js` (P4) — fetches a one-time challenge, signs the kind 22242 auth - event via `window.nostr`, POSTs it to `/login`. Keys never leave the - extension. -- `editor.js` (P5) — builds kind 30023 / kind 5 events, signs via the - extension, broadcasts to relays, POSTs to `/api/mirror`. Also owns the + event via `NbreadSigner`, POSTs it to `/login`. Also owns the login-page + method picker and per-method panels. +- `editor.js` (P5) — builds kind 30023 / kind 5 events, signs via + `NbreadSigner`, broadcasts to relays, POSTs to `/api/mirror`. Also owns the server-rendered preview fetch: it listens for the `nbread:preview-requested` event (dispatched by the Preview tab), caches the last previewed value, and calls @@ -24,8 +39,10 @@ time, no runtime dependencies — every file is a plain IIFE served as-is): draft autosave (`window.NbreadDraft`). All textarea mutations go through one `execCommand("insertText")` seam so native undo survives. -Load order on the editor page matters: `editor-md.js` → -`editor-toolbar.js` → `editor.js`. +Load order matters (classic `