alist-kratos-sdk
v2.2.1
Published
JavaScript SDK for AList file API with Ory Kratos authentication
Readme
alist-kratos-sdk
JavaScript / TypeScript SDK for the AList file API behind the Ory Oathkeeper edge. The edge validates the Kratos session cookie and injects the user's identity; the SDK sends cookie-authenticated requests and exposes typed file operations scoped to the user's own folder.
Install
npm install alist-kratos-sdkQuick start
import { AlistClient } from "alist-kratos-sdk";
// Browser: the ory_kratos_session cookie is sent automatically (credentials:"include").
// The URL MUST include the /.assets/alist mount prefix — AList's edge rules only
// match under it, and paths like /api/me are appended verbatim.
const client = await AlistClient.fromKratosSession(
"http://localhost:4455/.assets/alist", // edge + mount prefix (also the default)
);
// All paths are **relative to the user's BasePath**.
// The SDK lazily fetches base_path via /api/me on first use and
// forwards paths to AList, which enforces per-user containment server-side.
const { data } = await client.list("/"); // lists own root folder
await client.upload("/photos/sunset.jpg", fileBlob);
const blob = await client.download("/photos/sunset.jpg");Path semantics (v1.0 — breaking change)
Starting with v1.0, paths passed to SDK methods are resolved relative to the authenticated user's BasePath. Callers no longer need to (and cannot) include their own identity id in the URL.
| Input path | Behaviour |
|------------|-----------|
| "/" or "" | Returns the user's BasePath root (the /\<identity_id\> folder). |
| "photos/sunset.jpg" (no leading slash) | AList auto-prefixes BasePath server-side → /\<identity_id\>/photos/sunset.jpg. |
| "/photos/sunset.jpg" (leading slash) | Treated as absolute; must be inside BasePath or the server returns 403. |
| "/<other_identity_id>/..." | Always rejected with 403 (BasePath containment). |
The SDK never sends the literal BasePath prefix; it forwards whatever the caller supplies and lets AList enforce the boundary. The BasePath is discovered automatically via me() on first call (cached).
Migration from v0.x
- await client.upload("/abc123-id/photos/sunset.jpg", file);
+ await client.upload("/photos/sunset.jpg", file);API
User
const me = await client.me();
// → { id, username, sso_id: "kratos:<identity_id>", base_path: "/<identity_id>", role, ... }List directory
const res = await client.list("/", { page: 1, per_page: 50 });
for (const f of res.data.content) {
console.log(f.name, f.size, f.is_dir);
}Upload
// From a browser File (e.g. <input type=file>)
await client.upload("/photos/sunset.jpg", fileFromInput);
// From a Blob
const blob = new Blob([await response.arrayBuffer()]);
await client.upload("/data/file.bin", blob);
// From a Buffer (Node 18+)
const buf = await fs.promises.readFile("./local.pdf");
await client.upload("/docs/report.pdf", buf);Download
// Full Blob (small files)
const blob = await client.download("/photos/sunset.jpg");
const url = URL.createObjectURL(blob);
// Server-signed URL (large files, <img src>, <a href>, window.open)
const presigned = await client.downloadUrl("/photos/sunset.jpg");
window.open(presigned, "_blank");Both call /api/fs/get first so AList resolves the path under the user's
BasePath and returns its canonical raw_url (full path + sign + proxy
decision). The URL's host is rebased onto the edge origin this client was built
with, so it stays reachable behind Oathkeeper. This is the only correct way to
download — AList's /d/* route has no auth/identity, so the SDK cannot build
that URL itself.
Browser cache (on by default)
download() caches the returned Blob in the browser Cache Storage, keyed by a
content fingerprint (resolved path + size + mtime + hash from /api/fs/get).
An unchanged file is served instantly with zero bytes re-transferred; a new
version always re-fetches. Entries embed the user's BasePath, so they are
per-user and never leak across accounts.
// Defaults: cache ON, maxEntries 100, no TTL. All tunable / disableable:
const client = new AlistClient({
alistUrl: "http://localhost:4455/.assets/alist",
cache: { enabled: false }, // opt out entirely
// or: { maxEntries: 500, ttlMs: 3_600_000 } // tune limits
});
await client.clearCache(); // evict every cached BlobBacked by the Cache Storage API (persists across sessions) with an in-memory
fallback when caches is unavailable (Node) or over quota (private mode);
quota/put errors are non-fatal. downloadUrl() is not cached (it returns the
edge URL for <img src> etc., where the browser HTTP cache applies).
Other operations
await client.mkdir("/new-folder");
await client.rename("new-name.jpg", "/old-name.jpg");
await client.remove(["/file1.txt", "/file2.txt"]);
await client.move(["/file.txt"], "/archive/");
await client.copy(["/file.txt"], "/backup/");
const found = await client.search("/", "report", { scope: 0 });Auth flow
The SDK relies on the Oathkeeper edge, not a bearer token:
- Every request is sent with
credentials: "include", so the browser attaches theory_kratos_sessioncookie. NoAuthorizationheader is sent — the edge's AList mutator blanks it anyway. - Oathkeeper's
cookie_sessionauthenticator validates the cookie against Kratos/sessions/whoamiand, on success, injectsX-User-Id+ identity traits (and blanksAuthorization) before forwarding to AList. - AList's middleware (
server/middlewares/auth.go) reads the edge-injectedX-User-Id. If the identity has no AList user yet, one is auto-provisioned withBasePath = /<kratos_identity_id>andSsoID = "kratos:<kratos_identity_id>". - The
me()endpoint returns the cachedbase_path; the SDK stores it internally. - All subsequent file operations are scoped to that BasePath by AList's server-side containment check (
internal/model/user.go:JoinPath).
You never need to log in to AList directly — the Kratos session (validated at the edge) is the single source of truth.
Routing note: the
alistUrlmust be the edge origin with the/.assets/alistprefix (e.g.http://localhost:4455/.assets/alist, orhttps://backend.getkawai.com/.assets/alistin prod). The SDK appends/api/...,/d/...,/p/...,/pingto it; without the prefix those paths match no edge rule and return404. Direct-to-AList usage (bypassing the edge) is not supported in browser contexts — AList is localhost-only.
Browser vs Node
The SDK is designed for the browser, where the ory_kratos_session cookie is present and credentials: "include" delivers it to the edge:
fromKratosSession(url?)just constructs a client pointed at the edge; the cookie does the authentication.fetchandFormDataare native in browsers and Node 18+.- No DOM dependencies.
For server-side (Node) use without a browser cookie, talk to AList through the edge with a cookie forwarded manually, or run AList's own admin path out of band — the legacy kratos:<token> Authorization scheme is no longer used (the edge blanks Authorization).
Demo
import { AlistClient } from "alist-kratos-sdk";
const client = await AlistClient.fromKratosSession(); // default edge + prefix
// The SDK cannot know up-front whether the Kratos cookie is valid — the edge
// enforces that on the first request. A 401/403 from list()/me()/etc. means
// the user must log in (redirect to the Kratos login URL behind the same edge).
try {
const { data } = await client.list();
console.log(`You have ${data.total} files`);
} catch (err) {
window.location.href =
"http://localhost:4455/.ory/kratos/public/self-service/login/browser";
}Types
interface AlistFile {
name: string;
size: number;
is_dir: boolean;
modified: string;
created?: string;
sign?: string; // pre-signed URL token
thumb?: string; // thumbnail URL
type: number; // 0=unknown, 1=folder, 2=video, 3=audio, 4=text, 5=image, 6=archive
hashinfo?: string;
}
interface AlistUser {
id: number;
username: string;
sso_id?: string; // "kratos:<identity_id>"
base_path: string; // "/<identity_id>"
role: number;
permission: number;
disabled: boolean;
otp?: boolean;
}