@finueva/drive
v0.29.0
Published
Public browser, Worker, and Node client for Finueva Drive
Readme
@finueva/drive
The public TypeScript client for Finueva Drive. The package is published to npm; the API remains Local Experimental.
The package is ESM-only. Node.js 22 is the exact minimum; package CI qualifies Node.js 22 and 24. Modern browsers and Cloudflare Workers use Web Fetch, Streams, URL, and AbortSignal.
Exports
The organization-access candidate adds ensureOrganizationWorkspaceAccess() to user clients. First provision storage through the existing ecosystem service. Then call this bodyless operation with the member's own organization credential. It admits only that signed User and returns the current workspace/root. It creates no membership or quota. Auth removal stops new credentials; already-issued credentials retain the existing maximum fifteen-minute revocation bound. See Organization workspace access. Consumer integration requires the approved SDK release.
| Import | Runtime |
| ----------------------- | ------------------------------------ |
| @finueva/drive | Browser client, errors, types |
| @finueva/drive/client | Browser client |
| @finueva/drive/server | Worker/Node user and service clients |
| @finueva/drive/core | Error values |
| @finueva/drive/types | Public operation types |
Visitor Sharing
Managers can recover a still-active link with getVisitorGrant({ workspaceId, itemId, grantId }) for Copy link or pending-email redispatch without creating another grant. List entries expose direct/inherited source and sourceItemId; use that source item for recovery/revocation. A child's General access must reflect inherited public links.
The visitor-sharing source additions require the matching Worker and additive migration 0013. Managers use createVisitorGrant, listVisitorGrants and revokeVisitorGrant through the normal user client. Creation takes { workspaceId, itemId, idempotencyKey, command }, where command explicitly selects either { mode: "anyone", role: "viewer", expiresAt } or { mode: "email", role: "viewer", email, expiresAt }. Unknown email never enables public access; visitor Editor is rejected. The app owns email dispatch.
createDriveVisitorClient({ baseUrl, token }) is exported from the root, /client and /server. It exposes only readItem, listChildren, createDownload and redeemInvitation. It needs no Auth account, omits cookies and referrers, and sends the capability in an Authorization header. Put recipient-page tokens in fragments, never query strings or path segments. Anyone-link tokens read directly; email challenges must first be redeemed with a new createVisitorSessionToken() value. Retain that exact session token for explicit retry, then create a visitor client with it. Do not use the one-time challenge for file reads.
Grants expire within 30 days, challenges within 24 hours and sessions within seven days. Downloads are current-version GET/HEAD descriptors with a 60-second lifetime. Revocation blocks new authorization but cannot recall an already-issued URL. See the visitor contract for limits, delivery retry, folder boundaries and consumer integration.
Browser
Approved ecosystem servers can separately use createDriveProvisioningClient from /server to call provisionWorkspace(command). Configure an exact environment and getProvisioningCredential provider using Auth service credentials, not User credentials. Commands require a stable provisioning ID, exact workspace reference, and explicit versioned storage entitlement. The client does not infer quotas, follow redirects, or retry. See Workspace Provisioning for ownership and recovery boundaries.
import { createDriveClient, createVerifiedOperationKey } from "@finueva/drive";
const drive = createDriveClient({
baseUrl: "https://drive.example.com",
getCredential: async ({ signal }) => {
signal.throwIfAborted();
const credential = await auth.getDriveCredential({ workspace: { type: "personal" } });
signal.throwIfAborted();
return credential.token;
},
timeoutMilliseconds: 30_000,
});
const { data: workspace } = await drive.getCurrentWorkspace();
const page = await drive.listFolderChildren({
workspaceId: workspace.workspaceId,
itemId: workspace.rootItemId,
limit: 50,
});
const matches = await drive.searchWorkspaceItems({
workspaceId: workspace.workspaceId,
query: "Project",
limit: 50,
});
const completionIdempotencyKey = crypto.randomUUID();
const uploaded = await drive.uploadVerifiedFile({
workspaceId: workspace.workspaceId,
target: { type: "create", parentItemId: workspace.rootItemId, name: file.name },
source: file, // Blob; File is supported without being required
mediaType: file.type || "application/octet-stream",
protectionClassification: "ordinary",
idempotencyKey: createVerifiedOperationKey(),
completionIdempotencyKey,
onProgress: ({ phase, confirmedByteLength, totalByteLength }) => {
renderProgress(phase, confirmedByteLength, totalByteLength);
},
});The provider runs once for every request. The SDK sends its result only as Authorization: Bearer, always sets credentials: "omit" and cache: "no-store", and never reads cookies. Configuration does not accept default headers. Credential acquisition, Fetch, and response handling share one bounded request deadline: 30 seconds by default, configurable from 1 millisecond through 120 seconds. The object PUT behind uploadVerifiedFile is not bound by that default; see Upload progress and stall detection.
auth is a configured @finueva/auth client. Select the exact personal or organization workspace. Native Fetch needs no wrapper.
Approve the exact application origin in Drive's DRIVE_BROWSER_ORIGINS policy and configure archive CORS separately. API responses must expose X-Request-ID and ETag. See browser setup.
Worker And Node
import { createDriveServerClient } from "@finueva/drive/server";
const drive = createDriveServerClient({
baseUrl: "https://drive.example.com",
credential: ({ signal }) => requestCredential(request, signal),
});Create a client per request when using user authority. A fixed credential string is accepted for an explicitly scoped credential, but the SDK does not create, refresh, persist, or infer sessions, cookies, provider tokens, or service authority.
The server client exposes the current API operations and streaming downloadItem. Browser and user clients additionally expose Blob-based uploadVerifiedFile. The server client accepts explicit prepare, recover and publish commands rather than Blob orchestration.
Workspace Discovery
getCurrentWorkspaceSources() returns the actual managed volume separately from
known provider kinds. It does not invent external connections or provider health.
const recent = await drive.listWorkspaceItems({ workspaceId, view: "recent", limit: 25 });
const shared = await drive.listWorkspaceItems({ workspaceId, view: "shared" });
const trash = await drive.listWorkspaceItems({ workspaceId, view: "trash" });
const usage = await drive.getWorkspaceUsage({ workspaceId });Recent means recorded modification time, not viewing activity. Historical unknown
timestamps and creator user IDs stay null; unknown modification times are excluded
from recent results until a real mutation. Shared lists actual incoming direct
user/team grants, not inherited descendants or all workspace-visible items. Trash
requires delete authority and retains current ETags for the existing mutation
methods. Continuations are encrypted, principal/root/filter-bound and short-lived;
restart on stale-page errors. Optional kind and creatorUserId filters apply on
the service before pagination. Quota reads require usage:read and return exact
decimal byte strings, clamped availability and unresolved-reservation evidence.
These source additions require backend migration 0010, deployment and SDK release.
Operations
Methods use the current API operation IDs; downloadItem and uploadVerifiedFile are transport helpers:
createVisitorGrantgetVisitorGrantlistVisitorGrantsrevokeVisitorGrantgetFileAccessContextcreateEditingSessionrenewEditingSessioncloseEditingSessionrevokeEditingSessionlistOwnedItemstransferItemOwnershiparchiveItemgetWorkspaceSharingPolicyupdateWorkspaceSharingPolicyregisterItemRecipientlistItemSharingPeoplelistIncomingSharesgetCurrentWorkspaceensureOrganizationWorkspaceAccesslistWorkspaceItemsgetCurrentUserWorkspaceUsagegetWorkspaceUsagegetCurrentWorkspaceSourcesgetItemsearchWorkspaceItemslistFolderChildrencreateFolderupdateItemtrashItemrestoreItemdeleteItemlistItemVersionsgetItemVersionrestoreItemVersioncreateDownloaddownloadItemlistItemPermissionsreadEffectiveItemPermissionscreateItemPermissioncreateInternalItemPermissionupdateItemPermissionrevokeItemPermissionrevokeInternalItemPermissionprepareVerifiedTransferpublishVerifiedTransferrecoverVerifiedTransferuploadVerifiedFile
getCurrentWorkspace maps the credential's selected Auth context to the provisioned Drive workspace and canonical managed root. It accepts only an optional { signal } input, never provisions as a side effect, and returns no entitlement, credential, or private persistence fields.
getItem requires items:metadata:read. Absent, unauthorized, tombstoned, or unpublished items are concealed. Folders have currentVersion: null; files expose their current version. The strong ETag matches the item revision. Media type and protection classification are untrusted metadata, not content authority. Internal storage/provider state is never exposed.
searchWorkspaceItems matches NFC, case-sensitive name prefixes: 1-64 Unicode code points, at most 255 UTF-8 bytes, without C0/C1 controls or U+2028/U+2029. Frozen { items, nextCursor } pages default to 50 rows ordered by binary name then item ID. Results add volumeId and provider: "managed" to item metadata. An empty page cannot have a cursor.
Every mutation requires the API's explicit idempotency key when the operation defines one. Item lifecycle mutations also require the current strong item ETag as ifMatch; grant mutations use their documented collection or grant precondition. Mutations are never retried automatically. Sparse updates omit undefined properties and preserve explicit null expiry.
Successful calls return { data, status, requestId, validators }. A successful response must carry one valid X-Request-ID; canonical API errors must carry the same request ID in their header and body. Any missing, malformed, or mismatched value is an invalid_response. The only exposed validators are etag, permissionCollectionEtag, and permissionGrantEtag.
updateItem renames and moves one stable item ID. A move immediately changes inherited-grant ancestry; Drive does not copy grants to the item. trashItem retains a 30-day tombstone, restoreItem requires the original parent to remain active, and deleteItem starts permanent archive cleanup only after retention expires and no hold applies. Permanent deletion is currently bounded to at most 25 subtree items and 25 total versions; larger trees fail unavailable until a durable workflow exists. Exact retries resume retained deletion checkpoints and never trigger blind provider duplication.
listItemVersions pages immutable current/superseded versions, at most 50 per page. getItemVersion verifies all three workspace/item/version IDs and conceals unavailable history. Malformed retained state fails unavailable. restoreItemVersion atomically changes the current version and item revision without copying or overwriting archive bytes.
createDownload returns a short-lived archive descriptor for the current or an explicit immutable version, one visible restore-pending result for cold content, or a no-body 304 operation result whose validators retain the source ETag. Full, closed, open, and suffix ranges use the selected archive object's strong ETag; malformed or multiple ranges are served as full representations, while valid non-overlapping ranges fail with range_not_satisfiable. downloadItem follows a descriptor with no Drive credential, ambient browser credentials, or referrer and returns the provider body as a backpressured Response; it preserves the same 304 and cold-restore results without a provider request. It validates exact status, ETag, identity encoding, length, and Content-Range, cancels malformed upstream bodies, and keeps caller cancellation/deadline authority active until the stream closes or is cancelled. It never returns Blob, ArrayBuffer, text, or base64 whole-file convenience values.
The visitor client also exposes downloadItem(input?: VisitorDownloadInput): Promise<Response>
with optional itemId, method, range and signal. It uses the same descriptor
validator and streaming transport, including redirect rejection and exact streamed
byte counts. It does not hash or clone the complete response. For preview freshness,
capture createDownload({ itemId, method: "HEAD" }) before and after a bounded read;
compare descriptor response.versionId, byteLength, contentType and strong etag,
and compare the downloaded response ETag to that baseline. Reauthorization failure or
changed evidence discards the preview. These are content validators, not item lifecycle
ETags. Visitors remain viewer-only and cannot query private permissions or restore
cold content.
Verified transfers
See the coordinated SDK cutover for consumer and qualification boundaries.
uploadVerifiedFile prepares one immutable whole-object transfer, streams the exact Blob directly to its signed provider URL, and publishes only after Drive independently verifies the retained provider receipt. The ordinary path is prepare → PUT → publish; it does not poll an upload job or read metadata back. The final published result includes current item/version identity, exact bytes, full SHA-256, media type, protection classification and provider ETag.
A create target is { type: "create", parentItemId, name }. A replacement is { type: "replace", itemId, currentVersionId }; the explicit current version is the write precondition. Editing clients use the same replacement protocol with their scoped editing credential and audience. getFileAccessContext remains available for current file access and editing context.
Generate the prepare key with createVerifiedOperationKey(), then retain that key, the completion key and the exact source/intent for retry. The key has a fixed 24-hour replay deadline; new keys permit bounded client clock skew. Retired, expired or incorrectly issued keys return actionable closed reasons. They never authorize silently creating a new operation. An interrupted prepare or publication permits at most one immediate retry for network errors/timeouts. A lost PUT response or immutable create-only 412 proceeds to publication without a caller ETag; Drive must independently prove the exact retained object. A published prepare replay performs no provider PUT. Recovery returns only the current capability for that original admitted operation.
The generic helper supports zero through 1 GiB and incrementally hashes in 256 KiB windows. It does not materialize the whole source as an ArrayBuffer or base64 string. Signed provider headers require whole SHA-256, exact content type and length, cache-control: no-store, no-transform, immutable if-none-match: *, and the complete encryption tuple when present. Requests omit user authority, cookies and referrers; redirects are refused. The browser XMLHttpRequest progress path is the exception to the referrer and redirect guarantees: it applies the page's referrer policy and reports a followed redirect as refused (see Upload progress and stall detection). The SDK validates the returned canonical receipt against the source and requested classification. An ETag is provider identity, not a substitute for the checksum.
Progress callbacks are immutable and isolated from the operation. Uploading all bytes enters completing; success requires the canonical published result. See Upload progress and stall detection for byte-level progress. Caller cancellation stops further orchestration and reports aborted. It does not call a retired abort route or release a capacity hold: the service's bounded retention worker reconciles the original immutable operation.
The old multipart/session write and control API is intentionally absent: createUpload, getUploadStatus, createUploadPartDescriptor, recordUploadPart, completeUpload, abortUpload, createFileVersionUpload, commitFileVersion, uploadFile and uploadFileVersion. This is a coordinated breaking cutover. Deploy only with the matching current service and upgraded consumers or an isolated v2 deployment. Historical reads, downloads, editing, grants, visitor access and immutable version metadata remain available; absence of a new accounting authority is never treated as permission to adopt legacy data.
Upload progress and stall detection
onProgress reports bytes while hashing and while sending, in the order hashing, preparing, uploading, completing. phase, confirmedByteLength, totalByteLength and the part counts keep their meaning; stage, hashedByteLength and sentByteLength are optional additions. Every counter is non-decreasing, a stage never repeats or goes back, and a byte stage reports at most about 200 times.
await drive.uploadVerifiedFile({
...intent,
onProgress: ({ stage, hashedByteLength, sentByteLength, totalByteLength }) => {
const done = stage === "hashing" ? hashedByteLength : sentByteLength;
render(stage, (done ?? 0) / totalByteLength);
},
});Sent bytes need a transport that can observe an upload. With no custom fetch, browsers and webviews send a cross-origin object PUT through XMLHttpRequest, which reports real bytes. Node, Workers, a same-origin PUT, upload: { transport: "fetch" } and a custom fetch report only 0 and then the whole file at completing. If you supply a fetch, for example to fence each dispatch, make createUploadProgressFetch(fetch) its innermost transport to get the same progress. XMLHttpRequest cannot refuse a redirect before following it or set referrerPolicy: "no-referrer"; keep transport: "fetch" where those matter more than progress.
The object PUT has no fixed 30-second deadline any more. It ends when the capability expires (at most five minutes after admission, and publication is refused after that), when a measured transfer sends no new byte for upload.stallTimeoutMilliseconds (default 30 000), or at upload.maxDurationMilliseconds when set. An explicit timeoutMilliseconds still caps the PUT unless upload.maxDurationMilliseconds is set. A transfer that stops before its last byte rejects with timeout or network_error and is not published; retry with the original keys. The SDK builds no multipart or resumable upload, so a file must fit in the capability window at the available uplink speed. See the verified transfer guide.
const drive = createDriveClient({
baseUrl,
getCredential,
upload: { stallTimeoutMilliseconds: 20_000, maxDurationMilliseconds: 240_000 },
});Errors
Failures throw DriveError. Known canonical API envelopes preserve their closed API code, HTTP status, and request ID. SDK-owned codes are:
credential_unavailableinvalid_inputnetwork_errortimeoutabortedinvalid_response
Messages are fixed. details is empty or contains exactly one reason from DRIVE_API_ERROR_REASONS, read through typed error.details.reason. Transfer replay failures distinguish transfer_operation_retired, transfer_operation_expired and transfer_operation_clock_skew; callers must not substitute a new key automatically. Unknown details or reasons are invalid_response. Every operation validates input before credential lookup or Fetch. Error output never includes response bodies, URLs, credentials, provider exceptions or submitted values. JSON reads have byte, chunk and deadline bounds and are cancelled when rejected.
Base URL
Use an absolute HTTPS URL. Plain HTTP is accepted only for localhost, 127.0.0.1, or [::1]. Credentials, query strings, fragments, and other protocols are rejected.
Release Status
Releases are selected by reviewed Changesets. Qualification builds the exact compiled Drive Worker, obtains /api/v1/openapi.json through Miniflare, checks generated types byte-for-byte, packs the package, and installs that tarball into clean external projects with lifecycle scripts disabled.
@finueva/drive is published on npm. The source repository is private, so public provenance is unavailable. ADR 0016 records the completed maintainer-2FA bootstrap; later releases use the protected OIDC workflow and npm approval without a stored npm token.
