Skip to content

Commit 6b74b08

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

10 files changed

Lines changed: 97 additions & 46 deletions

File tree

packages/auth2/src/Auth2Stamper.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,18 @@ import type { Auth2StamperStorage } from "./Auth2StamperStorage";
1111
/** Refresh the access token when fewer than this many ms remain before expiry. */
1212
const TOKEN_REFRESH_BUFFER_MS = 15 * 60 * 1000;
1313

14+
export type Auth2Logger = {
15+
debug?(message: string): void;
16+
info?(message: string): void;
17+
warn?(message: string): void;
18+
error?(message: string): void;
19+
};
20+
1421
export type Auth2StamperRefreshConfig = {
1522
authApiBaseUrl: string;
1623
clientId: string;
1724
redirectUri: string;
25+
logger?: Auth2Logger;
1826
};
1927

2028
export class Auth2Stamper implements Auth2StamperWithKeyManagement {
@@ -123,16 +131,26 @@ export class Auth2Stamper implements Auth2StamperWithKeyManagement {
123131
* Returns true if a refresh succeeded, false otherwise.
124132
*/
125133
async maybeRefreshTokens(): Promise<boolean> {
134+
this.refreshConfig?.logger?.debug?.("Auth2Stamper.maybeRefreshTokens() called");
135+
126136
if (this.refreshingTokensPromise) {
137+
this.refreshConfig?.logger?.debug?.("Auth2Stamper.maybeRefreshTokens(): awaiting in-flight refresh");
127138
return this.refreshingTokensPromise;
128139
}
129140

130-
if (
131-
!this.refreshConfig ||
132-
!this._refreshToken ||
133-
!this._tokenExpiresAt ||
134-
Date.now() < this._tokenExpiresAt - TOKEN_REFRESH_BUFFER_MS
135-
) {
141+
if (!this.refreshConfig) {
142+
return false;
143+
}
144+
if (!this._refreshToken) {
145+
this.refreshConfig.logger?.debug?.("Auth2Stamper.maybeRefreshTokens(): skipped — missing refresh token");
146+
return false;
147+
}
148+
if (!this._tokenExpiresAt) {
149+
this.refreshConfig.logger?.debug?.("Auth2Stamper.maybeRefreshTokens(): skipped — missing token expiry");
150+
return false;
151+
}
152+
if (Date.now() < this._tokenExpiresAt - TOKEN_REFRESH_BUFFER_MS) {
153+
this.refreshConfig.logger?.debug?.("Auth2Stamper.maybeRefreshTokens(): skipped — token not near expiry");
136154
return false;
137155
}
138156

@@ -141,6 +159,7 @@ export class Auth2Stamper implements Auth2StamperWithKeyManagement {
141159

142160
this.refreshingTokensPromise = (async () => {
143161
try {
162+
refreshConfig.logger?.info?.("Auth2Stamper.maybeRefreshTokens(): attempting token refresh");
144163
const refreshed = await refreshTokenRequest({
145164
authApiBaseUrl: refreshConfig.authApiBaseUrl,
146165
clientId: refreshConfig.clientId,
@@ -154,9 +173,11 @@ export class Auth2Stamper implements Auth2StamperWithKeyManagement {
154173
refreshToken: refreshed.refreshToken,
155174
expiresInMs: refreshed.expiresInMs,
156175
});
176+
refreshConfig.logger?.info?.("Auth2Stamper.maybeRefreshTokens(): token refresh succeeded");
157177
return true;
158178
} catch (error) {
159-
console.error("Failed to refresh tokens:", error);
179+
const message = error instanceof Error ? error.message : String(error);
180+
refreshConfig.logger?.error?.(`Auth2Stamper.maybeRefreshTokens(): token refresh failed: ${message}`);
160181
return false;
161182
} finally {
162183
this.refreshingTokensPromise = null;
@@ -166,11 +187,15 @@ export class Auth2Stamper implements Auth2StamperWithKeyManagement {
166187
return this.refreshingTokensPromise;
167188
}
168189
async stamp(params: { data: Buffer; type?: "PKI" } | { data: Buffer; type: "OIDC" }): Promise<string> {
190+
this.refreshConfig?.logger?.debug?.("Auth2Stamper.stamp(): called");
191+
169192
if (!this._keyPair || !this._keyInfo) {
170193
throw new Error("Auth2Stamper not initialized. Call init() first.");
171194
}
172195

196+
this.refreshConfig?.logger?.debug?.("Auth2Stamper.stamp(): awaiting maybeRefreshTokens()");
173197
await this.maybeRefreshTokens();
198+
this.refreshConfig?.logger?.debug?.("Auth2Stamper.stamp(): maybeRefreshTokens() completed");
174199

175200
if (!this.auth2Token) {
176201
throw new Error("Auth2Stamper not initialized. Call init() first.");

packages/auth2/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export { Auth2KmsRpcClient, type Auth2KmsClientOptions } from "./Auth2KmsRpcClient";
2-
export { Auth2Stamper, type Auth2StamperRefreshConfig } from "./Auth2Stamper";
2+
export { Auth2Stamper, type Auth2StamperRefreshConfig, type Auth2Logger } from "./Auth2Stamper";
33
export type { Auth2StamperStorage, Auth2StamperStoredRecord } from "./Auth2StamperStorage";
44
export { Auth2Token, Auth2TokenExpiredError, decodeJwtClaims } from "./Auth2Token";
55
export { exchangeAuthCode, refreshToken } from "./tokenExchange";

packages/auth2/src/tokenExchange.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,17 @@ export async function refreshToken(options: {
3939
redirectUri: string;
4040
refreshToken: string;
4141
}): Promise<TokenExchangeResult<"refresh_token">> {
42-
const result = await _postTokenRequest(
43-
options.authApiBaseUrl,
44-
new URLSearchParams({
45-
grant_type: "refresh_token",
46-
client_id: options.clientId,
47-
redirect_uri: options.redirectUri,
48-
refresh_token: options.refreshToken,
49-
}),
50-
);
42+
const params = new URLSearchParams({
43+
grant_type: "refresh_token",
44+
client_id: options.clientId,
45+
refresh_token: options.refreshToken,
46+
});
47+
48+
if (options.redirectUri.trim().length > 0) {
49+
params.set("redirect_uri", options.redirectUri);
50+
}
51+
52+
const result = await _postTokenRequest(options.authApiBaseUrl, params);
5153
if (!result.refreshToken) {
5254
throw new Error("Auth2 refresh token request did not return a refresh_token.");
5355
}

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.3",
3+
"version": "1.0.4",
44
"description": "MCP Server for Phantom Wallet",
55
"repository": {
66
"type": "git",

packages/mcp-server/src/auth/dcr.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,4 +334,37 @@ describe("DCRClient", () => {
334334
}
335335
});
336336
});
337+
338+
describe("registerForDeviceFlow", () => {
339+
beforeEach(() => {
340+
dcrClient = new DCRClient("https://auth.phantom.app", "phantom-mcp");
341+
});
342+
343+
it("registers a public device-flow client with a wallet-tag audience matching its client_id", async () => {
344+
const mockResponse = {
345+
data: {
346+
client_id: "test-device-client-id",
347+
client_secret: "",
348+
client_id_issued_at: 1234567890,
349+
},
350+
};
351+
352+
mockedAxios.post.mockResolvedValueOnce(mockResponse);
353+
354+
await dcrClient.registerForDeviceFlow();
355+
356+
const callArgs = mockedAxios.post.mock.calls[0];
357+
const payload = callArgs[1] as {
358+
client_id: string;
359+
audience: string[];
360+
grant_types: string[];
361+
token_endpoint_auth_method: string;
362+
};
363+
364+
expect(payload.client_id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
365+
expect(payload.audience).toEqual([`urn:phantom:wallet-tag:${payload.client_id}`]);
366+
expect(payload.grant_types).toEqual(["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"]);
367+
expect(payload.token_endpoint_auth_method).toBe("none");
368+
});
369+
});
337370
});

packages/mcp-server/src/auth/dcr.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@
44
*/
55

66
import axios, { type AxiosError } from "axios";
7+
import { randomUUID } from "crypto";
78
import { Logger } from "../utils/logger";
89
import type { DCRClientConfig } from "../session/types";
910

1011
/**
1112
* RFC 7591 Dynamic Client Registration request payload
1213
*/
1314
interface DCRRegistrationRequest {
15+
client_id?: string;
1416
client_name: string;
1517
redirect_uris: string[];
1618
grant_types: string[];
1719
response_types: string[];
1820
application_type: string;
1921
token_endpoint_auth_method: string;
22+
audience?: string[];
2023
scope?: string;
2124
}
2225

@@ -91,13 +94,16 @@ export class DCRClient {
9194
async registerForDeviceFlow(): Promise<DCRClientConfig> {
9295
const registrationEndpoint = `${this.authBaseUrl}/oauth2/register`;
9396
const clientName = `${this.appId}-${Date.now()}`;
97+
const clientId = randomUUID();
9498

9599
const payload: DCRRegistrationRequest = {
100+
client_id: clientId,
96101
client_name: clientName,
97102
redirect_uris: [],
98103
grant_types: ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"],
99104
response_types: [],
100105
application_type: "native",
106+
audience: [`urn:phantom:wallet-tag:${clientId}`],
101107
// Device flow clients are public per RFC 8628 — no client secret
102108
token_endpoint_auth_method: "none",
103109
scope: "openid offline_access",

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,15 @@ describe("SessionManager", () => {
186186
const manager = new SessionManager({ authFlow: "device-code" });
187187
await manager.initialize();
188188

189-
expect(Auth2Stamper).toHaveBeenCalledWith(expect.anything(), {
190-
authApiBaseUrl: "https://auth.phantom.app",
191-
clientId: "phantom-mcp",
192-
redirectUri: "",
193-
});
189+
expect(Auth2Stamper).toHaveBeenCalledWith(
190+
expect.anything(),
191+
expect.objectContaining({
192+
authApiBaseUrl: "https://auth.phantom.app",
193+
clientId: "phantom-mcp",
194+
redirectUri: "",
195+
logger: expect.anything(),
196+
}),
197+
);
194198
expect(DeviceCodeAuthProvider).toHaveBeenCalledWith(
195199
expect.anything(),
196200
{

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ export class SessionManager {
258258
return Promise.resolve(false);
259259
}
260260
this.logger.info("tryRefreshSession: attempting stamper token refresh");
261-
return this.stamper.maybeRefreshTokens().then(result => {
261+
return this.stamper.maybeRefreshTokens().then((result: boolean) => {
262262
this.logger.info(`tryRefreshSession: refresh ${result ? "succeeded" : "did not run or failed"}`);
263263
return result;
264264
});
@@ -343,6 +343,7 @@ export class SessionManager {
343343
authApiBaseUrl: this.authBaseUrl,
344344
clientId: this.resolveAppId(),
345345
redirectUri: "",
346+
logger: this.logger.child("Auth2Stamper"),
346347
});
347348
const deviceFlow = new DeviceCodeAuthProvider(
348349
stamper,
@@ -447,6 +448,7 @@ export class SessionManager {
447448
authApiBaseUrl: this.authBaseUrl,
448449
clientId: session.appId ?? appId,
449450
redirectUri: "",
451+
logger: this.logger.child("Auth2Stamper"),
450452
});
451453
await stamper.init();
452454
this.stamper = stamper;

packages/phantom-openclaw-plugin/README.md

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -92,27 +92,6 @@ Supported optional config keys mirror the MCP server environment surface used by
9292
- **`PHANTOM_VERSION`**: Optional version header override
9393
- **`PHANTOM_MCP_DEBUG`**: Enable debug logging (set to `"1"` or `"true"`)
9494

95-
Example staging config:
96-
97-
```json
98-
{
99-
"plugins": {
100-
"enabled": true,
101-
"entries": {
102-
"phantom-openclaw-plugin": {
103-
"enabled": true,
104-
"config": {
105-
"PHANTOM_AUTH_BASE_URL": "https://staging-auth.phantom.app",
106-
"PHANTOM_CONNECT_BASE_URL": "https://staging-connect.phantom.app",
107-
"PHANTOM_WALLETS_API_BASE_URL": "https://staging-api.phantom.app/v1/wallets",
108-
"PHANTOM_API_BASE_URL": "https://staging-api.phantom.app"
109-
}
110-
}
111-
}
112-
}
113-
}
114-
```
115-
11695
## Available Tools
11796

11897
The plugin exposes the following tools from the Phantom MCP Server:

packages/phantom-openclaw-plugin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@phantom/phantom-openclaw-plugin",
3-
"version": "1.0.2",
3+
"version": "1.0.3",
44
"description": "Phantom Embedded Wallet — sign transactions, transfer tokens, and swap on Solana and Ethereum. Powered by Phantom.",
55
"repository": {
66
"type": "git",

0 commit comments

Comments
 (0)