From 91cd716137aa4bff9dacb89ce954a83f495465fd Mon Sep 17 00:00:00 2001 From: lyon Date: Sun, 14 Jun 2026 15:56:23 +0800 Subject: [PATCH] fix: allow user billing session summary proxy --- internal/cloud/server.ts | 4 ++-- .../cloud/user-billing-integration.test.ts | 24 ++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/internal/cloud/server.ts b/internal/cloud/server.ts index 1938a58a..8ac792a6 100644 --- a/internal/cloud/server.ts +++ b/internal/cloud/server.ts @@ -991,9 +991,9 @@ async function handleUsageSummaryHttp(request, response, url, options) { const nextOptions = await codeAgentOptions(request, response, options, { required: true }); if (!nextOptions) return; const client = nextOptions.userBillingClient; - const token = bearerTokenFromRequest(request); + const token = userBillingTokenFromRequest(request); if (nextOptions.userBillingAuth?.active !== true || !token) { - sendRestError(request, response, 403, "user_billing_auth_required", "usage summary requires a user-billing bearer token", { + sendRestError(request, response, 403, "user_billing_auth_required", "usage summary requires a user-billing session or API key", { operation: `GET ${route}`, target: { type: "route", id: route }, result: "rejected" diff --git a/internal/cloud/user-billing-integration.test.ts b/internal/cloud/user-billing-integration.test.ts index 94c9cbc8..5695e872 100644 --- a/internal/cloud/user-billing-integration.test.ts +++ b/internal/cloud/user-billing-integration.test.ts @@ -878,6 +878,20 @@ test("cloud api bridges user-billing register/login into HttpOnly web session co calls.push({ op: "listApiKeys", tokenPrefix: token.slice(0, 8) }); assert.equal(token, sessionToken); return { ok: true, status: 200, body: { contractVersion: "user-api-key-v1", keys: [{ id: "key_web_session", name: "Web session key", prefix: "hwl_web" }], count: 1 } }; + }, + async billingSummary(token, query = {}) { + calls.push({ op: "billingSummary", tokenPrefix: token.slice(0, 8), query }); + assert.equal(token, sessionToken); + return { + ok: true, + status: 200, + body: { + contractVersion: "user-billing-summary-v1", + planId: "default", + entitlements: [{ resourceType: "code_agent", enabled: true }], + valuesRedacted: true + } + }; } }; const server = createCloudApiServer({ env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" }, userBillingClient }); @@ -905,13 +919,21 @@ test("cloud api bridges user-billing register/login into HttpOnly web session co assert.equal(sessionBody.actor.username, "new-user"); assert.equal(JSON.stringify(sessionBody).includes(sessionToken), false); + const billingSummary = await fetch(`http://127.0.0.1:${port}/v1/billing/summary?limit=3`, { headers: { cookie } }); + assert.equal(billingSummary.status, 200); + const billingSummaryBody = await billingSummary.json(); + assert.equal(billingSummaryBody.planId, "default"); + assert.equal(billingSummaryBody.entitlements[0].resourceType, "code_agent"); + assert.equal(billingSummaryBody.proxy.source, "hwlab-user-billing"); + assert.equal(JSON.stringify(billingSummaryBody).includes(sessionToken), false); + const keys = await fetch(`http://127.0.0.1:${port}/v1/api-keys`, { headers: { cookie } }); assert.equal(keys.status, 200); const keysBody = await keys.json(); assert.equal(keysBody.proxy.source, "hwlab-user-billing"); assert.equal(keysBody.keys[0].id, "key_web_session"); assert.equal(JSON.stringify(keysBody).includes(sessionToken), false); - assert.deepEqual(calls.map((call) => call.op), ["register", "login", "introspect", "introspect", "introspect", "listApiKeys"]); + assert.deepEqual(calls.map((call) => call.op), ["register", "login", "introspect", "introspect", "introspect", "billingSummary", "introspect", "listApiKeys"]); } finally { await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); }