feat(sdk): add fetchFeaturebaseToken method [ENG-1673] (#10005)

* feat(sdk): add fetchFeaturebaseToken method

- Add fetchFeaturebaseToken() to ClineAccountService
- Add FeaturebaseTokenResponse type and endpoint constant
- Add E2E mock server handler and unit tests

Ref: ENG-1673

* fix: address review feedback - narrow test glob and add JSDoc

- Fix P2: scope mocharc glob to **/*.test.ts to avoid matching non-test files
- Fix P2: add JSDoc comment to fetchFeaturebaseToken for consistency

---------

Co-authored-by: John Choi <john.choi@cline.bot>
Co-authored-by: John Choi <johnwschoi@users.noreply.github.com>
This commit is contained in:
John Choi
2026-03-30 14:48:53 -07:00
committed by GitHub
co-authored by John Choi John Choi
parent 65e9727c65
commit 03211f1364
7 changed files with 87 additions and 5 deletions
+2 -1
View File
@@ -3,7 +3,8 @@
"ts"
],
"spec": [
"src/**/__tests__/*.ts"
"src/**/__tests__/*.ts",
"src/test/services/**/*.test.ts"
],
"require": [
"ts-node/register",
+16 -2
View File
@@ -1,5 +1,6 @@
import type {
BalanceResponse,
FeaturebaseTokenResponse,
OrganizationBalanceResponse,
OrganizationUsageTransaction,
PaymentTransaction,
@@ -82,9 +83,8 @@ export class ClineAccountService {
}
if (response.statusText === "No Content") {
return {} as T // Return empty object if no content
} else {
return response.data.data as T
}
return response.data.data as T
}
/**
@@ -160,6 +160,20 @@ export class ClineAccountService {
}
}
/**
* Fetches a short-lived Featurebase SSO JWT for the current user
* @returns FeaturebaseTokenResponse or undefined if failed
*/
async fetchFeaturebaseToken(): Promise<FeaturebaseTokenResponse | undefined> {
try {
const data = await this.authenticatedRequest<FeaturebaseTokenResponse>(CLINE_API_ENDPOINT.FEATUREBASE_TOKEN)
return data
} catch (error) {
Logger.error("Failed to fetch Featurebase token:", error)
return undefined
}
}
/**
* Fetches the current user's organizations
* @returns UserResponse["organizations"] or undefined if failed
+4
View File
@@ -21,6 +21,10 @@ export interface BalanceResponse {
userId: string
}
export interface FeaturebaseTokenResponse {
featurebaseJwt: string
}
export interface UsageTransaction {
aiInferenceProviderName: string
aiModelName: string
+1
View File
@@ -6,6 +6,7 @@ enum CLINE_API_AUTH_ENDPOINTS {
enum CLINE_API_ENDPOINT_V1 {
TOKEN_EXCHANGE = "/api/v1/auth/token",
USER_INFO = "/api/v1/users/me",
FEATUREBASE_TOKEN = "/api/v1/users/me/featurebase-token",
ACTIVE_ACCOUNT = "/api/v1/users/active-account",
REMOTE_CONFIG = "/api/v1/organizations/{id}/remote-config",
API_KEYS = "/api/v1/organizations/{id}/api-keys",
+3 -2
View File
@@ -7,6 +7,7 @@ export const E2E_REGISTERED_MOCK_ENDPOINTS = {
"/organizations/{orgId}/api-keys",
"/organizations/{orgId}/remote-config",
"/users/me",
"/users/me/featurebase-token",
"/users/{userId}/balance",
"/users/{userId}/usages",
"/users/{userId}/payments",
@@ -53,7 +54,7 @@ The user wants me to replace the name "john" with "cline" in the test.ts file. I
export const name = "john"
\`\`\`
I need to change "john" to "cline". This is a simple targeted edit, so I should use the replace_in_file tool rather than write_to_file since I\'m only changing one small part of the file.
I need to change "john" to "cline". This is a simple targeted edit, so I should use the replace_in_file tool rather than write_to_file since I'm only changing one small part of the file.
I need to:
1. Use replace_in_file to change "john" to "cline" in the test.ts file
@@ -61,7 +62,7 @@ I need to:
3. The REPLACE block should be: \`export const name = "cline"\`
</thinking>
I\'ll replace "john" with "cline" in the test.ts file.
I'll replace "john" with "cline" in the test.ts file.
<replace_in_file>
<path>test.ts</path>
+10
View File
@@ -213,6 +213,16 @@ export class ClineApiServerMock {
return sendApiResponse(currentUser)
}
if (endpoint === "/users/me/featurebase-token" && method === "GET") {
const currentUser = controller.currentUser
if (!currentUser) {
return sendApiError("Unauthorized", 401)
}
return sendApiResponse({
featurebaseJwt: `mock-featurebase-jwt-${currentUser.id}`,
})
}
if (endpoint === "/users/{userId}/balance" && method === "GET") {
const { userId } = params
const balance: BalanceResponse = {
@@ -0,0 +1,51 @@
import * as assert from "assert"
import { afterEach, beforeEach, describe, it } from "mocha"
import sinon from "sinon"
import { ClineAccountService } from "@/services/account/ClineAccountService"
import { AuthService } from "@/services/auth/AuthService"
describe("ClineAccountService.fetchFeaturebaseToken", () => {
let service: ClineAccountService
let sandbox: sinon.SinonSandbox
beforeEach(() => {
sandbox = sinon.createSandbox()
sandbox.stub(AuthService, "getInstance").returns({} as AuthService)
service = new ClineAccountService()
})
afterEach(() => {
sandbox.restore()
})
it("returns featurebaseJwt on a successful authenticated request", async () => {
sandbox
.stub(service as unknown as { authenticatedRequest: () => unknown }, "authenticatedRequest")
.resolves({ featurebaseJwt: "test-jwt-token-123" })
const result = await service.fetchFeaturebaseToken()
assert.ok(result !== undefined, "result should not be undefined")
assert.strictEqual(result?.featurebaseJwt, "test-jwt-token-123")
})
it("returns undefined when the request throws a network error", async () => {
sandbox
.stub(service as unknown as { authenticatedRequest: () => unknown }, "authenticatedRequest")
.rejects(new Error("Network error"))
const result = await service.fetchFeaturebaseToken()
assert.strictEqual(result, undefined)
})
it("returns undefined when the request throws due to missing auth token", async () => {
sandbox
.stub(service as unknown as { authenticatedRequest: () => unknown }, "authenticatedRequest")
.rejects(new Error("No Cline account auth token found"))
const result = await service.fetchFeaturebaseToken()
assert.strictEqual(result, undefined)
})
})