fix(android): allow network requests and relax WebView CSP

Enable cleartext HTTP in release APK, add network security config, and set connect-src CSP. Add CORS on Worker API for cross-origin fetches from packaged apps.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-06 13:53:05 +08:00
parent 58d5dc8ae8
commit f8b05c69d6
7 changed files with 191 additions and 16 deletions

View File

@@ -1,13 +1,20 @@
import type { Env } from "./env";
import { handleBuildsRequest } from "./routes/builds";
import { jsonResponse } from "./lib/response";
import {
corsPreflightResponse,
jsonResponseWithCors,
} from "./lib/response";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (request.method === "OPTIONS" && url.pathname.startsWith("/api/")) {
return corsPreflightResponse(request);
}
if (url.pathname === "/api/health") {
return jsonResponse({ ok: true });
return jsonResponseWithCors(request, { ok: true });
}
if (url.pathname.startsWith("/api/builds")) {

View File

@@ -1,3 +1,16 @@
export function corsHeaders(request: Request): Record<string, string> {
const origin = request.headers.get("Origin");
if (!origin) return {};
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
Vary: "Origin",
};
}
export function jsonResponse(
data: unknown,
status = 200,
@@ -12,6 +25,25 @@ export function jsonResponse(
});
}
export function jsonResponseWithCors(
request: Request,
data: unknown,
status = 200,
headers?: Record<string, string>,
): Response {
return jsonResponse(data, status, {
...corsHeaders(request),
...headers,
});
}
export function corsPreflightResponse(request: Request): Response {
return new Response(null, {
status: 204,
headers: corsHeaders(request),
});
}
export function errorResponse(message: string, status: number): Response {
return jsonResponse({ error: message }, status);
}

View File

@@ -8,7 +8,19 @@ import {
updateBuild,
type BuildRecord,
} from "../db/builds";
import { errorResponse, jsonResponse } from "../lib/response";
import { jsonResponseWithCors } from "../lib/response";
function apiJson(
request: Request,
data: unknown,
status = 200,
): Response {
return jsonResponseWithCors(request, data, status);
}
function apiError(request: Request, message: string, status: number): Response {
return jsonResponseWithCors(request, { error: message }, status);
}
import {
getActionsRunUrl,
getReleaseAssets,
@@ -45,7 +57,7 @@ export async function handleBuildsRequest(
if (path === "/api/builds" && request.method === "GET") {
const builds = (await listBuilds(env)).map(toPublicBuild);
return jsonResponse({ builds });
return apiJson(request, { builds });
}
const match = path.match(/^\/api\/builds\/([^/]+)$/);
@@ -55,24 +67,24 @@ export async function handleBuildsRequest(
if (request.method === "GET") {
const record = await getBuild(env, id);
if (!record) {
return errorResponse("Build not found", 404);
return apiError(request, "Build not found", 404);
}
const refreshed = await refreshBuildStatus(env, record);
return jsonResponse({
return apiJson(request, {
...toPublicBuild(refreshed),
actionsUrl: getActionsRunUrl(env, refreshed.workflow_run_id),
});
}
return errorResponse("Method not allowed", 405);
return apiError(request, "Method not allowed", 405);
}
if (path === "/api/builds" && request.method === "POST") {
return createBuild(request, env);
}
return errorResponse("Not found", 404);
return apiError(request, "Not found", 404);
}
async function createBuild(request: Request, env: Env): Promise<Response> {
@@ -91,11 +103,11 @@ async function createBuild(request: Request, env: Env): Promise<Response> {
const file = formData.get("file");
if (!(file instanceof File)) {
return errorResponse("file is required", 400);
return apiError(request, "file is required", 400);
}
if (!file.name.toLowerCase().endsWith(".zip")) {
return errorResponse("Only .zip files are supported", 400);
return apiError(request, "Only .zip files are supported", 400);
}
const buffer = new Uint8Array(await file.arrayBuffer());
@@ -143,7 +155,8 @@ async function createBuild(request: Request, env: Env): Promise<Response> {
status: "in_progress",
});
return jsonResponse(
return apiJson(
request,
{
id: jobId,
status: "in_progress",
@@ -157,11 +170,12 @@ async function createBuild(request: Request, env: Env): Promise<Response> {
error instanceof VersionValidationError ||
error instanceof IconValidationError
) {
return errorResponse(error.message, 400);
return apiError(request, error.message, 400);
}
console.error(error);
return errorResponse(
return apiError(
request,
error instanceof Error ? error.message : "Failed to create build",
500,
);