Repository: firebase/firebase-tools (ts)

Issue:
firestore#delete not working with the emulator after more or less recent change
<!-- DO NOT DELETE
validate_template=true
template_path=.github/ISSUE_TEMPLATE/bug_report.md
-->

<!--
Thank you for contributing to the Firebase community!

Think you found a bug?
=======================
Yeah, we're definitely not perfect! Please use this template and include a minimal repro when opening the issue. If you know how to solve the issue, please create a Pull Request, and we'd be happy to review it!

Have a feature request?
========================
Great, we love hearing how we can improve our products! However, do not use this template to submit a feature request. Please submit your feature requests to: https://firebase.google.com/support/contact/bugs-features/

Have a usage question?
=======================
We get lots of those and we love helping you, but GitHub is not the best place for them and they will be closed. Please take a look at the guide first: https://firebase.google.com/docs/cli/

If the official documentation doesn't help, try asking through our official support channel: https://firebase.google.com/support/

Additional locations to check for solutions or assistance from the community:
- Stack Overflow: https://stackoverflow.com/
- Firebase Slack Community: https://firebase.community/

*Please avoid duplicate posting across multiple channels!*
-->

### [REQUIRED] Environment info

<!-- What version of the Firebase CLI (`firebase-tools`) are you using? Note that your issue may already be fixed in the latest versions. The latest version can be found at https://github.com/firebase/firebase-tools/releases -->

<!-- Output of `firebase --version` -->

**firebase-tools:9.2.2**

<!-- e.g. macOS, Windows, Ubuntu -->

**Platform:macOS**

### [REQUIRED] Test case

<!-- Provide a minimal, complete, and verifiable example (http://stackoverflow.com/help/mcve) -->

Check the steps to reproduce. It does not require anything else. Downgrading firebase-tools to, say, 8.16.2, works with that same code.

### [REQUIRED] Steps to reproduce
Have emulators running, then

```
process.env.FIREBASE_AUTH_EMULATOR_HOST = 'localhost:9099'
process.env.FIRESTORE_EMULATOR_HOST = 'localhost:8080'
process.env.NODE_ENV = 'development'
process.env.FIREBASE_TOKEN = 'FAKE_TOKEN'

firebaseTools.firestore.delete(userRef.path, {
    project:   admin.instanceId().app.options.projectId,
    recursive: true,
    yes:       true
 })
```

<!-- Provide the steps needed to reproduce the issue with the above test case. -->

### [REQUIRED] Expected behavior
Before this commit (https://github.com/firebase/firebase-tools/commit/9d90a94ae17f8bf48ccf66a0be1f55c2de3f2bcc), which added apiv2 to requireAuth.ts, having `process.env.FIREBASE_TOKEN = 'FAKE_TOKEN'` was enough to work with the local emulator. After this change, it's not possible.

There was a fix for api.js here (https://github.com/firebase/firebase-tools/pull/2211/files/3a7515e2339c571d1f389ecd42dfc597703ea06e#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed) which uses `var getTokenPromise = secureRequest ? api.getAccessToken() : Promise.resolve({ access_token: "owner" });`. apiv2 on the other hand has just `const token = await this.getAccessToken()`

<!-- What is the expected behavior? -->

### [REQUIRED] Actual behavior
<!-- Run the command with --debug flag, and include the logs below. -->

Error is returned: "Failed to fetch page after undefined FirebaseError: HTTP Error: 400, invalid jwt" from apiv2.js


Pull request description:
fixes support for calls against local emulators as admin
<!--

Thank you for contributing to the Firebase community! Please fill out the form below.

Run the linter and test suite
==============================
Run `npm test` to make sure your changes compile properly and the tests all pass on your local machine. We've hooked up this repo with continuous integration to double check those things for you.

-->

### Description

Fixes #3084

Local emulators support a bearer token of `owner` to fake an admin token. This is done so that real admin tokens are not accidentally forwarded somewhere they're not supposed to go. The previous API package did this shimming, and we're replicating it here.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7886044c..60859c1f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
 - Adds support for uploading Android App Bundles (AABs) to App Distribution.
 - Adds support for batchDelete in Auth Emulator (#3091).
 - Adds support for createSessionCookie in Auth Emulator (#3094).
+- Fixes support for using the CLI as a library against locally running emulators (#3084).
diff --git a/src/apiv2.ts b/src/apiv2.ts
index cd67c25c8..3eaa4e779 100644
--- a/src/apiv2.ts
+++ b/src/apiv2.ts
@@ -1,6 +1,6 @@
 import { AbortSignal } from "abort-controller";
 import { Readable } from "stream";
-import { URLSearchParams } from "url";
+import { parse, URLSearchParams } from "url";
 import * as ProxyAgent from "proxy-agent";
 import AbortController from "abort-controller";
 import fetch, { HeadersInit, Response, RequestInit, Headers } from "node-fetch";
@@ -188,6 +188,7 @@ export class Client {
    * // typeof res.body === ResourceType
    *
    * @param reqOptions request options.
+   * @return the response.
    */
   async request<ReqT, ResT>(reqOptions: ClientRequestOptions<ReqT>): Promise<ClientResponse<ResT>> {
     // All requests default to JSON content types.
@@ -246,7 +247,12 @@ export class Client {
     if (!reqOptions.headers) {
       reqOptions.headers = new Headers();
     }
-    const token = await this.getAccessToken();
+    let token: string;
+    if (isLocalInsecureRequest(this.opts.urlPrefix)) {
+      token = "owner";
+    } else {
+      token = await this.getAccessToken();
+    }
     reqOptions.headers.set("Authorization", `Bearer ${token}`);
     return reqOptions;
   }
@@ -408,6 +414,11 @@ export class Client {
   }
 }
 
+function isLocalInsecureRequest(urlPrefix: string): boolean {
+  const u = parse(urlPrefix);
+  return u.protocol === "http:";
+}
+
 function bodyToString(body: unknown): string {
   if (isStream(body)) {
     // Don't attempt to read any stream type, in case the caller needs it.


Test patch:
diff --git a/src/test/apiv2.spec.ts b/src/test/apiv2.spec.ts
index f62d4f80b..3328cf0df 100644
--- a/src/test/apiv2.spec.ts
+++ b/src/test/apiv2.spec.ts
@@ -85,6 +85,21 @@ describe("apiv2", () => {
       expect(nock.isDone()).to.be.true;
     });
 
+    it("should set a bearer token to 'owner' if making an insecure, local request", async () => {
+      nock("http://localhost")
+        .get("/path/to/foo")
+        .matchHeader("Authorization", "Bearer owner")
+        .reply(200, { request: "insecure" });
+
+      const c = new Client({ urlPrefix: "http://localhost" });
+      const r = await c.request({
+        method: "GET",
+        path: "/path/to/foo",
+      });
+      expect(r.body).to.deep.equal({ request: "insecure" });
+      expect(nock.isDone()).to.be.true;
+    });
+
     it("should error with a FirebaseError if JSON is malformed", async () => {
       nock("https://example.com").get("/path/to/foo").reply(200, `{not:"json"}`);