Skip to content

Commit a7dce5e

Browse files
authored
Merge branch 'main' into feat/dart-cloud-run-eventarc
2 parents 37dff48 + e4f28d5 commit a7dce5e

41 files changed

Lines changed: 2287 additions & 173 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +0,0 @@
1-
- Add `MCP-Protocol-Version`, `Mcp-Method`, and `Mcp-Name` HTTP headers to `OneMcpServer` requests per the MCP 0728 standard release candidate (https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/ and https://modelcontextprotocol.io/seps/2243-http-standardization).
2-
- Fixes Storage Emulator to support JSON uploads larger than 100KB without hanging or throwing 413 error (#8355)
3-
- Add `extdeprecationwarnings` experiment to display phased deprecation notices and guidance across `ext:*` CLI commands.
4-
- Fixes Data Connect emulator crash when in-flight GraphQL requests are cancelled (#10821)

npm-shrinkwrap.json

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

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "firebase-tools",
3-
"version": "15.24.0",
3+
"version": "15.25.0",
44
"description": "Command-Line Interface for Firebase",
55
"main": "./lib/index.js",
66
"mcpName": "io.github.firebase/firebase-mcp",
@@ -173,7 +173,7 @@
173173
"undici": "^6.19.0",
174174
"winston": "^3.0.0",
175175
"winston-transport": "^4.4.0",
176-
"ws": "^7.5.10",
176+
"ws": "^8.21.1",
177177
"yaml": "^2.8.3",
178178
"zod": "^4.0.0"
179179
},
@@ -225,7 +225,7 @@
225225
"@types/triple-beam": "^1.3.0",
226226
"@types/universal-analytics": "^0.4.5",
227227
"@types/update-notifier": "^5.1.0",
228-
"@types/ws": "^7.2.3",
228+
"@types/ws": "^8.18.1",
229229
"@typescript-eslint/eslint-plugin": "^5.9.0",
230230
"@typescript-eslint/parser": "^5.9.0",
231231
"astro": "^2.2.3",

src/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export const developerConnectP4SADomain = () =>
3232

3333
export const artifactRegistryDomain = () =>
3434
utils.envOverride("ARTIFACT_REGISTRY_DOMAIN", "https://artifactregistry.googleapis.com");
35+
export const appCheckOrigin = () =>
36+
utils.envOverride("FIREBASE_APPCHECK_URL", "https://firebaseappcheck.googleapis.com");
3537
export const appDistributionOrigin = () =>
3638
utils.envOverride(
3739
"FIREBASE_APP_DISTRIBUTION_URL",

src/appcheck/api.spec.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { expect } from "chai";
2+
import * as sinon from "sinon";
3+
4+
import { createDebugToken, listDebugTokens, deleteDebugToken, client } from "./api";
5+
import { DebugToken } from "./types";
6+
import { FirebaseError } from "../error";
7+
8+
describe("appcheck", () => {
9+
const projectNumber = "123456789";
10+
const appId = "1:123456789:web:abc123def456";
11+
const parent = `projects/${projectNumber}/apps/${appId}`;
12+
const debugTokenId = "debug-token-id-123";
13+
const debugTokenName = `${parent}/debugTokens/${debugTokenId}`;
14+
const dummyDebugToken: DebugToken = {
15+
name: debugTokenName,
16+
displayName: "My Debug Token",
17+
token: "00000000-0000-0000-0000-000000000000",
18+
updateTime: "2023-01-01T00:00:00Z",
19+
};
20+
21+
let sandbox: sinon.SinonSandbox;
22+
23+
beforeEach(() => {
24+
sandbox = sinon.createSandbox();
25+
});
26+
27+
afterEach(() => {
28+
sandbox.restore();
29+
});
30+
31+
describe("createDebugToken", () => {
32+
it("should resolve with created DebugToken on success", async () => {
33+
const postStub = sandbox.stub(client, "post").resolves({ body: dummyDebugToken } as any);
34+
35+
const result = await createDebugToken(
36+
projectNumber,
37+
appId,
38+
dummyDebugToken.displayName,
39+
dummyDebugToken.token,
40+
);
41+
42+
expect(result).to.deep.equal(dummyDebugToken);
43+
expect(postStub.calledOnce).to.be.true;
44+
expect(postStub.firstCall.args[0]).to.match(/.*debugTokens.*/);
45+
expect(postStub.firstCall.args[1]).to.deep.equal({
46+
displayName: dummyDebugToken.displayName,
47+
token: dummyDebugToken.token,
48+
});
49+
});
50+
51+
it("should throw error on failure", async () => {
52+
const postStub = sandbox.stub(client, "post").rejects(new FirebaseError("Invalid request"));
53+
54+
await expect(
55+
createDebugToken(projectNumber, appId, dummyDebugToken.displayName, dummyDebugToken.token),
56+
).to.be.rejectedWith(FirebaseError, "Invalid request");
57+
expect(postStub.calledOnce).to.be.true;
58+
});
59+
});
60+
61+
describe("listDebugTokens", () => {
62+
it("should resolve with list of DebugTokens on success", async () => {
63+
const getStub = sandbox
64+
.stub(client, "get")
65+
.resolves({ body: { debugTokens: [dummyDebugToken] } } as any);
66+
67+
const result = await listDebugTokens(projectNumber, appId);
68+
expect(result).to.deep.equal([dummyDebugToken]);
69+
expect(getStub.calledOnce).to.be.true;
70+
expect(getStub.firstCall.args[0]).to.match(/.*debugTokens.*/);
71+
});
72+
73+
it("should handle pagination", async () => {
74+
const secondToken: DebugToken = {
75+
...dummyDebugToken,
76+
name: `${parent}/debugTokens/token-2`,
77+
};
78+
const getStub = sandbox.stub(client, "get");
79+
getStub
80+
.onFirstCall()
81+
.resolves({ body: { debugTokens: [dummyDebugToken], nextPageToken: "page-2" } } as any);
82+
getStub.onSecondCall().resolves({ body: { debugTokens: [secondToken] } } as any);
83+
84+
const result = await listDebugTokens(projectNumber, appId);
85+
expect(result).to.deep.equal([dummyDebugToken, secondToken]);
86+
expect(getStub.calledTwice).to.be.true;
87+
expect(getStub.secondCall.args[0]).to.match(/.*debugTokens.*/);
88+
expect(getStub.secondCall.args[1]).to.deep.equal({ queryParams: { pageToken: "page-2" } });
89+
});
90+
});
91+
92+
describe("deleteDebugToken", () => {
93+
it("should resolve on success", async () => {
94+
const deleteStub = sandbox.stub(client, "delete").resolves({ body: {} } as any);
95+
96+
await expect(deleteDebugToken(debugTokenName)).to.be.eventually.fulfilled;
97+
expect(deleteStub.calledOnce).to.be.true;
98+
expect(deleteStub.firstCall.args[0]).to.match(/.*debugTokens.*/);
99+
});
100+
});
101+
});

src/appcheck/api.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { appCheckOrigin } from "../api";
2+
import { Client } from "../apiv2";
3+
import { DebugToken, ListDebugTokensResponse } from "./types";
4+
5+
const API_VERSION = "v1";
6+
7+
export const client = new Client({
8+
urlPrefix: appCheckOrigin(),
9+
auth: true,
10+
apiVersion: API_VERSION,
11+
});
12+
13+
/**
14+
* Creates a new DebugToken for the specified app.
15+
*/
16+
export async function createDebugToken(
17+
projectNumber: string,
18+
appId: string,
19+
displayName: string,
20+
token: string,
21+
): Promise<DebugToken> {
22+
const parent = `projects/${projectNumber}/apps/${appId}`;
23+
const res = await client.post<Partial<DebugToken>, DebugToken>(`${parent}/debugTokens`, {
24+
displayName,
25+
token,
26+
});
27+
return res.body;
28+
}
29+
30+
/**
31+
* Lists all DebugTokens for the specified app.
32+
*/
33+
export async function listDebugTokens(projectNumber: string, appId: string): Promise<DebugToken[]> {
34+
const parent = `projects/${projectNumber}/apps/${appId}`;
35+
const debugTokens: DebugToken[] = [];
36+
let pageToken = "";
37+
do {
38+
const queryParams: Record<string, string> = {};
39+
if (pageToken) {
40+
queryParams.pageToken = pageToken;
41+
}
42+
const res = await client.get<ListDebugTokensResponse>(`${parent}/debugTokens`, { queryParams });
43+
if (res.body?.debugTokens) {
44+
debugTokens.push(...res.body.debugTokens);
45+
}
46+
pageToken = res.body?.nextPageToken || "";
47+
} while (pageToken);
48+
return debugTokens;
49+
}
50+
51+
/**
52+
* Deletes the specified DebugToken.
53+
*/
54+
export async function deleteDebugToken(name: string): Promise<void> {
55+
await client.delete<void>(name);
56+
}

src/appcheck/prompts.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { AppPlatform, listFirebaseApps, selectAppInteractively } from "../management/apps";
2+
import { needProjectId } from "../projectUtils";
3+
import { detectApps } from "../appUtils";
4+
import { FirebaseError } from "../error";
5+
import { logger } from "../logger";
6+
import * as clc from "colorette";
7+
import { AppCheckDebugOptions } from "./types";
8+
9+
/**
10+
* Gets the appId from options or prompts the user to select an app if multiple exist.
11+
* Uses needProjectId(options) to retrieve the active or specified project ID.
12+
*/
13+
export async function getOrPromptAppId(
14+
options: AppCheckDebugOptions,
15+
): Promise<{ projectId: string; appId: string }> {
16+
const projectId = needProjectId(options);
17+
18+
logger.info(`Active Project: ${clc.bold(projectId)}`);
19+
20+
if (options.app) {
21+
return { projectId, appId: options.app };
22+
}
23+
24+
const projectDir = options.cwd || process.cwd();
25+
let apps = await listFirebaseApps(projectId, AppPlatform.ANY);
26+
if (!apps.length) {
27+
throw new FirebaseError(`There are no apps associated with project ${projectId}.`);
28+
}
29+
30+
const localApps = await detectApps(projectDir);
31+
const localAppIds = localApps.map((a) => a.appId).filter(Boolean) as string[];
32+
if (localAppIds.length > 0) {
33+
const filteredApps = apps.filter((app) => localAppIds.includes(app.appId));
34+
if (filteredApps.length > 0) {
35+
apps = filteredApps;
36+
}
37+
}
38+
39+
if (apps.length === 1) {
40+
return { projectId, appId: apps[0].appId };
41+
} else if (options.nonInteractive) {
42+
throw new FirebaseError(`Project ${projectId} has multiple apps, must specify an app id.`);
43+
}
44+
45+
const selectedApp = await selectAppInteractively(apps, AppPlatform.ANY, {
46+
message: "Select the app to register a debug token for:",
47+
});
48+
49+
return { projectId, appId: selectedApp.appId };
50+
}
51+
52+
export const getOrPromptProjectAndAppId = getOrPromptAppId;

src/appcheck/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Options } from "../options";
2+
3+
export interface DebugToken {
4+
name: string;
5+
displayName: string;
6+
token: string;
7+
updateTime?: string;
8+
}
9+
10+
export interface ListDebugTokensResponse {
11+
debugTokens?: DebugToken[];
12+
nextPageToken?: string;
13+
}
14+
15+
export interface AppCheckDebugOptions extends Options {
16+
app?: string;
17+
displayName?: string;
18+
}

0 commit comments

Comments
 (0)