ccs-files-user-client
@central-code-station/ccs-files-user-client is the File Service client for a web app’s signed-in user. It runs the upload protocol for you (a ticket, one PUT or parallel parts straight to the store, with progress, retries, pause, resume and cancel, then the completion) and wraps the rest of the API the user may call: folders, files, versions, search, previews and download URLs, access entries, share links and the trash. It never holds a client secret; your backend is ccs-files-client. This page covers installation, configuration and the behaviour you rely on; every type and signature is in the generated reference, the release history in the changelog. For a walk-through, start with the web quickstart.
Install
Section titled “Install”Published privately on GitHub Packages, like the Node client. Map the scope in your project’s .npmrc and authenticate with a GitHub token that has read:packages, kept in your user ~/.npmrc or your CI secrets, never in the repository:
@central-code-station:registry=https://npm.pkg.github.comnpm install @central-code-station/ccs-files-user-clientNo runtime dependencies. Three entries: the framework-free core (@central-code-station/ccs-files-user-client), Angular (…/angular, Angular 17 or newer, Signals) and React (…/react, React 18 or newer, hooks). Angular and React are optional peer dependencies. The core needs fetch and Blob; it uses XMLHttpRequest for the store PUTs so it can report bytes in flight, and falls back to fetch (progress per finished part) where there is none.
Configure
Section titled “Configure”The client gets the user’s File Service token from your backend’s token route:
import { createTokenProvider, FilesClient } from '@central-code-station/ccs-files-user-client';import { getAppAccessToken } from '../shared/web-app';
// Your backend's token route (see "User tokens"), called with your app's own session.export const tokens = createTokenProvider({ url: '/api/files-token', headers: async () => ({ Authorization: `Bearer ${await getAppAccessToken()}` }), // credentials: 'include', // instead of headers, for a cookie session});
// The route answers api_url, tenant_id and application_id, so the client configures itself. The token is cached until// a minute before it expires, renewed with one request at a time, and asked for again once after a 401.export const files = await FilesClient.fromTokenRoute(tokens);
// Or configure it yourself:// new FilesClient({ apiUrl, tenantId: 'fitness-co', applicationId: 'fitness-app', getAccessToken: tokens.getAccessToken });// (apiUrl is optional: omitted, the production File Service.)
// On sign-out: cancels the uploads in progress, clears the URL cache and the token.export const signOut = () => files.signOut();createTokenProvider(options):
| Option | Required | Meaning |
|---|---|---|
url |
yes | Your token route, for example /api/files-token |
headers |
no | Headers for the route (your app’s own session), an object or a function read on every request |
credentials |
no | The fetch credentials mode for a cookie session, for example 'include' |
method |
no | 'GET' (default) or 'POST' |
expiryMarginSeconds |
no | Renew this long before expires_in runs out. Default 60 |
fetch |
no | Your own fetch |
It caches the token, shares one pending request between callers, and clear() forgets it. FilesClient.fromTokenRoute(tokens, overrides?) asks the route once and configures the client from its optional api_url, tenant_id and application_id; anything in overrides wins.
new FilesClient(options):
| Option | Required | Meaning |
|---|---|---|
apiUrl |
no | Default: production, https://files.centralcodestation.com/api/v1. A browser reads no environment: pass it for another environment, or use fromTokenRoute, whose route sends api_url |
tenantId, applicationId |
yes | The context. One client per context and signed-in user |
getAccessToken |
yes | Returns the user’s File Service token; normally tokens.getAccessToken |
concurrency |
no | Parts in flight per multipart upload. Default 4 |
checksum |
no | Send content_sha256, computed in the browser, for files up to 256 MiB. Default false (it reads the file twice) |
maxRetries |
no | Retries of an API request for 5xx and network errors. Default 3; 0 disables |
xhrFactory |
no | Builds the XMLHttpRequest of each store PUT |
fetch |
no | Your own fetch, for tests or server rendering |
debug |
no | Log to the console. Default false |
Request and response objects are the service’s snake_case wire format, unchanged; constructor options and the library’s own option objects are camelCase. The wire type File shadows the DOM’s File: import it as FileRecord when you need both.
What it covers
Section titled “What it covers”| Area | Calls |
|---|---|
| Uploads | upload(file, input?, options?), resumeUpload(snapshot, file), uploads.active(), uploads.views(), uploads.on('change', …); static FilesClient.uploadFromTicket(ticket, file, options?) |
| Files | files.get, update, move, copy, delete, restore, download, downloadUrl, myPermissions, waitUntilReady |
| Legal hold | files.hold.place(fileId, { reason, until? }), files.hold.release(fileId, { reason? }), holds.list({ placed_by?, after?, limit? }), for users with files.manage (see Legal hold) |
| Versions and renditions | files.versions.list, get, upload, restore, pin, delete; files.renditions.list, regenerate |
| Showing files | preview(fileId, { version }), urls(fileIds, { rendition, disposition }), invalidate(fileId), and the helper playbackSources(preview) |
| Folders | folders.root, create, get, children, path, update, move, copy, delete, restore; releaseManagement for the tenant owner (see Application-managed folders) |
| Search | search(query) (in: ['name'] matches names that start with the query; keywords and content match whole words; highlights), tags(query) |
| Sharing | permissions.get, replace, grant, revoke, roles; shares.create, list, revoke |
| The rest | trash.list, me.capabilities(), settings.public(), signOut(), dispose(); the helpers isReadOnly(item) and isFolderManagedError(error) |
New in 1.2.0
Section titled “New in 1.2.0”- Application-managed folders:
FileandFoldergainmanagedandmanaged_by(ManagedBy);isReadOnly(item)tells whether people may change an item;folders.releaseManagement(folderId, { reason })is the tenant owner’s emergency release;FilesForbiddenError.managedFolderIdandisFolderManagedError(error)read a 403folder_managed. See below.
New in 1.1.0
Section titled “New in 1.1.0”- Legal hold:
File.holdis typed (FileHold:reason,placed_by,placed_at,until);files.hold.place,files.hold.releaseandholds.listfor a user withfiles.manage(anyone else getsmissing_capability). Deleting a version of a held file rejects withFilesConflictErrorfile_on_hold, the hold inerror.body.hold. - Search wording:
in: ['name']is a prefix match, ignoring case;totalis exact up to 10,000 andnullabove (Search).
Uploads
Section titled “Uploads”client.upload(file, input, options) returns an UploadHandle at once. input is the upload request without name, size and mime_type, which default to the file’s own. The handle has id, name, size, fileId and uploadId (once the ticket exists), state, progress ({ loaded, total, parts: { done, total } }, bytes in flight included), error, done (a promise of the file), pause(), resume(), cancel(), on('progress' | 'state' | 'error', handler) (returns an unsubscribe function) and serialize().
| Behaviour | Rule |
|---|---|
| States | creating, uploading, paused, completing, done, failed, cancelled |
| Refusals | A ticket refused for quota_exceeded, file_too_large or type_not_allowed fails the upload before any byte is sent |
| Single PUT | Exactly the ticket’s headers, nothing else: no credentials, no Authorization |
| Multipart | concurrency parts in flight; the ticket’s first part URLs, then batches of 100; each PUT retried 5 times with backoff (1 to 30 seconds) on network errors and 5xx; a part URL that expired is requested again once; every part’s ETag collected |
| Pause | Stops the PUTs in flight; their bytes are sent again on resume. No effect while completing |
| Failure | state: 'failed', done rejects; the ticket is kept, so resume() continues and done becomes a new promise |
| Cancel | Stops the PUTs and releases the ticket; done rejects with UploadCancelledError |
| Resume after a reload | resumeUpload(snapshot, file) with the same file picked again: an open multipart ticket continues from the parts the store holds, a completed one resolves with its file, anything else starts over. Another file (size or lastModified) throws UploadFileMismatchError |
| Snapshots | A client upload’s snapshot holds no signed URL |
done resolves when the upload is complete: the file is available and processing has started. files.waitUntilReady(fileId) polls until it finished, or listen for file.ready through the Notification Service.
Uploads from a ticket of your backend
Section titled “Uploads from a ticket of your backend”FilesClient.uploadFromTicket(ticketOrSnapshot, file, options) runs the same engine on a ticket your backend created with the Node client, with no File Service token in the app. done resolves with { upload_id, file_id, method, parts } for your backend to complete. options.partUrls({ from, count }) asks your backend for more part URLs (above the first 100, or when one expired); without it a missing or expired part URL fails the upload. Cancelling only stops the PUTs: your backend releases the ticket. A ticket snapshot keeps the ticket as handed out, signed URLs included, because nothing else could mint them again: keep it with the user’s session data. See the web quickstart.
Previews and URLs
Section titled “Previews and URLs”preview(fileId) answers what to show (kind: image, video, audio, pdf, text or none) with a signed URL; urls(fileIds, { rendition }) mints a feed page in one request, one entry per id in order. Both are cached per file, version, rendition and disposition until a minute before the URLs expire; invalidate(fileId) drops a file’s entries, and so do the client’s own updates, deletes, version uploads and restores. files.download(fileId) always mints a fresh URL (each download is audited) and points the window at it; { navigate: false } only returns it. playbackSources(preview) lists the playable URLs of a video, best first: the ready mp4_1080p and mp4_720p renditions, then the URL the service picked. See Previews and renditions.
Application-managed folders
Section titled “Application-managed folders”A folder a tenant’s backend marks managed, and everything below it, is read-only for every user, the tenant owner included: uploads, versions, renames, moves, edits, copies into it, trash, restore, purge, access entries and share links answer 403 folder_managed. Viewing, downloads, previews, search, copying out and legal holds work as usual. my_actions still lists what the user holds, so hide the editing controls when isReadOnly(item) is true. See Application-managed folders.
import { isFolderManagedError, isReadOnly, type File, type Folder } from '@central-code-station/ccs-files-user-client';import { files } from './files-client';
// Decide the editing controls from `managed`, not from my_actions alone.export function canRename(item: File | Folder): boolean { return !isReadOnly(item) && item.my_actions.includes('edit');}
// "Managed by Fitness": the name, or the client id when the mark has none.export function managerLabel(item: File | Folder): string | null { return item.managed_by ? `Managed by ${item.managed_by.name ?? item.managed_by.client_id}` : null;}
// A change can still meet a mark set after the item was loaded: show why.export async function rename(fileId: string, name: string): Promise<string | null> { try { await files.files.update(fileId, { name }); return null; } catch (error) { if (isFolderManagedError(error)) return `Read-only: managed through folder ${error.managedFolderId}`; throw error; }}
// The tenant owner's emergency release, on the folder that carries the mark.export async function release(item: File | Folder, reason: string): Promise<Folder | null> { const { tenant_owner } = await files.me.capabilities(); if (!tenant_owner || !item.managed_by) return null; return files.folders.releaseManagement(item.managed_by.managed_folder_id, { reason });}folders.releaseManagement(folderId, { reason }) answers the Folder. It is for the tenant owner only (anyone else gets tenant_owner_required), needs a reason of 1 to 500 characters, and must name the folder that carries the mark, managed_by.managed_folder_id (400 invalid_request otherwise). It is never retried.
Angular and React
Section titled “Angular and React”| Entry | API |
|---|---|
…/angular |
createFiles(optionsOrClient, destroyRef?) → { client, uploads, upload, resumeUpload, signOut }; uploads is a Signal of immutable upload views. A plain factory: call it from your own service |
…/react |
useFiles(options) → { client, error } (options, { client } or { tokens, overrides }); useUpload(client) → { start, resume, uploads }; usePreview(client, fileId, { version, refresh }) → { preview, loading, error, reload } |
A client the hook or the factory built is disposed with the component or the injector: uploads in flight pause and their tickets stay resumable.
Errors and retries
Section titled “Errors and retries”Every API failure is a FilesApiError (status, code = the body’s error, body, retryAfterSeconds) of the class that matches its status, the same classes as the Node client: FilesUnauthorizedError (401), FilesForbiddenError (403, for example folder_managed with managedFolderId), FilesNotFoundError (404), FilesConflictError (409, for example scan_pending or file_on_hold), FilesPreconditionFailedError (412), FilesPayloadTooLargeError (413), FilesUnprocessableError (422), FilesRateLimitedError (429), FilesNotImplementedError (501). FilesContextError: no tenant or application. FilesUploadError (partNumber, uploadId, cause): the bytes could not be moved; its subclasses UploadFileMismatchError (code: 'upload_file_mismatch') and UploadCancelledError (code: 'upload_cancelled').
- Token: a 401 asks
getAccessToken({ forceRefresh: true })once and replays; a second 401 is thrown. - Retries: 5xx (not 501) and network errors, up to
maxRetries, on reads, idempotent writes and the writes that create (folders, copies, tickets, shares), which carry anIdempotency-Keyfixed before the first attempt. 4xx answers, 429 included, are never retried. - Sign-out:
signOut()cancels the uploads in progress, clears the URL cache and the token provider, and disposes the client.dispose()only pauses the uploads.
Module formats
Section titled “Module formats”ES modules and CommonJS with type declarations, for the three entries, chosen by exports. It works with any bundler and with TypeScript’s nodenext, bundler and legacy node resolution, and unused entries tree-shake away. Load it one way per app: the two builds are separate copies and instanceof does not cross them.