# Logto Protected API Mock Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a reusable protected-API client that pulls a Logto access token from the extension background, sends `Authorization: Bearer ` to a local mock API, and exposes a dev-only popup flow to verify the end-to-end request path before a real backend exists. **Architecture:** Keep Logto token ownership in the background and add a small reusable client in `src/shared` so both popup and future content code can call protected APIs the same way. Add a tiny local Node mock server that only checks for a Bearer header and returns fixed JSON. Use popup dev UI as the manual test entrypoint because it already has an authenticated extension context and avoids coupling the mock flow to the Xingtu page DOM. **Tech Stack:** Chrome MV3, TypeScript, Vitest, Node HTTP server, tsup, `@logto/chrome-extension` --- ## File Map ### New files - `src/shared/protected-api-client.ts` - `scripts/mock-protected-api.mjs` - `tests/protected-api-client.test.ts` - `tests/mock-protected-api.test.ts` ### Existing files to modify - `src/popup/index.ts` - `src/popup/view.ts` - `tests/popup-entry.test.ts` - `tests/background-index.test.ts` - `package.json` - `README.md` ### Responsibilities - `src/shared/protected-api-client.ts`: reusable client that asks background for a token, injects the Bearer header, and normalizes success / unauthorized / missing-token errors. - `scripts/mock-protected-api.mjs`: minimal local server with one protected endpoint and a CLI start mode. - `src/popup/view.ts`: add a dev-only “测试受保护接口” button and a response/error output area. - `src/popup/index.ts`: wire the dev button to the protected API client and refresh the popup state after calls. - `tests/protected-api-client.test.ts`: lock down token injection and error behavior. - `tests/mock-protected-api.test.ts`: verify the mock server authorizes Bearer requests and rejects missing headers. - `tests/popup-entry.test.ts`: verify popup dev action triggers the protected client and renders results. ## Task 1: Lock Down The Existing Token Message Contract **Files:** - Modify: `tests/background-index.test.ts` - Modify: `src/background/index.ts` only if the test exposes a gap - [ ] **Step 1: Write the failing background token response test** Add a case to `tests/background-index.test.ts`: ```ts test("responds to auth:get-access-token with the current token", async () => { const listeners: Array< (message: unknown, sender: unknown, sendResponse: (response: unknown) => void) => boolean | void > = []; const sendResponse = vi.fn(); registerBackgroundMessageHandler( { runtime: { onMessage: { addListener(listener) { listeners.push(listener); } } } }, { authController: { getAccessToken: vi.fn(async () => "test-access-token"), getAuthState: vi.fn(), signIn: vi.fn(), signOut: vi.fn() } } ); const result = listeners[0]({ type: "auth:get-access-token" }, {}, sendResponse); expect(result).toBe(true); await new Promise((resolve) => setTimeout(resolve, 0)); expect(sendResponse).toHaveBeenCalledWith({ ok: true, type: "auth:token", value: { accessToken: "test-access-token" } }); }); ``` - [ ] **Step 2: Run the focused background test** Run: `npm test -- tests/background-index.test.ts` Expected: - If it fails because token handling is broken or missing, continue to Step 3. - If it passes immediately, keep the test as a regression and skip production changes for this task. - [ ] **Step 3: Make the minimal background fix if needed** Ensure `src/background/index.ts` handles: ```ts if (message.type === "auth:get-access-token") { return { ok: true, type: "auth:token", value: { accessToken: await authController.getAccessToken() } }; } ``` - [ ] **Step 4: Re-run the background test and confirm green** Run: `npm test -- tests/background-index.test.ts` Expected: PASS - [ ] **Step 5: Commit the token-contract slice** Run: ```bash git add tests/background-index.test.ts src/background/index.ts git commit -m "test: cover auth access token responses" ``` ## Task 2: Build The Reusable Protected API Client **Files:** - Create: `src/shared/protected-api-client.ts` - Create: `tests/protected-api-client.test.ts` - [ ] **Step 1: Write the failing protected client tests** Create `tests/protected-api-client.test.ts` with cases for: ```ts import { describe, expect, test, vi } from "vitest"; import { createProtectedApiClient } from "../src/shared/protected-api-client"; describe("protected-api-client", () => { test("requests a token before calling the protected endpoint", async () => { const sendMessage = vi.fn(async () => ({ ok: true, type: "auth:token", value: { accessToken: "abc123" } })); const fetchImpl = vi.fn(async () => ({ ok: true, status: 200, json: async () => ({ ok: true }) })); const client = createProtectedApiClient({ baseUrl: "http://127.0.0.1:4319", fetchImpl, sendMessage }); await client.loadProtectedMockData(); expect(sendMessage).toHaveBeenCalledWith({ type: "auth:get-access-token" }); expect(fetchImpl).toHaveBeenCalledWith( "http://127.0.0.1:4319/api/mock/protected", expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer abc123" }), method: "GET" }) ); }); test("throws before fetch when the token is unavailable", async () => { const sendMessage = vi.fn(async () => ({ ok: false, type: "auth:error", error: "token missing" })); const fetchImpl = vi.fn(); const client = createProtectedApiClient({ baseUrl: "http://127.0.0.1:4319", fetchImpl, sendMessage }); await expect(client.loadProtectedMockData()).rejects.toThrow(/token/i); expect(fetchImpl).not.toHaveBeenCalled(); }); test("throws an authorization error on 401", async () => { const client = createProtectedApiClient({ baseUrl: "http://127.0.0.1:4319", fetchImpl: vi.fn(async () => ({ ok: false, status: 401, json: async () => ({ ok: false, error: "unauthorized" }) })), sendMessage: vi.fn(async () => ({ ok: true, type: "auth:token", value: { accessToken: "abc123" } })) }); await expect(client.loadProtectedMockData()).rejects.toThrow(/unauthorized/i); }); }); ``` - [ ] **Step 2: Run the protected client test to verify red** Run: `npm test -- tests/protected-api-client.test.ts` Expected: FAIL because `createProtectedApiClient` does not exist yet - [ ] **Step 3: Implement the minimal protected client** Create `src/shared/protected-api-client.ts` with: ```ts import { isAuthResponseMessage } from "./auth-messages"; interface FetchResponseLike { json(): Promise; ok: boolean; status: number; } type FetchLike = ( input: string, init?: RequestInit ) => Promise; type SendMessageLike = (message: unknown) => Promise; export function createProtectedApiClient(options: { baseUrl: string; fetchImpl?: FetchLike; sendMessage: SendMessageLike; }) { const fetchImpl = options.fetchImpl ?? fetch; return { async loadProtectedMockData() { const token = await readAccessToken(options.sendMessage); const response = await fetchImpl( new URL("/api/mock/protected", options.baseUrl).toString(), { headers: { Authorization: `Bearer ${token}` }, method: "GET" } ); if (response.status === 401 || response.status === 403) { throw new Error("protected api unauthorized"); } if (!response.ok) { throw new Error(`protected api request failed: ${response.status}`); } return response.json(); } }; } async function readAccessToken(sendMessage: SendMessageLike): Promise { const response = await sendMessage({ type: "auth:get-access-token" }); if ( !isAuthResponseMessage(response) || !response.ok || response.type !== "auth:token" || !response.value.accessToken.trim() ) { throw new Error("protected api token unavailable"); } return response.value.accessToken; } ``` - [ ] **Step 4: Run the protected client test to verify green** Run: `npm test -- tests/protected-api-client.test.ts` Expected: PASS - [ ] **Step 5: Refactor only if tests stay green** Optional small cleanup only: - extract URL builder helper - extract `buildAuthorizationHeaders(token)` helper if repeated Run: `npm test -- tests/protected-api-client.test.ts` Expected: PASS - [ ] **Step 6: Commit the client slice** Run: ```bash git add src/shared/protected-api-client.ts tests/protected-api-client.test.ts git commit -m "feat: add protected api client" ``` ## Task 3: Add A Popup Dev Trigger For Manual Verification **Files:** - Modify: `src/popup/index.ts` - Modify: `src/popup/view.ts` - Modify: `tests/popup-entry.test.ts` - [ ] **Step 1: Write the failing popup dev-flow tests** Add cases to `tests/popup-entry.test.ts` for: ```ts test("renders a protected api test button in the dev panel", async () => { const { bootPopup } = await import("../src/popup/index"); document.body.innerHTML = '
'; await bootPopup({ config: { enableDevAuthPanel: true }, document, sendMessage: vi.fn(async () => ({ ok: true, type: "auth:state", value: { isAuthenticated: true, tokenAvailable: true } })) }); expect(document.querySelector('[data-popup-test-protected-api="button"]')).not.toBeNull(); }); test("clicking the dev button runs the protected api client and prints the result", async () => { const sendMessage = vi.fn(async (message: { type: string }) => { if (message.type === "auth:get-state") { return { ok: true, type: "auth:state", value: { isAuthenticated: true, tokenAvailable: true } }; } if (message.type === "auth:get-access-token") { return { ok: true, type: "auth:token", value: { accessToken: "abc123" } }; } return { ok: true, type: "auth:ack" }; }); const fetchImpl = vi.fn(async () => ({ ok: true, status: 200, json: async () => ({ ok: true, message: "authorized" }) })); const { bootPopup } = await import("../src/popup/index"); document.body.innerHTML = '
'; await bootPopup({ config: { enableDevAuthPanel: true }, document, fetchProtectedApi: () => createProtectedApiClient({ baseUrl: "http://127.0.0.1:4319", fetchImpl, sendMessage }).loadProtectedMockData(), sendMessage }); document .querySelector('[data-popup-test-protected-api="button"]') ?.dispatchEvent(new MouseEvent("click")); await new Promise((resolve) => setTimeout(resolve, 0)); expect(document.body.textContent).toContain("authorized"); }); ``` - [ ] **Step 2: Run the popup tests to verify red** Run: `npm test -- tests/popup-entry.test.ts` Expected: FAIL because the popup dev test entrypoint does not exist yet - [ ] **Step 3: Add the minimal popup UI** Update `src/popup/view.ts` so `renderDevPanel()` also renders: ```html

```

Add a small helper for setting the output:

```ts
export function setProtectedApiResult(root: HTMLElement, value: string): void {
  root
    .querySelector('[data-popup-protected-api-result="output"]')
    ?.replaceChildren(value);
}
```

- [ ] **Step 4: Wire the popup button to the protected client**

Update `src/popup/index.ts`:
- inject an optional `fetchProtectedApi` dependency
- default it to a `createProtectedApiClient(...)` instance using `sendMessage`
- attach a click handler only when the dev panel is enabled and the user is logged in
- write either the JSON response or the thrown error message into the output area

Suggested shape:

```ts
interface BootPopupOptions {
  config?: Partial;
  document?: Document;
  fetchProtectedApi?: () => Promise;
  sendMessage?: (message: unknown) => Promise;
}
```

- [ ] **Step 5: Run the popup tests to verify green**

Run: `npm test -- tests/popup-entry.test.ts`

Expected: PASS

- [ ] **Step 6: Commit the popup dev slice**

Run:

```bash
git add src/popup/index.ts src/popup/view.ts tests/popup-entry.test.ts
git commit -m "feat: add popup protected api dev test"
```

## Task 4: Add The Local Mock Protected API Server

**Files:**
- Create: `scripts/mock-protected-api.mjs`
- Create: `tests/mock-protected-api.test.ts`
- Modify: `package.json`

- [ ] **Step 1: Write the failing mock server tests**

Create `tests/mock-protected-api.test.ts` with:

```ts
import { afterEach, describe, expect, test } from "vitest";

import { createMockProtectedApiServer } from "../scripts/mock-protected-api.mjs";

const servers: Array<{ close: () => Promise }> = [];

afterEach(async () => {
  while (servers.length > 0) {
    await servers.pop()?.close();
  }
});

describe("mock-protected-api", () => {
  test("returns mock data when a Bearer token is present", async () => {
    const server = createMockProtectedApiServer({ port: 0 });
    await server.start();
    servers.push(server);

    const response = await fetch(`${server.baseUrl}/api/mock/protected`, {
      headers: {
        Authorization: "Bearer abc123"
      }
    });

    expect(response.status).toBe(200);
    await expect(response.json()).resolves.toEqual(
      expect.objectContaining({
        ok: true,
        source: "mock-protected-api"
      })
    );
  });

  test("returns 401 when the Authorization header is missing", async () => {
    const server = createMockProtectedApiServer({ port: 0 });
    await server.start();
    servers.push(server);

    const response = await fetch(`${server.baseUrl}/api/mock/protected`);

    expect(response.status).toBe(401);
    await expect(response.json()).resolves.toEqual({
      ok: false,
      error: "unauthorized"
    });
  });
});
```

- [ ] **Step 2: Run the mock server tests to verify red**

Run: `npm test -- tests/mock-protected-api.test.ts`

Expected: FAIL because the mock server module does not exist yet

- [ ] **Step 3: Implement the minimal mock server**

Create `scripts/mock-protected-api.mjs` with:

```js
import http from "node:http";

export function createMockProtectedApiServer({ port = 4319 } = {}) {
  let server;

  return {
    get baseUrl() {
      const address = server?.address();
      const resolvedPort =
        typeof address === "object" && address ? address.port : port;
      return `http://127.0.0.1:${resolvedPort}`;
    },
    async start() {
      server = http.createServer((request, response) => {
        if (request.url !== "/api/mock/protected") {
          response.writeHead(404, { "content-type": "application/json" });
          response.end(JSON.stringify({ ok: false, error: "not-found" }));
          return;
        }

        const authHeader = request.headers.authorization ?? "";
        const isBearer =
          typeof authHeader === "string" &&
          authHeader.startsWith("Bearer ") &&
          authHeader.length > "Bearer ".length;

        if (!isBearer) {
          response.writeHead(401, { "content-type": "application/json" });
          response.end(JSON.stringify({ ok: false, error: "unauthorized" }));
          return;
        }

        response.writeHead(200, { "content-type": "application/json" });
        response.end(
          JSON.stringify({
            ok: true,
            source: "mock-protected-api",
            message: "authorized",
            receivedAuthHeader: authHeader
          })
        );
      });

      await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));
    },
    async close() {
      if (!server) {
        return;
      }
      await new Promise((resolve, reject) =>
        server.close((error) => (error ? reject(error) : resolve()))
      );
    }
  };
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const server = createMockProtectedApiServer();
  await server.start();
  console.log(`mock protected api listening on ${server.baseUrl}`);
}
```

- [ ] **Step 4: Add a package script to start the mock API**

Update `package.json`:

```json
{
  "scripts": {
    "mock:protected-api": "node scripts/mock-protected-api.mjs"
  }
}
```

- [ ] **Step 5: Run the mock server tests to verify green**

Run: `npm test -- tests/mock-protected-api.test.ts`

Expected: PASS

- [ ] **Step 6: Commit the mock server slice**

Run:

```bash
git add scripts/mock-protected-api.mjs tests/mock-protected-api.test.ts package.json
git commit -m "feat: add mock protected api server"
```

## Task 5: Document And Verify The End-To-End Local Flow

**Files:**
- Modify: `README.md`

- [ ] **Step 1: Write the failing documentation expectation**

Before editing docs, list the missing verification steps in a scratch note:
- how to enable the dev panel
- how to start the mock server
- how to click the popup test button
- what success and failure look like

Expected: current `README.md` does not explain this flow yet

- [ ] **Step 2: Add the manual verification section**

Update `README.md` with a short section:

```md
## Protected API Mock Test

1. Set `enableDevAuthPanel` to `true` in `src/shared/auth-config.ts`
2. Run `npm run mock:protected-api`
3. Run `npm run build`
4. Reload the unpacked extension
5. Open the popup and log in
6. Click `测试受保护接口`
7. Confirm the popup shows a JSON result containing `"source": "mock-protected-api"`
```

- [ ] **Step 3: Run the relevant automated tests together**

Run:

```bash
npm test -- tests/background-index.test.ts tests/protected-api-client.test.ts tests/popup-entry.test.ts tests/mock-protected-api.test.ts
```

Expected: PASS

- [ ] **Step 4: Perform the manual smoke test**

Run in separate terminals:

```bash
npm run mock:protected-api
npm run build
```

Expected:
- terminal 1 prints `mock protected api listening on http://127.0.0.1:4319`
- extension loads from `dist/`
- popup dev panel can trigger the protected endpoint
- success output includes `"message": "authorized"`

- [ ] **Step 5: Commit the docs and verification slice**

Run:

```bash
git add README.md
git commit -m "docs: add protected api mock verification steps"
```

## Final Verification

- [ ] Run the full test suite:

```bash
npm test
```

Expected: PASS

- [ ] Run the production build:

```bash
npm run build
```

Expected: build completes and writes updated assets to `dist/`

- [ ] Confirm the only functional additions are:
- background token response covered by tests
- reusable protected API client
- popup dev test button and result output
- local mock protected API server
- updated verification docs