All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 47s
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>
165 lines
4.7 KiB
TypeScript
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;
|
|
}
|