Skip to content

Commit 1497598

Browse files
authored
Merge branch 'main' into refactor/migrate-to-native-fetch
2 parents eca2e73 + 7781a66 commit 1497598

10 files changed

Lines changed: 88 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +0,0 @@
1-
- Migrate from `node-fetch` to Node.js's native `fetch` API, and replace `nock` with a custom `undici.MockAgent` compatibility helper.
2-
- Upgrade `zod` to v4 and drop the deprecated `zod-to-json-schema` dependency in favor of zod v4's built-in `z.toJSONSchema()`.
3-
- Updated the Firebase Data Connect local toolkit to v3.4.14, which includes the following changes:
4-
- Fix linter warnings in generated Kotlin SDK files.
5-
- Fixed an intermittent "Premature close" error during login and API requests by retrying once without keep-alive. (#10692)

npm-shrinkwrap.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "firebase-tools",
3-
"version": "15.22.1",
3+
"version": "15.22.3",
44
"description": "Command-Line Interface for Firebase",
55
"main": "./lib/index.js",
66
"mcpName": "io.github.firebase/firebase-mcp",

scripts/publish/cloudbuild.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,16 +134,17 @@ steps:
134134
sleep 60
135135
echo "Package firebase-tools@$(cat /workspace/version_number.txt) is now available on npm."
136136
137-
# Set up the hub credentials for firepit-builder.
137+
# Set up the hub and npm credentials for firepit-builder.
138138
- name: "gcr.io/$PROJECT_ID/firepit-builder"
139139
entrypoint: "bash"
140140
args:
141141
- "-c"
142142
- |
143143
if [ "${_VERSION}" != "preview" ]; then
144144
mkdir -vp ~/.config && cp -v hub ~/.config/hub
145+
cp -v npmrc ~/.npmrc
145146
else
146-
echo "Skipping hub credentials for firepit-builder for preview."
147+
echo "Skipping credentials for firepit-builder for preview."
147148
fi
148149
149150
# Publish the firepit builds.

src/apiv2.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ function proxyURIFromEnv(): string | undefined {
167167
// https://github.com/node-fetch/node-fetch/issues/1767.
168168
const httpAgentNoKeepAlive = new http.Agent({ keepAlive: false });
169169
const httpsAgentNoKeepAlive = new https.Agent({ keepAlive: false });
170-
function noKeepAliveAgent(parsedURL: URL): http.Agent | https.Agent {
170+
export function noKeepAliveAgent(parsedURL: URL): http.Agent | https.Agent {
171171
return parsedURL.protocol === "https:" ? httpsAgentNoKeepAlive : httpAgentNoKeepAlive;
172172
}
173173

src/extensions/checkProjectBilling.spec.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ describe("checkProjectBilling", () => {
4747
await checkProjectBilling.enableBilling(projectId);
4848
}
4949

50-
expect(listBillingAccountsStub.notCalled);
51-
expect(setBillingAccountStub.notCalled);
52-
expect(confirmStub.notCalled);
53-
expect(selectStub.notCalled);
50+
expect(listBillingAccountsStub.notCalled).to.be.true;
51+
expect(setBillingAccountStub.notCalled).to.be.true;
52+
expect(confirmStub.notCalled).to.be.true;
53+
expect(selectStub.notCalled).to.be.true;
5454
});
5555

5656
it("should list accounts if no billing account set, but accounts available.", async () => {
@@ -73,9 +73,11 @@ describe("checkProjectBilling", () => {
7373
await checkProjectBilling.enableBilling(projectId);
7474
}
7575

76-
expect(listBillingAccountsStub.calledOnce);
77-
expect(setBillingAccountStub.calledOnce);
78-
expect(setBillingAccountStub.calledWith(projectId, "test-cloud-billing-account-name"));
76+
expect(listBillingAccountsStub.calledOnce).to.be.true;
77+
expect(listBillingAccountsStub.calledWith(projectId)).to.be.true;
78+
expect(setBillingAccountStub.calledOnce).to.be.true;
79+
expect(setBillingAccountStub.calledWith(projectId, "test-cloud-billing-account-name")).to.be
80+
.true;
7981
});
8082

8183
it("should not list accounts if no billing accounts set or available.", async () => {
@@ -90,8 +92,9 @@ describe("checkProjectBilling", () => {
9092
await checkProjectBilling.enableBilling(projectId);
9193
}
9294

93-
expect(listBillingAccountsStub.calledOnce);
94-
expect(setBillingAccountStub.notCalled);
95+
expect(listBillingAccountsStub.calledOnce).to.be.true;
96+
expect(listBillingAccountsStub.calledWith(projectId)).to.be.true;
97+
expect(setBillingAccountStub.notCalled).to.be.true;
9598
expect(checkBillingEnabledStub.callCount).to.equal(2);
9699
});
97100
});

src/extensions/checkProjectBilling.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ async function setUpBillingAccount(projectId: string) {
9999
* @param {string} projectId
100100
*/
101101
export async function enableBilling(projectId: string): Promise<void> {
102-
const billingAccounts = await cloudbilling.listBillingAccounts();
102+
const billingAccounts = await cloudbilling.listBillingAccounts(projectId);
103103
if (billingAccounts) {
104104
const accounts = billingAccounts.filter((account) => account.open);
105105
return accounts.length > 0

src/gcp/cloudbilling.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
11
import { expect } from "chai";
22
import nock from "../test/helpers/nock";
3+
import * as sinon from "sinon";
34
import * as cloudbilling from "./cloudbilling";
45
import { cloudbillingOrigin } from "../api";
56
import { Setup } from "../init";
7+
import * as ensureApiEnabled from "../ensureApiEnabled";
68

79
const PROJECT_ID = "test-project";
810

911
describe("cloudbilling", () => {
12+
let ensureStub: sinon.SinonStub;
13+
14+
beforeEach(() => {
15+
ensureStub = sinon.stub(ensureApiEnabled, "ensure").resolves();
16+
});
17+
1018
afterEach(() => {
1119
nock.cleanAll();
1220
cloudbilling.clearCache();
21+
ensureStub.restore();
1322
});
1423

1524
describe("checkBillingEnabled", () => {
1625
it("should resolve with true if billing is enabled", async () => {
1726
nock(cloudbillingOrigin())
1827
.get(`/v1/projects/${PROJECT_ID}/billingInfo`)
28+
.matchHeader("x-goog-user-project", PROJECT_ID)
1929
.reply(200, { billingEnabled: true });
2030

2131
const result = await cloudbilling.checkBillingEnabled(PROJECT_ID);
@@ -27,6 +37,7 @@ describe("cloudbilling", () => {
2737
it("should resolve with false if billing is not enabled", async () => {
2838
nock(cloudbillingOrigin())
2939
.get(`/v1/projects/${PROJECT_ID}/billingInfo`)
40+
.matchHeader("x-goog-user-project", PROJECT_ID)
3041
.reply(200, { billingEnabled: false });
3142

3243
const result = await cloudbilling.checkBillingEnabled(PROJECT_ID);
@@ -38,6 +49,7 @@ describe("cloudbilling", () => {
3849
it("should reject if the API call fails", async () => {
3950
nock(cloudbillingOrigin())
4051
.get(`/v1/projects/${PROJECT_ID}/billingInfo`)
52+
.matchHeader("x-goog-user-project", PROJECT_ID)
4153
.reply(404, { error: { message: "Not Found" } });
4254

4355
await expect(cloudbilling.checkBillingEnabled(PROJECT_ID)).to.be.rejectedWith("Not Found");
@@ -106,6 +118,7 @@ describe("cloudbilling", () => {
106118
};
107119
nock(cloudbillingOrigin())
108120
.get(`/v1/projects/${PROJECT_ID}/billingInfo`)
121+
.matchHeader("x-goog-user-project", PROJECT_ID)
109122
.reply(200, { billingEnabled: true });
110123

111124
const result = await cloudbilling.isBillingEnabled(setup);
@@ -123,6 +136,7 @@ describe("cloudbilling", () => {
123136
.put(`/v1/projects/${PROJECT_ID}/billingInfo`, {
124137
billingAccountName: billingAccountName,
125138
})
139+
.matchHeader("x-goog-user-project", PROJECT_ID)
126140
.reply(200, { billingEnabled: true });
127141

128142
const result = await cloudbilling.setBillingAccount(PROJECT_ID, billingAccountName);
@@ -136,6 +150,7 @@ describe("cloudbilling", () => {
136150
.put(`/v1/projects/${PROJECT_ID}/billingInfo`, {
137151
billingAccountName: billingAccountName,
138152
})
153+
.matchHeader("x-goog-user-project", PROJECT_ID)
139154
.reply(403, { error: { message: "Permission Denied" } });
140155

141156
await expect(
@@ -195,5 +210,17 @@ describe("cloudbilling", () => {
195210
await expect(cloudbilling.listBillingAccounts()).to.be.rejectedWith("Not Found");
196211
expect(nock.isDone()).to.be.true;
197212
});
213+
214+
it("should include x-goog-user-project header when projectId is provided", async () => {
215+
nock(cloudbillingOrigin())
216+
.get("/v1/billingAccounts")
217+
.matchHeader("x-goog-user-project", PROJECT_ID)
218+
.reply(200, { billingAccounts: [billingAccount] });
219+
220+
const result = await cloudbilling.listBillingAccounts(PROJECT_ID);
221+
222+
expect(result).to.deep.equal([billingAccount]);
223+
expect(nock.isDone()).to.be.true;
224+
});
198225
});
199226
});

src/gcp/cloudbilling.ts

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { cloudbillingOrigin } from "../api";
22
import { Client } from "../apiv2";
33
import { Setup } from "../init";
44
import * as utils from "../utils";
5+
import { ensure } from "../ensureApiEnabled";
56

67
const API_VERSION = "v1";
78
const client = new Client({ urlPrefix: cloudbillingOrigin(), apiVersion: API_VERSION });
@@ -51,16 +52,21 @@ export function checkBillingEnabled(projectId: string, forceRefresh = false): Pr
5152
return cached;
5253
}
5354
}
54-
const promise = client
55-
.get<{ billingEnabled: boolean }>(utils.endpoint(["projects", projectId, "billingInfo"]), {
56-
retries: 3,
57-
retryCodes: [429, 500, 503],
58-
})
59-
.then((res) => res.body.billingEnabled)
60-
.catch((err) => {
61-
billingEnabledCache.delete(projectId);
62-
throw err;
63-
});
55+
const promise = (async () => {
56+
await ensure(projectId, "cloudbilling.googleapis.com", "billing", true);
57+
const res = await client.get<{ billingEnabled: boolean }>(
58+
utils.endpoint(["projects", projectId, "billingInfo"]),
59+
{
60+
retries: 3,
61+
retryCodes: [429, 500, 503],
62+
headers: { "x-goog-user-project": projectId },
63+
},
64+
);
65+
return res.body.billingEnabled;
66+
})().catch((err) => {
67+
billingEnabledCache.delete(projectId);
68+
throw err;
69+
});
6470

6571
billingEnabledCache.set(projectId, promise);
6672
return promise;
@@ -75,12 +81,16 @@ export async function setBillingAccount(
7581
projectId: string,
7682
billingAccountName: string,
7783
): Promise<boolean> {
84+
await ensure(projectId, "cloudbilling.googleapis.com", "billing", true);
7885
const res = await client.put<{ billingAccountName: string }, { billingEnabled: boolean }>(
7986
utils.endpoint(["projects", projectId, "billingInfo"]),
8087
{
8188
billingAccountName: billingAccountName,
8289
},
83-
{ retryCodes: [429, 500, 503] },
90+
{
91+
retryCodes: [429, 500, 503],
92+
headers: { "x-goog-user-project": projectId },
93+
},
8494
);
8595
const enabled = res.body.billingEnabled;
8696
billingEnabledCache.set(projectId, Promise.resolve(enabled));
@@ -91,10 +101,16 @@ export async function setBillingAccount(
91101
* Lists the billing accounts that the current authenticated user has permission to view.
92102
* @return {!Promise<Object[]>}
93103
*/
94-
export async function listBillingAccounts(): Promise<BillingAccount[]> {
104+
export async function listBillingAccounts(projectId?: string): Promise<BillingAccount[]> {
105+
if (projectId) {
106+
await ensure(projectId, "cloudbilling.googleapis.com", "billing", true);
107+
}
95108
const res = await client.get<{ billingAccounts: BillingAccount[] }>(
96109
utils.endpoint(["billingAccounts"]),
97-
{ retryCodes: [429, 500, 503] },
110+
{
111+
retryCodes: [429, 500, 503],
112+
headers: projectId ? { "x-goog-user-project": projectId } : undefined,
113+
},
98114
);
99115
return res.body.billingAccounts || [];
100116
}

src/requireAuth.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,18 @@ function getAuthClient(config: GoogleAuthOptions): GoogleAuth {
2828
return authClient;
2929
}
3030

31-
authClient = new GoogleAuth(config);
31+
const authConfig: GoogleAuthOptions = {
32+
...config,
33+
clientOptions: {
34+
...config.clientOptions,
35+
transporterOptions: {
36+
...config.clientOptions?.transporterOptions,
37+
agent: apiv2.noKeepAliveAgent,
38+
},
39+
},
40+
};
41+
42+
authClient = new GoogleAuth(authConfig);
3243
return authClient;
3344
}
3445

0 commit comments

Comments
 (0)