Skip to content

Commit 88005c7

Browse files
committed
feat: add base32 encode
1 parent 7bc3d91 commit 88005c7

9 files changed

+407
-226
lines changed

biome.json

+2-4
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@
2525
}
2626
},
2727
"files": {
28-
"ignore": [
29-
"dist"
30-
]
28+
"ignore": ["dist"]
3129
}
32-
}
30+
}

package.json

+79-81
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,80 @@
11
{
2-
"name": "@better-auth/utils",
3-
"version": "0.2.1",
4-
"license": "MIT",
5-
"description": "A collection of utilities for better-auth",
6-
"main": "./dist/index.cjs",
7-
"module": "./dist/index.mjs",
8-
"scripts": {
9-
"test": "vitest",
10-
"typecheck": "tsc --noEmit",
11-
"build": "unbuild",
12-
"lint:fix": "biome check . --write"
13-
},
14-
"keywords": [
15-
"auth",
16-
"utils",
17-
"typescript",
18-
"better-auth",
19-
"better-auth-utils"
20-
],
21-
"author": "Bereket Engida",
22-
"repository": {
23-
"type": "git",
24-
"url": "https://github.com/better-auth/utils"
25-
},
26-
"dependencies": {
27-
"uncrypto": "^0.1.3"
28-
},
29-
"devDependencies": {
30-
"@biomejs/biome": "^1.9.4",
31-
"@types/node": "^22.10.1",
32-
"bumpp": "^9.9.0",
33-
"happy-dom": "^15.11.7",
34-
"unbuild": "^2.0.0",
35-
"vitest": "^2.1.8"
36-
},
37-
"exports": {
38-
".": {
39-
"import": "./dist/index.mjs",
40-
"require": "./dist/index.cjs"
41-
},
42-
"./base64": {
43-
"import": "./dist/base64.mjs",
44-
"require": "./dist/base64.cjs"
45-
},
46-
"./binary": {
47-
"import": "./dist/binary.mjs",
48-
"require": "./dist/binary.cjs"
49-
},
50-
"./hash": {
51-
"import": "./dist/hash.mjs",
52-
"require": "./dist/hash.cjs"
53-
},
54-
"./ecdsa": {
55-
"import": "./dist/ecdsa.mjs",
56-
"require": "./dist/ecdsa.cjs"
57-
},
58-
"./hex": {
59-
"import": "./dist/hex.mjs",
60-
"require": "./dist/hex.cjs"
61-
},
62-
"./hmac": {
63-
"import": "./dist/hmac.mjs",
64-
"require": "./dist/hmac.cjs"
65-
},
66-
"./otp": {
67-
"import": "./dist/otp.mjs",
68-
"require": "./dist/otp.cjs"
69-
},
70-
"./random": {
71-
"import": "./dist/random.mjs",
72-
"require": "./dist/random.cjs"
73-
},
74-
"./rsa": {
75-
"import": "./dist/rsa.mjs",
76-
"require": "./dist/rsa.cjs"
77-
}
78-
},
79-
"files": [
80-
"dist"
81-
]
82-
}
2+
"name": "@better-auth/utils",
3+
"version": "0.2.1",
4+
"license": "MIT",
5+
"description": "A collection of utilities for better-auth",
6+
"main": "./dist/index.cjs",
7+
"module": "./dist/index.mjs",
8+
"scripts": {
9+
"test": "vitest",
10+
"typecheck": "tsc --noEmit",
11+
"build": "unbuild",
12+
"lint:fix": "biome check . --write"
13+
},
14+
"keywords": [
15+
"auth",
16+
"utils",
17+
"typescript",
18+
"better-auth",
19+
"better-auth-utils"
20+
],
21+
"author": "Bereket Engida",
22+
"repository": {
23+
"type": "git",
24+
"url": "https://github.com/better-auth/utils"
25+
},
26+
"dependencies": {
27+
"uncrypto": "^0.1.3"
28+
},
29+
"devDependencies": {
30+
"@biomejs/biome": "^1.9.4",
31+
"@types/node": "^22.10.1",
32+
"bumpp": "^9.9.0",
33+
"happy-dom": "^15.11.7",
34+
"unbuild": "^2.0.0",
35+
"vitest": "^2.1.8"
36+
},
37+
"exports": {
38+
".": {
39+
"import": "./dist/index.mjs",
40+
"require": "./dist/index.cjs"
41+
},
42+
"./base64": {
43+
"import": "./dist/base64.mjs",
44+
"require": "./dist/base64.cjs"
45+
},
46+
"./binary": {
47+
"import": "./dist/binary.mjs",
48+
"require": "./dist/binary.cjs"
49+
},
50+
"./hash": {
51+
"import": "./dist/hash.mjs",
52+
"require": "./dist/hash.cjs"
53+
},
54+
"./ecdsa": {
55+
"import": "./dist/ecdsa.mjs",
56+
"require": "./dist/ecdsa.cjs"
57+
},
58+
"./hex": {
59+
"import": "./dist/hex.mjs",
60+
"require": "./dist/hex.cjs"
61+
},
62+
"./hmac": {
63+
"import": "./dist/hmac.mjs",
64+
"require": "./dist/hmac.cjs"
65+
},
66+
"./otp": {
67+
"import": "./dist/otp.mjs",
68+
"require": "./dist/otp.cjs"
69+
},
70+
"./random": {
71+
"import": "./dist/random.mjs",
72+
"require": "./dist/random.cjs"
73+
},
74+
"./rsa": {
75+
"import": "./dist/rsa.mjs",
76+
"require": "./dist/rsa.cjs"
77+
}
78+
},
79+
"files": ["dist"]
80+
}

src/base32.ts

+193
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
//inspired by oslo implementation by pilcrowonpaper: https://github.com/pilcrowonpaper/oslo/blob/main/src/encoding/base32.ts
2+
3+
import type { TypedArray } from "./type";
4+
5+
/**
6+
* Returns the Base32 alphabet based on the encoding type.
7+
* @param hex - Whether to use the hexadecimal Base32 alphabet.
8+
* @returns The appropriate Base32 alphabet.
9+
*/
10+
function getAlphabet(hex: boolean): string {
11+
return hex
12+
? "0123456789ABCDEFGHIJKLMNOPQRSTUV"
13+
: "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
14+
}
15+
16+
/**
17+
* Creates a decode map for the given alphabet.
18+
* @param alphabet - The Base32 alphabet.
19+
* @returns A map of characters to their corresponding values.
20+
*/
21+
function createDecodeMap(alphabet: string): Map<string, number> {
22+
const decodeMap = new Map<string, number>();
23+
for (let i = 0; i < alphabet.length; i++) {
24+
decodeMap.set(alphabet[i]!, i);
25+
}
26+
return decodeMap;
27+
}
28+
29+
/**
30+
* Encodes a Uint8Array into a Base32 string.
31+
* @param data - The data to encode.
32+
* @param alphabet - The Base32 alphabet to use.
33+
* @param padding - Whether to include padding.
34+
* @returns The Base32 encoded string.
35+
*/
36+
function base32Encode(
37+
data: Uint8Array,
38+
alphabet: string,
39+
padding: boolean,
40+
): string {
41+
let result = "";
42+
let buffer = 0;
43+
let shift = 0;
44+
45+
for (const byte of data) {
46+
buffer = (buffer << 8) | byte;
47+
shift += 8;
48+
while (shift >= 5) {
49+
shift -= 5;
50+
result += alphabet[(buffer >> shift) & 0x1f];
51+
}
52+
}
53+
54+
if (shift > 0) {
55+
result += alphabet[(buffer << (5 - shift)) & 0x1f];
56+
}
57+
58+
if (padding) {
59+
const padCount = (8 - (result.length % 8)) % 8;
60+
result += "=".repeat(padCount);
61+
}
62+
63+
return result;
64+
}
65+
66+
/**
67+
* Decodes a Base32 string into a Uint8Array.
68+
* @param data - The Base32 encoded string.
69+
* @param alphabet - The Base32 alphabet to use.
70+
* @returns The decoded Uint8Array.
71+
*/
72+
function base32Decode(data: string, alphabet: string): Uint8Array {
73+
const decodeMap = createDecodeMap(alphabet);
74+
const result: number[] = [];
75+
let buffer = 0;
76+
let bitsCollected = 0;
77+
78+
for (const char of data) {
79+
if (char === "=") break;
80+
const value = decodeMap.get(char);
81+
if (value === undefined) {
82+
throw new Error(`Invalid Base32 character: ${char}`);
83+
}
84+
buffer = (buffer << 5) | value;
85+
bitsCollected += 5;
86+
87+
while (bitsCollected >= 8) {
88+
bitsCollected -= 8;
89+
result.push((buffer >> bitsCollected) & 0xff);
90+
}
91+
}
92+
93+
return Uint8Array.from(result);
94+
}
95+
96+
/**
97+
* Base32 encoding and decoding utility.
98+
*/
99+
export const base32 = {
100+
/**
101+
* Encodes data into a Base32 string.
102+
* @param data - The data to encode (ArrayBuffer, TypedArray, or string).
103+
* @param options - Encoding options.
104+
* @returns The Base32 encoded string.
105+
*/
106+
encode(
107+
data: ArrayBuffer | TypedArray | string,
108+
options: { padding?: boolean } = {},
109+
): string {
110+
const alphabet = getAlphabet(false);
111+
const buffer =
112+
typeof data === "string"
113+
? new TextEncoder().encode(data)
114+
: new Uint8Array(data);
115+
return base32Encode(buffer, alphabet, options.padding ?? true);
116+
},
117+
118+
/**
119+
* Decodes a Base32 string into a Uint8Array.
120+
* @param data - The Base32 encoded string or ArrayBuffer/TypedArray.
121+
* @returns The decoded Uint8Array.
122+
*/
123+
decode(data: string | ArrayBuffer | TypedArray): Uint8Array {
124+
if (typeof data !== "string") {
125+
data = new TextDecoder().decode(data);
126+
}
127+
const alphabet = getAlphabet(false);
128+
return base32Decode(data, alphabet);
129+
},
130+
};
131+
132+
/**
133+
* Base32hex encoding and decoding utility.
134+
*/
135+
export const base32hex = {
136+
/**
137+
* Encodes data into a Base32hex string.
138+
* @param data - The data to encode (ArrayBuffer, TypedArray, or string).
139+
* @param options - Encoding options.
140+
* @returns The Base32hex encoded string.
141+
*/
142+
encode(
143+
data: ArrayBuffer | TypedArray | string,
144+
options: { padding?: boolean } = {},
145+
): string {
146+
const alphabet = getAlphabet(true);
147+
const buffer =
148+
typeof data === "string"
149+
? new TextEncoder().encode(data)
150+
: new Uint8Array(data);
151+
return base32Encode(buffer, alphabet, options.padding ?? true);
152+
},
153+
154+
/**
155+
* Decodes a Base32hex string into a Uint8Array.
156+
* @param data - The Base32hex encoded string.
157+
* @returns The decoded Uint8Array.
158+
*/
159+
decode(data: string): Uint8Array {
160+
const alphabet = getAlphabet(true);
161+
return base32Decode(data, alphabet);
162+
},
163+
};
164+
165+
/** @deprecated Use `base32.encode()` instead */
166+
export function encodeBase32(
167+
data: ArrayBuffer | TypedArray,
168+
options?: { padding?: boolean },
169+
): string {
170+
return base32.encode(data, {
171+
padding: options?.padding ?? true,
172+
});
173+
}
174+
175+
/** @deprecated Use `base32.decode()` instead */
176+
export function decodeBase32(data: string): Uint8Array {
177+
return base32.decode(data);
178+
}
179+
180+
/** @deprecated Use `base32hex.encode()` instead */
181+
export function encodeBase32hex(
182+
data: ArrayBuffer | TypedArray,
183+
options?: { padding?: boolean },
184+
): string {
185+
return base32hex.encode(data, {
186+
padding: options?.padding ?? true,
187+
});
188+
}
189+
190+
/** @deprecated Use `base32hex.decode()` instead */
191+
export function decodeBase32hex(data: string): Uint8Array {
192+
return base32hex.decode(data);
193+
}

src/hash.test.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,18 @@ describe("digest", () => {
4141
});
4242

4343
it("handles input as an ArrayBufferView", async () => {
44-
const hash = await createHash("SHA-256").digest(new Uint8Array(inputBuffer));
44+
const hash = await createHash("SHA-256").digest(
45+
new Uint8Array(inputBuffer),
46+
);
4547
expect(hash).toBeInstanceOf(ArrayBuffer);
4648
});
4749
});
4850

4951
describe("Error handling", () => {
5052
it("throws an error for unsupported hash algorithms", async () => {
51-
await expect(createHash("SHA-10" as any).digest(inputString)).rejects.toThrow();
53+
await expect(
54+
createHash("SHA-10" as any).digest(inputString),
55+
).rejects.toThrow();
5256
});
5357

5458
it("throws an error for invalid input types", async () => {

0 commit comments

Comments
 (0)