Skip to content

Commit 60a986d

Browse files
chore: sync from internal
1 parent 6dc5a41 commit 60a986d

35 files changed

Lines changed: 105 additions & 181 deletions

packages/auth2/src/Auth2KmsRpcClient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export class Auth2KmsRpcClient {
4343
const axiosInstance = axios.create();
4444

4545
axiosInstance.interceptors.request.use(async config => {
46+
await this.stamper.maybeRefreshTokens();
47+
4648
config.headers = config.headers || {};
4749
config.headers["x-app-id"] = options.appId;
4850
config.headers["x-api-version"] = DEFAULT_KMS_API_VERSION;

packages/auth2/src/Auth2Stamper.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,17 @@ export class Auth2Stamper implements Auth2StamperWithKeyManagement {
2727

2828
private refreshingTokensPromise: Promise<boolean> | null = null;
2929

30-
// Because TOKEN_REFRESH_BUFFER_MS proactively handles refresh, we can safely use void here
31-
// and return the existing cached values immediately.
3230
get bearerToken(): string | null {
3331
if (!this._idType || !this._accessToken) {
3432
return null;
3533
}
36-
void this.maybeRefreshTokens();
3734
return `${this._idType} ${this._accessToken}`;
3835
}
3936

4037
get auth2Token(): Auth2Token | null {
4138
if (!this._accessToken) {
4239
return null;
4340
}
44-
void this.maybeRefreshTokens();
4541
return Auth2Token.fromAccessToken(this._accessToken);
4642
}
4743

@@ -170,7 +166,13 @@ export class Auth2Stamper implements Auth2StamperWithKeyManagement {
170166
return this.refreshingTokensPromise;
171167
}
172168
async stamp(params: { data: Buffer; type?: "PKI" } | { data: Buffer; type: "OIDC" }): Promise<string> {
173-
if (!this._keyPair || !this._keyInfo || !this.auth2Token) {
169+
if (!this._keyPair || !this._keyInfo) {
170+
throw new Error("Auth2Stamper not initialized. Call init() first.");
171+
}
172+
173+
await this.maybeRefreshTokens();
174+
175+
if (!this.auth2Token) {
174176
throw new Error("Auth2Stamper not initialized. Call init() first.");
175177
}
176178

packages/auth2/src/__tests__/Auth2KmsRpcClient.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ function makeStamper(
4141
) {
4242
return {
4343
stamp: jest.fn().mockResolvedValue("mock-stamp"),
44+
maybeRefreshTokens: jest.fn().mockResolvedValue(false),
4445
getKeyInfo: jest.fn().mockReturnValue({
4546
keyId: "key-id-1",
4647
publicKey: "7EcDshMsTHCs2f2HU2a3n36x9JkEVVenF9oQQGy5U3s",
@@ -107,6 +108,7 @@ describe("Auth2KmsRpcClient", () => {
107108
expect(headers["x-app-id"]).toBe("app-123");
108109
expect(headers["x-api-version"]).toBe("2025-11-24");
109110
expect(headers["x-phantom-stamp"]).toBe("mock-stamp");
111+
expect(stamper.maybeRefreshTokens).toHaveBeenCalledTimes(1);
110112
expect(stamper.stamp).toHaveBeenCalledWith(expect.objectContaining({ data: expect.anything() }));
111113
});
112114

packages/auth2/src/__tests__/Auth2Stamper.test.ts

Lines changed: 24 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,22 @@ describe("Auth2Stamper", () => {
337337
});
338338

339339
describe("stamp()", () => {
340+
it("awaits maybeRefreshTokens() before building the OIDC stamp", async () => {
341+
const storage = makeStorage();
342+
const stamper = new Auth2Stamper(storage);
343+
344+
await stamper.init();
345+
346+
storage.load.mockResolvedValueOnce({ keyPair: mockKeyPair, keyInfo: stamper.getKeyInfo()! });
347+
await stamper.setTokens({ accessToken: "my-access-token", idType: "Bearer" });
348+
349+
const maybeRefreshSpy = jest.spyOn(stamper, "maybeRefreshTokens").mockResolvedValue(false);
350+
351+
await stamper.stamp({ type: "OIDC", data: Buffer.from("payload") });
352+
353+
expect(maybeRefreshSpy).toHaveBeenCalled();
354+
});
355+
340356
it("throws before init()", async () => {
341357
const stamper = new Auth2Stamper(makeStorage());
342358

@@ -593,19 +609,8 @@ describe("Auth2Stamper", () => {
593609
});
594610

595611
describe("token refresh", () => {
596-
it("triggers a background refresh when the token is near expiry", async () => {
612+
it("does not trigger refresh as a side effect of bearerToken access", async () => {
597613
const mockFetch = globalThis.fetch as jest.Mock;
598-
mockFetch.mockResolvedValueOnce({
599-
ok: true,
600-
json: () =>
601-
Promise.resolve({
602-
access_token: "new-access",
603-
refresh_token: "new-refresh",
604-
token_type: "Bearer",
605-
expires_in: 3600,
606-
}),
607-
});
608-
609614
const storage = makeStorage(STORED_RECORD);
610615
const stamper = new Auth2Stamper(storage, {
611616
authApiBaseUrl: "https://auth.example.com",
@@ -614,84 +619,28 @@ describe("Auth2Stamper", () => {
614619
});
615620

616621
await stamper.init();
617-
618-
// Force the token to be near expiry.
619622
(stamper as any)._tokenExpiresAt = Date.now() - 1000;
620623
(stamper as any)._refreshToken = "stored-refresh-token";
621624

622-
// Access bearerToken to fire the background refresh.
623-
stamper.bearerToken;
624-
625-
// Flush microtasks so the background refresh completes.
626-
await new Promise(resolve => setTimeout(resolve, 0));
627-
628-
expect(mockFetch).toHaveBeenCalledWith(
629-
"https://auth.example.com/oauth2/token",
630-
expect.objectContaining({ method: "POST" }),
631-
);
632-
expect(stamper.bearerToken).toBe("Bearer new-access");
633-
});
634-
635-
it("does not refresh when no refreshConfig is provided", async () => {
636-
const mockFetch = globalThis.fetch as jest.Mock;
637-
638-
const storage = makeStorage(STORED_RECORD);
639-
const stamper = new Auth2Stamper(storage);
640-
641-
await stamper.init();
642-
643-
(stamper as any)._tokenExpiresAt = Date.now() - 1000;
644-
645-
// Access bearerToken to fire the background refresh.
646-
stamper.bearerToken;
647-
648-
// Flush microtasks so the background refresh completes.
649-
await new Promise(resolve => setTimeout(resolve, 0));
650-
625+
expect(stamper.bearerToken).toBe("Bearer stored-access-token");
651626
expect(mockFetch).not.toHaveBeenCalled();
652627
});
653628

654-
it("does not fire a second refresh while one is already in flight", async () => {
629+
it("does not trigger refresh as a side effect of auth2Token access", async () => {
655630
const mockFetch = globalThis.fetch as jest.Mock;
656-
// Slow refresh that we can control.
657-
let resolveRefresh!: () => void;
658-
mockFetch.mockReturnValueOnce(
659-
new Promise(resolve => {
660-
resolveRefresh = () =>
661-
resolve({
662-
ok: true,
663-
json: () =>
664-
Promise.resolve({
665-
access_token: "new",
666-
refresh_token: "new-r",
667-
token_type: "Bearer",
668-
expires_in: 3600,
669-
}),
670-
});
671-
}),
672-
);
673-
674631
const storage = makeStorage(STORED_RECORD);
675632
const stamper = new Auth2Stamper(storage, {
676633
authApiBaseUrl: "https://auth.example.com",
677-
clientId: "c",
678-
redirectUri: "https://app.example.com/cb",
634+
clientId: "client-1",
635+
redirectUri: "https://app.example.com/callback",
679636
});
680637

681638
await stamper.init();
682-
683639
(stamper as any)._tokenExpiresAt = Date.now() - 1000;
684-
(stamper as any)._refreshToken = "r";
685-
686-
// Trigger two concurrent getter accesses.
687-
stamper.bearerToken;
688-
stamper.bearerToken;
689-
690-
resolveRefresh();
691-
await new Promise(resolve => setTimeout(resolve, 0));
640+
(stamper as any)._refreshToken = "stored-refresh-token";
692641

693-
// fetch should only have been called once despite two accesses.
694-
expect(mockFetch).toHaveBeenCalledTimes(1);
642+
expect(stamper.auth2Token?.sub).toBe("default-user");
643+
expect(mockFetch).not.toHaveBeenCalled();
695644
});
696645
});
697646
});

packages/mcp-server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@phantom/mcp-server",
3-
"version": "1.0.2",
3+
"version": "1.0.3",
44
"description": "MCP Server for Phantom Wallet",
55
"repository": {
66
"type": "git",

packages/mcp-server/src/session/manager.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,16 @@ export class SessionManager {
252252
*/
253253
tryRefreshSession(): Promise<boolean> {
254254
if (this.session?.authFlow !== "device-code" || !this.stamper) {
255+
this.logger.info(
256+
`tryRefreshSession: skipping refresh (authFlow=${this.session?.authFlow ?? "none"}, hasStamper=${Boolean(this.stamper)})`,
257+
);
255258
return Promise.resolve(false);
256259
}
257-
return this.stamper.maybeRefreshTokens();
260+
this.logger.info("tryRefreshSession: attempting stamper token refresh");
261+
return this.stamper.maybeRefreshTokens().then(result => {
262+
this.logger.info(`tryRefreshSession: refresh ${result ? "succeeded" : "did not run or failed"}`);
263+
return result;
264+
});
258265
}
259266

260267
/**

packages/mcp-server/src/tools/buy-token.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,6 @@ export const buyTokenTool: ToolHandler = {
3535
inputSchema: {
3636
type: "object",
3737
properties: {
38-
walletId: {
39-
type: "string",
40-
description: "Optional wallet ID (defaults to authenticated wallet)",
41-
},
4238
sellChainId: {
4339
type: "string",
4440
description:

packages/mcp-server/src/tools/cancel-perp-order.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@ export const cancelPerpOrderTool = createTool<Params>({
2222
inputSchema: {
2323
type: "object",
2424
properties: {
25-
walletId: {
26-
type: "string",
27-
description: "Optional wallet ID (defaults to authenticated wallet)",
28-
},
2925
derivationIndex: {
3026
type: "integer",
3127
description: "Optional derivation index (default: 0)",

packages/mcp-server/src/tools/close-perp-position.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ describe("close_perp_position", () => {
3030

3131
it("calls closePosition with market and no sizePercent by default", async () => {
3232
await closePerpPositionTool.handler({ market: "BTC" }, makeContext() as any);
33+
const { createPerpsClient } = jest.requireMock("../utils/perps.js");
34+
expect(createPerpsClient).toHaveBeenCalledWith(expect.anything(), "wallet-1", undefined);
3335
expect(mockPerpsClient.closePosition).toHaveBeenCalledWith({ market: "BTC", sizePercent: undefined });
3436
});
3537

packages/mcp-server/src/tools/close-perp-position.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ export const closePerpPositionTool = createTool<Params>({
2424
inputSchema: {
2525
type: "object",
2626
properties: {
27-
walletId: {
28-
type: "string",
29-
description: "Optional wallet ID (defaults to authenticated wallet)",
30-
},
3127
derivationIndex: {
3228
type: "integer",
3329
description: "Optional derivation index (default: 0)",

0 commit comments

Comments
 (0)