star-chart-search-enhancer/docs/superpowers/plans/2026-04-22-logto-protected-api-mock.md

19 KiB

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 <token> 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:

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:

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:

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:

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:

import { isAuthResponseMessage } from "./auth-messages";

interface FetchResponseLike {
  json(): Promise<unknown>;
  ok: boolean;
  status: number;
}

type FetchLike = (
  input: string,
  init?: RequestInit
) => Promise<FetchResponseLike>;

type SendMessageLike = (message: unknown) => Promise<unknown>;

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<string> {
  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:

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:

test("renders a protected api test button in the dev panel", async () => {
  const { bootPopup } = await import("../src/popup/index");

  document.body.innerHTML = '<div id="app"></div>';
  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 = '<div id="app"></div>';

  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:

<button type="button" data-popup-test-protected-api="button">
  测试受保护接口
</button>
<pre data-popup-protected-api-result="output"></pre>

Add a small helper for setting the output:

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:

interface BootPopupOptions {
  config?: Partial<AuthConfig>;
  document?: Document;
  fetchProtectedApi?: () => Promise<unknown>;
  sendMessage?: (message: unknown) => Promise<unknown>;
}
  • 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:

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:

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

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

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

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:

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:

{
  "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:

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:

## 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:

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:

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:

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

Final Verification

  • Run the full test suite:
npm test

Expected: PASS

  • Run the production build:
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