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.tsscripts/mock-protected-api.mjstests/protected-api-client.test.tstests/mock-protected-api.test.ts
Existing files to modify
src/popup/index.tssrc/popup/view.tstests/popup-entry.test.tstests/background-index.test.tspackage.jsonREADME.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.tsonly 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
fetchProtectedApidependency - default it to a
createProtectedApiClient(...)instance usingsendMessage - 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