This is an automated email from the ASF dual-hosted git repository.
hubcio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 1b4ebeecf fix(node): stop dropping and corrupting tokens in list
responses (#4171)
1b4ebeecf is described below
commit 1b4ebeecfd546d7b5f1aec25e7ebc26337ed9119
Author: Matthew Patton <[email protected]>
AuthorDate: Mon Sep 14 03:37:26 2026 -0400
fix(node): stop dropping and corrupting tokens in list responses (#4171)
---
foreign/node/src/wire/token/token.utils.test.ts | 88 +++++++++++++++++++++++++
foreign/node/src/wire/token/token.utils.ts | 10 +--
2 files changed, 91 insertions(+), 7 deletions(-)
diff --git a/foreign/node/src/wire/token/token.utils.test.ts
b/foreign/node/src/wire/token/token.utils.test.ts
new file mode 100644
index 000000000..571146bd2
--- /dev/null
+++ b/foreign/node/src/wire/token/token.utils.test.ts
@@ -0,0 +1,88 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { describe, it } from "node:test";
+import assert from "node:assert/strict";
+import { deserializeTokens } from "./token.utils.js";
+
+// Real server wire shape: [nameLength: u8][name][expiry: 8-byte LE, always
present, 0 = never-expiring].
+// See core/binary_protocol/.../get_personal_access_tokens.rs and
core/server/src/responses.rs:1195-1209.
+const tokenRecord = (name: string, expiry: bigint = 0n): Buffer => {
+ const nameBuf = Buffer.from(name, "utf-8");
+ const head = Buffer.from([nameBuf.length]);
+ const expiryBuf = Buffer.alloc(8);
+ expiryBuf.writeBigUInt64LE(expiry);
+ return Buffer.concat([head, nameBuf, expiryBuf]);
+};
+
+describe("deserializeTokens", () => {
+ it("reads all tokens in a 3-token buffer, including the one after the 2nd",
() => {
+ const t1 = tokenRecord("ci-a", 123n);
+ const t2 = tokenRecord("ci-b");
+ const t3 = tokenRecord("x");
+ const buffer = Buffer.concat([t1, t2, t3]);
+
+ const tokens = deserializeTokens(buffer);
+
+ assert.deepEqual(
+ tokens.map((t) => t.name),
+ ["ci-a", "ci-b", "x"],
+ );
+ assert.notEqual(tokens[0].expiry, null);
+ assert.equal(tokens[1].expiry, null);
+ assert.equal(tokens[2].expiry, null);
+ });
+
+ it("reads all tokens cleanly regardless of record length ordering", () => {
+ const t1 = tokenRecord("short");
+ const t2 = tokenRecord("second");
+ const t3 = tokenRecord("a-much-longer-token-name-here");
+ const t4 = tokenRecord("last");
+ const buffer = Buffer.concat([t1, t2, t3, t4]);
+
+ const tokens = deserializeTokens(buffer);
+
+ assert.deepEqual(
+ tokens.map((t) => t.name),
+ ["short", "second", "a-much-longer-token-name-here", "last"],
+ );
+ assert.ok(tokens.every((t) => t.expiry === null));
+ });
+
+ it("decodes a single token with no trailing data", () => {
+ const buffer = tokenRecord("solo");
+
+ const tokens = deserializeTokens(buffer);
+
+ assert.deepEqual(tokens, [{ name: "solo", expiry: null }]);
+ });
+
+ it("decodes a non-zero expiry into the correct point in time", () => {
+ const expiryMicros = 1700000000000000n;
+ const buffer = tokenRecord("with-expiry", expiryMicros);
+
+ const tokens = deserializeTokens(buffer);
+
+ assert.equal(tokens[0].expiry?.getTime(), Number(expiryMicros / 1000n));
+ });
+
+ it("throws on a buffer truncated before the expiry field", () => {
+ const partial = Buffer.concat([Buffer.from([4]), Buffer.from("iggy")]);
+
+ assert.throws(() => deserializeTokens(partial), RangeError);
+ });
+});
diff --git a/foreign/node/src/wire/token/token.utils.ts
b/foreign/node/src/wire/token/token.utils.ts
index e4b7a641c..efe1b3de4 100644
--- a/foreign/node/src/wire/token/token.utils.ts
+++ b/foreign/node/src/wire/token/token.utils.ts
@@ -78,13 +78,9 @@ export const deserializeCreateToken = (p: Buffer, pos = 0):
TokenDeserialized =>
export const deserializeToken = (p: Buffer, pos = 0): TokenSerialized => {
const nameLength = p.readUInt8(pos);
const name = p.subarray(pos + 1, pos + 1 + nameLength).toString();
- const rest = p.subarray(pos + 1 + nameLength);
- let expiry = null;
- let bytesRead = pos + 1 + nameLength;
- if (rest.length >= 8) {
- expiry = toDate(rest.readBigUInt64LE(0));
- bytesRead += 8;
- }
+ const expiryRaw = p.readBigUInt64LE(pos + 1 + nameLength);
+ const expiry = expiryRaw === 0n ? null : toDate(expiryRaw);
+ const bytesRead = 1 + nameLength + 8;
return {
bytesRead,
data: {