@bigdreamsweb3/wordbin 1.1.8 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONTRIBUTING.md CHANGED
@@ -45,7 +45,7 @@ If you find a bug:
45
45
  - Expected vs actual behaviour
46
46
  - Node.js version (`node -v`)
47
47
  - Dictionary version used (v1 / v2 / custom)
48
- - The payload format involved (hex / base58 / base64 / bin21 / bytes)
48
+ - The payload format involved (hex / base58 / base64 / bytes)
49
49
  - A minimal code snippet or failing test case
50
50
 
51
51
  ### Suggesting Features or Improvements
@@ -64,7 +64,7 @@ For larger architectural changes — new payload formats, new dictionary structu
64
64
  1. **Fork** the repository and **clone** your fork
65
65
  2. Create a focused branch:
66
66
  ```bash
67
- git checkout -b fix/decode-bin21-format
67
+ git checkout -b fix/decode-format-detection
68
68
  # or
69
69
  git checkout -b feat/add-top-20k-dictionary
70
70
  ```
@@ -76,10 +76,10 @@ For larger architectural changes — new payload formats, new dictionary structu
76
76
  ```
77
77
  6. Commit with clear, semantic messages:
78
78
  ```
79
- fix: correct bin21 format detection in detectAndConvert
79
+ fix: correct format detection in detectAndConvert
80
80
  feat: add partialScan fallback for non-WordBin payloads
81
81
  docs: update decode API in README
82
- test: add round-trip cases for base58 and bin21 formats
82
+ test: add round-trip cases for base58 and base64 formats
83
83
  ```
84
84
  7. Push your branch and open a pull request against `main`
85
85
 
@@ -180,26 +180,26 @@ Set any flag to `false` to skip that suite entirely. Skipped suites are reported
180
180
 
181
181
  | Suite | What it tests |
182
182
  | ---------------------- | -------------------------------------------------------------------------------------------- |
183
- | **Encode only** | All payload formats (hex, base58, base64, bin21), compression ratio, format validity |
183
+ | **Encode only** | All payload formats (hex, base58, base64), compression ratio, format validity |
184
184
  | **Decode only** | Known hex payload, known base64 payload, non-WordBin fallback |
185
- | **Encode then decode** | Full round-trip for hex, base58, base64, bin21, and raw `Uint8Array` |
185
+ | **Encode then decode** | Full round-trip for hex, base58, base64, and raw `Uint8Array` |
186
186
  | **Non-WordBin decode** | 4 foreign payloads — verifies `isWordBin: false`, `notice` is set, `text` is always a string |
187
187
 
188
188
  ---
189
189
 
190
190
  ## Where Things Live
191
191
 
192
- | What you want to change | File |
193
- | -------------------------------------------------------- | ------------------------------------ |
194
- | Encode / decode logic | `src/core/wordbin.ts` |
195
- | Payload format detection (hex / base58 / base64 / bin21) | `detectAndConvert()` in `wordbin.ts` |
196
- | `DecodeResult` type or `PayloadFormat` union | top of `wordbin.ts` |
197
- | Partial scan / best-effort fallback | `partialScan()` in `wordbin.ts` |
198
- | Dictionary loading and versioning | `src/dict/dictionary-loader.ts` |
199
- | Building a dictionary from a wordlist | `src/dict/builder.ts` |
200
- | Buffer utilities (varint, hex, utf8) | `src/utils/buffer.ts` |
201
- | Shared constants (LITERAL byte value) | `src/constants.ts` |
202
- | Tests | `test/test.spec.ts` |
192
+ | What you want to change | File |
193
+ | ------------------------------------------------ | ------------------------------------ |
194
+ | Encode / decode logic | `src/core/wordbin.ts` |
195
+ | Payload format detection (hex / base58 / base64) | `detectAndConvert()` in `wordbin.ts` |
196
+ | `DecodeResult` type or `PayloadFormat` union | top of `wordbin.ts` |
197
+ | Partial scan / best-effort fallback | `partialScan()` in `wordbin.ts` |
198
+ | Dictionary loading and versioning | `src/dict/dictionary-loader.ts` |
199
+ | Building a dictionary from a wordlist | `src/dict/builder.ts` |
200
+ | Buffer utilities (varint, hex, utf8) | `src/utils/buffer.ts` |
201
+ | Shared constants (LITERAL byte value) | `src/constants.ts` |
202
+ | Tests | `test/test.spec.ts` |
203
203
 
204
204
  ---
205
205
 
@@ -209,7 +209,7 @@ These improvements would have the biggest impact right now:
209
209
 
210
210
  - **Case-insensitive dictionaries** — normalise to lowercase during dictionary building so `"Hello"` and `"hello"` encode identically
211
211
  - **Smaller curated dictionaries** — top 10k–50k common English words, programming keywords, domain-specific lists (DeFi, medical, etc.)
212
- - **Bin21 safety**audit which Latin-1 characters survive common transports (databases, JSON fields) without corruption; document safe subsets
212
+ - Note: Bin21 removedhex is primary payload. Remove any Bin21-specific tests or docs.
213
213
  - **Performance benchmarks** — encode/decode throughput across dictionary sizes and phrase lengths
214
214
  - **Browser demo** — a minimal CodeSandbox or static HTML page showing encode/decode live
215
215
  - **CLI enhancements** — progress bars, `--lowercase` flag, `--output-dir`, improved help text
package/README.md CHANGED
@@ -22,7 +22,6 @@ Saved : 38 bytes — 47% of original size
22
22
  Hex : 0108c424409e363270f7d64deba55e2e11ba716eba59926de2f50282599fc5afd1a8
23
23
  Base58 : 2MepGpLHGPPmnrdzjmpqet2XFQ2YGMSpQoDXDex7toUBdZ
24
24
  Base64 : AQjEJECeNjJw99ZN66VeLhG6cW66WZJt4vUCglmfxa/RqA==
25
- Bin21 : ☺◄Ä$@ž6rp÷ÖMë¥^.►ºqnºY™mâõ☻™Å¯Ñ¨
26
25
 
27
26
  Decoded: "stock ridge avoid school honey trap wait wheel worry face differ wedding" ✓
28
27
  ```
@@ -34,7 +33,7 @@ Decoded: "stock ridge avoid school honey trap wait wheel worry face differ weddi
34
33
  - **40–70% size reduction** on typical short phrases
35
34
  - **Deterministic** — same input + same dictionary = same output, every time
36
35
  - **Lossless** — decode is always a perfect round-trip
37
- - **Universal decoder** — accepts hex, Base58, Base64, Bin21, or raw bytes; format is auto-detected
36
+ - **Universal decoder** — accepts hex, Base58, Base64, or raw bytes; format is auto-detected
38
37
  - **Resilient** — non-WordBin payloads are never rejected; partial word extraction is attempted before falling back gracefully
39
38
  - **No runtime dependencies** — works in Node.js and the browser
40
39
  - **Flexible dictionaries** — BIP-39 (v1, bundled), large English (v2), or custom wordlists
@@ -58,28 +57,29 @@ import { WordBin } from "@bigdreamsweb3/wordbin";
58
57
 
59
58
  const wb = await WordBin.create();
60
59
 
61
- const phrase =
62
- "stock ridge avoid school honey trap wait wheel worry face differ wedding";
60
+ const phrase = "the quick brown fox jumps over thirteen lazy dogs";
63
61
 
64
62
  // ── Encode ────────────────────────────────────────────────────────────────────
65
63
  const encoded = await wb.encode(phrase);
66
64
 
65
+ console.log(encoded.dictVersion); // 2
67
66
  console.log(encoded.hexPayload); // standard hex string
68
67
  console.log(encoded.base58Payload); // Base58 string
69
68
  console.log(encoded.base64Payload); // Base64 string
70
- console.log(encoded.payload); // Bin21 (1 char per byte, most compact printable form)
71
- console.log(encoded.encodedBytes); // 34
72
- console.log(encoded.originalBytes); // 72
73
- console.log(encoded.ratioPercent); // 47.22
69
+ console.log(encoded.payload); // hex payload (primary)
70
+ console.log(encoded.encodedBytes); // 24
71
+ console.log(encoded.originalBytes); // 49
72
+ console.log(encoded.bytesSaved); // 25
73
+ console.log(encoded.ratioPercent); // 48.98
74
74
 
75
75
  // ── Decode — pass any format, it's auto-detected ──────────────────────────────
76
76
  const r1 = await wb.decode(encoded.hexPayload); // DetectedFormat: "hex"
77
77
  const r2 = await wb.decode(encoded.base58Payload); // DetectedFormat: "base58"
78
78
  const r3 = await wb.decode(encoded.base64Payload); // DetectedFormat: "base64"
79
- const r4 = await wb.decode(encoded.payload); // DetectedFormat: "bin21"
79
+ const r4 = await wb.decode(encoded.payload); // DetectedFormat: "hex"
80
80
  const r5 = await wb.decode(encoded.encoded); // DetectedFormat: "bytes" (Uint8Array)
81
81
 
82
- console.log(r1.text); // "stock ridge avoid school honey trap..."
82
+ console.log(r1.text); // "the quick brown fox jumps over..."
83
83
  console.log(r1.isWordBin); // true
84
84
  ```
85
85
 
@@ -101,15 +101,14 @@ console.log(r1.isWordBin); // true
101
101
 
102
102
  WordBin produces four interchangeable representations of the same encoded bytes. Pass any of them to `decode()` — the format is detected automatically.
103
103
 
104
- | Format | Field | Description | Size |
105
- | ---------- | --------------- | -------------------------------------- | -------------------------------- |
106
- | **Hex** | `hexPayload` | Lowercase hex, 2 chars per byte | 2× raw |
107
- | **Base58** | `base58Payload` | URL-safe, no ambiguous chars (0/O/I/l) | ~1.4× raw |
108
- | **Base64** | `base64Payload` | Standard Base64 with `=` padding | ~1.33× raw |
109
- | **Bin21** | `payload` | Latin-1 string, 1 char per byte | 1× raw — smallest printable form |
110
- | **Bytes** | `encoded` | Raw `Uint8Array` | 1× raw |
104
+ | Format | Field | Description | Size |
105
+ | ---------- | --------------- | -------------------------------------- | ---------- |
106
+ | **Hex** | `hexPayload` | Lowercase hex, 2 chars per byte | 2× raw |
107
+ | **Base58** | `base58Payload` | URL-safe, no ambiguous chars (0/O/I/l) | ~1.4× raw |
108
+ | **Base64** | `base64Payload` | Standard Base64 with `=` padding | ~1.33× raw |
109
+ | **Bytes** | `encoded` | Raw `Uint8Array` | 1× raw |
111
110
 
112
- > **Bin21** is WordBin's signature format: each encoded byte maps to exactly one character. No expansion. A 34-byte payload is a 34-character string.
111
+ > `payload` is now hex (lowercase, two characters per byte). A 34-byte payload is a 68-character hex string.
113
112
 
114
113
  ---
115
114
 
@@ -121,7 +120,7 @@ WordBin produces four interchangeable representations of the same encoded bytes.
121
120
  interface DecodeResult {
122
121
  text: string; // decoded words, or best-effort extraction
123
122
  isWordBin: boolean; // true = valid WordBin payload, perfectly decoded
124
- detectedFormat: PayloadFormat; // "hex" | "base58" | "base64" | "bin21" | "bytes"
123
+ detectedFormat: PayloadFormat; // "hex" | "base58" | "base64" | "bytes"
125
124
  notice?: string; // present when payload is not a valid WordBin stream
126
125
  rawSegments?: string[]; // unmatched bytes shown as [0xXX], non-WordBin only
127
126
  }
@@ -133,7 +132,7 @@ interface DecodeResult {
133
132
  Payload received
134
133
 
135
134
 
136
- Format detection ──── hex / base58 / base64 / bin21 / bytes
135
+ Format detection ──── hex / base58 / base64 / bytes
137
136
 
138
137
 
139
138
  Strict WordBin parse (all installed dictionary versions)
@@ -150,11 +149,14 @@ Strict WordBin parse (all installed dictionary versions)
150
149
 
151
150
  ```ts
152
151
  // Non-WordBin payload — still handled gracefully
153
- const result = await wb.decode("48656c6c6f20576f726c64"); // "Hello World" as hex
152
+ const result = await wb.decode("68656c6c6f20776f726c6421"); // Plain "hello world!" as hex
154
153
 
154
+ // console.log("Input payload :", foreignHex);
155
+ console.log(result.text); // hello world[raw:!]
156
+ console.log(result.detectedFormat); // hex
155
157
  console.log(result.isWordBin); // false
156
158
  console.log(result.notice); // "This does not appear to be a valid WordBin payload..."
157
- console.log(result.rawSegments); // ["[0x48]", "[0x65]", ...] — unmatched bytes
159
+ console.log(result.rawSegments); // [ '[raw:!]' ] — unmatched bytes
158
160
  ```
159
161
 
160
162
  ---
@@ -164,7 +166,7 @@ console.log(result.rawSegments); // ["[0x48]", "[0x65]", ...] — unmatched byte
164
166
  1. **Version header** — first byte identifies the dictionary version (`0x01` for v1)
165
167
  2. **Dictionary lookup** — each word in the phrase is replaced by its compact binary ID (1–4 bytes)
166
168
  3. **Literal fallback** — words not in the dictionary are stored as `varint length + UTF-8 bytes`
167
- 4. **Payload representations** — the raw bytes are encoded into hex, Base58, Base64, and Bin21
169
+ 4. **Payload representations** — the raw bytes are encoded into hex, Base58, and Base64
168
170
 
169
171
  Payloads are **self-describing** (the version byte is embedded) and **fully lossless**.
170
172
 
@@ -329,14 +331,14 @@ The `./data/` directory is gitignored. Built dictionaries are loaded automatical
329
331
 
330
332
  The most common contribution points:
331
333
 
332
- | What you want to change | Where to look |
333
- | ------------------------------------------ | -------------------------------------------------------- |
334
- | Encode / decode logic | `src/core/wordbin.ts` |
335
- | Format detection (hex/base58/base64/bin21) | `detectAndConvert()` in `wordbin.ts` |
336
- | Dictionary loading / versioning | `src/dict/dictionary-loader.ts` |
337
- | Dictionary building from a wordlist | `src/dict/builder.ts` |
338
- | Buffer utilities (varint, hex, utf8) | `src/utils/buffer.ts` |
339
- | Add a new payload format | `PayloadFormat` type + `detectAndConvert()` + `encode()` |
334
+ | What you want to change | Where to look |
335
+ | ------------------------------------ | -------------------------------------------------------- |
336
+ | Encode / decode logic | `src/core/wordbin.ts` |
337
+ | Format detection (hex/base58/base64) | `detectAndConvert()` in `wordbin.ts` |
338
+ | Dictionary loading / versioning | `src/dict/dictionary-loader.ts` |
339
+ | Dictionary building from a wordlist | `src/dict/builder.ts` |
340
+ | Buffer utilities (varint, hex, utf8) | `src/utils/buffer.ts` |
341
+ | Add a new payload format | `PayloadFormat` type + `detectAndConvert()` + `encode()` |
340
342
 
341
343
  ### Submitting a pull request
342
344
 
@@ -1 +1 @@
1
- {"version":3,"file":"builder-vFphFQMU.js","sources":["../src/core/tiers.ts","../src/core/id.ts","../src/utils/buffer.ts","../src/dict/builder.ts"],"sourcesContent":["export function getIdByteLength(wordLength: number): number {\r\n if (wordLength <= 4) return 2;\r\n if (wordLength <= 9) return 3;\r\n return 4;\r\n}\r\n\r\nexport function getWrapByteLength(wordLength: number): number {\r\n if (wordLength <= 4) return 2;\r\n if (wordLength <= 9) return 3;\r\n return 4;\r\n}\r\n\r\nexport async function getTextEncoder(): Promise<TextEncoder> {\r\n if (typeof TextEncoder !== \"undefined\") return new TextEncoder();\r\n const { TextEncoder: NodeTextEncoder } = await import(\"node:util\");\r\n // @ts-ignore Node typings\r\n return new NodeTextEncoder();\r\n}\r\n\r\nexport async function wrapBase64(data: string): Promise<Uint8Array> {\r\n const normalized = data.trim().toLowerCase();\r\n if (!normalized) throw new Error(\"Cannot generate ID for empty string\");\r\n\r\n const encoder = await getTextEncoder();\r\n const result = encoder.encode(normalized);\r\n\r\n // Browser + Node compatible SHA-256\r\n let hash: ArrayBuffer;\r\n const anyCrypto: any = (globalThis as any).crypto;\r\n if (anyCrypto && anyCrypto.subtle) {\r\n hash = await anyCrypto.subtle.digest(\"SHA-256\", result);\r\n } else {\r\n const { createHash } = await import(\"node:crypto\");\r\n hash = createHash(\"sha256\").update(Buffer.from(result)).digest().buffer;\r\n }\r\n\r\n const hashBytes = new Uint8Array(hash);\r\n const size = getWrapByteLength(normalized.length);\r\n return hashBytes.slice(0, size);\r\n}\r\n","import { getIdByteLength } from './tiers.js'\r\n\r\n/**\r\n * Deterministic word \t ID generator\r\n * Same output on browser and node (when using compatible input)\r\n */\r\nexport async function generateWordId(word: string): Promise<Uint8Array> {\r\n const normalized = word.trim().toLowerCase()\r\n if (!normalized) throw new Error('Cannot generate ID for empty string')\r\n\r\n const encoder = await getTextEncoder()\r\n const data = encoder.encode(normalized)\r\n\r\n // Browser + Node compatible SHA-256\r\n let hash: ArrayBuffer\r\n const anyCrypto: any = (globalThis as any).crypto\r\n if (anyCrypto && anyCrypto.subtle) {\r\n hash = await anyCrypto.subtle.digest('SHA-256', data)\r\n } else {\r\n const { createHash } = await import('node:crypto')\r\n hash = createHash('sha256').update(Buffer.from(data)).digest().buffer\r\n }\r\n\r\n const hashBytes = new Uint8Array(hash)\r\n const size = getIdByteLength(normalized.length)\r\n return hashBytes.slice(0, size)\r\n}\r\n\r\nasync function getTextEncoder(): Promise<TextEncoder> {\r\n if (typeof TextEncoder !== 'undefined') return new TextEncoder()\r\n const { TextEncoder: NodeTextEncoder } = await import('node:util')\r\n // @ts-ignore Node typings\r\n return new NodeTextEncoder()\r\n}\r\n","export function toHex(bytes: Uint8Array): string {\r\n return Array.from(bytes)\r\n .map((b) => b.toString(16).padStart(2, '0'))\r\n .join('')\r\n}\r\n\r\nexport function fromHex(hex: string): Uint8Array {\r\n if (hex.length % 2 !== 0) throw new Error('Invalid hex string length')\r\n const bytes = new Uint8Array(hex.length / 2)\r\n for (let i = 0; i < hex.length; i += 2) {\r\n bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16)\r\n }\r\n return bytes\r\n}\r\n\r\nexport function toBase64(bytes: Uint8Array): string {\r\n const b64 = (globalThis as any).btoa\r\n if (typeof b64 === 'function') {\r\n return b64(String.fromCharCode(...bytes))\r\n }\r\n // Node fallback\r\n return Buffer.from(bytes).toString('base64')\r\n}\r\n\r\nexport function fromBase64(base64: string): Uint8Array {\r\n const at = (globalThis as any).atob\r\n if (typeof at === 'function') {\r\n const binary = at(base64)\r\n const out = new Uint8Array(binary.length)\r\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i)\r\n return out\r\n }\r\n // Node fallback\r\n return new Uint8Array(Buffer.from(base64, 'base64'))\r\n}\r\n\r\n// UTF-8 helpers\r\nexport function utf8Encode(str: string): Uint8Array {\r\n if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(str)\r\n // Node fallback\r\n return new Uint8Array(Buffer.from(str, 'utf8'))\r\n}\r\n\r\nexport function utf8Decode(bytes: Uint8Array): string {\r\n if (typeof TextDecoder !== 'undefined') return new TextDecoder().decode(bytes)\r\n // Node fallback\r\n return Buffer.from(bytes).toString('utf8')\r\n}\r\n\r\n// Varint (LEB128 7-bit groups) helpers\r\nexport function encodeVarint(n: number): Uint8Array {\r\n if (n < 0) throw new Error('Varint cannot encode negative numbers')\r\n const out: number[] = []\r\n do {\r\n let byte = n & 0x7f\r\n n >>>= 7\r\n if (n !== 0) byte |= 0x80\r\n out.push(byte)\r\n } while (n !== 0)\r\n return new Uint8Array(out)\r\n}\r\n\r\nexport function decodeVarint(bytes: Uint8Array, offset: number): { value: number; bytesRead: number } {\r\n let result = 0\r\n let shift = 0\r\n let pos = offset\r\n while (pos < bytes.length) {\r\n const byte = bytes[pos++]\r\n result |= (byte & 0x7f) << shift\r\n if ((byte & 0x80) === 0) {\r\n return { value: result, bytesRead: pos - offset }\r\n }\r\n shift += 7\r\n if (shift > 35) throw new Error('Varint too large')\r\n }\r\n throw new Error('Truncated varint')\r\n}\r\n","// File: src\\dict\\builder.ts\r\n\r\nimport type { WordBinDictionary } from \"../types\";\r\nimport { generateWordId } from \"../core/id.js\";\r\nimport { toHex } from \"../utils/buffer.js\";\r\n\r\nexport interface BuildDictionaryOptions {\r\n /**\r\n * Dictionary version number (used in header and for format compatibility)\r\n * @default 1\r\n */\r\n version?: number;\r\n\r\n /**\r\n * Human-readable description of this dictionary\r\n * @default \"WordBin dictionary v${version}\"\r\n */\r\n description?: string;\r\n\r\n /**\r\n * Optional: custom prefix or identifier for this dictionary build\r\n * (can be used in logs, filenames, etc.)\r\n */\r\n name?: string;\r\n}\r\n\r\nexport async function buildDictionary(\r\n words: string[],\r\n options: BuildDictionaryOptions = {},\r\n): Promise<WordBinDictionary> {\r\n const { version = 1, description = `WordBin dictionary v${version}` } =\r\n options;\r\n\r\n const map: Record<string, string[]> = {};\r\n\r\n const normalizedWords = words\r\n .map((w) => w.trim().toLowerCase())\r\n .filter((w) => w);\r\n\r\n await Promise.all(\r\n normalizedWords.map(async (word) => {\r\n let attempt = 0;\r\n let key: string;\r\n\r\n while (true) {\r\n const id = await generateWordId(\r\n attempt === 0 ? word : `${word}:${attempt}`,\r\n );\r\n\r\n key = toHex(id);\r\n\r\n // If no collision, break\r\n if (!map[key]) {\r\n map[key] = [word];\r\n break;\r\n }\r\n\r\n // Collision detected → try again\r\n attempt++;\r\n }\r\n }),\r\n );\r\n\r\n Object.values(map).forEach((collisions) => {\r\n collisions.sort((a, b) => a.localeCompare(b));\r\n });\r\n\r\n return {\r\n version,\r\n description,\r\n words: map,\r\n };\r\n}\r\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"builder-vFphFQMU.js","sources":["../src/core/tiers.ts","../src/core/id.ts","../src/utils/buffer.ts","../src/dict/builder.ts"],"sourcesContent":["export function getIdByteLength(wordLength: number): number {\r\n if (wordLength <= 4) return 2;\r\n if (wordLength <= 9) return 3;\r\n return 4;\r\n}\r\n\r\nexport function getWrapByteLength(wordLength: number): number {\r\n if (wordLength <= 4) return 2;\r\n if (wordLength <= 9) return 3;\r\n return 4;\r\n}\r\n\r\nexport async function getTextEncoder(): Promise<TextEncoder> {\r\n if (typeof TextEncoder !== \"undefined\") return new TextEncoder();\r\n const { TextEncoder: NodeTextEncoder } = await import(\"node:util\");\r\n // @ts-ignore Node typings\r\n return new NodeTextEncoder();\r\n}\r\n\r\nexport async function wrapBase64(data: string): Promise<Uint8Array> {\r\n const normalized = data.trim().toLowerCase();\r\n if (!normalized) throw new Error(\"Cannot generate ID for empty string\");\r\n\r\n const encoder = await getTextEncoder();\r\n const result = encoder.encode(normalized);\r\n\r\n // Browser + Node compatible SHA-256\r\n let hash: ArrayBuffer;\r\n const anyCrypto: any = (globalThis as any).crypto;\r\n if (anyCrypto && anyCrypto.subtle) {\r\n hash = await anyCrypto.subtle.digest(\"SHA-256\", result);\r\n } else {\r\n const { createHash } = await import(\"node:crypto\");\r\n hash = createHash(\"sha256\").update(Buffer.from(result)).digest().buffer;\r\n }\r\n\r\n const hashBytes = new Uint8Array(hash);\r\n const size = getWrapByteLength(normalized.length);\r\n return hashBytes.slice(0, size);\r\n}\r\n","// File: src/core/id.ts\r\n\r\nimport { getIdByteLength } from './tiers.js'\r\n\r\n/**\r\n * Deterministic word \t ID generator\r\n * Same output on browser and node (when using compatible input)\r\n */\r\nexport async function generateWordId(word: string): Promise<Uint8Array> {\r\n const normalized = word.trim().toLowerCase()\r\n if (!normalized) throw new Error('Cannot generate ID for empty string')\r\n\r\n const encoder = await getTextEncoder()\r\n const data = encoder.encode(normalized)\r\n\r\n // Browser + Node compatible SHA-256\r\n let hash: ArrayBuffer\r\n const anyCrypto: any = (globalThis as any).crypto\r\n if (anyCrypto && anyCrypto.subtle) {\r\n hash = await anyCrypto.subtle.digest('SHA-256', data)\r\n } else {\r\n const { createHash } = await import('node:crypto')\r\n hash = createHash('sha256').update(Buffer.from(data)).digest().buffer\r\n }\r\n\r\n const hashBytes = new Uint8Array(hash)\r\n const size = getIdByteLength(normalized.length)\r\n return hashBytes.slice(0, size)\r\n}\r\n\r\nasync function getTextEncoder(): Promise<TextEncoder> {\r\n if (typeof TextEncoder !== 'undefined') return new TextEncoder()\r\n const { TextEncoder: NodeTextEncoder } = await import('node:util')\r\n // @ts-ignore Node typings\r\n return new NodeTextEncoder()\r\n}\r\n","export function toHex(bytes: Uint8Array): string {\r\n return Array.from(bytes)\r\n .map((b) => b.toString(16).padStart(2, '0'))\r\n .join('')\r\n}\r\n\r\nexport function fromHex(hex: string): Uint8Array {\r\n if (hex.length % 2 !== 0) throw new Error('Invalid hex string length')\r\n const bytes = new Uint8Array(hex.length / 2)\r\n for (let i = 0; i < hex.length; i += 2) {\r\n bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16)\r\n }\r\n return bytes\r\n}\r\n\r\nexport function toBase64(bytes: Uint8Array): string {\r\n const b64 = (globalThis as any).btoa\r\n if (typeof b64 === 'function') {\r\n return b64(String.fromCharCode(...bytes))\r\n }\r\n // Node fallback\r\n return Buffer.from(bytes).toString('base64')\r\n}\r\n\r\nexport function fromBase64(base64: string): Uint8Array {\r\n const at = (globalThis as any).atob\r\n if (typeof at === 'function') {\r\n const binary = at(base64)\r\n const out = new Uint8Array(binary.length)\r\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i)\r\n return out\r\n }\r\n // Node fallback\r\n return new Uint8Array(Buffer.from(base64, 'base64'))\r\n}\r\n\r\n// UTF-8 helpers\r\nexport function utf8Encode(str: string): Uint8Array {\r\n if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(str)\r\n // Node fallback\r\n return new Uint8Array(Buffer.from(str, 'utf8'))\r\n}\r\n\r\nexport function utf8Decode(bytes: Uint8Array): string {\r\n if (typeof TextDecoder !== 'undefined') return new TextDecoder().decode(bytes)\r\n // Node fallback\r\n return Buffer.from(bytes).toString('utf8')\r\n}\r\n\r\n// Varint (LEB128 7-bit groups) helpers\r\nexport function encodeVarint(n: number): Uint8Array {\r\n if (n < 0) throw new Error('Varint cannot encode negative numbers')\r\n const out: number[] = []\r\n do {\r\n let byte = n & 0x7f\r\n n >>>= 7\r\n if (n !== 0) byte |= 0x80\r\n out.push(byte)\r\n } while (n !== 0)\r\n return new Uint8Array(out)\r\n}\r\n\r\nexport function decodeVarint(bytes: Uint8Array, offset: number): { value: number; bytesRead: number } {\r\n let result = 0\r\n let shift = 0\r\n let pos = offset\r\n while (pos < bytes.length) {\r\n const byte = bytes[pos++]\r\n result |= (byte & 0x7f) << shift\r\n if ((byte & 0x80) === 0) {\r\n return { value: result, bytesRead: pos - offset }\r\n }\r\n shift += 7\r\n if (shift > 35) throw new Error('Varint too large')\r\n }\r\n throw new Error('Truncated varint')\r\n}\r\n","// File: src\\dict\\builder.ts\r\n\r\nimport type { WordBinDictionary } from \"../types\";\r\nimport { generateWordId } from \"../core/id.js\";\r\nimport { toHex } from \"../utils/buffer.js\";\r\n\r\nexport interface BuildDictionaryOptions {\r\n /**\r\n * Dictionary version number (used in header and for format compatibility)\r\n * @default 1\r\n */\r\n version?: number;\r\n\r\n /**\r\n * Human-readable description of this dictionary\r\n * @default \"WordBin dictionary v${version}\"\r\n */\r\n description?: string;\r\n\r\n /**\r\n * Optional: custom prefix or identifier for this dictionary build\r\n * (can be used in logs, filenames, etc.)\r\n */\r\n name?: string;\r\n}\r\n\r\nexport async function buildDictionary(\r\n words: string[],\r\n options: BuildDictionaryOptions = {},\r\n): Promise<WordBinDictionary> {\r\n const { version = 1, description = `WordBin dictionary v${version}` } =\r\n options;\r\n\r\n const map: Record<string, string[]> = {};\r\n\r\n const normalizedWords = words\r\n .map((w) => w.trim().toLowerCase())\r\n .filter((w) => w);\r\n\r\n await Promise.all(\r\n normalizedWords.map(async (word) => {\r\n let attempt = 0;\r\n let key: string;\r\n\r\n while (true) {\r\n const id = await generateWordId(\r\n attempt === 0 ? word : `${word}:${attempt}`,\r\n );\r\n\r\n key = toHex(id);\r\n\r\n // If no collision, break\r\n if (!map[key]) {\r\n map[key] = [word];\r\n break;\r\n }\r\n\r\n // Collision detected → try again\r\n attempt++;\r\n }\r\n }),\r\n );\r\n\r\n Object.values(map).forEach((collisions) => {\r\n collisions.sort((a, b) => a.localeCompare(b));\r\n });\r\n\r\n return {\r\n version,\r\n description,\r\n words: map,\r\n };\r\n}\r\n"],"names":[],"mappings":"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"}
package/dist/cli.mjs CHANGED
@@ -57,7 +57,7 @@ async function buildV1() {
57
57
  1,
58
58
  "WordBin dictionary v1 – BIP-39 English (2048 words)",
59
59
  words,
60
- "wordbin-v1-bip39.json"
60
+ "dict-v1-bip39.json"
61
61
  );
62
62
  }
63
63
  async function buildV2() {
@@ -68,7 +68,7 @@ async function buildV2() {
68
68
  2,
69
69
  `WordBin dictionary v2 – dwyl/english-words (${words.length} words)`,
70
70
  words,
71
- "wordbin-v2-dwyl.json"
71
+ "dict-v2-dwyl.json"
72
72
  );
73
73
  }
74
74
  async function buildCustom(source) {
@@ -82,7 +82,7 @@ async function buildCustom(source) {
82
82
  }
83
83
  const version = 3;
84
84
  const desc = `WordBin custom dictionary v${version} – from ${source} (${words.length} words)`;
85
- await saveDict(version, desc, words, `wordbin-v${version}-custom.json`);
85
+ await saveDict(version, desc, words, `dict-v${version}-custom.json`);
86
86
  }
87
87
  async function interactiveMode() {
88
88
  console.log("Interactive Dictionary Builder");
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\r\n\r\nimport { writeFile, mkdir, readFile } from \"node:fs/promises\";\r\nimport { resolve } from \"node:path\";\r\nimport { createInterface } from \"node:readline\";\r\nimport { stdin as input, stdout as output } from \"node:process\";\r\nimport { wordlists } from \"bip39\";\r\nimport { buildDictionary } from \"./index\";\r\n\r\nconst rl = createInterface({ input, output });\r\n\r\nconst help = `\r\nWordBin CLI – Dictionary Builder\r\n\r\nUsage:\r\n npx wordbin build [options]\r\n\r\nOptions:\r\n --version <num> Build specific version (1 or 2)\r\n --all Build all versions\r\n --custom <source> Build from custom URL or local file path\r\n --help Show this help\r\n\r\nIf no options provided, enters interactive mode.\r\n`;\r\n\r\nconst args = process.argv.slice(2);\r\nconst cmd = args[0];\r\n\r\nasync function prompt(question: string): Promise<string> {\r\n return new Promise((resolve) => {\r\n rl.question(question, resolve);\r\n });\r\n}\r\n\r\nasync function saveDict(\r\n version: number,\r\n desc: string,\r\n words: string[],\r\n filename: string,\r\n) {\r\n const dict = await buildDictionary(words, {\r\n version,\r\n description: desc,\r\n });\r\n\r\n const outDir = resolve(process.cwd(), \"data\");\r\n await mkdir(outDir, { recursive: true });\r\n\r\n const outPath = resolve(outDir, filename);\r\n await writeFile(outPath, JSON.stringify(dict, null, 2), \"utf8\");\r\n\r\n console.log(`Saved ${filename} (${words.length} words) to ${outPath}`);\r\n}\r\n\r\nasync function fetchWordsFromUrl(url: string) {\r\n const res = await fetch(url);\r\n if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`);\r\n\r\n const text = await res.text();\r\n return text\r\n .split(\"\\n\")\r\n .map((w) => w.trim())\r\n .filter(Boolean);\r\n}\r\n\r\nasync function readWordsFromFile(path: string) {\r\n const text = await readFile(path, \"utf8\");\r\n return text\r\n .split(\"\\n\")\r\n .map((w) => w.trim())\r\n .filter(Boolean);\r\n}\r\n\r\nasync function buildV1() {\r\n const words = wordlists.english;\r\n\r\n if (!Array.isArray(words) || words.length !== 2048) {\r\n throw new Error(\"Invalid BIP-39 wordlist\");\r\n }\r\n\r\n await saveDict(\r\n 1,\r\n \"WordBin dictionary v1 – BIP-39 English (2048 words)\",\r\n words,\r\n \"wordbin-v1-bip39.json\",\r\n );\r\n}\r\n\r\nasync function buildV2() {\r\n console.log(\"Downloading dwyl/english-words...\");\r\n\r\n const url =\r\n \"https://raw.githubusercontent.com/dwyl/english-words/master/words.txt\";\r\n\r\n const words = await fetchWordsFromUrl(url);\r\n\r\n await saveDict(\r\n 2,\r\n `WordBin dictionary v2 – dwyl/english-words (${words.length} words)`,\r\n words,\r\n \"wordbin-v2-dwyl.json\",\r\n );\r\n}\r\n\r\nasync function buildCustom(source: string) {\r\n let words: string[];\r\n\r\n if (source.startsWith(\"http://\") || source.startsWith(\"https://\")) {\r\n console.log(`Fetching from URL: ${source}`);\r\n words = await fetchWordsFromUrl(source);\r\n } else {\r\n console.log(`Reading from local file: ${source}`);\r\n words = await readWordsFromFile(source);\r\n }\r\n\r\n const version = 3;\r\n const desc = `WordBin custom dictionary v${version} – from ${source} (${words.length} words)`;\r\n\r\n await saveDict(version, desc, words, `wordbin-v${version}-custom.json`);\r\n}\r\n\r\nasync function interactiveMode() {\r\n console.log(\"Interactive Dictionary Builder\");\r\n console.log(\"Options: 1 (BIP-39), 2 (dwyl large), all, custom, or q to quit\");\r\n\r\n const choice =\r\n (await prompt(\"Choose dictionary to build (default: 2): \")) || \"2\";\r\n\r\n if (choice === \"q\") {\r\n rl.close();\r\n return;\r\n }\r\n\r\n if (choice === \"1\") await buildV1();\r\n else if (choice === \"2\") await buildV2();\r\n else if (choice === \"all\") {\r\n await buildV1();\r\n await buildV2();\r\n } else if (choice === \"custom\") {\r\n const source = await prompt(\"Enter URL or local file path: \");\r\n if (source) await buildCustom(source);\r\n } else {\r\n console.error(\"Invalid choice\");\r\n }\r\n\r\n rl.close();\r\n}\r\n\r\nasync function main() {\r\n if (cmd === \"--help\" || cmd === \"-h\") {\r\n console.log(help);\r\n process.exit(0);\r\n }\r\n\r\n if (cmd === \"build\") {\r\n if (args.length === 1) {\r\n await interactiveMode();\r\n return;\r\n }\r\n\r\n const option = args[1];\r\n\r\n if (option === \"--version\") {\r\n const ver = parseInt(args[2]);\r\n if (ver === 1) await buildV1();\r\n else if (ver === 2) await buildV2();\r\n else console.error(\"Invalid version\");\r\n } else if (option === \"--all\") {\r\n await buildV1();\r\n await buildV2();\r\n } else if (option === \"--custom\") {\r\n const source = args[2];\r\n if (!source) {\r\n console.error(\"Provide URL or file path\");\r\n process.exit(1);\r\n }\r\n await buildCustom(source);\r\n } else {\r\n console.log(help);\r\n }\r\n\r\n return;\r\n }\r\n\r\n console.log(help);\r\n process.exit(1);\r\n}\r\n\r\nmain().catch((err) => {\r\n console.error(\"Error:\", err);\r\n process.exit(1);\r\n});\r\n"],"names":["input","output","resolve"],"mappings":"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"}
1
+ {"version":3,"file":"cli.mjs","sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\r\n\r\nimport { writeFile, mkdir, readFile } from \"node:fs/promises\";\r\nimport { resolve } from \"node:path\";\r\nimport { createInterface } from \"node:readline\";\r\nimport { stdin as input, stdout as output } from \"node:process\";\r\nimport { wordlists } from \"bip39\";\r\nimport { buildDictionary } from \"./index\";\r\n\r\nconst rl = createInterface({ input, output });\r\n\r\nconst help = `\r\nWordBin CLI – Dictionary Builder\r\n\r\nUsage:\r\n npx wordbin build [options]\r\n\r\nOptions:\r\n --version <num> Build specific version (1 or 2)\r\n --all Build all versions\r\n --custom <source> Build from custom URL or local file path\r\n --help Show this help\r\n\r\nIf no options provided, enters interactive mode.\r\n`;\r\n\r\nconst args = process.argv.slice(2);\r\nconst cmd = args[0];\r\n\r\nasync function prompt(question: string): Promise<string> {\r\n return new Promise((resolve) => {\r\n rl.question(question, resolve);\r\n });\r\n}\r\n\r\nasync function saveDict(\r\n version: number,\r\n desc: string,\r\n words: string[],\r\n filename: string,\r\n) {\r\n const dict = await buildDictionary(words, {\r\n version,\r\n description: desc,\r\n });\r\n\r\n const outDir = resolve(process.cwd(), \"data\");\r\n await mkdir(outDir, { recursive: true });\r\n\r\n const outPath = resolve(outDir, filename);\r\n await writeFile(outPath, JSON.stringify(dict, null, 2), \"utf8\");\r\n\r\n console.log(`Saved ${filename} (${words.length} words) to ${outPath}`);\r\n}\r\n\r\nasync function fetchWordsFromUrl(url: string) {\r\n const res = await fetch(url);\r\n if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`);\r\n\r\n const text = await res.text();\r\n return text\r\n .split(\"\\n\")\r\n .map((w) => w.trim())\r\n .filter(Boolean);\r\n}\r\n\r\nasync function readWordsFromFile(path: string) {\r\n const text = await readFile(path, \"utf8\");\r\n return text\r\n .split(\"\\n\")\r\n .map((w) => w.trim())\r\n .filter(Boolean);\r\n}\r\n\r\nasync function buildV1() {\r\n const words = wordlists.english;\r\n\r\n if (!Array.isArray(words) || words.length !== 2048) {\r\n throw new Error(\"Invalid BIP-39 wordlist\");\r\n }\r\n\r\n await saveDict(\r\n 1,\r\n \"WordBin dictionary v1 – BIP-39 English (2048 words)\",\r\n words,\r\n \"dict-v1-bip39.json\",\r\n );\r\n}\r\n\r\nasync function buildV2() {\r\n console.log(\"Downloading dwyl/english-words...\");\r\n\r\n const url =\r\n \"https://raw.githubusercontent.com/dwyl/english-words/master/words.txt\";\r\n\r\n const words = await fetchWordsFromUrl(url);\r\n\r\n await saveDict(\r\n 2,\r\n `WordBin dictionary v2 – dwyl/english-words (${words.length} words)`,\r\n words,\r\n \"dict-v2-dwyl.json\",\r\n );\r\n}\r\n\r\nasync function buildCustom(source: string) {\r\n let words: string[];\r\n\r\n if (source.startsWith(\"http://\") || source.startsWith(\"https://\")) {\r\n console.log(`Fetching from URL: ${source}`);\r\n words = await fetchWordsFromUrl(source);\r\n } else {\r\n console.log(`Reading from local file: ${source}`);\r\n words = await readWordsFromFile(source);\r\n }\r\n\r\n const version = 3;\r\n const desc = `WordBin custom dictionary v${version} – from ${source} (${words.length} words)`;\r\n\r\n await saveDict(version, desc, words, `dict-v${version}-custom.json`);\r\n}\r\n\r\nasync function interactiveMode() {\r\n console.log(\"Interactive Dictionary Builder\");\r\n console.log(\"Options: 1 (BIP-39), 2 (dwyl large), all, custom, or q to quit\");\r\n\r\n const choice =\r\n (await prompt(\"Choose dictionary to build (default: 2): \")) || \"2\";\r\n\r\n if (choice === \"q\") {\r\n rl.close();\r\n return;\r\n }\r\n\r\n if (choice === \"1\") await buildV1();\r\n else if (choice === \"2\") await buildV2();\r\n else if (choice === \"all\") {\r\n await buildV1();\r\n await buildV2();\r\n } else if (choice === \"custom\") {\r\n const source = await prompt(\"Enter URL or local file path: \");\r\n if (source) await buildCustom(source);\r\n } else {\r\n console.error(\"Invalid choice\");\r\n }\r\n\r\n rl.close();\r\n}\r\n\r\nasync function main() {\r\n if (cmd === \"--help\" || cmd === \"-h\") {\r\n console.log(help);\r\n process.exit(0);\r\n }\r\n\r\n if (cmd === \"build\") {\r\n if (args.length === 1) {\r\n await interactiveMode();\r\n return;\r\n }\r\n\r\n const option = args[1];\r\n\r\n if (option === \"--version\") {\r\n const ver = parseInt(args[2]);\r\n if (ver === 1) await buildV1();\r\n else if (ver === 2) await buildV2();\r\n else console.error(\"Invalid version\");\r\n } else if (option === \"--all\") {\r\n await buildV1();\r\n await buildV2();\r\n } else if (option === \"--custom\") {\r\n const source = args[2];\r\n if (!source) {\r\n console.error(\"Provide URL or file path\");\r\n process.exit(1);\r\n }\r\n await buildCustom(source);\r\n } else {\r\n console.log(help);\r\n }\r\n\r\n return;\r\n }\r\n\r\n console.log(help);\r\n process.exit(1);\r\n}\r\n\r\nmain().catch((err) => {\r\n console.error(\"Error:\", err);\r\n process.exit(1);\r\n});\r\n"],"names":["input","output","resolve"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export type PayloadFormat = "bytes" | "base58" | "base64" | "hex";
2
+ export declare function detectAndConvert(payload: string): {
3
+ buffer: Uint8Array;
4
+ detectedFormat: PayloadFormat;
5
+ };
@@ -0,0 +1 @@
1
+ export declare function bytesToHex(bytes: Uint8Array): string;
@@ -1,21 +1,10 @@
1
1
  import { EncodeResult, WordBinDictionary } from '../types.js';
2
- type PayloadFormat = "bytes" | "base58" | "base64" | "hex" | "bin21";
2
+ import { PayloadFormat } from './format-detection.js';
3
3
  export interface DecodeResult {
4
- /** The decoded text — words for WordBin payloads, best-effort for others. */
5
4
  text: string;
6
- /** True only when the payload was a valid, fully-parsed WordBin stream. */
7
5
  isWordBin: boolean;
8
- /** Auto-detected wire format of the input. */
9
6
  detectedFormat: PayloadFormat;
10
- /**
11
- * Human-readable notice when the payload is not a valid WordBin stream.
12
- * Includes information about what the decoder did as a fallback.
13
- */
14
7
  notice?: string;
15
- /**
16
- * Present when partial scanning was used (non-WordBin payloads).
17
- * Lists raw byte sequences that had no dictionary match, in order.
18
- */
19
8
  rawSegments?: string[];
20
9
  }
21
10
  export declare class WordBin {
@@ -30,29 +19,11 @@ export declare class WordBin {
30
19
  debug?: boolean;
31
20
  }): Promise<WordBin>;
32
21
  private getMapsForVersion;
22
+ private tryRecoverWordsFromHex;
23
+ private validateDecodedWords;
33
24
  encode(text: string | EncodeResult | Uint8Array, options?: {
34
25
  dictVersion?: number;
35
26
  }): Promise<EncodeResult>;
36
- /**
37
- * Decodes any supported payload format back to human-readable text.
38
- *
39
- * For valid WordBin payloads: returns the exact original words.
40
- * For non-WordBin payloads: scans byte-by-byte, extracts dictionary words
41
- * wherever possible, and preserves unrecognised
42
- * bytes as "[0xXX]" markers.
43
- */
44
27
  decode(payload: Uint8Array | string): Promise<DecodeResult>;
45
- /**
46
- * O(n) longest-match-first decode. Returns null if any byte has no match.
47
- * This is the fast path; tryDecode is used as a backtracking fallback.
48
- */
49
28
  private greedyDecode;
50
- /**
51
- * Scans through the buffer extracting any recognised dictionary words.
52
- * Unrecognised bytes are collected as raw segments and rendered as [0xXX].
53
- * Always consumes the entire buffer — never returns null.
54
- */
55
- private partialScan;
56
- private tryDecode;
57
29
  }
58
- export {};