Skip to content

Commit 96b784c

Browse files
committed
fix(msteams): bound user token JSON reads
1 parent 3d20614 commit 96b784c

2 files changed

Lines changed: 62 additions & 3 deletions

File tree

extensions/msteams/src/monitor-handler.sso.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,58 @@ describe("handleSigninTokenExchangeInvoke", () => {
141141
expect(stored).toBeNull();
142142
});
143143

144+
it("bounds successful User Token service JSON reads and cancels the stream", async () => {
145+
const state = { canceled: false, enqueued: 0 };
146+
const chunkBytes = 1024 * 1024;
147+
const stream = new ReadableStream<Uint8Array>({
148+
pull(controller) {
149+
if (state.enqueued >= 64) {
150+
controller.close();
151+
return;
152+
}
153+
state.enqueued += 1;
154+
controller.enqueue(new Uint8Array(chunkBytes).fill(0x61));
155+
},
156+
cancel() {
157+
state.canceled = true;
158+
},
159+
});
160+
const jsonSpy = vi.spyOn(Response.prototype, "json").mockImplementation(async () => {
161+
throw new Error("raw response.json() should not be used");
162+
});
163+
const fetchImpl: MSTeamsSsoFetch = vi.fn(async () => {
164+
return new Response(stream, {
165+
status: 200,
166+
headers: { "content-type": "application/json" },
167+
});
168+
});
169+
const { sso, tokenStore } = createSsoDeps({ fetchImpl });
170+
171+
try {
172+
const result = await handleSigninTokenExchangeInvoke({
173+
value: { id: "flow-1", connectionName: "GraphConnection", token: "exchangeable-token" },
174+
user: { userId: "aad-user-guid", channelId: "msteams" },
175+
deps: sso,
176+
});
177+
178+
expect(result.ok).toBe(false);
179+
if (!result.ok) {
180+
expect(result.code).toBe("unexpected_response");
181+
expect(result.message).toMatch(/JSON response exceeds 16777216 bytes/);
182+
}
183+
expect(jsonSpy).not.toHaveBeenCalled();
184+
expect(state.enqueued).toBeLessThan(32);
185+
expect(state.canceled).toBe(true);
186+
const stored = await tokenStore.get({
187+
connectionName: "GraphConnection",
188+
userId: "aad-user-guid",
189+
});
190+
expect(stored).toBeNull();
191+
} finally {
192+
jsonSpy.mockRestore();
193+
}
194+
});
195+
144196
it("refuses to exchange without a user id", async () => {
145197
const { fetchImpl, calls } = createFakeFetch([]);
146198
const { sso } = createSsoDeps({ fetchImpl });

extensions/msteams/src/sso.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* that ack; these helpers encapsulate token exchange and persistence.
2525
*/
2626

27+
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
2728
import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
2829
import { readMSTeamsHttpErrorDetail } from "./http-error.js";
2930
import type { MSTeamsSsoTokenStore } from "./sso-token-store.js";
@@ -130,9 +131,15 @@ async function callUserTokenService(
130131
}
131132
let parsed: unknown;
132133
try {
133-
parsed = await response.json();
134-
} catch {
135-
return { error: "invalid JSON from User Token service", status: response.status };
134+
parsed = await readProviderJsonResponse<unknown>(
135+
response,
136+
"invalid JSON from User Token service",
137+
);
138+
} catch (error) {
139+
return {
140+
error: error instanceof Error ? error.message : "invalid JSON from User Token service",
141+
status: response.status,
142+
};
136143
}
137144
if (!parsed || typeof parsed !== "object") {
138145
return { error: "empty response from User Token service", status: response.status };

0 commit comments

Comments
 (0)