Skip to content

Commit e03131b

Browse files
authored
[v8] Replace leb and qs deps with vanilla implementations (#1382)
## Description This pull request removes the dependency on the external `leb` and `qs` packages by introducing an in-house LEB128 encoding/decoding utility and a custom query string serializer. It updates all relevant imports to use these new utilities, ensuring compatibility and maintainability. Comprehensive unit tests for the new LEB128 implementation are also included. This is to improve cross-runtime compatibility support. **Dependency Removal and Internal Utility Replacement:** * Removed the `leb` and `qs` packages from `package.json` and replaced their usage with internal implementations. (`package.json`, [package.jsonL45-R45](diffhunk://#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519L45-R45)) * Replaced all imports of `leb`'s `encodeUInt32`/`decodeUInt32` with internal versions in `src/vault/vault.ts`. * Removed the old `toQueryString` implementation from `src/client/utils.ts` and updated all imports in `src/client/sso.ts` and `src/client/user-management.ts` to use the new internal utility. [[1]](diffhunk://#diff-3973d52ad7d2360214857fd42a183273a2e3904458c1eb573c34b3ec6151ab02L1-L20) [[2]](diffhunk://#diff-aba556dc64a77e993f9ce2de8ffd20b276128d1f6f4ba69bf2967e05dc1f7676L1-R1) [[3]](diffhunk://#diff-b5a04503adce4aaadee02b4511ee9bd11ec26a46927bde7c07d85ad31786e4bbL1-R1) **New Utility Implementations:** * Added `src/common/utils/leb128.ts`: Implements `encodeUInt32` and `decodeUInt32` for LEB128 encoding/decoding of unsigned 32-bit integers, with input validation and error handling. * Added `src/common/utils/query-string.ts`: Implements `toQueryString`, matching the old behavior (RFC1738 encoding, key sorting, array/object handling) without external dependencies. **Testing and Compatibility:** * Added comprehensive unit tests for the new LEB128 implementation in `src/common/utils/leb128.spec.ts`, including boundary values, invalid input handling, and compatibility with the previous `leb` package's output. ## Documentation Does this require changes to the WorkOS Docs? E.g. the [API Reference](https://workos.com/docs/reference) or code snippets need updates. ``` [ ] Yes ``` If yes, link a related docs PR and add a docs maintainer as a reviewer. Their approval is required.
1 parent ac02374 commit e03131b

File tree

10 files changed

+1509
-645
lines changed

10 files changed

+1509
-645
lines changed

package-lock.json

Lines changed: 1056 additions & 618 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@
4141
},
4242
"dependencies": {
4343
"iron-session": "^8.0.4",
44-
"jose": "~6.1.0",
45-
"leb": "^1.0.0",
46-
"qs": "6.14.0"
44+
"jose": "~6.1.0"
4745
},
4846
"devDependencies": {
4947
"@babel/plugin-transform-modules-commonjs": "^7.26.3",

src/client/sso.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { toQueryString } from './utils';
1+
import { toQueryString } from '../common/utils/query-string';
22
import type { SSOAuthorizationURLOptions as BaseSSOAuthorizationURLOptions } from '../sso/interfaces';
33

44
// Extend the base options to include baseURL for internal use

src/client/user-management.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { toQueryString } from './utils';
1+
import { toQueryString } from '../common/utils/query-string';
22

33
// Re-export necessary interfaces for client use
44
export interface AuthorizationURLOptions {

src/client/utils.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

src/common/utils/leb128.spec.ts

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import { encodeUInt32, decodeUInt32 } from './leb128';
2+
3+
describe('leb128', () => {
4+
describe('encodeUInt32', () => {
5+
describe('boundary values', () => {
6+
test('encodes 0 (1 byte: 0x00)', () => {
7+
const result = encodeUInt32(0);
8+
expect(result).toEqual(new Uint8Array([0x00]));
9+
});
10+
11+
test('encodes 127 (1 byte: 0x7F) - largest 1-byte value', () => {
12+
const result = encodeUInt32(127);
13+
expect(result).toEqual(new Uint8Array([0x7f]));
14+
});
15+
16+
test('encodes 128 (2 bytes: 0x80 0x01) - smallest 2-byte value', () => {
17+
const result = encodeUInt32(128);
18+
expect(result).toEqual(new Uint8Array([0x80, 0x01]));
19+
});
20+
21+
test('encodes 16383 (2 bytes: 0xFF 0x7F) - largest 2-byte value', () => {
22+
const result = encodeUInt32(16383);
23+
expect(result).toEqual(new Uint8Array([0xff, 0x7f]));
24+
});
25+
26+
test('encodes 16384 (3 bytes: 0x80 0x80 0x01)', () => {
27+
const result = encodeUInt32(16384);
28+
expect(result).toEqual(new Uint8Array([0x80, 0x80, 0x01]));
29+
});
30+
31+
test('encodes 2097151 (3 bytes: 0xFF 0xFF 0x7F)', () => {
32+
const result = encodeUInt32(2097151);
33+
expect(result).toEqual(new Uint8Array([0xff, 0xff, 0x7f]));
34+
});
35+
36+
test('encodes 2097152 (4 bytes: 0x80 0x80 0x80 0x01)', () => {
37+
const result = encodeUInt32(2097152);
38+
expect(result).toEqual(new Uint8Array([0x80, 0x80, 0x80, 0x01]));
39+
});
40+
41+
test('encodes 268435455 (4 bytes: 0xFF 0xFF 0xFF 0x7F)', () => {
42+
const result = encodeUInt32(268435455);
43+
expect(result).toEqual(new Uint8Array([0xff, 0xff, 0xff, 0x7f]));
44+
});
45+
46+
test('encodes 268435456 (5 bytes: 0x80 0x80 0x80 0x80 0x01)', () => {
47+
const result = encodeUInt32(268435456);
48+
expect(result).toEqual(new Uint8Array([0x80, 0x80, 0x80, 0x80, 0x01]));
49+
});
50+
51+
test('encodes 4294967295 (5 bytes: 0xFF 0xFF 0xFF 0xFF 0x0F) - MAX_UINT32', () => {
52+
const result = encodeUInt32(4294967295);
53+
expect(result).toEqual(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0x0f]));
54+
});
55+
});
56+
57+
describe('common values', () => {
58+
test('encodes 42', () => {
59+
const result = encodeUInt32(42);
60+
expect(result).toEqual(new Uint8Array([0x2a]));
61+
});
62+
63+
test('encodes 300', () => {
64+
const result = encodeUInt32(300);
65+
expect(result).toEqual(new Uint8Array([0xac, 0x02]));
66+
});
67+
68+
test('encodes 1000000', () => {
69+
const result = encodeUInt32(1000000);
70+
expect(result).toEqual(new Uint8Array([0xc0, 0x84, 0x3d]));
71+
});
72+
});
73+
74+
describe('invalid inputs', () => {
75+
test('throws for negative numbers', () => {
76+
expect(() => encodeUInt32(-1)).toThrow('Value must be non-negative');
77+
});
78+
79+
test('throws for numbers > MAX_UINT32', () => {
80+
expect(() => encodeUInt32(4294967296)).toThrow(
81+
'Value must not exceed 4294967295',
82+
);
83+
});
84+
85+
test('throws for non-integers', () => {
86+
expect(() => encodeUInt32(3.14)).toThrow('Value must be an integer');
87+
});
88+
89+
test('throws for NaN', () => {
90+
expect(() => encodeUInt32(NaN)).toThrow(
91+
'Value must be a finite number',
92+
);
93+
});
94+
95+
test('throws for Infinity', () => {
96+
expect(() => encodeUInt32(Infinity)).toThrow(
97+
'Value must be a finite number',
98+
);
99+
});
100+
});
101+
});
102+
103+
describe('decodeUInt32', () => {
104+
describe('round-trip verification', () => {
105+
test('round-trips encode/decode for 0', () => {
106+
const encoded = encodeUInt32(0);
107+
const { value, nextIndex } = decodeUInt32(encoded);
108+
expect(value).toBe(0);
109+
expect(nextIndex).toBe(1);
110+
});
111+
112+
test('round-trips encode/decode for 127', () => {
113+
const encoded = encodeUInt32(127);
114+
const { value, nextIndex } = decodeUInt32(encoded);
115+
expect(value).toBe(127);
116+
expect(nextIndex).toBe(1);
117+
});
118+
119+
test('round-trips encode/decode for 128', () => {
120+
const encoded = encodeUInt32(128);
121+
const { value, nextIndex } = decodeUInt32(encoded);
122+
expect(value).toBe(128);
123+
expect(nextIndex).toBe(2);
124+
});
125+
126+
test('round-trips encode/decode for 16383', () => {
127+
const encoded = encodeUInt32(16383);
128+
const { value, nextIndex } = decodeUInt32(encoded);
129+
expect(value).toBe(16383);
130+
expect(nextIndex).toBe(2);
131+
});
132+
133+
test('round-trips encode/decode for 300', () => {
134+
const encoded = encodeUInt32(300);
135+
const { value, nextIndex } = decodeUInt32(encoded);
136+
expect(value).toBe(300);
137+
expect(nextIndex).toBe(2);
138+
});
139+
140+
test('round-trips encode/decode for 1000000', () => {
141+
const encoded = encodeUInt32(1000000);
142+
const { value, nextIndex } = decodeUInt32(encoded);
143+
expect(value).toBe(1000000);
144+
expect(nextIndex).toBe(3);
145+
});
146+
147+
test('round-trips encode/decode for MAX_UINT32', () => {
148+
const encoded = encodeUInt32(4294967295);
149+
const { value, nextIndex } = decodeUInt32(encoded);
150+
expect(value).toBe(4294967295);
151+
expect(nextIndex).toBe(5);
152+
});
153+
154+
test('round-trips 1000 random values between 0 and 1000000', () => {
155+
for (let i = 0; i < 1000; i++) {
156+
const original = Math.floor(Math.random() * 1000000);
157+
const encoded = encodeUInt32(original);
158+
const { value } = decodeUInt32(encoded);
159+
expect(value).toBe(original);
160+
}
161+
});
162+
});
163+
164+
describe('offset handling', () => {
165+
test('decodes from offset 0 by default', () => {
166+
const data = new Uint8Array([0x2a]); // 42
167+
const { value, nextIndex } = decodeUInt32(data);
168+
expect(value).toBe(42);
169+
expect(nextIndex).toBe(1);
170+
});
171+
172+
test('decodes from specified offset', () => {
173+
const data = new Uint8Array([0xff, 0xff, 0xac, 0x02]); // garbage, then 300
174+
const { value, nextIndex } = decodeUInt32(data, 2);
175+
expect(value).toBe(300);
176+
expect(nextIndex).toBe(4);
177+
});
178+
179+
test('returns correct nextIndex after decoding', () => {
180+
const data = encodeUInt32(128);
181+
const { nextIndex } = decodeUInt32(data);
182+
expect(nextIndex).toBe(2);
183+
});
184+
185+
test('decodes multiple values in sequence using nextIndex', () => {
186+
// Encode three values: 42, 300, 1000000
187+
const encoded1 = encodeUInt32(42);
188+
const encoded2 = encodeUInt32(300);
189+
const encoded3 = encodeUInt32(1000000);
190+
191+
// Concatenate into single buffer
192+
const buffer = new Uint8Array(
193+
encoded1.length + encoded2.length + encoded3.length,
194+
);
195+
buffer.set(encoded1, 0);
196+
buffer.set(encoded2, encoded1.length);
197+
buffer.set(encoded3, encoded1.length + encoded2.length);
198+
199+
// Decode sequentially
200+
const result1 = decodeUInt32(buffer, 0);
201+
expect(result1.value).toBe(42);
202+
203+
const result2 = decodeUInt32(buffer, result1.nextIndex);
204+
expect(result2.value).toBe(300);
205+
206+
const result3 = decodeUInt32(buffer, result2.nextIndex);
207+
expect(result3.value).toBe(1000000);
208+
});
209+
});
210+
211+
describe('invalid inputs', () => {
212+
test('throws for offset beyond buffer bounds', () => {
213+
const data = new Uint8Array([0x2a]);
214+
expect(() => decodeUInt32(data, 5)).toThrow(
215+
'Offset 5 is out of bounds',
216+
);
217+
});
218+
219+
test('throws for negative offset', () => {
220+
const data = new Uint8Array([0x2a]);
221+
expect(() => decodeUInt32(data, -1)).toThrow(
222+
'Offset -1 is out of bounds',
223+
);
224+
});
225+
226+
test('throws for truncated encoding (incomplete byte sequence)', () => {
227+
// Create a truncated encoding: 0x80 indicates more bytes follow, but none present
228+
const data = new Uint8Array([0x80]);
229+
expect(() => decodeUInt32(data)).toThrow('Truncated LEB128 encoding');
230+
});
231+
232+
test('throws for truncated multi-byte encoding', () => {
233+
// 0x80 0x80 indicates at least 3 bytes needed, but only 2 present
234+
const data = new Uint8Array([0x80, 0x80]);
235+
expect(() => decodeUInt32(data)).toThrow('Truncated LEB128 encoding');
236+
});
237+
238+
test('throws for encoding that exceeds uint32 range', () => {
239+
// 6 bytes with continuation bits (should never happen for uint32)
240+
const data = new Uint8Array([0x80, 0x80, 0x80, 0x80, 0x80, 0x01]);
241+
expect(() => decodeUInt32(data)).toThrow(
242+
'LEB128 sequence exceeds maximum length for uint32',
243+
);
244+
});
245+
});
246+
});
247+
248+
describe('compatibility with leb package', () => {
249+
// These tests ensure wire-format compatibility with the original leb package
250+
// If these tests pass, existing encrypted data can be decrypted after the migration
251+
252+
test('produces identical output to leb.encodeUInt32 for value 42', () => {
253+
const result = encodeUInt32(42);
254+
// Known output from leb package
255+
expect(result).toEqual(new Uint8Array([0x2a]));
256+
});
257+
258+
test('produces identical output to leb.encodeUInt32 for value 300', () => {
259+
const result = encodeUInt32(300);
260+
// Known output from leb package
261+
expect(result).toEqual(new Uint8Array([0xac, 0x02]));
262+
});
263+
264+
test('produces identical output to leb.encodeUInt32 for value 1000000', () => {
265+
const result = encodeUInt32(1000000);
266+
// Known output from leb package
267+
expect(result).toEqual(new Uint8Array([0xc0, 0x84, 0x3d]));
268+
});
269+
270+
test('decodes leb.encodeUInt32 output correctly', () => {
271+
// These are known outputs from the leb package
272+
const knownEncodings: Array<[number, number[]]> = [
273+
[0, [0x00]],
274+
[42, [0x2a]],
275+
[127, [0x7f]],
276+
[128, [0x80, 0x01]],
277+
[300, [0xac, 0x02]],
278+
[1000000, [0xc0, 0x84, 0x3d]],
279+
[4294967295, [0xff, 0xff, 0xff, 0xff, 0x0f]],
280+
];
281+
282+
for (const [expectedValue, bytes] of knownEncodings) {
283+
const { value } = decodeUInt32(new Uint8Array(bytes));
284+
expect(value).toBe(expectedValue);
285+
}
286+
});
287+
});
288+
});

0 commit comments

Comments
 (0)