Skip to content

Commit fc15aff

Browse files
committed
refactor: add cross-environment decodeBase64 method
1 parent ee70132 commit fc15aff

File tree

5 files changed

+98
-8
lines changed

5 files changed

+98
-8
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ jobs:
1212
matrix:
1313
node-version:
1414
- 18
15-
- '*'
15+
- 25 # Node.js 25 support Uint8Array.fromBase64()
16+
- 'lts/*'
1617
steps:
1718
- uses: actions/checkout@v6
1819
- uses: actions/setup-node@v6

globals.d.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Adapted from file://./node_modules/typescript/lib/lib.dom.d.ts so we don't have to include the entire DOM lib
2+
// Ref: https://github.com/microsoft/TypeScript/issues/31535, https://github.com/microsoft/TypeScript/issues/41727, https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1685
3+
4+
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */
5+
declare function atob(data: string): string;
6+
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */
7+
declare function btoa(data: string): string;
8+
9+
type AllowSharedBufferSource =
10+
| ArrayBufferLike
11+
| ArrayBufferView<ArrayBufferLike>;
12+
13+
interface TextDecodeOptions {
14+
stream?: boolean;
15+
}
16+
17+
interface TextDecoderOptions {
18+
fatal?: boolean;
19+
ignoreBOM?: boolean;
20+
}
21+
22+
/**
23+
* The **`TextDecoder`** interface represents a decoder for a specific text encoding, such as `UTF-8`, `ISO-8859-2`, `KOI8-R`, `GBK`, etc.
24+
*
25+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder)
26+
*/
27+
interface TextDecoder extends TextDecoderCommon {
28+
/**
29+
* The **`TextDecoder.decode()`** method returns a string containing text decoded from the buffer passed as a parameter.
30+
*
31+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/decode)
32+
*/
33+
decode(input?: AllowSharedBufferSource, options?: TextDecodeOptions): string;
34+
}
35+
36+
// eslint-disable-next-line no-var
37+
declare var TextDecoder: {
38+
prototype: TextDecoder;
39+
new (label?: string, options?: TextDecoderOptions): TextDecoder;
40+
};
41+
42+
interface TextDecoderCommon {
43+
/**
44+
* Returns encoding's name, lowercased.
45+
*
46+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/encoding)
47+
*/
48+
readonly encoding: string;
49+
/**
50+
* Returns true if error mode is "fatal", otherwise false.
51+
*
52+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/fatal)
53+
*/
54+
readonly fatal: boolean;
55+
/**
56+
* Returns the value of ignore BOM.
57+
*
58+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/ignoreBOM)
59+
*/
60+
readonly ignoreBOM: boolean;
61+
}

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
},
2828
"devDependencies": {
2929
"@borderless/ts-scripts": "^0.15.0",
30-
"@types/node": "^20.19.35",
3130
"@vitest/coverage-v8": "^3.2.4",
3231
"typescript": "^5.9.3",
3332
"vitest": "^3.2.4"

src/index.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
* MIT Licensed
77
*/
88

9-
import { Buffer } from 'node:buffer';
10-
119
export = auth;
1210

1311
/**
@@ -101,14 +99,45 @@ const CREDENTIALS_REGEXP =
10199

102100
const USER_PASS_REGEXP = /^([^:]*):(.*)$/;
103101

102+
type Uint8ArrayWithBase64 = typeof Uint8Array & {
103+
fromBase64?: (str: string) => Uint8Array;
104+
};
105+
106+
type BufferLike = {
107+
from(
108+
input: string,
109+
encoding: 'base64',
110+
): { toString(encoding: 'utf-8'): string };
111+
};
112+
113+
const NodeBuffer = (globalThis as any).Buffer as BufferLike | undefined;
114+
115+
const textDecoder = new TextDecoder('utf-8');
116+
104117
/**
105118
* Decode base64 string.
106119
* @private
107120
*/
121+
const decodeBase64: (str: string) => string = (() => {
122+
// 1) Modern Web / some runtimes
123+
if (typeof (Uint8Array as Uint8ArrayWithBase64).fromBase64 === 'function') {
124+
return (str: string) =>
125+
textDecoder.decode((Uint8Array as Uint8ArrayWithBase64).fromBase64!(str));
126+
}
108127

109-
function decodeBase64(str: string): string {
110-
return Buffer.from(str, 'base64').toString();
111-
}
128+
// 2) Node.js (fast path)
129+
if (typeof NodeBuffer?.from === 'function') {
130+
return (str: string) => NodeBuffer.from(str, 'base64').toString('utf-8');
131+
}
132+
133+
// 3) Browser fallback
134+
return (str: string) => {
135+
const binary = atob(str);
136+
return textDecoder.decode(
137+
Uint8Array.from(binary, (char) => char.charCodeAt(0)),
138+
);
139+
};
140+
})();
112141

113142
/**
114143
* Get the Authorization header from request object.

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"outDir": "dist",
88
"module": "nodenext",
99
"moduleResolution": "nodenext",
10-
"types": ["node"]
10+
"types": ["./globals.d.ts"]
1111
},
1212
"include": ["src/**/*"]
1313
}

0 commit comments

Comments
 (0)