Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 28 additions & 19 deletions src/grok/reset-coupons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,34 @@ export function encodeVarint(value: number | bigint): Uint8Array {
* Decodes a protobuf varint from bytes at offset.
*/
export function decodeVarint(bytes: Uint8Array, offset: number): { value: number; bytesRead: number } {
if (!Number.isInteger(offset) || offset < 0 || offset >= bytes.length) {
throw new Error("Invalid protobuf varint offset");
}

let result = 0;
let shift = 0;
let count = 0;

while (offset + count < bytes.length) {
const b = bytes[offset + count];
count++;
result |= (b & 0x7f) << shift;
if ((b & 0x80) === 0) break;
shift += 7;
if (shift > 35) {
// For timestamps seconds, JS safe integers suffice.
break;
const part = (b & 0x7f) * (2 ** (7 * count));
if (!Number.isSafeInteger(part) || result > Number.MAX_SAFE_INTEGER - part) {
throw new Error("Protobuf varint exceeds JavaScript safe integer range");
}
result += part;
count++;
if ((b & 0x80) === 0) return { value: result, bytesRead: count };
}

return { value: result, bytesRead: count };
throw new Error("Truncated protobuf varint");
}

function decodeLength(bytes: Uint8Array, offset: number): { start: number; end: number } {
const { value: length, bytesRead } = decodeVarint(bytes, offset);
const start = offset + bytesRead;
if (!Number.isSafeInteger(length) || length < 0 || length > bytes.length - start) {
throw new Error("Invalid protobuf length-delimited field");
}
return { start, end: start + length };
}

/**
Expand Down Expand Up @@ -109,8 +120,8 @@ function decodeTimestamp(bytes: Uint8Array): number {
offset += bytesRead;
if (fieldNum === 1) seconds = value;
} else if (wireType === 2) {
const { value: len, bytesRead } = decodeVarint(bytes, offset);
offset += bytesRead + len;
const { end } = decodeLength(bytes, offset);
offset = end;
} else {
break;
}
Expand All @@ -135,10 +146,9 @@ function decodeConsumerResetToken(bytes: Uint8Array): GrokResetCoupon | null {
const wireType = tag & 0x7;

if (wireType === 2) {
const { value: len, bytesRead: lenRead } = decodeVarint(bytes, offset);
offset += lenRead;
const sub = bytes.subarray(offset, offset + len);
offset += len;
const { start, end } = decodeLength(bytes, offset);
const sub = bytes.subarray(start, end);
offset = end;

if (fieldNum === 10) {
tokenId = new TextDecoder("utf-8").decode(sub);
Expand Down Expand Up @@ -178,10 +188,9 @@ export function decodeGetRemainingResetsResponse(payload: Uint8Array): GrokReset
const wireType = tag & 0x7;

if (wireType === 2) {
const { value: len, bytesRead: lenRead } = decodeVarint(payload, offset);
offset += lenRead;
const sub = payload.subarray(offset, offset + len);
offset += len;
const { start, end } = decodeLength(payload, offset);
const sub = payload.subarray(start, end);
offset = end;

if (fieldNum === 10) {
const token = decodeConsumerResetToken(sub);
Expand Down
27 changes: 27 additions & 0 deletions tests/providers/xai/grok-reset-coupons.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import {
getGrokRemainingResets,
decodeGetRemainingResetsResponse,
decodeVarint,
encodeRedeemResetRequest,
encodeVarint,
GROK_GET_REMAINING_RESETS_ENDPOINT,
Expand Down Expand Up @@ -135,6 +136,32 @@ describe("grok reset coupons", () => {
expect(tokens[0].validityEnd).toBe(new Date(1728788400 * 1000).toISOString());
});

it("rejects oversized and truncated protobuf lengths", () => {
const oversizedLength = new Uint8Array([0x52, 0x80, 0x80, 0x80, 0x80, 0x08]);
expect(() => decodeGetRemainingResetsResponse(oversizedLength)).toThrow(
"Invalid protobuf length-delimited field",
);

expect(() => decodeVarint(new Uint8Array([0x80]), 0)).toThrow("Truncated protobuf varint");
});

it("rejects protobuf varints beyond the JavaScript safe integer range", () => {
const maxSafe = decodeVarint(encodeVarint(BigInt(Number.MAX_SAFE_INTEGER)), 0);
expect(maxSafe.value).toBe(Number.MAX_SAFE_INTEGER);

const unsafeVarint = encodeVarint(BigInt(Number.MAX_SAFE_INTEGER) + 1n);
expect(() => decodeVarint(unsafeVarint, 0)).toThrow(
"Protobuf varint exceeds JavaScript safe integer range",
);

const unsafeLength = new Uint8Array(1 + unsafeVarint.length);
unsafeLength[0] = 0x52; // field 10, wire type 2
unsafeLength.set(unsafeVarint, 1);
expect(() => decodeGetRemainingResetsResponse(unsafeLength)).toThrow(
"Protobuf varint exceeds JavaScript safe integer range",
);
});

it("asserts auth headers and tokenAuth compatibility header on request", async () => {
let capturedHeaders: Headers | undefined;
let capturedBody: Uint8Array | undefined;
Expand Down
Loading