clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

add a bech32 encoder to the browser helpers

Dami committed 80f76c9
commit
80f76c966179c95328cde763f94c3d74261153d0
parent
48dadd1795339ccf4ed7ba616053d65fae17c399
author
Dami <dami.etoile@gmail.com>
2 files changed, +48 -1
Minternal/webui/tiny.js32+++++++++++++++++++++++++++++-
Mscripts/tiny-namespace.test.mjs17++++++++++++++++
M internal/webui/tiny.js +31 -1
@@ -56,7 +56,37 @@
5656 const text = value.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((value.length + 3) % 4);5757 return Uint8Array.from(atob(text), char => char.charCodeAt(0));5858 };59- tiny.util = Object.freeze({...tiny.util, bytes, hex, sha256, fromHex, element, relayURL, b64url, fromB64url});59+ // bech32Encode renders bytes as a NIP-19 bare string such as an nsec or60+ // npub. The browser bundle decodes these but has no plain encoder.61+ const bech32Charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";62+ const bech32Polymod = values => {63+ const generators = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];64+ let checksum = 1;65+ for (const value of values) {66+ const top = checksum >>> 25;67+ checksum = (((checksum & 0x1ffffff) << 5) ^ value) >>> 0;68+ generators.forEach((generator, index) => { if ((top >>> index) & 1) checksum = (checksum ^ generator) >>> 0; });69+ }70+ return checksum;71+ };72+ const bech32Encode = (prefix, value) => {73+ const words = [];74+ let accumulator = 0, bits = 0;75+ for (const byte of bytes(value)) {76+ accumulator = ((accumulator << 8) | byte) >>> 0;77+ bits += 8;78+ while (bits >= 5) {79+ bits -= 5;80+ words.push((accumulator >>> bits) & 31);81+ }82+ }83+ if (bits > 0) words.push((accumulator << (5 - bits)) & 31);84+ const expanded = [...prefix].map(char => char.charCodeAt(0) >>> 5).concat(0, [...prefix].map(char => char.charCodeAt(0) & 31));85+ const polymod = bech32Polymod(expanded.concat(words, [0, 0, 0, 0, 0, 0])) ^ 1;86+ const checksum = Array.from({length: 6}, (_, index) => (polymod >>> (5 * (5 - index))) & 31);87+ return prefix + "1" + words.concat(checksum).map(word => bech32Charset[word]).join("");88+ };89+ tiny.util = Object.freeze({...tiny.util, bytes, hex, sha256, fromHex, element, relayURL, b64url, fromB64url, bech32Encode});6090 // signer returns the active signer: a resumed remote signer first, then a6191 // NIP-07 extension.6292 tiny.signer = () => globalThis.tinySigner || globalThis.nostr;
M scripts/tiny-namespace.test.mjs +17 -0
@@ -3,6 +3,7 @@ import {webcrypto} from "node:crypto";
33 import {readFile} from "node:fs/promises";44 import test from "node:test";55 import vm from "node:vm";6+import {generateSecretKey, getPublicKey, nip19} from "nostr-tools";67 78 const read = name => readFile(new URL(`../internal/webui/${name}`, import.meta.url), "utf8");89 const [tiny, encryption, manifests, upload] = await Promise.all([
@@ -23,3 +24,19 @@ test("feature modules bootstrap through the shared tiny namespace", () => {
2324 vm.runInNewContext(tiny + encryption + manifests + upload, sandbox);2425 assert.equal(typeof sandbox.tiny.blossom.manifests.decodeManifest, "function");2526 });27+28+test("bech32Encode matches the reference nsec and npub encodings", () => {29+ const sandbox = {crypto: webcrypto, TextEncoder, TextDecoder, URL, URLSearchParams, Uint8Array, ArrayBuffer};30+ sandbox.globalThis = sandbox;31+ vm.runInNewContext(tiny, sandbox);32+ const {bech32Encode} = sandbox.tiny.util;33+ for (let i = 0; i < 8; i++) {34+ const secret = generateSecretKey();35+ assert.equal(bech32Encode("nsec", secret), nip19.nsecEncode(secret));36+ const pubkey = getPublicKey(secret);37+ assert.equal(bech32Encode("npub", Uint8Array.from(Buffer.from(pubkey, "hex"))), nip19.npubEncode(pubkey));38+ }39+ // The NIP-19 example key.40+ const example = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d";41+ assert.equal(bech32Encode("npub", Uint8Array.from(Buffer.from(example, "hex"))), "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6");42+});