@brashkie/signalis-core 0.3.1 → 0.4.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/CHANGELOG.md CHANGED
@@ -5,6 +5,94 @@ All notable changes to `@brashkie/signalis-core` are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] — 2026-07-08
9
+
10
+ ### ✨ Added — Encoding helpers + Android x86_64
11
+
12
+ v0.4.0 introduces native encoding helpers (Base64, Hex, UTF-8) and adds
13
+ Android x86_64 to the supported platform list. Fully backwards compatible
14
+ with v0.3.x.
15
+
16
+ #### 🆕 Encoding namespaces (Rust-side implementations)
17
+
18
+ Three new namespaces exported from the top level, backed by the new
19
+ `sc-encoding` crate. All operations are audited RustCrypto ecosystem
20
+ routines with strict validation (no lossy conversions).
21
+
22
+ - **`Base64`**
23
+ - `encode(bytes) → string` — RFC 4648 standard (with `=` padding)
24
+ - `decode(string) → Buffer` — throws on invalid input
25
+ - `encodeUrlSafe(bytes) → string` — `-` and `_` alphabet, no padding
26
+ - `decodeUrlSafe(string) → Buffer`
27
+
28
+ - **`Hex`**
29
+ - `encode(bytes) → string` — lowercase output
30
+ - `encodeUpper(bytes) → string` — uppercase output
31
+ - `decode(string) → Buffer` — case-insensitive
32
+ - `isValid(string) → boolean` — cheap format check
33
+
34
+ - **`Utf8`**
35
+ - `encode(string) → Buffer` — UTF-8 bytes
36
+ - `decode(bytes) → string` — **strict** validation, throws on invalid
37
+ UTF-8 (unlike `Buffer.toString('utf-8')` which silently substitutes U+FFFD)
38
+ - `isValid(bytes) → boolean`
39
+
40
+ Quick example:
41
+
42
+ ```typescript
43
+ import { Base64, Hex, Utf8 } from '@brashkie/signalis-core';
44
+
45
+ const bytes = Utf8.encode('Hola 🦀');
46
+ const b64 = Base64.encode(bytes); // 'SG9sYSDwn6aA'
47
+ const hex = Hex.encode(bytes); // '486f6c6120f09fa680'
48
+ const back = Utf8.decode(Base64.decode(b64)); // 'Hola 🦀'
49
+ ```
50
+
51
+ #### 🆕 New crate: `sc-encoding`
52
+
53
+ A dedicated Rust crate for encoding routines, separated from `sc-utils`
54
+ to keep encoding logic isolated from cryptographic utilities. This makes
55
+ the codebase easier to audit and lets adopters pull in just what they need.
56
+
57
+ Dependencies (all RustCrypto ecosystem):
58
+ - `base64` v0.22 — RFC 4648 encoder/decoder
59
+ - `hex` v0.4 — Base16 encoder/decoder
60
+ - Rust standard library for UTF-8 validation
61
+
62
+ #### 🆕 Android x86_64 support
63
+
64
+ New sub-package `@brashkie/signalis-core-android-x64` published to npm.
65
+ Primary use cases:
66
+ - **Android Emulator** — runs x86_64, so devs testing apps in Android
67
+ Studio's emulator now get native binaries automatically
68
+ - **Termux on x86 tablets / Chromebooks** — install via
69
+ `pkg install nodejs && npm install @brashkie/signalis-core`
70
+
71
+ Total platforms supported: **10** (was 9 in v0.3.1).
72
+
73
+ ### 🔄 Changed
74
+ - `VERSION` bumped to `'0.4.0'`
75
+ - `Cargo.toml` workspace: version 0.3.x → 0.4.0
76
+ - `sc-node`: 10 new `#[napi]` exports for the encoding functions
77
+ - `optionalDependencies` in `package.json`: adds `android-x64` (10 sub-packages total)
78
+ - `napi.triples.additional`: adds `x86_64-linux-android`
79
+ - `.github/workflows/release.yml`: adds `x86_64-linux-android` build job
80
+ - `scripts/create-npm-dirs.js`: adds `android-x64` to `PLATFORMS` array
81
+
82
+ ### ✅ Compatibility
83
+ **100% backwards compatible with v0.3.x.** No API changes. No breaking
84
+ behavior. Existing code continues to work; the encoding namespaces are
85
+ purely additive.
86
+
87
+ ### 📋 What's next (v0.5.0 candidates)
88
+ - Buffer / ByteArray utility helpers (`sc-encoding` extension)
89
+ - XChaCha20-Poly1305 (extended-nonce AEAD)
90
+ - Argon2id password hashing
91
+ - PBKDF2-SHA256
92
+ - HKDF-SHA512
93
+
94
+ ---
95
+
8
96
  ## [0.3.0] — 2026-06-17
9
97
 
10
98
  ### ✨ Added — Multi-Platform Expansion + New Primitives
package/README.es.md CHANGED
@@ -32,6 +32,71 @@ Construida con **Rust** para seguridad y velocidad, expuesta a Node.js mediante
32
32
 
33
33
  ---
34
34
 
35
+ ## 🎉 Novedades en v0.4.0
36
+
37
+ **v0.4.0 introduce helpers nativos de encoding (Base64, Hex, UTF-8) + soporte para Android x86_64 — totalmente retrocompatible con v0.3.x.**
38
+
39
+ | Nuevo | Descripción |
40
+ |-------|-------------|
41
+ | 🆕 **Namespace `Base64`** | RFC 4648 estándar + variante URL-safe (sin padding), implementación Rust |
42
+ | 🆕 **Namespace `Hex`** | Encode minúsculas/mayúsculas, decode case-insensitive, `isValid()` chequeo rápido |
43
+ | 🆕 **Namespace `Utf8`** | Validación UTF-8 **estricta** — tira error en bytes malformados (a diferencia de `Buffer.toString('utf-8')` que sustituye silenciosamente por U+FFFD) |
44
+ | 🆕 **Android x86_64** | Binario nativo para el Android Emulator + Chromebooks + tablets x86 |
45
+ | 🆕 **Crate `sc-encoding`** | Crate Rust dedicado — separa la lógica de encoding de la criptografía para facilitar auditoría |
46
+
47
+ ### Ejemplo rápido: Base64 / Hex / Utf8
48
+
49
+ ```ts
50
+ import { Base64, Hex, Utf8 } from '@brashkie/signalis-core';
51
+
52
+ // UTF-8 con validación estricta
53
+ const bytes = Utf8.encode('Hola 🦀'); // Buffer <48 6f 6c 61 20 f0 9f a6 80>
54
+ const roundTrip = Utf8.decode(bytes); // "Hola 🦀"
55
+
56
+ // Base64 estándar (con padding)
57
+ const b64 = Base64.encode(bytes); // "SG9sYSDwn6aA"
58
+ const decoded = Base64.decode(b64); // coincide con el original
59
+
60
+ // Base64 URL-safe (sin padding, sin + ni /)
61
+ const urlSafe = Base64.encodeUrlSafe(bytes); // seguro en URLs, filenames, JWTs
62
+
63
+ // Hex
64
+ const hex = Hex.encode(bytes); // "486f6c6120f09fa680"
65
+ const hexUpper = Hex.encodeUpper(bytes); // "486F6C6120F09FA680"
66
+ const back = Hex.decode('DEADBEEF'); // case-insensitive
67
+ Hex.isValid('deadbeef'); // true
68
+ Hex.isValid('nope!'); // false
69
+
70
+ // Validación UTF-8 (barata, sin allocation)
71
+ Utf8.isValid(Buffer.from([0xff])); // false (0xff nunca es UTF-8 válido)
72
+ ```
73
+
74
+ ### ¿Por qué importa la validación UTF-8 estricta?
75
+
76
+ El `Buffer.toString('utf-8')` nativo de Node **sustituye silenciosamente por U+FFFD** (el carácter de reemplazo) cuando encuentra bytes inválidos. Para código de aplicación normal está bien. Pero cuando estás procesando mensajes firmados, verificando MACs, o manejando input de un atacante potencial, **querés saber** si los bytes están malformados en lugar de continuar con datos silenciosamente corruptos.
77
+
78
+ ```ts
79
+ const badBytes = Buffer.from([0xff, 0xfe]);
80
+
81
+ // Node built-in: no error, produce "\uFFFD\uFFFD"
82
+ badBytes.toString('utf-8');
83
+
84
+ // signalis-core: tira error
85
+ Utf8.decode(badBytes); // → RangeError
86
+ ```
87
+
88
+ ### Casos de uso de Android x86_64
89
+
90
+ - **Android Emulator** (Android Studio) — el emulador corre x86_64, así que devs testeando apps ahora obtienen binarios nativos automáticamente
91
+ - **Termux en Chromebooks / tablets x86** — `pkg install nodejs && npm install @brashkie/signalis-core`
92
+ - **NodeJS-on-Android** con device farms x86
93
+
94
+ **Total de plataformas soportadas ahora: 10** (era 9 en v0.3.1).
95
+
96
+ Ver [CHANGELOG.md](./CHANGELOG.md) para todos los detalles.
97
+
98
+ ---
99
+
35
100
  ## 🎉 Novedades en v0.3.0
36
101
 
37
102
  **v0.3.0 trae soporte Android + ChaCha20-Poly1305 — totalmente retrocompatible con v0.2.0.**
@@ -83,7 +148,7 @@ Ver [MIGRATION.md](./MIGRATION.md) para detalles de upgrade (es un drop-in repla
83
148
 
84
149
  ## 🌍 Plataformas Soportadas
85
150
 
86
- `@brashkie/signalis-core` envía binarios nativos pre-compilados para **9 plataformas** vía `optionalDependencies` de npm. El binario correcto se descarga automáticamente según tu sistema operativo + arquitectura.
151
+ `@brashkie/signalis-core` envía binarios nativos pre-compilados para **10 plataformas** vía `optionalDependencies` de npm. El binario correcto se descarga automáticamente según tu sistema operativo + arquitectura.
87
152
 
88
153
  | SO | Arquitectura | Sub-paquete | Estado |
89
154
  |----|--------------|-------------|--------|
@@ -96,13 +161,16 @@ Ver [MIGRATION.md](./MIGRATION.md) para detalles de upgrade (es un drop-in repla
96
161
  | 🪟 Windows | arm64 | `signalis-core-win32-arm64-msvc` | ✅ |
97
162
  | 🤖 Android | arm64-v8a | `signalis-core-android-arm64` | 🆕 v0.3.0 |
98
163
  | 🤖 Android | armv7 | `signalis-core-android-arm-eabi` | 🆕 v0.3.0 |
164
+ | 🤖 Android | x86_64 | `signalis-core-android-x64` | 🆕 v0.4.0 |
99
165
 
100
- Próximas plataformas en v0.4.0: **iOS arm64**, **WASM (navegadores)**, **FreeBSD x64**.
166
+ Próximas plataformas: **WASM (navegadores)**, **iOS arm64**, **FreeBSD x64**, **RISC-V**.
101
167
 
102
168
  ### Instalación en Android
103
169
 
104
170
  El mismo `npm install` funciona en cualquier entorno Node.js corriendo en Android, incluyendo:
105
171
  - **Termux** en celulares Android (`pkg install nodejs`)
172
+ - **Termux en Chromebooks / tablets x86** → usa el binario `android-x64` automáticamente
173
+ - **Android Emulator** (Android Studio) → host x86_64, usa el binario `android-x64`
106
174
  - **React Native** con target Android
107
175
  - Apps con **NodeJS-Mobile**
108
176
  - Builds personalizados de Node embebido para IoT
@@ -111,7 +179,7 @@ El mismo `npm install` funciona en cualquier entorno Node.js corriendo en Androi
111
179
  # En Android (Termux por ejemplo):
112
180
  pkg install nodejs
113
181
  npm install @brashkie/signalis-core
114
- # → npm descarga automáticamente signalis-core-android-arm64
182
+ # → npm descarga automáticamente el sub-paquete correcto para tu arquitectura
115
183
  ```
116
184
 
117
185
  ---
@@ -148,7 +216,7 @@ npm install @brashkie/signalis-core
148
216
  | 📦 **Paquete Dual** | Funciona en proyectos CommonJS, ESM y TypeScript |
149
217
  | 🎯 **Tipado Estricto** | Definiciones TypeScript completas con tipos branded y clases de error |
150
218
  | ✅ **Vectores de Test** | Validado contra RFC 5869, RFC 7748, RFC 8032, RFC 4231 y vectores NIST |
151
- | 🌍 **Multi-Plataforma** | Binarios pre-compilados para Windows, macOS, Linux (x64, ARM) + **Android arm64/armv7** |
219
+ | 🌍 **Multi-Plataforma** | Binarios pre-compilados para Windows, macOS, Linux (x64, ARM) + **Android arm64/armv7/x86_64** |
152
220
  | 🔒 **Tiempo Constante** | Comparaciones resistentes a side-channel via crate `subtle` |
153
221
  | 🧹 **Auto-Borrado** | Secretos se borran de memoria automáticamente |
154
222
  | 📊 **Cobertura 99%+** | Suite de tests con 269+ aserciones |
@@ -498,6 +566,44 @@ ChaCha20Poly1305.TAG_SIZE; // 16 (anexado al ciphertext)
498
566
 
499
567
  **Seguridad:** ChaCha20-Poly1305 y AES-GCM tienen propiedades de seguridad equivalentes. La decisión es puramente sobre rendimiento en tu target.
500
568
 
569
+ ### Base64 / Hex / UTF-8 🆕
570
+
571
+ Helpers nativos (Rust-side) de codificación introducidos en v0.4.0. Respaldados por el crate `sc-encoding`, que usa el ecosistema RustCrypto auditado (crates `base64` y `hex`) más la validación UTF-8 estricta integrada de Rust.
572
+
573
+ **¿Por qué no simplemente usar `Buffer.toString('base64')` de Node?**
574
+ - `Buffer.toString('utf-8')` sustituye silenciosamente `U+FFFD` en bytes inválidos. El nuestro tira error — mejor para verificación de firmas, parsing forense, input adversarial.
575
+ - Superficie de error consistente en toda la librería (mismo shape de `RangeError` que las primitivas criptográficas).
576
+ - Misma superficie de auditoría — un crate para revisar, no "lo que sea que Node envíe".
577
+
578
+ ```typescript
579
+ import { Base64, Hex, Utf8 } from '@brashkie/signalis-core';
580
+
581
+ // ─── Base64 (estándar, RFC 4648) ───────────────────────────────────
582
+ Base64.encode(Buffer.from('hola')); // "aG9sYQ=="
583
+ Base64.decode('aG9sYQ=='); // <Buffer 68 6f 6c 61>
584
+
585
+ // ─── Base64 URL-safe (sin padding, sin + ni /) ─────────────────────
586
+ Base64.encodeUrlSafe(Buffer.from('hola')); // "aG9sYQ"
587
+ Base64.decodeUrlSafe('aG9sYQ');
588
+
589
+ // ─── Hex ────────────────────────────────────────────────────────────
590
+ Hex.encode(Buffer.from([0xde, 0xad])); // "dead"
591
+ Hex.encodeUpper(Buffer.from([0xde, 0xad])); // "DEAD"
592
+ Hex.decode('DEADBEEF'); // case-insensitive
593
+ Hex.isValid('deadbeef'); // true
594
+ Hex.isValid('nope'); // false — chars inválidos
595
+
596
+ // ─── UTF-8 (estricto) ───────────────────────────────────────────────
597
+ Utf8.encode('Hola 🦀'); // Buffer con 9 bytes
598
+ Utf8.decode(Buffer.from([0xc3, 0xb1])); // "ñ"
599
+ Utf8.decode(Buffer.from([0xff])); // → tira RangeError
600
+ Utf8.isValid(Buffer.from([0xff])); // false (no throw)
601
+ ```
602
+
603
+ **Errores:** Cada operación de decode tira `RangeError` (subclase de `Error`) con mensaje descriptivo. Caracteres inválidos, longitudes incorrectas, padding malformado y secuencias UTF-8 inválidas siguen el mismo patrón.
604
+
605
+ **Rendimiento:** Estas funciones tienen un hop NAPI cada una. Para strings chicos el overhead es despreciable; para hot loops (millones de ops/seg en inputs pequeños) preferí `Buffer.toString()` del built-in de Node y recurrí a `Utf8.decode` solo cuando necesitás validación estricta.
606
+
501
607
  ### HMAC-SHA256
502
608
 
503
609
  Autenticación de mensajes.
package/README.md CHANGED
@@ -32,6 +32,71 @@ Built with **Rust** for safety and speed, exposed to Node.js via [napi-rs](https
32
32
 
33
33
  ---
34
34
 
35
+ ## 🎉 What's New in v0.4.0
36
+
37
+ **v0.4.0 ships native encoding helpers (Base64, Hex, UTF-8) + Android x86_64 target — fully backwards compatible with v0.3.x.**
38
+
39
+ | New | Description |
40
+ |-----|-------------|
41
+ | 🆕 **`Base64` namespace** | RFC 4648 standard + URL-safe variant (no padding), Rust-side implementation |
42
+ | 🆕 **`Hex` namespace** | Lowercase/uppercase encode, case-insensitive decode, `isValid()` cheap check |
43
+ | 🆕 **`Utf8` namespace** | **Strict** UTF-8 validation — throws on malformed bytes (unlike `Buffer.toString('utf-8')` which silently substitutes U+FFFD) |
44
+ | 🆕 **Android x86_64** | Native binary for the Android Emulator + Chromebooks + x86 tablets |
45
+ | 🆕 **`sc-encoding` crate** | Dedicated Rust crate — separates encoding logic from crypto for easier audit |
46
+
47
+ ### Quick Example: Base64 / Hex / Utf8
48
+
49
+ ```ts
50
+ import { Base64, Hex, Utf8 } from '@brashkie/signalis-core';
51
+
52
+ // UTF-8 with strict validation
53
+ const bytes = Utf8.encode('Hola 🦀'); // Buffer <48 6f 6c 61 20 f0 9f a6 80>
54
+ const roundTrip = Utf8.decode(bytes); // "Hola 🦀"
55
+
56
+ // Base64 standard (with padding)
57
+ const b64 = Base64.encode(bytes); // "SG9sYSDwn6aA"
58
+ const decoded = Base64.decode(b64); // matches original
59
+
60
+ // Base64 URL-safe (no padding, no + or /)
61
+ const urlSafe = Base64.encodeUrlSafe(bytes); // safe in URLs, filenames, JWTs
62
+
63
+ // Hex
64
+ const hex = Hex.encode(bytes); // "486f6c6120f09fa680"
65
+ const hexUpper = Hex.encodeUpper(bytes); // "486F6C6120F09FA680"
66
+ const back = Hex.decode('DEADBEEF'); // case-insensitive
67
+ Hex.isValid('deadbeef'); // true
68
+ Hex.isValid('nope!'); // false
69
+
70
+ // UTF-8 validation (cheap, no allocation)
71
+ Utf8.isValid(Buffer.from([0xff])); // false (0xff is never valid UTF-8)
72
+ ```
73
+
74
+ ### Why Strict UTF-8 Validation Matters
75
+
76
+ Node's built-in `Buffer.toString('utf-8')` **silently substitutes U+FFFD** (the replacement character) when it encounters invalid bytes. For most app code that's fine. But when you're processing signed messages, verifying MACs, or handling attacker-supplied input, **you want to know** if bytes are malformed rather than continue with silently-corrupted data.
77
+
78
+ ```ts
79
+ const badBytes = Buffer.from([0xff, 0xfe]);
80
+
81
+ // Node's built-in: no error, produces "\uFFFD\uFFFD"
82
+ badBytes.toString('utf-8');
83
+
84
+ // signalis-core: throws
85
+ Utf8.decode(badBytes); // → RangeError
86
+ ```
87
+
88
+ ### Android x86_64 Use Cases
89
+
90
+ - **Android Emulator** (Android Studio) — emulator runs x86_64, so devs testing apps now get native binaries automatically
91
+ - **Termux on Chromebooks / x86 tablets** — `pkg install nodejs && npm install @brashkie/signalis-core`
92
+ - **NodeJS-on-Android** with x86 device farms
93
+
94
+ **Total platforms supported now: 10** (was 9 in v0.3.1).
95
+
96
+ See [CHANGELOG.md](./CHANGELOG.md) for full details.
97
+
98
+ ---
99
+
35
100
  ## 🎉 What's New in v0.3.0
36
101
 
37
102
  **v0.3.0 ships Android support + ChaCha20-Poly1305 — fully backwards compatible with v0.2.0.**
@@ -83,7 +148,7 @@ See [MIGRATION.md](./MIGRATION.md) for upgrade details (it's a drop-in replaceme
83
148
 
84
149
  ## 🌍 Supported Platforms
85
150
 
86
- `@brashkie/signalis-core` ships prebuilt native binaries for **9 platforms** via npm `optionalDependencies`. The right binary downloads automatically based on your host OS + arch.
151
+ `@brashkie/signalis-core` ships prebuilt native binaries for **10 platforms** via npm `optionalDependencies`. The right binary downloads automatically based on your host OS + arch.
87
152
 
88
153
  | OS | Architecture | Sub-package | Status |
89
154
  |----|--------------|-------------|--------|
@@ -96,13 +161,16 @@ See [MIGRATION.md](./MIGRATION.md) for upgrade details (it's a drop-in replaceme
96
161
  | 🪟 Windows | arm64 | `signalis-core-win32-arm64-msvc` | ✅ |
97
162
  | 🤖 Android | arm64-v8a | `signalis-core-android-arm64` | 🆕 v0.3.0 |
98
163
  | 🤖 Android | armv7 | `signalis-core-android-arm-eabi` | 🆕 v0.3.0 |
164
+ | 🤖 Android | x86_64 | `signalis-core-android-x64` | 🆕 v0.4.0 |
99
165
 
100
- Coming in v0.4.0: **iOS arm64**, **WASM (browsers)**, **FreeBSD x64**.
166
+ Coming in future releases: **WASM (browsers)**, **iOS arm64**, **FreeBSD x64**, **RISC-V**.
101
167
 
102
168
  ### Android Installation
103
169
 
104
170
  Same `npm install` works in any Node.js environment running on Android, including:
105
171
  - **Termux** on Android phones (`pkg install nodejs`)
172
+ - **Termux on Chromebooks / x86 tablets** → uses `android-x64` binary automatically
173
+ - **Android Emulator** (Android Studio) → x86_64 host, uses `android-x64` binary
106
174
  - **React Native** with Android target
107
175
  - **NodeJS-Mobile** apps
108
176
  - Custom embedded Node builds for IoT
@@ -111,7 +179,7 @@ Same `npm install` works in any Node.js environment running on Android, includin
111
179
  # On Android (Termux for example):
112
180
  pkg install nodejs
113
181
  npm install @brashkie/signalis-core
114
- # → npm automatically downloads signalis-core-android-arm64 sub-package
182
+ # → npm automatically downloads the right sub-package for your arch
115
183
  ```
116
184
 
117
185
  ---
@@ -120,6 +188,7 @@ npm install @brashkie/signalis-core
120
188
 
121
189
  - [🔐 Signalis Core](#-signalis-core)
122
190
  - [✨ What is Signalis Core?](#-what-is-signalis-core)
191
+ - [🎉 What's New in v0.4.0](#-whats-new-in-v040)
123
192
  - [🎉 What's New in v0.3.0](#-whats-new-in-v030)
124
193
  - [🌍 Supported Platforms](#-supported-platforms)
125
194
  - [📋 Table of Contents](#-table-of-contents)
@@ -178,7 +247,7 @@ npm install @brashkie/signalis-core
178
247
  | 📦 **Dual Package** | Works in CommonJS, ESM, and TypeScript projects |
179
248
  | 🎯 **Type-Safe** | Full TypeScript definitions with branded types and rich error classes |
180
249
  | ✅ **Test Vectors** | Validated against RFC 5869, RFC 7748, RFC 8032, RFC 4231, and NIST vectors |
181
- | 🌍 **Cross-Platform** | Prebuilt binaries for Windows, macOS, Linux (x64, ARM) + **Android arm64/armv7** |
250
+ | 🌍 **Cross-Platform** | Prebuilt binaries for Windows, macOS, Linux (x64, ARM) + **Android arm64/armv7/x86_64** |
182
251
  | 🔒 **Constant-Time** | Side-channel resistant comparisons via `subtle` crate |
183
252
  | 🧹 **Auto-Zeroization** | Secrets are wiped from memory automatically |
184
253
  | 📊 **99%+ Coverage** | Comprehensive test suite with 269+ assertions |
@@ -532,6 +601,44 @@ ChaCha20Poly1305.TAG_SIZE; // 16 (appended to ciphertext)
532
601
 
533
602
  **Security:** ChaCha20-Poly1305 and AES-GCM have equivalent security properties. The choice is purely about performance on your target.
534
603
 
604
+ ### Base64 / Hex / UTF-8 🆕
605
+
606
+ Native (Rust-side) encoding helpers introduced in v0.4.0. Backed by the `sc-encoding` crate, which uses the audited RustCrypto ecosystem (`base64` and `hex` crates) plus Rust's built-in strict UTF-8 validation.
607
+
608
+ **Why not just use Node's `Buffer.toString('base64')`?**
609
+ - `Buffer.toString('utf-8')` silently substitutes `U+FFFD` for invalid bytes. Ours throws — better for signature verification, forensic parsing, adversarial input.
610
+ - Consistent error surface across the whole library (same `RangeError` shape as the crypto primitives).
611
+ - Same audit surface — one crate to review, not "whatever Node happens to ship".
612
+
613
+ ```typescript
614
+ import { Base64, Hex, Utf8 } from '@brashkie/signalis-core';
615
+
616
+ // ─── Base64 (standard, RFC 4648) ───────────────────────────────────
617
+ Base64.encode(Buffer.from('hello')); // "aGVsbG8="
618
+ Base64.decode('aGVsbG8='); // <Buffer 68 65 6c 6c 6f>
619
+
620
+ // ─── Base64 URL-safe (no padding, no + or /) ───────────────────────
621
+ Base64.encodeUrlSafe(Buffer.from('hello')); // "aGVsbG8"
622
+ Base64.decodeUrlSafe('aGVsbG8');
623
+
624
+ // ─── Hex ────────────────────────────────────────────────────────────
625
+ Hex.encode(Buffer.from([0xde, 0xad])); // "dead"
626
+ Hex.encodeUpper(Buffer.from([0xde, 0xad])); // "DEAD"
627
+ Hex.decode('DEADBEEF'); // case-insensitive
628
+ Hex.isValid('deadbeef'); // true
629
+ Hex.isValid('nope'); // false — invalid chars
630
+
631
+ // ─── UTF-8 (strict) ─────────────────────────────────────────────────
632
+ Utf8.encode('Hola 🦀'); // Buffer with 9 bytes
633
+ Utf8.decode(Buffer.from([0xc3, 0xb1])); // "ñ"
634
+ Utf8.decode(Buffer.from([0xff])); // → throws RangeError
635
+ Utf8.isValid(Buffer.from([0xff])); // false (no throw)
636
+ ```
637
+
638
+ **Errors:** Every decode operation throws `RangeError` (subclass of `Error`) with a descriptive message. Invalid characters, wrong lengths, malformed padding, and invalid UTF-8 sequences all follow the same pattern.
639
+
640
+ **Performance:** These functions have one NAPI hop each. For small strings the overhead is negligible; for hot loops (millions of ops/sec on tiny inputs) prefer `Buffer.toString()` from Node's built-in and only reach for `Utf8.decode` when you need strict validation.
641
+
535
642
  ### HMAC-SHA256
536
643
 
537
644
  Message authentication.
package/dist/index.cjs CHANGED
@@ -40,6 +40,7 @@ __export(index_exports, {
40
40
  AES_GCM_MAX_PLAINTEXT_SIZE: () => AES_GCM_MAX_PLAINTEXT_SIZE,
41
41
  AES_GCM_RECOMMENDED_MESSAGES_PER_KEY: () => AES_GCM_RECOMMENDED_MESSAGES_PER_KEY,
42
42
  AuthenticationError: () => AuthenticationError,
43
+ Base64: () => Base64,
43
44
  CHACHA20_POLY1305_KEY_SIZE: () => CHACHA20_POLY1305_KEY_SIZE,
44
45
  CHACHA20_POLY1305_NONCE_SIZE: () => CHACHA20_POLY1305_NONCE_SIZE,
45
46
  CHACHA20_POLY1305_TAG_SIZE: () => CHACHA20_POLY1305_TAG_SIZE,
@@ -59,6 +60,7 @@ __export(index_exports, {
59
60
  HKDF_PRK_SIZE: () => HKDF_PRK_SIZE,
60
61
  HMAC: () => HMAC,
61
62
  HMAC_SHA256_TAG_SIZE: () => HMAC_SHA256_TAG_SIZE,
63
+ Hex: () => Hex,
62
64
  KeyDerivationError: () => KeyDerivationError,
63
65
  LengthError: () => LengthError,
64
66
  SHA256: () => SHA256,
@@ -66,6 +68,7 @@ __export(index_exports, {
66
68
  SHA256_OUTPUT_SIZE: () => SHA256_OUTPUT_SIZE,
67
69
  SignalisError: () => SignalisError,
68
70
  SignatureError: () => SignatureError,
71
+ Utf8: () => Utf8,
69
72
  VERSION: () => VERSION,
70
73
  ValidationError: () => ValidationError,
71
74
  XED25519_PRIVATE_KEY_SIZE: () => XED25519_PRIVATE_KEY_SIZE,
@@ -833,6 +836,123 @@ function nativeSecureRandom(size) {
833
836
  }
834
837
  return native.secureRandom(size);
835
838
  }
839
+ var Base64 = Object.freeze({
840
+ /**
841
+ * Encode bytes to standard Base64 (with `=` padding).
842
+ */
843
+ encode(input) {
844
+ if (!Buffer.isBuffer(input)) {
845
+ throw new TypeError("Base64.encode: input must be a Buffer");
846
+ }
847
+ return native.base64Encode(input);
848
+ },
849
+ /**
850
+ * Decode a standard Base64 string back to bytes.
851
+ *
852
+ * @throws {RangeError} on invalid characters, wrong length, or invalid padding.
853
+ */
854
+ decode(input) {
855
+ if (typeof input !== "string") {
856
+ throw new TypeError("Base64.decode: input must be a string");
857
+ }
858
+ return native.base64Decode(input);
859
+ },
860
+ /**
861
+ * Encode bytes to URL-safe Base64 without padding.
862
+ * Uses `-` and `_` instead of `+` and `/`.
863
+ */
864
+ encodeUrlSafe(input) {
865
+ if (!Buffer.isBuffer(input)) {
866
+ throw new TypeError("Base64.encodeUrlSafe: input must be a Buffer");
867
+ }
868
+ return native.base64EncodeUrlSafe(input);
869
+ },
870
+ /**
871
+ * Decode a URL-safe Base64 string (no padding) back to bytes.
872
+ *
873
+ * @throws {RangeError} on invalid characters or wrong length.
874
+ */
875
+ decodeUrlSafe(input) {
876
+ if (typeof input !== "string") {
877
+ throw new TypeError("Base64.decodeUrlSafe: input must be a string");
878
+ }
879
+ return native.base64DecodeUrlSafe(input);
880
+ }
881
+ });
882
+ var Hex = Object.freeze({
883
+ /**
884
+ * Encode bytes to a lowercase hex string.
885
+ */
886
+ encode(input) {
887
+ if (!Buffer.isBuffer(input)) {
888
+ throw new TypeError("Hex.encode: input must be a Buffer");
889
+ }
890
+ return native.hexEncode(input);
891
+ },
892
+ /**
893
+ * Encode bytes to an uppercase hex string.
894
+ * (Rare, but included for legacy protocols.)
895
+ */
896
+ encodeUpper(input) {
897
+ if (!Buffer.isBuffer(input)) {
898
+ throw new TypeError("Hex.encodeUpper: input must be a Buffer");
899
+ }
900
+ return native.hexEncodeUpper(input);
901
+ },
902
+ /**
903
+ * Decode a hex string to bytes. Case-insensitive.
904
+ *
905
+ * @throws {RangeError} on odd-length input or non-hex characters.
906
+ */
907
+ decode(input) {
908
+ if (typeof input !== "string") {
909
+ throw new TypeError("Hex.decode: input must be a string");
910
+ }
911
+ return native.hexDecode(input);
912
+ },
913
+ /**
914
+ * Cheap validation: is this a well-formed hex string?
915
+ */
916
+ isValid(input) {
917
+ if (typeof input !== "string") {
918
+ throw new TypeError("Hex.isValid: input must be a string");
919
+ }
920
+ return native.hexIsValid(input);
921
+ }
922
+ });
923
+ var Utf8 = Object.freeze({
924
+ /**
925
+ * Encode a string to its UTF-8 byte representation.
926
+ */
927
+ encode(input) {
928
+ if (typeof input !== "string") {
929
+ throw new TypeError("Utf8.encode: input must be a string");
930
+ }
931
+ return native.utf8Encode(input);
932
+ },
933
+ /**
934
+ * Decode UTF-8 bytes to a string.
935
+ *
936
+ * @throws {RangeError} on invalid UTF-8 (truncated multi-byte, lone surrogate,
937
+ * invalid start byte, etc.). Unlike `Buffer.toString('utf-8')`, this does
938
+ * NOT silently substitute U+FFFD.
939
+ */
940
+ decode(input) {
941
+ if (!Buffer.isBuffer(input)) {
942
+ throw new TypeError("Utf8.decode: input must be a Buffer");
943
+ }
944
+ return native.utf8Decode(input);
945
+ },
946
+ /**
947
+ * Check whether the given bytes are valid UTF-8.
948
+ */
949
+ isValid(input) {
950
+ if (!Buffer.isBuffer(input)) {
951
+ throw new TypeError("Utf8.isValid: input must be a Buffer");
952
+ }
953
+ return native.utf8IsValid(input);
954
+ }
955
+ });
836
956
 
837
957
  // src/types.ts
838
958
  function asPublicKey(buf) {
@@ -920,7 +1040,7 @@ function xor(a, b) {
920
1040
  }
921
1041
 
922
1042
  // src/index.ts
923
- var VERSION = "0.2.0";
1043
+ var VERSION = "0.4.0";
924
1044
  var SignalisCore = Object.freeze({
925
1045
  // Crypto primitives
926
1046
  Curve25519,
@@ -929,22 +1049,29 @@ var SignalisCore = Object.freeze({
929
1049
  HKDF,
930
1050
  AES_GCM,
931
1051
  AES_CBC,
1052
+ ChaCha20Poly1305,
932
1053
  HMAC,
933
1054
  SHA256,
934
1055
  // Random
935
1056
  secureRandom: secureRandom2,
1057
+ nativeSecureRandom,
936
1058
  randomNonce,
937
1059
  randomIv,
938
1060
  randomKey,
939
- // Encoding
1061
+ // Encoding (JS-side legacy helpers — use Base64/Hex/Utf8 for native)
940
1062
  toHex,
941
1063
  fromHex,
942
1064
  toBase64,
943
1065
  fromBase64,
1066
+ // Encoding (NEW v0.4.0 — native, RFC-compliant)
1067
+ Base64,
1068
+ Hex,
1069
+ Utf8,
944
1070
  // Security
945
1071
  constantTimeEqual,
1072
+ constantTimeEq: constantTimeEq2,
946
1073
  // Version
947
- VERSION: "0.2.0",
1074
+ VERSION,
948
1075
  nativeVersion
949
1076
  });
950
1077
  var index_default = SignalisCore;
@@ -960,6 +1087,7 @@ var index_default = SignalisCore;
960
1087
  AES_GCM_MAX_PLAINTEXT_SIZE,
961
1088
  AES_GCM_RECOMMENDED_MESSAGES_PER_KEY,
962
1089
  AuthenticationError,
1090
+ Base64,
963
1091
  CHACHA20_POLY1305_KEY_SIZE,
964
1092
  CHACHA20_POLY1305_NONCE_SIZE,
965
1093
  CHACHA20_POLY1305_TAG_SIZE,
@@ -979,6 +1107,7 @@ var index_default = SignalisCore;
979
1107
  HKDF_PRK_SIZE,
980
1108
  HMAC,
981
1109
  HMAC_SHA256_TAG_SIZE,
1110
+ Hex,
982
1111
  KeyDerivationError,
983
1112
  LengthError,
984
1113
  SHA256,
@@ -986,6 +1115,7 @@ var index_default = SignalisCore;
986
1115
  SHA256_OUTPUT_SIZE,
987
1116
  SignalisError,
988
1117
  SignatureError,
1118
+ Utf8,
989
1119
  VERSION,
990
1120
  ValidationError,
991
1121
  XED25519_PRIVATE_KEY_SIZE,