diff --git a/backend/static/api/warpbox.ps1 b/backend/static/api/warpbox.ps1 new file mode 100644 index 0000000..4cec7a3 --- /dev/null +++ b/backend/static/api/warpbox.ps1 @@ -0,0 +1,81 @@ +#requires -version 5 +<# +.SYNOPSIS + warpbox: command line uploader for Warpbox +.DESCRIPTION + Set the server once, then upload anything: + setx WARPBOX_HOST "https://your.warpbox.host" + warpbox .\report.pdf + + Install (PowerShell): + iwr "$env:WARPBOX_HOST/static/api/warpbox.ps1" -OutFile $HOME\warpbox.ps1 + # add a function to your $PROFILE: function warpbox { & "$HOME\warpbox.ps1" @args } + + Auth: set the token once so it never lands in your command history. + setx WARPBOX_TOKEN "wbx_your_token" + Create a token under Account, Access tokens. + +.EXAMPLE + .\warpbox.ps1 .\report.pdf +.EXAMPLE + .\warpbox.ps1 -Password 123 -Expiry 2d .\photo.png .\clip.mp4 +#> +[CmdletBinding()] +param( + [Alias('p')][string]$Password, + [Alias('e')][string]$Expiry, + [Alias('n')][int]$MaxDownloads, + [Alias('o')][switch]$Obfuscate, + [string]$Server = $env:WARPBOX_HOST, + [string]$Auth = $env:WARPBOX_TOKEN, + [string]$AuthFile, + [switch]$Json, + [switch]$Help, + [Parameter(ValueFromRemainingArguments = $true)][string[]]$Files +) + +if ($Help -or -not $Files) { + Write-Host 'warpbox: upload files to Warpbox' + Write-Host 'USAGE: warpbox.ps1 [-Password pw] [-Expiry 2d] [-MaxDownloads n] [-Obfuscate] [-Json] [file ...]' + Write-Host 'SERVER: set WARPBOX_HOST in your environment (setx WARPBOX_HOST "https://your.host")' + Write-Host 'AUTH: set WARPBOX_TOKEN in your environment (setx WARPBOX_TOKEN "wbx_...")' + if (-not $Files -and -not $Help) { exit 2 } else { exit 0 } +} + +if (-not $Server) { + Write-Error 'warpbox: no server set. Use -Server or set WARPBOX_HOST' + exit 2 +} +if ($AuthFile) { $Auth = (Get-Content -Raw $AuthFile).Trim() } + +function ConvertTo-Minutes($v) { + if ($v -match '^(\d+)([mhdw]?)$') { + $n = [int]$Matches[1] + switch ($Matches[2]) { + 'h' { return $n * 60 } + 'd' { return $n * 1440 } + 'w' { return $n * 10080 } + default { return $n } + } + } + return $v +} + +# Expand wildcards (PowerShell does not expand them in arguments). +$expanded = @() +foreach ($f in $Files) { + $hits = Get-ChildItem -Path $f -File -ErrorAction SilentlyContinue + if ($hits) { $expanded += $hits.FullName } else { $expanded += $f } +} + +$curlArgs = @('-fS') +foreach ($f in $expanded) { $curlArgs += @('-F', "file=@$f") } +if ($Password) { $curlArgs += @('-F', "password=$Password") } +if ($Expiry) { $curlArgs += @('-F', "expires_minutes=$(ConvertTo-Minutes $Expiry)") } +if ($MaxDownloads) { $curlArgs += @('-F', "max_downloads=$MaxDownloads") } +if ($Obfuscate) { $curlArgs += @('-F', 'obfuscate_metadata=on') } +if ($Auth) { $curlArgs += @('-H', "Authorization: Bearer $Auth") } +if ($Json) { $curlArgs += @('-H', 'Accept: application/json') } +$curlArgs += "$($Server.TrimEnd('/'))/api/v1/upload" + +& curl.exe @curlArgs diff --git a/backend/static/api/warpbox.sh b/backend/static/api/warpbox.sh new file mode 100644 index 0000000..ae859ad --- /dev/null +++ b/backend/static/api/warpbox.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# +# warpbox: command line uploader for Warpbox +# +# Set the server once, then upload anything: +# export WARPBOX_HOST=https://your.warpbox.host +# warpbox ./report.pdf +# +# Install: +# curl -fsSL "$WARPBOX_HOST/static/api/warpbox.sh" -o ~/.local/bin/warpbox +# chmod +x ~/.local/bin/warpbox +# # make sure ~/.local/bin is on your PATH +# +set -eo pipefail + +WARPBOX_HOST="${WARPBOX_HOST:-}" +AUTH="${WARPBOX_TOKEN:-}" +PASSWORD="" +EXPIRY="" +MAX_DOWNLOADS="" +OBFUSCATE="" +AS_JSON=0 +FILES=() + +usage() { + cat <<'EOF' +warpbox: upload files to Warpbox from the terminal + +USAGE: + warpbox [options] [file ...] + +OPTIONS: + -p, --password Require a password to view/download the box + -e, --expiry Lifetime before expiry: 30m, 6h, 2d, 1w (or bare minutes) + -n, --max-downloads Expire after N downloads + -o, --obfuscate Hide file names/counts until unlocked (needs --password) + --host Warpbox server to upload to (or set WARPBOX_HOST) + --auth API token (prefer the WARPBOX_TOKEN env var, see AUTH) + --auth-file Read the API token from a file (safer than --auth) + --json Print the full JSON response instead of just the URL + -h, --help Show this help + +AUTH: + Uploads are anonymous unless a token is supplied. The most secure option is the + WARPBOX_TOKEN environment variable, so the token never lands in your shell + history or the process list: + + export WARPBOX_TOKEN=wbx_your_token + warpbox ./photo.png + + Create a token under Account, Access tokens. Avoid --auth on shared machines. + +EXAMPLES: + warpbox ./report.pdf + warpbox --password 123 --expiry 2d ./first_file.zip ./whatever.png ./all_*_photos.jpg + warpbox --max-downloads 5 --json ./build.zip +EOF +} + +expiry_to_minutes() { + local v="$1" num unit + num="${v%%[mhdw]*}" + unit="${v##*[0-9]}" + case "$unit" in + h) echo $(( num * 60 )) ;; + d) echo $(( num * 1440 )) ;; + w) echo $(( num * 10080 )) ;; + m|"") echo "$num" ;; + *) echo "$num" ;; + esac +} + +while [ $# -gt 0 ]; do + case "$1" in + -p|--password) PASSWORD="$2"; shift 2 ;; + -e|--expiry) EXPIRY="$2"; shift 2 ;; + -n|--max-downloads) MAX_DOWNLOADS="$2"; shift 2 ;; + -o|--obfuscate) OBFUSCATE="on"; shift ;; + --host) WARPBOX_HOST="$2"; shift 2 ;; + --auth) AUTH="$2"; shift 2 ;; + --auth-file) AUTH="$(cat "$2")"; shift 2 ;; + --json) AS_JSON=1; shift ;; + -h|--help) usage; exit 0 ;; + --) shift; while [ $# -gt 0 ]; do FILES+=("$1"); shift; done ;; + -*) echo "warpbox: unknown option $1" >&2; exit 2 ;; + *) FILES+=("$1"); shift ;; + esac +done + +if [ -z "$WARPBOX_HOST" ]; then + echo "warpbox: no server set. Use --host or export WARPBOX_HOST=" >&2 + exit 2 +fi + +if [ ${#FILES[@]} -eq 0 ]; then + echo "warpbox: no files given" >&2 + echo >&2 + usage >&2 + exit 2 +fi + +CURL_ARGS=() +for f in "${FILES[@]}"; do + if [ ! -f "$f" ]; then + echo "warpbox: not a file: $f" >&2 + exit 2 + fi + CURL_ARGS+=(-F "file=@${f}") +done + +[ -n "$PASSWORD" ] && CURL_ARGS+=(-F "password=${PASSWORD}") +[ -n "$EXPIRY" ] && CURL_ARGS+=(-F "expires_minutes=$(expiry_to_minutes "$EXPIRY")") +[ -n "$MAX_DOWNLOADS" ] && CURL_ARGS+=(-F "max_downloads=${MAX_DOWNLOADS}") +[ -n "$OBFUSCATE" ] && CURL_ARGS+=(-F "obfuscate_metadata=on") + +HEADERS=() +[ -n "$AUTH" ] && HEADERS+=(-H "Authorization: Bearer ${AUTH}") +[ "$AS_JSON" -eq 1 ] && HEADERS+=(-H "Accept: application/json") + +exec curl -fS "${HEADERS[@]}" "${CURL_ARGS[@]}" "${WARPBOX_HOST%/}/api/v1/upload" diff --git a/backend/static/css/16-retro.css b/backend/static/css/16-retro.css index f61eb3b..8d255c3 100644 --- a/backend/static/css/16-retro.css +++ b/backend/static/css/16-retro.css @@ -152,16 +152,16 @@ /* Links: classic blue, underlined, purple when visited. Sidebar links and tabs are styled as their own Win98 controls below, so they're excluded here. */ -:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link) { +:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link):not(.api-nav-link):not(.shortcut-card):not(.link-pill) { color: #0000ee; text-decoration: underline; } -:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link):visited { +:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link):not(.api-nav-link):not(.shortcut-card):not(.link-pill):visited { color: #551a8b; } -:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link):hover { +:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link):not(.api-nav-link):not(.shortcut-card):not(.link-pill):hover { color: #ee0000; } @@ -741,3 +741,219 @@ :root[data-theme="retro"] .file-main small { color: inherit; } + +/* ------------------------------------------------------------------------- */ +/* API documentation: sidebar + panels as Win98 windows */ +/* The new .api-docs layout uses dark revamp tokens by default, which are */ +/* unreadable on the black retro desktop. Re-skin it as Win98 chrome: a */ +/* raised silver sidebar window, plain light section intros on the desktop, */ +/* and each card a silver window with a navy title bar from its heading. */ +/* ------------------------------------------------------------------------- */ + +/* Sidebar = raised silver window with a real title bar from its

. */ +:root[data-theme="retro"] .api-sidebar { + background: linear-gradient(to bottom, #ffffff, 4%, #c0c0c0 8%); + background-color: #c0c0c0; + border: 1px solid #000000; + box-shadow: var(--shadow); + padding: 0.5rem; + gap: 0.5rem; +} + +:root[data-theme="retro"] .api-sidebar > .kicker { + display: none; +} + +:root[data-theme="retro"] .api-sidebar-title { + margin: -0.5rem -0.5rem 0.5rem; + font-size: 0.9rem; +} + +:root[data-theme="retro"] .api-nav { + border-left: 0; + padding-left: 0; + gap: 0.2rem; +} + +/* Nav entries are flat silver list items; the active one is a navy bar. */ +:root[data-theme="retro"] .api-nav-link { + color: #000000; + font-weight: 700; + text-decoration: none; + border: 1px solid transparent; +} + +:root[data-theme="retro"] .api-nav-link:hover { + background: #d4d0c8; + color: #000000; +} + +:root[data-theme="retro"] .api-nav-link.is-active { + background: linear-gradient(to right, #000078, 80%, #0f80cd); + color: #ffffff; + border-color: #000000; +} + +:root[data-theme="retro"] .api-sidebar-meta { + border-top: 1px solid #808080; + box-shadow: 0 -1px 0 #ffffff; + padding-top: 0.5rem; + margin-top: 0.5rem; +} + +/* Section intros sit on the black desktop: gold kicker, white title, light + subtitle, and readable inline code (not the default black-on-black). */ +:root[data-theme="retro"] .panel-head .kicker { + color: #ffd966; + display: block; +} + +:root[data-theme="retro"] .panel-head h2, +:root[data-theme="retro"] .section-label { + color: #ffffff; +} + +:root[data-theme="retro"] .panel-head .lead { + color: #cfd8ff; +} + +:root[data-theme="retro"] .panel-head .lead code { + color: #ffffff; + background: #000078; + padding: 0 0.2rem; +} + +/* Each card heading becomes a Win98 title bar with a fake close button. + Headings bleed to the window edges; only the first hugs the top edge so a + multi-step card (e.g. ShareX) reads as stacked group bars, not overlaps. */ +:root[data-theme="retro"] .api-content .card > .card-content > h3 { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + margin: 1.5rem -1.5rem 1rem; + padding: 0.35rem 0.5rem; + background: linear-gradient(to right, #000078, 80%, #0f80cd); + color: #ffffff; + font-size: 1rem; + font-weight: 700; +} + +:root[data-theme="retro"] .api-content .card > .card-content > h3:first-child { + margin-top: -1.5rem; +} + +/* The upload endpoint card leads with a method + path row; make that the bar. */ +:root[data-theme="retro"] .api-content .endpoint-head { + margin: -1.5rem -1.5rem 1rem; + padding: 0.3rem 0.5rem; + background: linear-gradient(to right, #000078, 80%, #0f80cd); +} + +:root[data-theme="retro"] .endpoint-head .endpoint-path { + color: #ffffff; +} + +:root[data-theme="retro"] .api-content .card > .card-content > h3::after, +:root[data-theme="retro"] .api-content .endpoint-head::after { + content: "\2715"; + display: grid; + place-items: center; + width: 1.15rem; + height: 1rem; + margin-left: auto; + background: #c0c0c0; + color: #000000; + font-size: 0.7rem; + font-weight: 700; + box-shadow: inset -1px -1px 0 #404040, inset 1px 1px 0 #ffffff, inset -2px -2px 0 #808080, inset 2px 2px 0 #dfdfdf; +} + +/* Body text inside windows reads black, not muted purple. */ +:root[data-theme="retro"] .api-content .card p, +:root[data-theme="retro"] .api-content .card h4, +:root[data-theme="retro"] .api-content .field-grid span, +:root[data-theme="retro"] .endpoint-list div em, +:root[data-theme="retro"] .faq-item summary, +:root[data-theme="retro"] .faq-item p { + color: #1a1a1a; +} + +/* Sub-labels (Request fields, Example, ...) become small black headers. */ +:root[data-theme="retro"] .api-content .card h4 { + text-transform: none; + letter-spacing: 0; +} + +/* Endpoint rows are sunken white fields. */ +:root[data-theme="retro"] .endpoint-list div { + background: #ffffff; + border: 1px solid #000000; + box-shadow: inset 1px 1px 0 #808080, inset -1px -1px 0 #ffffff; +} + +/* Home shortcut tiles and quick links: silver windows / sunken white fields. */ +:root[data-theme="retro"] .shortcut-card { + background: linear-gradient(to bottom, #ffffff, 6%, #c0c0c0 10%); + background-color: #c0c0c0; + border: 1px solid #000000; + box-shadow: inset -1px -1px 0 #404040, inset 1px 1px 0 #ffffff, inset -2px -2px 0 #808080, inset 2px 2px 0 #dfdfdf; +} + +:root[data-theme="retro"] .shortcut-card:hover { + transform: none; + background-color: #d4d0c8; +} + +:root[data-theme="retro"] .shortcut-eyebrow { + color: #000078; +} + +:root[data-theme="retro"] .shortcut-title, +:root[data-theme="retro"] .shortcut-sub { + color: #1a1a1a; +} + +:root[data-theme="retro"] .link-pill { + background: #ffffff; + border: 1px solid #000000; + box-shadow: inset 1px 1px 0 #808080, inset -1px -1px 0 #ffffff; + color: #000000; +} + +:root[data-theme="retro"] .link-pill span { + background: #000078; + color: #ffffff; + border: 1px solid #000000; +} + +/* CLI download cards = silver windows. */ +:root[data-theme="retro"] .download-card { + background: linear-gradient(to bottom, #ffffff, 4%, #c0c0c0 8%); + background-color: #c0c0c0; + border: 1px solid #000000; + box-shadow: var(--shadow); +} + +:root[data-theme="retro"] .download-card .download-os, +:root[data-theme="retro"] .download-card p { + color: #1a1a1a; +} + +/* FAQ entries are silver windows; the +/- marker stays. */ +:root[data-theme="retro"] .faq-item { + background: linear-gradient(to bottom, #ffffff, 4%, #c0c0c0 8%); + background-color: #c0c0c0; + border: 1px solid #000000; + box-shadow: var(--shadow); +} + +:root[data-theme="retro"] .faq-item summary::after { + color: #000000; +} + +/* Copy buttons: stay visible (retro already paints them as silver buttons). */ +:root[data-theme="retro"] .code-block .copy-btn { + background: #c0c0c0; + opacity: 1; +} diff --git a/backend/static/css/40-docs.css b/backend/static/css/40-docs.css index 5c2675d..8b3aced 100644 --- a/backend/static/css/40-docs.css +++ b/backend/static/css/40-docs.css @@ -10,6 +10,408 @@ padding: 2rem 0 3rem; } +/* ============================================================ + API documentation — sidebar layout + ============================================================ */ + +.api-docs { + width: min(74rem, calc(100% - 2rem)); + margin: 0 auto; + padding: 2rem 0 3rem; + display: grid; + grid-template-columns: 13.5rem minmax(0, 1fr); + gap: 2rem; + align-items: start; +} + +.api-sidebar { + position: sticky; + top: 1.5rem; + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.api-sidebar-title { + margin: 0 0 0.75rem; + font-size: 1.15rem; +} + +.api-nav { + display: flex; + flex-direction: column; + gap: 0.15rem; + border-left: 1px solid var(--border); + padding-left: 0.3rem; +} + +.api-nav-link { + display: block; + padding: 0.45rem 0.7rem; + border-radius: calc(var(--radius) - 0.3rem); + color: var(--muted-foreground); + font-size: 0.9rem; + font-weight: 600; + text-decoration: none; + line-height: 1.2; + transition: background 0.12s ease, color 0.12s ease; +} + +.api-nav-link:hover { + background: var(--muted); + color: var(--foreground); +} + +.api-nav-link.is-active { + background: color-mix(in srgb, var(--primary) 16%, transparent); + color: var(--foreground); +} + +.api-sidebar-meta { + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 0.35rem; + font-size: 0.8rem; +} + +.api-sidebar-meta a { + color: var(--muted-foreground); +} + +/* --- Panels: only one visible at a time --- */ +.api-content { + min-width: 0; +} + +.doc-panel { + display: none; + outline: none; +} + +.doc-panel.is-active { + display: block; + animation: doc-fade 0.18s ease; +} + +@keyframes doc-fade { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: none; } +} + +.panel-head { + max-width: 46rem; + margin-bottom: 1.5rem; +} + +.panel-head h2 { + margin: 0; + font-size: 1.5rem; +} + +.panel-head .lead { + margin: 0.6rem 0 0; + color: var(--muted-foreground); + font-size: 0.95rem; + line-height: 1.6; +} + +.api-content .card + .card, +.api-content .quickstart { + margin-top: 1rem; +} + +.api-content h3 { + margin: 0; + font-size: 1.05rem; +} + +.api-content h4 { + margin: 1.4rem 0 0; + font-size: 0.8rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.03em; + color: var(--muted-foreground); +} + +.api-content .card p { + margin: 0.65rem 0 0; + color: var(--muted-foreground); + font-size: 0.9rem; + line-height: 1.6; +} + +.api-content code { + color: var(--foreground); +} + +.api-content .field-grid p { + margin: 0; +} + +.section-label { + margin: 1.75rem 0 0.75rem !important; + font-size: 0.8rem !important; + text-transform: uppercase; + letter-spacing: 0.03em; + color: var(--muted-foreground); +} + +/* --- Home shortcuts --- */ +.shortcut-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); + gap: 0.75rem; + margin-bottom: 1.25rem; +} + +.shortcut-card { + display: flex; + flex-direction: column; + gap: 0.2rem; + padding: 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: color-mix(in srgb, var(--card) 94%, transparent); + text-decoration: none; + transition: border-color 0.12s ease, transform 0.12s ease; +} + +.shortcut-card:hover { + border-color: var(--ring); + transform: translateY(-2px); +} + +.shortcut-eyebrow { + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--primary); +} + +.shortcut-title { + font-size: 1rem; + font-weight: 650; + color: var(--foreground); +} + +.shortcut-sub { + font-size: 0.82rem; + color: var(--muted-foreground); +} + +.link-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); + gap: 0.5rem; +} + +.link-pill { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.6rem 0.85rem; + border: 1px solid var(--border); + border-radius: calc(var(--radius) - 0.2rem); + background: var(--card); + color: var(--foreground); + font-size: 0.88rem; + text-decoration: none; + transition: border-color 0.12s ease; +} + +.link-pill:hover { + border-color: var(--ring); +} + +.link-pill span { + flex: none; + min-width: 2.6rem; + text-align: center; + padding: 0.15rem 0.35rem; + border-radius: 0.3rem; + background: var(--muted); + color: var(--muted-foreground); + font-size: 0.66rem; + font-weight: 700; + letter-spacing: 0.03em; +} + +/* --- Code blocks with copy button --- */ +.code-block { + position: relative; + margin: 0; +} + +.code-block .copy-btn { + position: absolute; + top: 0.5rem; + right: 0.5rem; + padding: 0.3rem 0.6rem; + border: 1px solid var(--border); + border-radius: 0.4rem; + background: color-mix(in srgb, var(--card) 80%, transparent); + color: var(--muted-foreground); + font-size: 0.72rem; + font-weight: 600; + cursor: pointer; + opacity: 0; + transition: opacity 0.12s ease, color 0.12s ease, border-color 0.12s ease; +} + +.code-block:hover .copy-btn, +.code-block .copy-btn:focus-visible { + opacity: 1; +} + +.code-block .copy-btn:hover { + color: var(--foreground); + border-color: var(--ring); +} + +/* --- Endpoint blocks --- */ +.endpoint-head { + display: flex; + align-items: center; + gap: 0.6rem; + flex-wrap: wrap; +} + +.endpoint-path { + font-size: 0.95rem; + font-weight: 600; +} + +.method { + flex: none; + display: inline-block; + padding: 0.2rem 0.5rem; + border-radius: 0.35rem; + font-size: 0.68rem; + font-weight: 800; + letter-spacing: 0.04em; + color: #fff; +} + +.method-get { background: #2563eb; } +.method-post { background: #16a34a; } +.method-put { background: #d97706; } + +.endpoint-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin: 1rem 0 0; +} + +.endpoint-list div { + display: flex; + align-items: center; + gap: 0.6rem; + flex-wrap: wrap; + padding: 0.55rem 0.7rem; + border: 1px solid var(--border); + border-radius: calc(var(--radius) - 0.3rem); + background: var(--background); +} + +.endpoint-list div code { + font-size: 0.82rem; + word-break: break-all; +} + +.endpoint-list div em { + margin-left: auto; + color: var(--muted-foreground); + font-size: 0.8rem; + font-style: normal; +} + +/* --- CLI download cards --- */ +.download-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); + gap: 0.75rem; + margin-bottom: 1rem; +} + +.download-card { + padding: 1.25rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: color-mix(in srgb, var(--card) 94%, transparent); + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.download-card .download-os { + font-size: 1.05rem; + font-weight: 650; + color: var(--foreground); +} + +.download-card p { + margin: 0; + color: var(--muted-foreground); + font-size: 0.88rem; +} + +.download-card .button { + margin-top: auto; +} + +/* --- FAQ --- */ +.faq-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.faq-item { + border: 1px solid var(--border); + border-radius: calc(var(--radius) - 0.2rem); + background: color-mix(in srgb, var(--card) 94%, transparent); + padding: 0 1rem; +} + +.faq-item summary { + padding: 0.9rem 0; + cursor: pointer; + font-weight: 600; + color: var(--foreground); + list-style: none; + position: relative; + padding-right: 1.5rem; +} + +.faq-item summary::-webkit-details-marker { + display: none; +} + +.faq-item summary::after { + content: "+"; + position: absolute; + right: 0.1rem; + top: 50%; + transform: translateY(-50%); + color: var(--muted-foreground); + font-size: 1.1rem; +} + +.faq-item[open] summary::after { + content: "\2212"; +} + +.faq-item p { + margin: 0 0 0.95rem; + color: var(--muted-foreground); + font-size: 0.9rem; + line-height: 1.6; +} + .docs-header { max-width: 44rem; } @@ -63,42 +465,19 @@ grid-column: 1 / -1; } -.endpoint-list, .field-grid { display: grid; gap: 0.65rem; margin: 1rem 0 0; -} - -.endpoint-list div, -.field-grid { min-width: 0; } -.endpoint-list div { - display: grid; - grid-template-columns: 7rem minmax(0, 1fr); - gap: 0.75rem; - align-items: baseline; -} - -.endpoint-list dt, -.endpoint-list dd { - margin: 0; - min-width: 0; -} - -.endpoint-list dt, .field-grid span { color: var(--muted-foreground); font-size: 0.78rem; font-weight: 700; } -.endpoint-list dd code { - display: block; -} - .docs-steps { margin: 0.85rem 0 0; padding-left: 1.1rem; diff --git a/backend/static/css/90-responsive.css b/backend/static/css/90-responsive.css index 825fd34..72045f1 100644 --- a/backend/static/css/90-responsive.css +++ b/backend/static/css/90-responsive.css @@ -57,6 +57,44 @@ grid-template-columns: 1fr; } + .api-docs { + grid-template-columns: 1fr; + gap: 1.25rem; + } + + .api-sidebar { + position: static; + top: auto; + } + + .api-sidebar-title { + margin-bottom: 0.5rem; + } + + .api-nav { + flex-direction: row; + flex-wrap: wrap; + border-left: 0; + padding-left: 0; + gap: 0.35rem; + } + + .api-nav-link { + border: 1px solid var(--border); + } + + .api-sidebar-meta { + flex-direction: row; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 0.5rem; + } + + .endpoint-list div em { + margin-left: 0; + width: 100%; + } + .app-sidebar { position: static; width: 100%; diff --git a/backend/static/js/48-api-docs.js b/backend/static/js/48-api-docs.js new file mode 100644 index 0000000..6769f7b --- /dev/null +++ b/backend/static/js/48-api-docs.js @@ -0,0 +1,94 @@ +(function () { + const root = document.querySelector("[data-api-docs]"); + if (!root) { + return; + } + + const panels = Array.from(root.querySelectorAll("[data-doc-panel]")); + const navLinks = Array.from(root.querySelectorAll("[data-doc-link]")); + const DEFAULT = "home"; + + function activate(name, focus) { + let matched = false; + panels.forEach((panel) => { + const on = panel.dataset.docPanel === name; + panel.classList.toggle("is-active", on); + if (on) { + matched = true; + } + }); + if (!matched) { + return false; + } + root.querySelectorAll(".api-nav-link").forEach((link) => { + link.classList.toggle( + "is-active", + link.getAttribute("href") === "#" + name + ); + }); + if (focus) { + const panel = root.querySelector('[data-doc-panel="' + name + '"]'); + if (panel) { + panel.focus({ preventScroll: true }); + } + } + return true; + } + + // Resolve the current hash to a panel. The hash can point at a panel id + // (e.g. #endpoints) or at any element inside a panel (e.g. #ep-upload), + // letting FAQ answers deep-link straight into the reference. + function resolveHash(focus) { + const id = (location.hash || "").slice(1); + if (!id) { + activate(DEFAULT, focus); + return; + } + const target = document.getElementById(id); + if (!target) { + activate(DEFAULT, focus); + return; + } + const panel = target.closest("[data-doc-panel]"); + const name = panel ? panel.dataset.docPanel : DEFAULT; + activate(name, focus && target === panel); + if (panel && target !== panel) { + // Scroll the deep-linked element into view once its panel is visible. + window.requestAnimationFrame(() => { + target.scrollIntoView({ block: "start", behavior: "smooth" }); + }); + } else { + window.scrollTo({ top: 0, behavior: "smooth" }); + } + } + + window.addEventListener("hashchange", () => resolveHash(true)); + + navLinks.forEach((link) => { + link.addEventListener("click", () => { + // hashchange handles activation; this keeps top-level nav clicks snappy. + if (link.getAttribute("href") === location.hash) { + resolveHash(true); + } + }); + }); + + // Add a copy button to every code block. + root.querySelectorAll(".code-block").forEach((block) => { + const pre = block.querySelector("pre"); + if (!pre) { + return; + } + const button = document.createElement("button"); + button.type = "button"; + button.className = "copy-btn"; + button.textContent = "Copy"; + button.setAttribute("aria-label", "Copy code"); + button.addEventListener("click", () => { + window.Warpbox.copyText(pre.innerText.trim(), button, "Copied"); + }); + block.appendChild(button); + }); + + resolveHash(false); +})(); diff --git a/backend/templates/layouts/base.html b/backend/templates/layouts/base.html index beaf049..0631e6e 100644 --- a/backend/templates/layouts/base.html +++ b/backend/templates/layouts/base.html @@ -81,6 +81,7 @@ + diff --git a/backend/templates/pages/api.html b/backend/templates/pages/api.html index a3c94b4..542f8d3 100644 --- a/backend/templates/pages/api.html +++ b/backend/templates/pages/api.html @@ -1,69 +1,132 @@ {{define "api.html"}}{{template "base" .}}{{end}} {{define "content"}} -
-
+
+
+

Warpbox API

+ + + -
-
-
+
+ + +
+
+

Get started

+

Upload files anywhere, from anything

+

Warpbox is a one endpoint upload API. Send a multipart file with curl, a script, ShareX, or the warpbox CLI and get back a shareable box link. Request JSON to also receive private manage and delete URLs.

+
+ + + +
+
+

Your first upload

+

No account required. This prints one plain box URL you can share immediately.

+
+
curl -F file=@./report.pdf {{.Data.UploadURL}}
+
+

Want file URLs, a manage link, and a delete link back? Add -H 'Accept: application/json'. See the JSON response.

+
+
+ + + +
+ + +
+
+

Reference

Endpoints

-
-
Upload
POST /api/v1/upload
-
Resumable create
POST /api/v1/uploads/resumable
-
Resumable status
GET /api/v1/uploads/resumable/{sessionID}
-
Resumable chunk
PUT /api/v1/uploads/resumable/{sessionID}/files/{fileID}/chunks/{index}
-
Resumable complete
POST /api/v1/uploads/resumable/{sessionID}/complete
-
Health
GET /health
- - -
-
-
+

Base URL {{.Data.BaseURL}}. Authentication is optional: send Authorization: Bearer <token> to upload as your account and use your account limits, or omit it to upload anonymously.

+ -
-
-

Resumable uploads

-

Browser uploads use the resumable API by default. Custom clients can use the same flow: create a session with file metadata, upload exact-sized chunks, then complete the session. Chunks are temporary and are cleaned if the session expires.

-
# 1. Create a session.
-curl -s {{.Data.BaseURL}}/api/v1/uploads/resumable \
-  -H 'Accept: application/json' \
-  -H 'Content-Type: application/json' \
-  -d '{"files":[{"name":"report.pdf","size":1048576,"contentType":"application/pdf"}],"expiresMinutes":1440}'
+      
+
+
+ POST + /api/v1/upload +
+

The core endpoint. Accepts a multipart/form-data body with one or more files. Returns a plain box URL by default, or the full JSON object when you send Accept: application/json.

-# 2. Upload each chunk using the returned sessionId, file id, and chunkSize. -dd if=./report.pdf bs=8388608 count=1 skip=0 2>/dev/null | \ - curl -X PUT --data-binary @- \ - {{.Data.BaseURL}}/api/v1/uploads/resumable/SESSION_ID/files/FILE_ID/chunks/0 +

Request fields

+
+ file

One or more files. Repeat the field for multiple files. Used by curl, browsers, and the CLI.

+ sharex

Alternative file field used by ShareX custom uploader configs. Same behaviour as file.

+ max_days

Optional. Days before the box expires. Defaults to 7.

+ expires_minutes

Optional. Lifetime in minutes. Takes precedence over max_days when > 0. Use it for expiries under a day (e.g. 60 = one hour).

+ max_downloads

Optional. Auto-expire the box after this many downloads.

+ password

Optional. Password required before viewing or downloading.

+ obfuscate_metadata

Optional on. Hides file names/counts until unlock (only meaningful with a password).

+
-# 3. Complete after all chunks are present. The response is the normal upload JSON. -curl -X POST -H 'Accept: application/json' \ - {{.Data.BaseURL}}/api/v1/uploads/resumable/SESSION_ID/complete
-

For authenticated uploads, send the same Authorization: Bearer <token> header on every resumable request. Incomplete chunks are stored under data/tmp/uploads before finalizing into the selected storage backend.

-
-
+

Request headers

+
+ Accept

application/json to receive the JSON body; otherwise a single plain-text URL.

+ Authorization

Optional Bearer <token>. Attributes the upload to your account.

+ X-Warpbox-Batch

Optional grouping key. Uploads sharing a value within {{.Data.ShareXGroupWindow}} land in the same box. See Integrations.

+
-
-
-

Curl upload

-

Without a JSON Accept header, Warpbox prints one plain box URL for shell-friendly usage.

-
curl -F file=@./report.pdf {{.Data.UploadURL}}
-

For automation, request JSON to get file URLs and the private manage/delete URLs.

-
curl -F file=@./report.pdf \
+          

Example

+
+
curl -F file=@./report.pdf \
+  -F max_downloads=5 \
+  -F expires_minutes=1440 \
   -H 'Accept: application/json' \
   {{.Data.UploadURL}}
-
-
+ +
+ -
-
-

JSON response

-

The raw delete token is returned only once inside manageUrl and deleteUrl. Keep those links private. On error the body is { "error": "message" } with a non-2xx status (e.g. rate limited or over a limit).

-
{
+      
+
+

JSON response

+

Returned when Accept: application/json is sent. The raw delete token appears only once, inside manageUrl and deleteUrl, so store them privately. Full schema: upload-response.json.

+
+
{
   "boxId": "abc123",
   "boxUrl": "{{.Data.BaseURL}}/d/abc123",
   "zipUrl": "{{.Data.BaseURL}}/d/abc123/zip",
@@ -81,29 +144,177 @@ curl -X POST -H 'Accept: application/json' \
     }
   ]
 }
+
+

On error the body is { "error": "message" } with a non-2xx status. Common causes: 413 over the size limit, 429 rate limited or over your daily quota, 401 bad token.

+
+
+ +
+
+

Resumable uploads

+

For large files. Browser uploads use this by default. Create a session with file metadata, PUT exact-sized chunks, then complete. Chunks are temporary and cleaned if the session expires. Send the same Authorization header on every request for authenticated sessions.

+
+
POST/api/v1/uploads/resumableCreate a session
+
GET/api/v1/uploads/resumable/{sessionID}Session status
+
PUT/api/v1/uploads/resumable/{sessionID}/files/{fileID}/chunks/{index}Upload one chunk
+
POST/api/v1/uploads/resumable/{sessionID}/completeFinalize (returns the upload JSON)
+
+
+
# 1. Create a session.
+curl -s {{.Data.BaseURL}}/api/v1/uploads/resumable \
+  -H 'Accept: application/json' \
+  -H 'Content-Type: application/json' \
+  -d '{"files":[{"name":"report.pdf","size":1048576,"contentType":"application/pdf"}],"expiresMinutes":1440}'
+
+# 2. Upload each chunk using the returned sessionId, file id, and chunkSize.
+dd if=./report.pdf bs=8388608 count=1 skip=0 2>/dev/null | \
+  curl -X PUT --data-binary @- \
+  {{.Data.BaseURL}}/api/v1/uploads/resumable/SESSION_ID/files/FILE_ID/chunks/0
+
+# 3. Complete after all chunks are present. The response is the normal upload JSON.
+curl -X POST -H 'Accept: application/json' \
+  {{.Data.BaseURL}}/api/v1/uploads/resumable/SESSION_ID/complete
+
+

Incomplete chunks are stored under data/tmp/uploads before finalizing into the selected storage backend.

+
+
+ +
+
+

Health & schemas

+
+
GET/healthLiveness check
+
GET/api/v1/schemas/upload-request.jsonRequest JSON Schema
+
GET/api/v1/schemas/upload-response.jsonResponse JSON Schema
+
+
+
+
+ + +
+
+

Terminal

+

The warpbox CLI

+

A tiny uploader script that wraps the API. It only needs curl (already on macOS, Linux, and Windows 10+). Point it at this instance once by setting WARPBOX_HOST to {{.Data.BaseURL}}, then upload from anywhere.

+
+ +
+
+
macOS & Linux
+

POSIX shell script (warpbox.sh).

+ Download for macOS / Linux +
+
+
Windows
+

PowerShell script (warpbox.ps1).

+ Download for Windows +
- -
-
+
+
+

Install & add to PATH

+ +

macOS / Linux

+

Download into a directory on your PATH, then make it executable. ~/.local/bin is the recommended location.

+
+
mkdir -p ~/.local/bin
+curl -fsSL {{.Data.BaseURL}}/static/api/warpbox.sh -o ~/.local/bin/warpbox
+chmod +x ~/.local/bin/warpbox
+
+# Point it at this instance (add to ~/.profile or ~/.zshrc to keep it set)
+echo 'export WARPBOX_HOST={{.Data.BaseURL}}' >> ~/.profile
+
+# If 'warpbox: command not found', add the dir to PATH:
+echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.profile
+# zsh users: use ~/.zshrc, then reload with: source ~/.profile
+
+

Verify with warpbox --help. Prefer a system wide install? Drop it in /usr/local/bin with sudo.

+ +

Windows (PowerShell)

+

Save the script, then add a function to your PowerShell profile so warpbox works anywhere.

+
+
# Save it to your home folder
+iwr {{.Data.BaseURL}}/static/api/warpbox.ps1 -OutFile $HOME\warpbox.ps1
+
+# Point it at this instance, and add a 'warpbox' command (run once)
+setx WARPBOX_HOST "{{.Data.BaseURL}}"
+Add-Content $PROFILE 'function warpbox { & "$HOME\warpbox.ps1" @args }'
+. $PROFILE   # reload the profile
+
+

If scripts are blocked, allow local scripts for your user: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned.

+
+
+ +
+
+

Usage

+

A password, an expiry of two days, and a glob the shell expands for you:

+
+
warpbox --password 123 --expiry 2d ./first_file.zip ./whatever.png ./all_*_photos.jpg
+
+
+ -p, --password

Require a password to open the box.

+ -e, --expiry

Lifetime: 30m, 6h, 2d, 1w (or bare minutes).

+ -n, --max-downloads

Expire after N downloads.

+ -o, --obfuscate

Hide names/counts until unlock (needs --password).

+ --json

Print the full JSON response instead of just the URL.

+ --host

Server to upload to. Defaults to your WARPBOX_HOST.

+
+

Windows uses PowerShell flags: warpbox -Password 123 -Expiry 2d .\file.zip.

+
+
+ +
+
+

Secure authentication

+

To upload as your account (and use your account's size, daily, and retention limits), the CLI needs an API token. Set it in your environment so it never appears in your shell history or in the process list that any user on the machine can read:

+
+
# macOS / Linux (add to ~/.profile or ~/.zshrc to persist)
+export WARPBOX_TOKEN=wbx_your_token
+warpbox ./photo.png
+
+# Windows (persist for your user)
+setx WARPBOX_TOKEN "wbx_your_token"
+
+

For CI or shared machines, keep the token in a file with locked down permissions and point the CLI at it. This avoids putting the secret on the command line at all:

+
+
printf '%s' "wbx_your_token" > ~/.warpbox-token
+chmod 600 ~/.warpbox-token
+warpbox --auth-file ~/.warpbox-token ./photo.png
+
+

--auth <token> exists for quick tests but is discouraged: it leaks into shell history and ps. Create or revoke tokens under Account, Access tokens.

+
+
+
+ + +
+
+

Integrations

ShareX setup

-

Import the uploader, then add your API key to upload as your account — with your account's size, daily, and retention limits — instead of as an anonymous guest.

+

Import the uploader once, then optionally add your API key to upload as your account instead of as an anonymous guest.

+
-

1 · Import the uploader

-
    -
  1. Download warpbox-anonymous.sxcu.
  2. -
  3. In ShareX: Destinations → Custom uploader settings → Import → From file, then pick the .sxcu.
  4. -
+
+
+

1. Import the uploader

+
    +
  1. Download warpbox-anonymous.sxcu.
  2. +
  3. In ShareX: Destinations → Custom uploader settings → Import → From file, then pick the .sxcu.
  4. +
-

2 · Add your API key (upload as your account)

-
    -
  1. Create a personal access token under Account → Access tokens and copy it.
  2. -
  3. In Custom uploader settings, select the Warpbox uploader and open the Headers section.
  4. -
  5. Add a header — Name Authorization, Value Bearer <your token>.
  6. -
-

Without that header, uploads stay anonymous. With it, they're attributed to your account and use your account's limits.

+

2. Add your API key (optional, upload as your account)

+
    +
  1. Create a personal access token under Account, Access tokens and copy it.
  2. +
  3. In Custom uploader settings, select the Warpbox uploader and open the Headers section.
  4. +
  5. Add a header. Name Authorization, Value Bearer <your token>.
  6. +
+

Without that header, uploads stay anonymous. With it, they're attributed to your account and use your account's limits.

-
{
+          
+
{
   "Version": "1.0.0",
   "Name": "Warpbox (my account)",
   "DestinationType": "ImageUploader, FileUploader, TextUploader",
@@ -121,27 +332,183 @@ curl -X POST -H 'Accept: application/json' \
   "DeletionURL": "{json:deleteUrl}",
   "ErrorMessage": "{json:error}"
 }
+
-

Grouping multiple files into one box

-

Grouping is opt-in via the X-Warpbox-Batch request header — without it, every file becomes its own box (the default). When the header is present, uploads sharing the same value (per account, or per IP for anonymous) within {{.Data.ShareXGroupWindow}} of each other are added to the same box, so a multi-file ShareX selection produces one shareable link instead of one per file. The shipped config sets X-Warpbox-Batch: sharex; remove that header for one box per file.

-

The response also exposes {json:thumbnailUrl} for ShareX previews, {json:deleteUrl} for the deletion URL, and {json:error} so ShareX surfaces messages like rate limiting.

-
-
- -
-
-

Multipart fields

-
- file

One or more files for curl, browser, and generic multipart clients.

- sharex

One or more files from ShareX custom uploader configs.

- max_days

Optional number of days before expiration. Defaults to 7.

- expires_minutes

Optional lifetime in minutes. Takes precedence over max_days when greater than zero — useful for sub-day expiries (e.g. 60 for one hour).

- max_downloads

Optional download count limit.

- password

Optional password required before viewing/downloading.

- obfuscate_metadata

Optional on; hides names/counts until unlock when a password is set.

+

Grouping multiple files into one box

+

Grouping is opt in via the X-Warpbox-Batch request header. Without it, every file becomes its own box (the default). When the header is present, uploads sharing the same value (per account, or per IP for anonymous) within {{.Data.ShareXGroupWindow}} of each other are added to the same box, so a ShareX selection of several files produces one shareable link instead of one per file. The shipped config sets X-Warpbox-Batch: sharex; remove that header for one box per file.

+

The response also exposes {json:thumbnailUrl} for ShareX previews, {json:deleteUrl} for the deletion URL, and {json:error} so ShareX surfaces messages like rate limiting.

+
+
+ + +
+
+

Cookbook

+

Examples

+

Every snippet hits POST {{.Data.UploadURL}}. Add -H 'Authorization: Bearer <token>' to any of them to upload as your account.

+
+ +
+
+

curl

+

Plain text (one URL) for the shell; JSON for automation.

+
+
# Just the box URL
+curl -F file=@./report.pdf {{.Data.UploadURL}}
+
+# Full JSON with manage + delete URLs, password and 1-hour expiry
+curl -F file=@./report.pdf \
+  -F password=hunter2 \
+  -F expires_minutes=60 \
+  -H 'Accept: application/json' \
+  {{.Data.UploadURL}}
+
+
+
+ +
+
+

wget

+

The endpoint needs a real multipart/form-data body, which wget can't assemble on its own, so build the body by hand and post it. It also shows the wire format:

+
+
B=----warpbox$$
+{ printf -- '--%s\r\nContent-Disposition: form-data; name="file"; filename="report.pdf"\r\nContent-Type: application/octet-stream\r\n\r\n' "$B"
+  cat ./report.pdf
+  printf -- '\r\n--%s--\r\n' "$B"; } > /tmp/wb.body
+
+wget --quiet --output-document=- \
+  --header="Content-Type: multipart/form-data; boundary=$B" \
+  --header="Accept: application/json" \
+  --post-file=/tmp/wb.body \
+  {{.Data.UploadURL}}
+
+

Add more form fields (password, expires_minutes, …) by repeating the --%s … Content-Disposition: form-data; name="…" block before the closing boundary. If this feels fiddly, curl or the CLI build the body for you.

+
+
+ +
+
+

HTTPie

+

Multipart with form fields:

+
+
http --multipart POST {{.Data.UploadURL}} \
+  Accept:application/json \
+  file@./report.pdf \
+  max_downloads=3 \
+  expires_minutes=1440
+
+
+
+ +
+
+

Python (requests)

+
+
import requests
+
+with open("report.pdf", "rb") as f:
+    r = requests.post(
+        "{{.Data.UploadURL}}",
+        headers={"Accept": "application/json"},  # add "Authorization": "Bearer "
+        files={"file": f},
+        data={"expires_minutes": 1440, "max_downloads": 5},
+    )
+r.raise_for_status()
+print(r.json()["boxUrl"])
+
+
+
+ +
+
+

Node.js (fetch)

+
+
import { readFile } from "node:fs/promises";
+
+const form = new FormData();
+form.set("file", new Blob([await readFile("report.pdf")]), "report.pdf");
+form.set("expires_minutes", "1440");
+
+const res = await fetch("{{.Data.UploadURL}}", {
+  method: "POST",
+  headers: { Accept: "application/json" }, // add Authorization: "Bearer "
+  body: form,
+});
+const box = await res.json();
+console.log(box.boxUrl);
+
+
+
+ +
+
+

PowerShell

+

PowerShell 7+ has native multipart with -Form:

+
+
$resp = Invoke-RestMethod -Uri "{{.Data.UploadURL}}" -Method Post -Headers @{ Accept = "application/json" } -Form @{
+  file            = Get-Item ".\report.pdf"
+  expires_minutes = 1440
+}
+$resp.boxUrl
+
+

On Windows PowerShell 5.1, use the bundled curl.exe (the same approach the CLI takes) or the warpbox.ps1 script.

+
+
+
+ + +
+
+

Help

+

FAQ & troubleshooting

+

Quick answers, each linking back to the relevant part of the docs.

+
+ +
+
+ Do I need an account or API key? +

No. Anonymous uploads work without one, see the quickstart. Add a token only to upload as your account and use your account's limits; set one up under Account, Access tokens and pass it as described in CLI authentication.

+
+
+ How do I send a password, expiry, or download limit? +

They're multipart form fields on the upload endpoint: password, expires_minutes (or max_days), and max_downloads. See the full list under Endpoints, request fields, or use the CLI flags in CLI usage.

+
+
+ How do I get file URLs and a delete link back? +

Send Accept: application/json. The response includes boxUrl, per-file urls, and the private manageUrl/deleteUrl (shown only once). See the JSON response.

+
+
+ How do I upload one big file reliably? +

Use the resumable endpoints: create a session, PUT chunks, then complete. Interrupted uploads can resume from the last chunk.

+
+
+ Can I upload several files into one shareable link? +

Yes. Send the X-Warpbox-Batch header with a shared value within {{.Data.ShareXGroupWindow}}. Details in Integrations, grouping.

+
+
+ Where's the keep-it-secret way to store my token? +

Use the WARPBOX_TOKEN environment variable or --auth-file, not --auth on the command line. Full guidance in CLI authentication.

+
+
+ My upload returns an error, what do the codes mean? +

Errors come back as { "error": "message" } with a non-2xx status: 413 too large, 429 rate limited / over quota, 401 invalid token. See error responses.

+
+
+ How do I use Warpbox from ShareX? +

Import the .sxcu and (optionally) add your token header. Step by step with the config in Integrations, ShareX setup.

+
+
+ warpbox: command not found after install? +

The install directory isn't on your PATH. Fix it per your platform in Install & add to PATH.

+
+
+ Is there a machine-readable schema? +

Yes: upload-request.json and upload-response.json (JSON Schema 2020-12).

+
- +
+ {{end}}