bech32-label
v0.1.0
Published
Convert 32-byte public keys to DNS-safe bech32 labels for per-identity subdomains
Maintainers
Readme
bech32-label
A tiny, zero-dependency JavaScript library and CLI that converts 32-byte public keys (64-char hex) ↔ DNS-safe bech32 data-only labels (52 chars) for per-identity subdomains.
Why?
Problem: You want per-user subdomains like <user-id>.<provider-domain> to isolate browser origins (cookies, localStorage, service workers) and reduce cross-tenant XSS/CSRF attacks.
Challenge: 64-char hex public keys exceed DNS label limits (63 chars), and full npub... encodings are display-oriented with HRP/checksum that aren't suitable as normative origin identifiers.
Solution: Convert 32-byte keys to exactly 52-char bech32 data-only labels that:
- Fit comfortably in DNS labels (63 char limit)
- Use only DNS-safe characters
- Are deterministic and canonical
- Have no HRP or checksum (Test of Independent Invention compliant)
Installation
npm install bech32-labelAPI Usage
import { encodeHexToLabel, decodeLabelToHex, isValidHex, isValidLabel } from 'bech32-label';
// Convert 64-char hex to 52-char bech32 label
const hex = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
const label = encodeHexToLabel(hex);
console.log(label); // "qgplqa02h2wqvdnn54k2vdnrpznrpznrpznrpznrpznrpznrpzn"
// Convert back
const decodedHex = decodeLabelToHex(label);
console.log(decodedHex === hex.toLowerCase()); // true
// Validation
console.log(isValidHex(hex)); // true
console.log(isValidLabel(label)); // trueCLI Usage
# Install globally
npm install -g bech32-label
# Encode hex to label
bech32-label encode 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
# Decode label to hex
bech32-label decode qgplqa02h2wqvdnn54k2vdnrpznrpznrpznrpznrpznrpznrpznSpecification
Input/Output Format
- Input: 32-byte public key as 64-character lowercase hex string
- Output: 52-character bech32 data-only label using charset
qpzry9x8gf2tvdw0s3jn54khce6mua7l
Encoding Process
- Validation: Input must match
/^[0-9a-f]{64}$/i(case-insensitive input, lowercase output) - Conversion: Transform bytes using 8→5 bit conversion, MSB-first
- Padding: Zero-pad the tail as needed (
pad=true) - Mapping: Map each 5-bit word to bech32 charset character
- Result: Exactly 52 lowercase characters
Decoding Process
- Validation: Label must match
/^[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{52}$/ - Conversion: Map characters to 5-bit words, then 5→8 bit conversion
- Length Check: Result must be exactly 32 bytes
- Canonicality: Re-encode and verify it matches the original label exactly
Error Messages
"Expected 64-char hex"- Invalid hex input"Invalid bech32 character"- Non-alphabet character in label"Invalid label length"- Label not exactly 52 characters"Decoded length is not 32 bytes"- Invalid padding or corruption"Non-canonical encoding"- Label doesn't round-trip correctly
Implementation Details
- Zero dependencies: Pure JavaScript, no external libraries
- ESM only: Modern module format (
"type": "module") - Node.js ≥18: Uses built-in
node:testfor testing - Fast & light: Optimized for performance and minimal allocations
- Deterministic: Same input always produces same output
- Canonical: Rejects non-canonical encodings to prevent confusion
Domain Usage Example
import { encodeHexToLabel } from 'bech32-label';
// User's public key
const userPubkey = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
const userLabel = encodeHexToLabel(userPubkey);
// Create isolated subdomain
const userOrigin = `https://${userLabel}.myapp.com`;
// → https://qgplqkq5h2wqvdnn54k2vdnrpznrpznrpznrpznrpznrpznrpzn.myapp.com
// Each user gets their own origin for complete isolationLicense
MIT
Contributing
This package prioritizes simplicity and compliance with the functional specification. Please ensure any changes maintain:
- Zero dependencies
- Deterministic behavior
- Full round-trip compatibility
- Comprehensive error handling
- Performance optimization
