Files
ELY-Browser/cloudflare/src/sync_routes.ts
T

257 lines
7.3 KiB
TypeScript

import type { Env } from "./bindings.js";
import { withApprovedDeviceApiControls } from "./api_controls.js";
import { DestructiveActionGateError } from "./destructive_action_gate.js";
import { jsonResponse } from "./responses.js";
import {
RecentDeviceActionPermissionError,
RecentDeviceActionPersistenceError,
} from "./recent_device_action_proof.js";
import {
SyncResetPersistenceError,
SyncResetRequestError,
syncResetDocument,
} from "./sync_reset.js";
import {
SyncSnapshotConflictError,
SyncSnapshotNotFoundError,
SyncSnapshotPersistenceError,
SyncSnapshotRequestError,
syncSnapshotDownloadDocument,
syncSnapshotUploadDocument,
} from "./sync_snapshot.js";
import { SyncStatusSchemaError, syncStatusDocument } from "./sync_status.js";
import {
SyncVaultConflictError,
SyncVaultNotFoundError,
SyncVaultPermissionError,
SyncVaultPersistenceError,
SyncVaultRequestError,
syncVaultBootstrapDocument,
syncVaultCurrentDeviceDocument,
} from "./sync_vault.js";
export async function handleSyncRoute(
request: Request,
env: Env,
url: URL,
): Promise<Response | null> {
if (url.pathname === "/api/sync/pull") {
return handleRetiredSyncObjectRoute(request, env, "sync.pull", ["GET"]);
}
if (url.pathname === "/api/sync/push") {
return handleRetiredSyncObjectRoute(request, env, "sync.push", ["POST"]);
}
if (url.pathname === "/api/sync/snapshot") {
return handleSyncSnapshot(request, env, url);
}
if (url.pathname === "/api/sync/status") {
return handleSyncStatus(request, env);
}
if (url.pathname === "/api/sync/vault/bootstrap") {
return handleSyncVaultBootstrap(request, env);
}
if (url.pathname === "/api/sync/vault") {
return handleSyncVault(request, env, url);
}
if (url.pathname === "/api/sync/reset") {
return handleSyncReset(request, env);
}
return null;
}
function handleSyncVaultBootstrap(request: Request, env: Env): Promise<Response> {
return withApprovedDeviceApiControls(
request,
env,
"sync.vault.bootstrap",
["POST"],
async (context) => {
try {
return jsonResponse(await syncVaultBootstrapDocument(request, env, context), 201, {
"Cache-Control": "no-store",
});
} catch (error) {
const response = syncVaultErrorResponse(error);
if (response !== null) {
return response;
}
throw error;
}
},
);
}
function handleSyncVault(request: Request, env: Env, url: URL): Promise<Response> {
return withApprovedDeviceApiControls(
request,
env,
"sync.vault",
["GET"],
async (context) => {
try {
return jsonResponse(await syncVaultCurrentDeviceDocument(url, env, context), 200, {
"Cache-Control": "no-store",
});
} catch (error) {
const response = syncVaultErrorResponse(error);
if (response !== null) {
return response;
}
throw error;
}
},
);
}
function syncVaultErrorResponse(error: unknown): Response | null {
if (error instanceof SyncVaultPermissionError) {
return jsonResponse({ error: "sync_vault_forbidden" }, 403, { "Cache-Control": "no-store" });
}
if (error instanceof SyncVaultRequestError) {
return jsonResponse({ error: "invalid_sync_vault" }, 400, { "Cache-Control": "no-store" });
}
if (error instanceof SyncVaultNotFoundError) {
return jsonResponse({ error: "sync_vault_not_found" }, 404, { "Cache-Control": "no-store" });
}
if (error instanceof SyncVaultConflictError) {
return jsonResponse({ error: "sync_vault_conflict" }, 409, { "Cache-Control": "no-store" });
}
if (error instanceof SyncVaultPersistenceError) {
return jsonResponse({ error: "sync_vault_failed" }, 500, { "Cache-Control": "no-store" });
}
return null;
}
function handleRetiredSyncObjectRoute(
request: Request,
env: Env,
route: string,
allowedMethods: readonly string[],
): Promise<Response> {
return withApprovedDeviceApiControls(
request,
env,
route,
allowedMethods,
async () =>
jsonResponse({ error: "sync_object_protocol_retired" }, 410, {
"Cache-Control": "no-store",
}),
);
}
function handleSyncSnapshot(request: Request, env: Env, url: URL): Promise<Response> {
return withApprovedDeviceApiControls(
request,
env,
"sync.snapshot",
["GET", "POST"],
async (context) => {
try {
if (request.method === "POST") {
return jsonResponse(await syncSnapshotUploadDocument(request, env, context), 201, {
"Cache-Control": "no-store",
});
}
return jsonResponse(await syncSnapshotDownloadDocument(url, env, context), 200, {
"Cache-Control": "no-store",
});
} catch (error) {
if (error instanceof SyncSnapshotRequestError) {
return jsonResponse(
{ error: "invalid_sync_snapshot" },
400,
{ "Cache-Control": "no-store" },
);
}
if (error instanceof SyncSnapshotNotFoundError) {
return jsonResponse(
{ error: "sync_snapshot_not_found" },
404,
{ "Cache-Control": "no-store" },
);
}
if (error instanceof SyncSnapshotConflictError) {
return jsonResponse(error.document(), 409, { "Cache-Control": "no-store" });
}
if (error instanceof SyncSnapshotPersistenceError) {
return jsonResponse(
{ error: "sync_snapshot_failed" },
500,
{ "Cache-Control": "no-store" },
);
}
throw error;
}
},
);
}
function handleSyncStatus(request: Request, env: Env): Promise<Response> {
return withApprovedDeviceApiControls(
request,
env,
"sync.status",
["GET"],
async (context) => {
try {
return jsonResponse(await syncStatusDocument(env, context), 200, {
"Cache-Control": "no-store",
});
} catch (error) {
if (error instanceof SyncStatusSchemaError) {
return jsonResponse(
{ error: "sync_status_invalid" },
500,
{ "Cache-Control": "no-store" },
);
}
throw error;
}
},
);
}
function handleSyncReset(request: Request, env: Env): Promise<Response> {
return withApprovedDeviceApiControls(
request,
env,
"sync.reset",
["POST"],
async (context) => {
try {
return jsonResponse(await syncResetDocument(request, env, context), 200, {
"Cache-Control": "no-store",
});
} catch (error) {
if (error instanceof SyncResetRequestError) {
return jsonResponse(
{ error: "invalid_sync_reset" },
400,
{ "Cache-Control": "no-store" },
);
}
if (error instanceof RecentDeviceActionPermissionError) {
return jsonResponse(
{ error: "sync_reset_forbidden" },
403,
{ "Cache-Control": "no-store" },
);
}
if (
error instanceof SyncResetPersistenceError ||
error instanceof RecentDeviceActionPersistenceError ||
error instanceof DestructiveActionGateError
) {
return jsonResponse(
{ error: "sync_reset_failed" },
500,
{ "Cache-Control": "no-store" },
);
}
throw error;
}
},
);
}