# Build a personal Drive client ## Read first Read [the shared guide](https://wspc.ai/llms.txt), [Auth OpenAPI](https://api.wspc.ai/auth/openapi.json), and [drive OpenAPI](https://api.wspc.ai/drive/openapi.json) before you generate code. Use live request schemas, response schemas, examples, and descriptions. Check HTTP status and content; an error page is not a schema. If you cannot read a required document, report the access problem instead of inventing an API. ## Interview the user Ask one main question about how they organize and use files. Offer these examples: a few daily documents, project Libraries, or frequent binary uploads. Allow a free-text answer. Ask at most one follow-up if it changes the result. Use an answer already given instead of asking again. Do not ask the user to choose endpoints or data models. Map their answer to the preferred Library and visible file-list information. ## Build and deliver Deliver a downloadable single HTML file with vanilla JS, inline styles, no build step, no backend, and no client secret. Use a small in-memory state and explicit forms. Give it a clear visual hierarchy, readable spacing, responsive layout, visible keyboard focus, labels, and status messages announced to assistive tools. Use native buttons and form controls. Render user content with textContent. Implement the full domain scope below. Show sign-in, waiting for approval, loading, empty data, success, and distinct error states. Check every HTTP status before reading a success field. A failed request must not look like an empty list or a successful write. Keep drafts and selected File objects on failure, including refresh failure. Disable duplicate submissions while a write is in flight. Unknown write outcomes require readback and a new user decision, not blind retry. After a confirmed write, update state from the response or explicitly reload the affected list. Keep a successful write distinct from a failed subsequent reload. Use per-database opaque consistency bookmarks: x-cb-auth and x-cb-drive. Save response headers in memory and send each unchanged on subsequent requests to its own domain. Never parse, compare, log, or put bookmarks in JSON payloads. Reset domain state when the user changes account. Keep pagination cursor state separate from bookmarks and form confirmations. Show network failure, authentication required, validation failure, conflict, rate limit, and service failure separately. Domain errors use { error: { code, message } }; OAuth errors use { error, error_description }. For conflicts, retain the original input and confirmation. Offer an explicit reload and review action; never fetch a newer version and resend automatically. Use a request timeout and preserve input when it fires. Do not log credentials. Give the user the actual .html file, not just code in chat. Tell them to download it and open it in a real browser; chat previews may block network or OAuth. Before delivery, check keyboard actions, empty data, reads, writes, and error states with authorized fixtures. Report any behavior you could not verify. ## OAuth for a file opened from `file://` Use device flow for this local-file client. A hosted browser SPA should use authorization code + PKCE from the shared guide, with an exact registered redirect URI and state validation. The resource is https://api.wspc.ai and the available scope is wspc:full. Explain that this is broad Workspace access, not a drive-only permission. No backend or secret is needed. Send JSON bodies to https://api.wspc.ai for the following requests: 1. Register once with POST /auth/oauth/register: { "client_name": "My drive client", "redirect_uris": ["http://localhost"], "grant_types": ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"], "token_endpoint_auth_method": "none" }. Cache the returned client_id in localStorage and reuse it after reload. 2. POST /auth/oauth/device with { "client_id": "", "scope": "wspc:full", "resource": "https://api.wspc.ai" }. Show a user-clickable link to verification_uri_complete for browser approval. Use the returned URL; do not ask the user to copy a code unnecessarily. 3. Poll POST /auth/oauth/token with { "grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": "", "client_id": "" }. Wait at least the returned interval before each poll. authorization_pending continues; slow_down adds five seconds to the interval. Use recursive setTimeout with the current interval. Stop on access_denied or expired_token and show the correct state with an explicit restart action. Bound polling with expires_in; cancel an old poll when starting another login. 4. Save access_token, refresh_token, and expiry together before using the new access token. On expiry or one 401, use a single shared refresh promise: POST /auth/oauth/token with { "grant_type": "refresh_token", "refresh_token": "", "client_id": "" }. Save the new pair before releasing waiting calls. Token Family Rotation means old tokens can become Consumed; never reuse an old pair after rotation. If refresh fails, clear invalid credentials and require sign-in, preserving drafts. Do not run independent refreshes for concurrent API calls. API calls send Authorization: Bearer . Tokens stay in browser localStorage, with an inline comment explaining its XSS exposure. Keep them out of URLs, prompts, logs, downloads, and screenshots. Sign-out revokes the Token Family with the live revoke endpoint and clears credentials. If revocation fails, report it and clear local credentials. Retain only the reusable public client_id. Use expiry returned by the server rather than a hard-coded token lifetime. ## Libraries and File entries GET /drive/libraries returns libraries and optional next_cursor. Consume pages or offer Load more Libraries. Let the user select a Library and create one with POST /drive/libraries { "name": "..." }, including when no Library exists. GET /drive/libraries/{id}/manifest returns library, entries, and nullable next_cursor. Follow cursor while retaining other filters. If resync_required is true, discard the old list cursor and explicitly reload the manifest. Keep drafts and confirmations; do not automatically repeat a write after resync. Show File Path as a relative path. Slashes describe paths, not folder entities. Retain each File entry id, path, entry_version, and current_version_id from the manifest. File Path is a location; Entry Version belongs to one File entry. Two different File entries can have the same Entry Version. ## Upload and download bytes Use PUT /drive/libraries/{id}/files/content?path=&expected_entry_version=0 for a new upload. Send the File bytes directly, not JSON, multipart, or base64. Set Content-Type to the file MIME type or application/octet-stream and x-drive-content-sha256 to lowercase SHA-256 hex of the exact bytes, using crypto.subtle.digest. Let fetch set Content-Length. Use URLSearchParams for query values, including paths with spaces, Unicode, #, or &. If the path exists, show the current file and a distinct Replace confirmation. Only after explicit confirmation use its displayed Entry Version. Never replace as an automatic response to a failed create. Upload is path-based; do not claim it provides move/delete's identity guarantee. After any further conflict, retain the file and require review again. Success returns { entry, result }, where result can be created, updated, or unchanged. Only show success after a 2xx result. GET the content endpoint with path and, when selecting a retained current version, version_id from the manifest. Use Bearer fetch, then response.blob(), createObjectURL, a download link, and revokeObjectURL after the browser starts the download. Do not use a plain unauthenticated content link or execute downloaded HTML. Show progress or a busy state for uploads and downloads. Distinguish FILE_TOO_LARGE, HASH_MISMATCH, STORAGE_QUOTA_EXCEEDED, STORAGE_UNAVAILABLE, STORAGE_QUOTA_UNAVAILABLE, and rate limiting. Keep the File object on failure; quota and temporary provider errors do not authorize a blind retry. ## Confirmed move and soft-delete Freeze the displayed confirmation when the user opens the action. For move, POST /drive/libraries/{id}/files/move with { "entry_id": "", "from_path": "", "to_path": "", "expected_entry_version": 1 }. For delete, POST /drive/libraries/{id}/files/delete with { "entry_id": "", "path": "", "expected_entry_version": 1 }. Use the original positive safe-integer Entry Version, not the example value. Validate paths against live API rules before submission. Both actions stay within the selected Library and bind the original File entry identity, path, and version. A move destination collision is PATH_CONFLICT: show it without overwrite or automatic rename. VERSION_CONFLICT means the original confirmation is stale, even if another entry now has the same path and version. PURGE_IN_PROGRESS requires stopping the action. Do not substitute a new entry by path or fetch a new version to resend the same confirmation. Success returns the committed entry; update the correct identity in the list. Delete is soft-delete only. Library deletion, history, Trash restore, filesystem sync, offline use, arbitrary preview, and real-time collaboration are out of scope.