diff --git a/src/client.ts b/src/client.ts index f08103d..f4acd8e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -429,6 +429,21 @@ export class ConfigClient { return new ConfigWatcher(this.configStub, this.metadata, this.timeout, tenantId); } + /** + * Replace the bearer token used for all subsequent RPCs (including watcher reconnects). + * + * Switches the client to JWT auth mode: removes any metadata-header credentials + * (x-subject, x-role, x-tenant-id) that may have been set at construction time. + * + * @param token - Raw JWT (without the "Bearer " prefix). + */ + setToken(token: string): void { + this.metadata.remove("x-subject"); + this.metadata.remove("x-role"); + this.metadata.remove("x-tenant-id"); + this.metadata.set("authorization", `Bearer ${token}`); + } + /** * Close the underlying gRPC channels. */ diff --git a/test/client.test.ts b/test/client.test.ts index 677313a..029e177 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -828,6 +828,61 @@ describe("ConfigClient", () => { }); tenantClient.close(); }); + + describe("setToken()", () => { + it("subsequent RPCs use the new Bearer token", async () => { + client.setToken("rotated-token"); + + configStub.getField.mockImplementation( + (_req: unknown, meta: Metadata, _opts: unknown, cb: (...args: unknown[]) => void) => { + expect(meta.get("authorization")).toEqual(["Bearer rotated-token"]); + cb(null, { + value: { fieldPath: "a", value: { stringValue: "v" }, checksum: "c" }, + }); + }, + ); + + await client.get("tenant-1", "a"); + }); + + it("clears metadata headers when switching from header-mode to token-mode", async () => { + client.setToken("jwt-abc"); + + configStub.getField.mockImplementation( + (_req: unknown, meta: Metadata, _opts: unknown, cb: (...args: unknown[]) => void) => { + expect(meta.get("x-subject")).toEqual([]); + expect(meta.get("x-role")).toEqual([]); + expect(meta.get("authorization")).toEqual(["Bearer jwt-abc"]); + cb(null, { + value: { fieldPath: "a", value: { stringValue: "v" }, checksum: "c" }, + }); + }, + ); + + await client.get("tenant-1", "a"); + }); + + it("rotates token on a token-mode client", async () => { + const tokenClient = new ConfigClient("localhost:9090", { + token: "initial-token", + retry: false, + }); + + tokenClient.setToken("refreshed-token"); + + configStub.getField.mockImplementation( + (_req: unknown, meta: Metadata, _opts: unknown, cb: (...args: unknown[]) => void) => { + expect(meta.get("authorization")).toEqual(["Bearer refreshed-token"]); + cb(null, { + value: { fieldPath: "a", value: { stringValue: "v" }, checksum: "c" }, + }); + }, + ); + + await tokenClient.get("tenant-1", "a"); + tokenClient.close(); + }); + }); }); describe("AbortSignal", () => {