Repository navigation
Configuration
You can configure per-instance SDK behaviors (such as request timeouts, retry behavior, and offline queues) through the constructor or ld.configure(). Every Litedirecte instance owns its config: changing one never affects another.
import { Litedirecte } from "litedirecte";
const ld = new Litedirecte({
maxRetries: 5,
retryDelay: 1000,
timeout: 10000, // 10 seconds timeout
offlineQueue: true, // Queue actions if offline!
});
// …or update part of the config later (validated exactly like the constructor):
ld.configure({ maxRetries: 2 });| Option | Type | Default | Description |
|---|---|---|---|
userAgent |
string |
(Modern iOS mobile user agent) | Custom User-Agent header for API requests. |
proxyUrl |
string |
undefined |
Base URL of a proxy server to relay all API requests through (e.g. to bypass CORS in browsers). Must be a valid absolute http(s) URL; a trailing slash is stripped. See Proxy below. |
maxRetries |
number |
3 |
Number of times to automatically retry failed requests (e.g. on HTTP 500 or timeout). Integer >= 0. |
retryDelay |
number |
500 |
Initial delay between retries in milliseconds (uses exponential backoff with ±20% jitter, capped at 8 s; HTTP 429 honors Retry-After, capped at 30 s). Finite >= 0. |
concurrency |
number |
3 |
Maximum number of concurrent network requests allowed at once. Integer >= 1. |
timeout |
number |
15000 |
Request timeout in milliseconds. Finite > 0. Applies to data requests as well as login, 2FA and token refresh. |
storage |
StorageAdapter |
auto-detected | Session and data storage adapter (a fresh in-memory adapter per instance when nothing is detected). |
storageNamespace |
string |
'' |
Prefix applied to every persisted key, so several instances can share one adapter without overwriting each other. Must be a string without whitespace. |
maxRequestsPerMinute |
number |
100 |
Sliding-window rate budget (see Rate limit). Integer >= 0; 0 disables it. |
passkey |
string |
undefined |
Key used to transparently encrypt everything saved in your storage adapter using AES-GCM. |
offlineQueue |
boolean |
false |
Enable or disable the offline mutation queue. |
fetchInterval |
number |
60 |
Built-in polling interval in seconds. 0 disables the timer; otherwise a finite number >= 5. See Cache & sync. |
fetchIgnore |
Resource[] |
[] |
Resources skipped by the login/switch prefetch and by every poll tick. Same values and validation as getAll({ ignore }). Ignored resources become manual cache entries. |
isOnline |
() => boolean |
navigator.onLine where available, else online |
Online probe. Never throws. When offline, reads serve the newest cached copy (even a dead one) and report offline through ld.sync; a read with nothing cached rejects with EdOfflineError. |
on2faRequired |
Function |
undefined |
Instance callback to handle 2FA challenges. |
onCredentialsRequired |
Function |
undefined |
Callback to supply credentials on token refresh failure. Only called for authentication failures — never when the refresh fails because the network is down. |
onError |
(error: EdError) => void |
undefined |
Plain observer called with reported errors. It takes a single argument and never influences retrying. |
Invalid values throw EdValidationError and leave the previous configuration untouched.
Every verbe=get read goes through the per-instance cache (CACHE_TTL_MS = 60_000, no opt-in) and every read accepts { force: true } to skip the cache, fetch, and write through. Results are shared memoized objects: treat them as read-only. Constructing the instance arms the built-in getAll poller; ld.stopPolling() / ld.resumePolling() control it, and ld.getAll({ ignore }) fetches every resource at once. See Cache & sync for the fresh/stale/dead model, watch, ld.sync, offline behaviour and privacy.
Reads are retried on network errors, timeouts, parse errors and 5xx responses. Mutations (any request with queued: true, or any endpoint whose verbe is not get) are not retried on those failures — replaying a non-idempotent request could duplicate it server-side. Mutations are only retried after an HTTP 429 (the request was not processed) and through the expired-session refresh path.
The SDK never keeps your process alive: polling intervals, the token keepalive, the rate-limit timer and request-timeout fallbacks are all unref'd where the runtime supports it, and fallback timeout handles are cleared as soon as their request settles. A script that only polls exits by itself. ld.destroy() stops every timer, removes every global listener the instance registered, and rejects queued rate-limited requests; using the instance afterwards throws EdValidationError('Instance destroyed').
Namespaces that map to an EcoleDirecte module check the current account before any network request. If the account lacks the module, an EdUnsupportedModuleError is thrown (and never retried):
| Namespace | Module code | Verified how |
|---|---|---|
ld.grades |
NOTES |
BlocksDirecte MarkModules + this repo's mock account |
ld.timetable |
EDT |
BlocksDirecte TimetableModules
|
ld.homework |
CAHIER_DE_TEXTES |
BlocksDirecte HomeworkModules
|
ld.attendance |
VIE_SCOLAIRE |
BlocksDirecte SchoolLifeModules (/viescolaire.awp) |
"Has the module" means the code is present in account.modules and enable is truthy. The check fails open (missing or empty modules never blocks) and is skipped in mockMode. Messages, timeline, cloud, documents, QCMs and settings have no verified module code and are unchecked. The friendly names live in the exported ED_MODULES const:
import { Litedirecte } from "litedirecte";
const ld = new Litedirecte();
await ld.auth.login("your_username", "your_password");
if (ld.supports("grades")) {
await ld.grades.getGrades();
}
console.log(ld.capabilities()); // { grades: true, timetable: false, … }EcoleDirecte's real limit is unknown, so each instance budgets at most maxRequestsPerMinute (default 100, a conservative guess) request starts per rolling 60 seconds (sliding window). Excess requests wait in FIFO order and start as soon as a slot frees up — they are never rejected or dropped. The limiter composes with concurrency, and every real network request counts, including retries. Login, 2FA and token-refresh requests count but never wait, so a queued backlog cannot block the refresh that would unblock it. 0 disables the limit. The limiter uses a single unref'd setTimeout and leaves nothing scheduled once the queue drains.
Mock Mode lets you develop and test your app without a real EcoleDirecte account or network access. When mockMode: true is passed to any data function, edFetch bypasses the network entirely and returns raw, API-shaped mock data (same keys as the real EcoleDirecte responses — including messy date strings and Base64-encoded fields). The mock payload is then passed through the exact same transform pipeline as real data, so dates become Date objects and Base64 fields are decoded, exactly like production.
This is perfect for unit tests, CI, offline demos, or building your UI before credentials are available.
import { Litedirecte } from "litedirecte";
const ld = new Litedirecte();
// Every data method accepts a `mockMode` option
const grades = await ld.grades.getGrades({ mockMode: true });
const inbox = await ld.messages.getMessages({ mockMode: true });
const attendance = await ld.attendance.getAttendance({ mockMode: true });
// Mutations work too — they return plausible raw envelopes
await ld.homework.markAsDone([11243], { mockMode: true });
await ld.messages.sendMessage(
{ subject: "Hi", content: "Hello", destinataires: [{ id: 1, type: "P" }] },
{ mockMode: true },
);-
mockModereplaces only the network layer. Authentication (ld.auth.login), token refresh, and the session store are untouched — you still need a logged-in account (e.g. via a test fixture) because methods likeld.grades.getGradescallrequireCurrentAccount(). - Mock responses use the standard
{ code: 200, token, message, data }envelope, so all existing validation andtransformlogic runs unchanged. Module checks are skipped inmockMode. - File downloads (
ld.downloads.download,ld.downloads.downloadPhoto) return a small mockResponsewhenmockModeis set.
ld.grades.getGrades (+ correlate), ld.messages (getMessages, getMessage, getContacts, sendMessage, markAsRead, markAsUnread), ld.attendance.getAttendance, ld.homework (getHomework, getHomeworkForDate, markAsDone, markAsUndone, sendHomeworkComment), ld.timetable (getTimetable, getTimetableIcalUrl), ld.cloud (getCloud, createFolder, deleteNodes), ld.documents.getDocuments, ld.qcms (getQcms, getQcmDetail, updateQcmStatus, submitQcmAnswer), ld.settings (getSettings, updateSettings, updateAccessibility), ld.timeline (getTimeline, getCommonTimeline), ld.downloads (download, downloadPhoto).
Litedirecte has built-in proxy support. When proxyUrl is set, all API requests and file downloads are routed through that URL instead of hitting EcoleDirecte directly. This is essential when running in browsers, where CORS blocks direct calls to the EcoleDirecte API.
The recommended proxy is Procsy, a lightweight Cloudflare Workers proxy purpose-built for this use case. It handles CORS headers, IP spoofing, and SSRF protection out of the box.
-
Deploy Procsy — follow the Procsy README to deploy it on Cloudflare Workers.
-
Point Litedirecte at your Procsy instance:
import { Litedirecte } from "litedirecte";
const ld = new Litedirecte({
proxyUrl: "https://myprocsyinstance.hithisismyname.workers.dev",
});That's it. Every outgoing request will now be relayed through Procsy.
When proxyUrl is configured, Litedirecte rewrites all outgoing request URLs to point at the proxy base. It also:
- Moves the user agent from the
User-Agentheader toX-Procsy-User-Agent, which bypasses Chromium's 40450316 issue. - Attaches an
X-Procsy-Base-URLheader containing the original EcoleDirecte API base URL, so the proxy knows where to forward the request.
Note: When
proxyUrlis not set (the default), Litedirecte talks directly tohttps://api.ecoledirecte.com/v3. This works fine in server-side environments where CORS is not a concern.
By default, each instance automatically detects and selects the best storage option for your environment (a fresh in-memory adapter when nothing is detected):
- IndexedDB — used in IndexedDB-capable runtimes (browsers, CF Workers, Deno).
- localStorage — used in standard Web Storage runtimes (browsers if IndexedDB unavailable, React Native).
- Node Storage — used in Node.js or Bun environments.
- Memory Storage — falls back to volatile in-memory storage if nothing else is available.
You can explicitly force an adapter or supply a custom one.
Backed by IndexedDB under a database named litedirecte. Perfect for standard web browsers.
import { Litedirecte, indexedDBStorage } from "litedirecte";
const ld = new Litedirecte({ storage: indexedDBStorage });Backed by the browser's localStorage API. Excellent for simple React Native, Capacitor, or Chrome extension usage.
import { Litedirecte, localStorageStorage } from "litedirecte";
const ld = new Litedirecte({ storage: localStorageStorage });Saves your session to a local JSON file. Excellent for command-line tools or servers running in Node.js or Bun.
import { Litedirecte, nodeStorage } from "litedirecte";
const ld = new Litedirecte({
// Saves to './litedirecte-session.json' by default
storage: nodeStorage(),
// Or specify a custom path:
// storage: nodeStorage("/var/data/session.json"),
});Writes are serialized per file, atomic (temporary file + rename), and created with mode 0600. A truncated/corrupt file is moved aside to <file>.corrupt instead of silently wiping your session.
Isolated in-memory storage. Each instance creates its own default adapter when none is configured, so instances never share state unless you pass one adapter explicitly.
import { Litedirecte, createMemoryStorage } from "litedirecte";
const ld = new Litedirecte({ storage: createMemoryStorage() });Two instances can share one adapter safely by giving each a storageNamespace:
const shared = createMemoryStorage();
const alice = new Litedirecte({ storage: shared, storageNamespace: "alice:" });
const bob = new Litedirecte({ storage: shared, storageNamespace: "bob:" });Wraps a Cloudflare KV namespace.
import { Litedirecte, cloudflareKVStorage } from "litedirecte";
export default {
async fetch(request, env) {
const ld = new Litedirecte({ storage: cloudflareKVStorage(env.MY_SESSION_KV) });
// ...
}
};Allows you to wrap any asynchronous key-value storage engine. Here's how to wrap React Native's @react-native-async-storage/async-storage:
import AsyncStorage from "@react-native-async-storage/async-storage";
import { Litedirecte, asyncStorage } from "litedirecte";
const ld = new Litedirecte({
storage: asyncStorage({
getItem: (key) => AsyncStorage.getItem(key),
setItem: (key, value) => AsyncStorage.setItem(key, value),
removeItem: (key) => AsyncStorage.removeItem(key),
}),
});Concerned about storing credentials on disk or in the browser? Simply provide a passkey!
When a passkey is specified, Litedirecte will automatically wrap your active storage adapter with an encrypted wrapper. All session tokens, IDs, and account info will be encrypted using AES-GCM before writing, and decrypted on read.
A random 16-byte salt is stored once under the __ld_salt key and the AES-GCM key is derived a single time per adapter (a fresh random 12-byte IV is used per write). A value that fails to decrypt (e.g. wrong passkey) reads back as null and is reported once per key through onError with an EdError coded STORAGE_DECRYPT_FAILED. See encryptedStorage below to use the wrapper directly.
import { Litedirecte, nodeStorage } from "litedirecte";
const ld = new Litedirecte({
storage: nodeStorage(),
passkey: "super-secret-password" // Encryption enabled!
});The wrapper that passkey installs is also exported, so you can encrypt a value yourself (a token you cache separately, a settings blob) with the exact same scheme:
import { Litedirecte, encryptedStorage, localStorageStorage, type EdError } from "litedirecte";
const vault = encryptedStorage(localStorageStorage, "super-secret-password", () => (error) => {
console.warn("decrypt failed", (error as EdError).code); // STORAGE_DECRYPT_FAILED
});
await vault.set("my_token", "s3cret");
const ld = new Litedirecte({ storage: vault });| Parameter | Type | Description |
|---|---|---|
backend |
StorageAdapter |
The adapter to wrap. |
secret |
string |
Passphrase the AES-GCM key is derived from (PBKDF2). |
onError |
() => ErrorMiddleware | undefined |
Lazy resolver for the error reporter, so importing an error class cannot create a cycle. |
The __ld_salt key and the derived key are computed once per wrapper, not once per write, so wrapping a hot adapter costs nothing extra at call time. Reads never reject: a value that fails to decrypt resolves null and is reported once per key.
Scheme: PBKDF2-SHA256, 100 000 iterations over the 16-byte random salt stored at __ld_salt, producing a 256-bit AES-GCM key. Each write draws a fresh random 12-byte IV and stores it as the ciphertext prefix.
Retrieves documents and other resources from EcoleDirecte.
ld.downloads.download(options?: DownloadOptions): Promise<ArrayBuffer | Blob | ReadableStream>-
as("buffer" | "blob" | "stream"): The format to return. Defaults to"buffer". -
params(Record<string, any>): Extra post body parameters. -
mockMode(boolean): Return a small mockResponseinstead of requesting.
import { Litedirecte } from "litedirecte";
import { writeFile } from "node:fs/promises";
const ld = new Litedirecte();
await ld.auth.login("your_username", "your_password");
const fileArrayBuffer = await ld.downloads.download({
params: {
forceDownload: 0,
id: 12345,
type: "bulletin",
},
});
await writeFile("./report-card.pdf", Buffer.from(fileArrayBuffer));
console.log("PDF written to disk!");Retrieves the profile picture of the currently active account.
ld.downloads.downloadPhoto(options?: { as?: "buffer" | "blob" | "stream" }): Promise<ArrayBuffer | Blob | ReadableStream | null>When no internet is available, you don't want actions like marking homework as completed to be lost. By enabling offlineQueue: true in your configuration, supported mutating requests will be recorded locally if the user is offline. The following methods are queued: ld.homework.markAsDone, ld.homework.markAsUndone, ld.homework.sendHomeworkComment, ld.cloud.createFolder, ld.cloud.deleteNodes, and ld.messages.sendMessage. (ld.settings.updateSettings is deliberately not queued: a password change must never be replayed later from storage.)
Queued entries are flushed automatically (fire and forget) after a successful login or loadSession, and when the runtime fires an online event. Each entry carries an attempt counter and a creation timestamp: entries the server permanently rejects (API error), entries that failed 5 times, and entries older than 7 days are dropped; entries that failed only because of a network error are kept. Each instance owns its queue: entries queued on one instance are never visible to another.
You can synchronize them once the connection is restored:
import { Litedirecte } from "litedirecte";
const ld = new Litedirecte({ offlineQueue: true });
// Check the queue
const pendingCount = ld.offlineQueue.getQueue().length;
console.log(`You have ${pendingCount} offline actions pending.`);
// Flush the queue to send them to EcoleDirecte
await ld.offlineQueue.flush();Defines per-instance parameters passed to new Litedirecte(config) or ld.configure(partial).
interface EdConfig {
userAgent?: string;
proxyUrl?: string;
maxRetries?: number;
retryDelay?: number;
concurrency?: number;
timeout?: number;
/**
* Poll interval in seconds for the built-in `getAll` poller. Default `60`.
* `0` disables the timer. Otherwise a finite number `>= 5`.
*/
fetchInterval?: number;
/**
* Resources skipped by the login/switch prefetch and by every poll tick.
* Same values and validation as `getAll({ ignore })`. Default `[]`.
*/
fetchIgnore?: Resource[];
/** Online probe. Default: `navigator.onLine` where available, else online. */
isOnline?: () => boolean;
storage?: StorageAdapter;
passkey?: string;
offlineQueue?: boolean;
onError?: ErrorMiddleware;
on2faRequired?: (
question: string,
choices: string[],
) => number | string | Promise<number | string>;
onCredentialsRequired?: () =>
| { identifiant: string; motdepasse: string }
| Promise<{ identifiant: string; motdepasse: string }>;
/**
* Prefix applied to every persisted storage key, so several instances can
* share one storage adapter without overwriting each other. Default `''`.
* Must be a string without whitespace.
*/
storageNamespace?: string;
/**
* At most N request starts per rolling 60 seconds (sliding window).
* Excess requests wait in FIFO order. `0` disables the limit.
* Default `100` (conservative guess, EcoleDirecte's real limit is unknown).
*/
maxRequestsPerMinute?: number;
}
/** Plain observer called with errors that are reported, never retried through it. */
type ErrorMiddleware = (error: any) => void | Promise<void>;The standard interface for defining custom data storage persistence.
interface StorageAdapter {
get(key: string): string | null | Promise<string | null>;
set(key: string, value: string): void | Promise<void>;
delete(key: string): void | Promise<void>;
}© 2026 typeof (Scolup) | Licensed under AGPL 3.0