Files
wisp/client/src/api.ts
explewd 266f9708e3
All checks were successful
Docker / build-and-push (push) Successful in 3m9s
Implement scoped upload invites
Adds a second, narrower credential type alongside UPLOAD_PASSWORD: an
admin (ADMIN_PASSWORD-gated, /admin) can mint time-boxed, use-limited,
independently revocable invite links (?invite=<token>) that skip the
password screen for one-off sharing without handing out the master
password. Invite consumption is an atomic check-and-increment to avoid
a race on single-use invites; admin surface 503s (not boot failure)
when ADMIN_PASSWORD is unset.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:29:46 +02:00

53 lines
1.7 KiB
TypeScript

export type Credential = { kind: 'password' | 'hash' | 'invite'; value: string }
function credentialHeaders(cred: Credential): Record<string, string> {
if (cred.kind === 'password') return { 'X-Upload-Password': cred.value }
if (cred.kind === 'hash') return { 'X-Upload-Password-Hash': cred.value }
return { 'X-Upload-Invite': cred.value }
}
export async function checkCredential(cred: Credential): Promise<boolean> {
const res = await fetch('/api/auth/check', {
method: 'POST',
headers: credentialHeaders(cred),
})
const body = (await res.json()) as { ok: boolean }
return body.ok
}
export async function uploadDrop(
ciphertext: Uint8Array,
cred: Credential,
): Promise<{ id: string; ttl: number }> {
const res = await fetch('/api/drops', {
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
...credentialHeaders(cred),
},
body: ciphertext.slice().buffer,
})
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as { error?: string }
throw new Error(body.error ?? `upload failed: ${res.status}`)
}
return res.json()
}
export async function downloadDrop(
id: string,
): Promise<{ ciphertext: Uint8Array; confirmToken: string }> {
const res = await fetch(`/api/drops/${id}`)
if (!res.ok) throw new Error(`download failed: ${res.status}`)
const confirmToken = res.headers.get('X-Confirm-Token') ?? ''
const ciphertext = new Uint8Array(await res.arrayBuffer())
return { ciphertext, confirmToken }
}
export async function confirmDrop(id: string, confirmToken: string): Promise<void> {
await fetch(`/api/drops/${id}/confirm`, {
method: 'POST',
headers: { Authorization: `Bearer ${confirmToken}` },
})
}