npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@colixsystems/filestore-client

v0.5.0

Published

Typed client for the AppStudio Filestore subsystem (folders, file archive, previews, presigned object streaming, sharing, trash) at /filestore/*, plus BankID file signing at /signatures. snake_case JSON wire contract, no transform.

Readme

@colixsystems/filestore-client

Typed client for the AppStudio Filestore subsystem — the file archive mounted at /filestore/*. It covers:

  • files — the file archive: list, get metadata, multipart upload, rename/move, soft-delete, and preview via files.{list,get,upload,update,remove,preview}.
  • folders — folder tree CRUD via folders.{list,create,update,remove}.
  • shares — folder-scoped permissions (the Filestore ACL) via shares.{list,create,remove,sharedWithMe}. Permissions live on folders only (no file-level grant); each grant gives a user/group a VIEW/DOWNLOAD/MANAGE tier on a folder. Folder visibility (INHERIT/RESTRICTED) is set via folders.update(id, { visibility }).
  • trash — the recycle bin: list trashed items, restore, and permanent purge via trash.{list,restoreFile,restoreFolder,purgeFile,purgeFolder}.
  • presigned object streamingobjectUrl(token) builds the byte-stream URL and fetchObject(token) fetches it.

What this is NOT. This is the Filestore subsystem client for external / server-side integrations and the widget runtime. It is not the Asset Manager. The widget host injects an instance into WidgetContext.filestore, where the SDK hooks useFilestoreFiles / useFilestoreFolders / useFileSignature read it (mirroring how @colixsystems/datastore-client backs WidgetContext.datastore). For non-widget use you instantiate it yourself with createFilestoreClient({ baseUrl, getToken, getTenantId }).

Status

v0.5.0 — pre-publish. Not yet published to npm.

0.5.0 (breaking + additive): shares are now folder-scoped (the Filestore ACL, REQ-FSH). The polymorphic target_type/target_id pair is gone: shares.list({ folder_id }) lists a folder's grants, shares.create({ folder_id, subject_type, subject_id, permission }) grants a VIEW/DOWNLOAD/MANAGE tier (re-granting updates the tier), shares.remove(id) revokes. Granting/listing/revoking is a MANAGE action on the folder (the backend 403s a non-manager, 404s an unseeable folder). shares.sharedWithMe() now returns { folders, shares } (no files — permissions are folder-scoped). Folder visibility (INHERIT/RESTRICTED) is set via the existing folders.update(id, { visibility }). Additive: signatures.roster(fileId, { limit, offset }) — the MANAGE-gated signer roster (GET /signatures/roster): the file's folder audience, each annotated signed/not-signed, { data, meta } with meta.signed_count. Also: signatures.list(fileIds) is now self-scoped (the caller's own signatures only) — the backend contract tightened; the method signature is unchanged.

0.4.0 (fix): presigned_url on every file/preview the client returns is now absolutized against the client's baseUrl origin. The backend mints presigned URLs as a relative path (/api/v1/filestore/object?token=…), which only resolves same-origin — so file thumbnails/downloads broke for a cross-origin web build (dev :5173 vs API :5000) and for the native export (a device has no page origin). The client now glues its own origin onto the relative path (leaving already-absolute URLs and same-origin relative bases untouched), so URLs work everywhere the client runs. Applies to files.{list,get,upload,update,preview}, trash.list, and shares.sharedWithMe. No method signature changed.

0.3.0 (additive): added signatures.list(fileIds) — a batch "is this file signed?" lookup (GET /signatures?subject_kind=file&file_ids=csv) returning the { data, meta } envelope where data is the latest completed FileSignatureSummary ({ signature_id, subject_file_id, signer_name, signed_at }) per accessible, signed file. Lets a file browser mark already-signed files without an N+1. No existing method changed.

0.2.0 (additive): added the signatures namespace (REQ-SIGN) — BankID file signing over /signatures (NOT /filestore/*): signatures.initiate(fileId){ signature_id, order_ref, auto_start_token, qr, status }; status(id) / cancel(id) / verify(id) key off the returned signature_id. The backend opens a sign order against the FsFile, hashes its bytes server-side, and verifies the proof offline before storing it. snake_case verbatim. No existing namespace changed. This release is also the first injected into WidgetContext.filestore (read by the SDK filestore hooks), and corrects the multipart upload field-name docs to snake_case (space_type / owner_id / folder_id), matching what the controller actually reads.

Contract: snake_case JSON, no transform

The wire format for JSON bodies and query params is snake_case in both directions and that is the client contract too. The SDK does no case mapping:

  • JSON request bodies are sent snake_case verbatim — you pass { space_type, owner_id, parent_folder_id }, not { spaceType, ownerId, parentFolderId }.
  • Query params are sent snake_case verbatimspace_type, owner_id, folder_id, parent_folder_id, file_id, type, q, limit, offset.
  • Response objects are returned snake_case verbatim — you read row.created_at, row.owner_id, share.folder_id. (The one value the client rewrites is presigned_url, resolved to an absolute URL against the baseUrl origin — see 0.4.0 above. It's a URL-origin fix, not a case transform.)
  • The list endpoints (files.list, folders.list, shares.list, signatures.list, signatures.roster) return the { data, meta } envelope verbatim (no unwrap). trash.list returns { files, folders } and shares.sharedWithMe() returns { folders, shares } — those are NOT { data, meta } envelopes.

Multipart upload field names

POST /filestore/files is multipart/form-data, which bypasses the backend's snake_case request middleware, so the file controller reads the form field names verbatim — and it reads them snake_case, the same as every JSON body. files.upload(formData) sends your FormData verbatim — you must use exactly these field names:

| FormData field | Required | Notes | | --- | --- | --- | | file | yes | The binary file part. | | space_type | yes | e.g. PERSONAL / PROJECT. | | owner_id | yes | The space owner id. | | folder_id | no | Target folder id; omit for the space root. |

Multipart fields travel in whatever casing the controller reads them in (multipart is not transformed); here that is snake_case, matching the rest of the wire contract.

Public API

import {
  createFilestoreClient,
  FilestoreError,
  NotFoundError,
  ForbiddenError,
  ValidationError,
  GoneError,
  PayloadTooLargeError,
  UnsupportedMediaTypeError,
  RateLimitedError,
  ServerError,
} from "@colixsystems/filestore-client";

const client = createFilestoreClient({
  baseUrl: "https://api.appstudio.io/api/v1",
  getToken: () => "Bearer ...",        // Studio or AppUser JWT; "Bearer " prefix added if missing
  getTenantId: () => "tenant_abc",     // x-tenant-id header
  getRequestHeaders: ({ namespace, operation }) => ({ "X-Trace": "..." }), // optional
  // fetchImpl defaults to globalThis.fetch
});

// Files — list is { data, meta } verbatim; query keys are snake_case.
const { data: files, meta } = await client.files.list({
  space_type: "PERSONAL",
  owner_id: "user_123",
  folder_id: "folder_1",   // optional
  q: "invoice",            // optional
  type: "image",           // optional
  limit: 50,
  offset: 0,
});

const file = await client.files.get("file_1");

// Upload — multipart. FormData field NAMES are snake_case (see table above).
const fd = new FormData();
fd.append("file", blob, "photo.jpg");
fd.append("space_type", "PERSONAL");
fd.append("owner_id", "user_123");
fd.append("folder_id", "folder_1"); // optional
const uploaded = await client.files.upload(fd);

await client.files.update("file_1", { name: "renamed.jpg", folder_id: "folder_2" });
await client.files.remove("file_1");          // soft delete -> trash
const preview = await client.files.preview("file_1");

// Folders
const { data: folders } = await client.folders.list({
  space_type: "PROJECT",
  owner_id: "project_9",
  parent_folder_id: "folder_root", // optional
});
await client.folders.create({ name: "Docs", space_type: "PERSONAL", owner_id: "user_123", parent_folder_id: null });
await client.folders.update("folder_1", { name: "Renamed", parent_folder_id: "folder_0" });
await client.folders.remove("folder_1");

// Shares
const { data: shares } = await client.shares.list({ target_type: "FILE", target_id: "file_1" });
await client.shares.create({
  target_type: "FILE",
  target_id: "file_1",
  subject_type: "USER",
  subject_id: "user_456",
  permission: "READ",
});
await client.shares.remove("share_1");
const inbox = await client.shares.sharedWithMe(); // { files, folders, shares } — AppUser only

// Trash
const { files: trashedFiles, folders: trashedFolders } = await client.trash.list({
  space_type: "PERSONAL",
  owner_id: "user_123",
});
await client.trash.restoreFile("file_1");
await client.trash.restoreFolder("folder_1");
await client.trash.purgeFile("file_1");        // permanent
await client.trash.purgeFolder("folder_1");     // permanent, recursive

// Presigned object streaming — the HMAC token IS the auth (no headers sent).
const href = client.objectUrl(token);           // build a URL for <img>/<a>
const res = await client.fetchObject(token);     // fetch the raw byte stream (Response)

// Signatures (BankID file signing — REQ-SIGN; hits /signatures, not /filestore)
const order = await client.signatures.initiate("file_1"); // { signature_id, qr, ... }
const st = await client.signatures.status(order.signature_id);
await client.signatures.cancel(order.signature_id);
const verdict = await client.signatures.verify(order.signature_id); // { valid, content_status, ... }

Surface

| Method | HTTP | Returns | | --- | --- | --- | | files.list(query) | GET /filestore/files | Page<FilestoreFile> ({ data, meta }) | | files.get(id) | GET /filestore/files/{id} | FilestoreFile | | files.upload(formData) | POST /filestore/files (multipart) | FilestoreFile | | files.update(id, body) | PUT /filestore/files/{id} | FilestoreFile | | files.remove(id) | DELETE /filestore/files/{id} | void (soft delete) | | files.preview(id) | GET /filestore/files/{id}/preview | Preview | | folders.list(query) | GET /filestore/folders | Page<Folder> | | folders.create(body) | POST /filestore/folders | Folder | | folders.update(id, body) | PUT /filestore/folders/{id} | Folder | | folders.remove(id) | DELETE /filestore/folders/{id} | void | | shares.list(query) | GET /filestore/shares | Page<Share> | | shares.create(body) | POST /filestore/shares | Share | | shares.remove(id) | DELETE /filestore/shares/{id} | void | | shares.sharedWithMe() | GET /filestore/shared-with-me | { files, folders, shares } (AppUser only) | | trash.list(query) | GET /filestore/trash | { files, folders } | | trash.restoreFile(id) | POST /filestore/trash/files/{id}/restore | FilestoreFile | | trash.restoreFolder(id) | POST /filestore/trash/folders/{id}/restore | Folder | | trash.purgeFile(id) | DELETE /filestore/trash/files/{id} | void (permanent) | | trash.purgeFolder(id) | DELETE /filestore/trash/folders/{id} | void (permanent, recursive) | | objectUrl(token) | builds GET /filestore/object?token=... | string (no request) | | fetchObject(token) | GET /filestore/object?token=... | Response (raw byte stream) |

Query params (snake_case on the wire)

| Endpoint | Required | Optional | | --- | --- | --- | | files.list | space_type, owner_id | folder_id, q, type, limit, offset | | folders.list | space_type, owner_id | parent_folder_id, q, limit, offset | | shares.list | target_type, target_id | limit, offset | | trash.list | space_type, owner_id | — |

Factory options

| Option | Type | Notes | | --- | --- | --- | | baseUrl | string | Required. The API base (e.g. .../api/v1). | | getToken | () => string \| Promise<string> | Required. Studio or AppUser JWT; Bearer prefix added if missing. | | getTenantId | () => string \| Promise<string> | Required. Returns the x-tenant-id value. | | getRequestHeaders | ({ namespace, operation }) => object \| Promise<object> | Optional. Extra headers merged per request. namespace is one of files / folders / shares / trash. Not applied to objectUrl / fetchObject (the presigned token is the auth). | | fetchImpl | typeof fetch | Optional. Defaults to globalThis.fetch. |

Transport

| Concern | Behaviour | | --- | --- | | Auth header | From getToken (host-injected); Bearer prefix normalised. Not sent on fetchObject (token is the auth). | | Tenant header | x-tenant-id from getTenantId. AppUser JWTs make their own tenantId authoritative server-side. | | Multipart | files.upload sends FormData verbatim; no hand-set content-type so fetch supplies the boundary. | | Retries | Idempotent GETs retried 3× with exponential backoff (200/400/800 ms). Mutations and uploads are not retried. | | Timeouts | 10 s default, configurable per call via timeoutMs. | | Error model | Typed FilestoreError hierarchy: NotFoundError (404), ForbiddenError (403), ValidationError (400), GoneError (410 — expired object token), PayloadTooLargeError (413 — file over the 5 GB cap), UnsupportedMediaTypeError (415), RateLimitedError (429), ServerError (5xx). | | Platform | Browser and React Native / Node 18+ (uses fetch, FormData, AbortController). |

Dependencies

None. The client uses only platform fetch, FormData, and AbortController, all available in modern browsers, Node 18+, and React Native.

Tests

node --test src

Fully self-contained — no npm install, no cross-package deps.