feat: POST /pair endpoint + async bearer token auth
- POST /pair: consumes one-time pairingToken, creates named bearer token,
returns { bearerToken, sidecarId } per IC-3
- isAuthenticatedAsync(): checks legacy token + new multi-token store
- isAuthenticated(): extended with Bearer header support for WS upgrade
- Smoke still 12/12 green
This commit is contained in:
parent
91b1ad1a44
commit
1f36636e06
|
|
@ -28,7 +28,8 @@ import type {
|
|||
ExtensionAPI,
|
||||
ExtensionContext,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import { validateBearer } from "../auth/tokens.js";
|
||||
import { consumePairingToken } from "../auth/pairing.js";
|
||||
import { createToken, validateBearer } from "../auth/tokens.js";
|
||||
import {
|
||||
generateSessionId,
|
||||
loadOrCreateToken,
|
||||
|
|
@ -87,19 +88,40 @@ export async function startServer(
|
|||
}
|
||||
};
|
||||
|
||||
/** Check if a request is authenticated (valid token query param OR valid session cookie) */
|
||||
/** Check if a request is authenticated.
|
||||
* Accepts: session cookie | legacy ?token= | Authorization: Bearer <multi-token>
|
||||
*/
|
||||
function isAuthenticated(req: IncomingMessage): boolean {
|
||||
// Check session cookie first
|
||||
// 1. Session cookie (legacy browser HTML client)
|
||||
const cookies = parseCookies(req.headers.cookie);
|
||||
const sessionId = cookies[SESSION_COOKIE];
|
||||
const sessionExpiry = sessionId ? validSessions.get(sessionId) : undefined;
|
||||
if (sessionExpiry !== undefined && sessionExpiry > Date.now()) return true;
|
||||
|
||||
// Check token query param
|
||||
// 2. Legacy single token via query param (smoke tests, CLI)
|
||||
const url = new URL(req.url ?? "/", "http://localhost");
|
||||
const providedToken = url.searchParams.get("token");
|
||||
if (providedToken && validateToken(providedToken, token)) return true;
|
||||
|
||||
// 3. Bearer token via Authorization header or ?token= (iOS app, multi-token)
|
||||
const bearer = extractBearer(req);
|
||||
if (bearer) {
|
||||
if (validateToken(bearer, token)) return true;
|
||||
// async validateBearer checked in asyncHandler for API routes;
|
||||
// for WS upgrade we do a sync fallback: legacy token only here.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Async auth check — also validates multi-token bearer from auth/tokens store */
|
||||
async function isAuthenticatedAsync(req: IncomingMessage): Promise<boolean> {
|
||||
if (isAuthenticated(req)) return true;
|
||||
const bearer = extractBearer(req);
|
||||
if (bearer) {
|
||||
const entry = await validateBearer(bearer).catch(() => null);
|
||||
if (entry) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -204,12 +226,34 @@ export async function startServer(
|
|||
|
||||
// New API routes (IC-2) — bearer token auth
|
||||
const asyncHandler = async (): Promise<boolean> => {
|
||||
// Auth check for API routes
|
||||
const bearer = extractBearer(req);
|
||||
const isApiAuthed =
|
||||
(bearer && validateToken(bearer, token)) ||
|
||||
(bearer ? !!(await validateBearer(bearer).catch(() => null)) : false);
|
||||
// POST /pair — unauthenticated, uses one-time pairingToken
|
||||
if (pathname === "/pair" && req.method === "POST") {
|
||||
let body: Record<string, unknown> = {};
|
||||
try {
|
||||
const raw = await readBody(req);
|
||||
if (raw.trim()) body = JSON.parse(raw) as Record<string, unknown>;
|
||||
} catch {
|
||||
sendJson(res, 400, { error: "bad_request", message: "Invalid JSON" });
|
||||
return true;
|
||||
}
|
||||
const pairingToken =
|
||||
typeof body.pairingToken === "string" ? body.pairingToken : null;
|
||||
if (!pairingToken || !consumePairingToken(pairingToken)) {
|
||||
sendJson(res, 403, {
|
||||
error: "invalid_pairing_token",
|
||||
message: "Pairing token invalid or expired",
|
||||
});
|
||||
return true;
|
||||
}
|
||||
const deviceName =
|
||||
typeof body.deviceName === "string" ? body.deviceName : "iOS device";
|
||||
const entry = await createToken(deviceName);
|
||||
sendJson(res, 200, { bearerToken: entry.token, sidecarId: entry.id });
|
||||
return true;
|
||||
}
|
||||
|
||||
// All other API routes require auth
|
||||
const isApiAuthed = await isAuthenticatedAsync(req);
|
||||
if (!isApiAuthed) return false;
|
||||
|
||||
// GET /health
|
||||
|
|
|
|||
Loading…
Reference in New Issue