anydrop/web/src/lib/api.ts
ordinarthur 2452f2642a
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 47s
feat: inbox of received transfers in /settings
Auto-claim on receive so /settings shows transfers others sent you.
Filename decryption stays client-side using the key stored in localStorage
when the share link is opened.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 12:47:10 +02:00

165 lines
4.7 KiB
TypeScript

export interface ApiUser {
id: string;
email: string;
plan: string;
createdAt: string;
}
export interface ApiDevice {
id: string;
deviceId: string;
name: string;
type: string;
avatar: string | null;
linkedAt: string;
lastSeenAt: string;
}
export interface MeResponse {
user: ApiUser;
devices: ApiDevice[];
}
async function call(path: string, init?: RequestInit): Promise<Response> {
return fetch(path, {
credentials: "include",
...init,
headers: {
"Content-Type": "application/json",
...(init?.headers ?? {}),
},
});
}
export async function requestMagicLink(email: string): Promise<void> {
await call("/api/auth/request-link", {
method: "POST",
body: JSON.stringify({ email }),
});
}
export async function fetchMe(): Promise<MeResponse | null> {
const res = await call("/api/me");
if (res.status === 401) return null;
if (!res.ok) throw new Error(`fetchMe failed: ${res.status}`);
return (await res.json()) as MeResponse;
}
export async function registerDevice(input: {
deviceId: string;
name: string;
type: string;
avatar: string | null;
}): Promise<ApiDevice> {
const res = await call("/api/devices", {
method: "POST",
body: JSON.stringify(input),
});
if (!res.ok) throw new Error(`registerDevice failed: ${res.status}`);
return (await res.json()) as ApiDevice;
}
export async function unlinkDevice(id: string): Promise<void> {
const res = await call(`/api/devices/${encodeURIComponent(id)}`, { method: "DELETE" });
if (!res.ok && res.status !== 204) throw new Error(`unlinkDevice failed: ${res.status}`);
}
export async function signOut(): Promise<void> {
await call("/api/auth/logout", { method: "POST" });
}
export interface CreateTransferResponse {
transferId: string;
uploadUrl: string;
expiresAt: string;
}
export interface TransferHead {
transferId: string;
encryptedMetadata: string;
sizeBytes: number;
maxDownloads: number;
downloadCount: number;
expiresAt: string;
requiresPassword: boolean;
}
export interface InboxTransfer {
id: string;
sizeBytes: number;
encryptedMetadata: string;
createdAt: string;
expiresAt: string;
maxDownloads: number;
downloadCount: number;
firstDownloadAt: string | null;
senderUserId: string | null;
recipientUserId: string | null;
direction: "sent" | "received";
}
export async function createTransfer(input: {
sizeBytes: number;
encryptedMetadata: string;
recipientEmail?: string;
maxDownloads?: number;
expiresInDays?: number;
deviceId?: string;
password?: string;
}): Promise<CreateTransferResponse> {
const res = await call("/api/transfers", {
method: "POST",
body: JSON.stringify(input),
headers: input.deviceId ? { "X-Device-Id": input.deviceId } : {},
});
if (!res.ok) throw new Error(`createTransfer failed: ${res.status}`);
return (await res.json()) as CreateTransferResponse;
}
export async function getTransferHead(id: string): Promise<TransferHead> {
const res = await call(`/api/transfers/${encodeURIComponent(id)}`);
if (res.status === 404) throw new Error("transfer_not_found");
if (res.status === 410) {
const body = await res.json().catch(() => ({}));
throw new Error((body as { error?: string }).error ?? "transfer_gone");
}
if (!res.ok) throw new Error(`getTransferHead failed: ${res.status}`);
return (await res.json()) as TransferHead;
}
export async function consumeTransfer(
id: string,
password?: string,
): Promise<{ downloadUrl: string }> {
const res = await call(`/api/transfers/${encodeURIComponent(id)}/consume`, {
method: "POST",
body: JSON.stringify(password ? { password } : {}),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error((body as { error?: string }).error ?? `consume failed: ${res.status}`);
}
return (await res.json()) as { downloadUrl: string };
}
export async function listInboxTransfers(): Promise<InboxTransfer[]> {
const res = await call("/api/transfers");
if (res.status === 401) return [];
if (!res.ok) throw new Error(`listTransfers failed: ${res.status}`);
const body = (await res.json()) as { transfers: InboxTransfer[] };
return body.transfers;
}
export async function deleteTransfer(id: string): Promise<void> {
const res = await call(`/api/transfers/${encodeURIComponent(id)}`, { method: "DELETE" });
if (!res.ok && res.status !== 204) throw new Error(`deleteTransfer failed: ${res.status}`);
}
export async function claimTransfer(id: string): Promise<boolean> {
const res = await call(`/api/transfers/${encodeURIComponent(id)}/claim`, { method: "POST" });
if (res.status === 401) return false;
if (!res.ok) return false;
const body = (await res.json()) as { claimed: boolean };
return body.claimed;
}