@ecosplay/chastify-sdk
v0.1.0
Published
TypeScript/JS client for the Chastify Developer API. Developed free of charge by e-cosplay.fr for Chastify.net.
Maintainers
Readme
@e-cosplay/chastify-sdk (TypeScript / JavaScript)
A typed client for the Chastify Developer API, for Bun and Node.js (≥ 18). Ships ESM + types, pluggable cache (in-memory / Redis), structured logging, typed errors, automatic retry/backoff, webhook verification, and a browser iframe bridge.
Developed free of charge by the e-cosplay.fr association for the Chastify.net community. Chastify.net retains all rights over its API and data. Source-available — see LICENSE. AI/automated ingestion or training is forbidden without written agreement — see AGENTS.md.
Install
bun add @e-cosplay/chastify-sdk # Bun
npm add @e-cosplay/chastify-sdk # NodeFrom this repo:
cd js && bun installQuick start
import { ChastifyClient } from "@e-cosplay/chastify-sdk";
const chastify = new ChastifyClient({
devToken: process.env.CHASTIFY_DEV_TOKEN!, // user-wide DEV token
});
const session = await chastify.session.get();
console.log(session.lockData?.remainingSeconds);
await chastify.lock.addTime(600); // +10 min
await chastify.lock.freeze(1800); // freeze 30 min
await chastify.device.shock({ intensityPct: 40, durationSeconds: 5 });Configuration
new ChastifyClient({
devToken?: string, // External API (/api/apps/v1/*) — Authorization: Bearer
appKey?: string, // Extensions API (/api/extensions/*) — app-scoped dev key
appId?: string, // for app-scoped file staging
baseUrl?: string, // default "https://chastify.net"
timeoutMs?: number, // default 30000
maxRetries?: number, // default 4 (429 + 5xx + network)
retryBaseDelayMs?: number,// default 500 (exponential, jittered, honors Retry-After)
cache?: CacheStore, // default MemoryCache()
cacheTtlMs?: number, // default 0 → caching OFF (lock state is real-time)
logger?: Logger, // default NoopLogger
logLevel?: LogLevel, // "debug" | "info" | "warn" | "error" | "silent"
redactSecrets?: boolean, // default true (Authorization / main-token redacted in logs)
fetch?: typeof fetch, // injectable (testing / custom transport)
});Caching (local or Redis)
Caching applies to GET reads only and is off by default (cacheTtlMs: 0)
because lock state is real-time. Enable a TTL to cache reads; writes
automatically invalidate the related session keys.
import { ChastifyClient, MemoryCache, RedisCache } from "@e-cosplay/chastify-sdk";
// In-memory (local), 3s TTL on reads:
const a = new ChastifyClient({ devToken, cache: new MemoryCache({ maxEntries: 500 }), cacheTtlMs: 3000 });
// Redis (works with ioredis or node-redis v4 — pass any client exposing get/set/del):
import Redis from "ioredis";
const b = new ChastifyClient({ devToken, cache: new RedisCache(new Redis(process.env.REDIS_URL!)), cacheTtlMs: 5000 });
// Per-call override:
await chastify.session.get({ cacheTtlMs: 10_000 }); // cache this read 10s
await chastify.session.get({ cache: false }); // bypass + refreshImplement your own backend with the CacheStore interface:
interface CacheStore {
get(key: string): Promise<string | undefined>;
set(key: string, value: string, ttlMs: number): Promise<void>;
delete(key: string): Promise<void>;
clear?(): Promise<void>;
}Logging (debug requests/responses)
import { ChastifyClient, ConsoleLogger } from "@e-cosplay/chastify-sdk";
const chastify = new ChastifyClient({
devToken,
logger: new ConsoleLogger(),
logLevel: "debug", // logs method, URL, redacted headers/body, status, duration, retries
});At debug you get full request/response traces (secrets redacted). At info,
one line per request (method url status durationMs). Bring your own logger by
implementing Logger (debug/info/warn/error(message, meta?)) — e.g. wrap
pino.
API coverage
External API — your own active lock (devToken)
chastify.session.get() // GET /api/apps/v1/session
chastify.lock.applyTime(deltaSeconds) // POST /api/apps/v1/lock/apply-time
chastify.lock.addTime(seconds) // applyTime(+)
chastify.lock.removeTime(seconds) // applyTime(-)
chastify.lock.freeze(durationSeconds) // POST /api/apps/v1/lock/freeze
chastify.lock.unfreeze() // POST /api/apps/v1/lock/unfreeze
chastify.lock.toggleFreeze()
chastify.lock.pillory({ durationSeconds, reason })
chastify.lock.endPillory()
chastify.tasks.assign({ taskText, points, durationSeconds, verificationRequired })
chastify.tasks.startTimer()
chastify.tasks.complete({ successful, reason })
chastify.hygiene.startUnlock(durationSeconds)
chastify.settings.patch({ ... })
chastify.device.shock({ intensityPct, durationSeconds, message })
chastify.device.stopShock()
chastify.device.vibrate({ intensityPct, durationSeconds, frequencyPct, message })
chastify.device.stopVibration()
chastify.device.allStop()
chastify.device.setRandomShock({ enabled, minIntensityPct, maxIntensityPct, message })
chastify.device.setBerserkShock({ enabled, message })
chastify.logs.custom({ title, description, role, icon, color })
chastify.actions.run(name, params) // generic POST /api/apps/v1/action
chastify.device.command(command, params) // generic POST /api/apps/v1/device-commandExtensions session API — backend extension routes (appKey + per-session mainToken)
const s = chastify.extensions.session(sessionId, mainToken);
await s.get(); // GET .../sessions/:id
await s.getState(); await s.setState(data); await s.patchState(partial);
await s.getMetadata(); await s.patchMetadata({ unlockBlockers, homeActions });
await s.action(name, params); // POST .../action
await s.deviceCommand(command, params);
await s.regularActions(); await s.submitRegularAction({ kind, score });
await s.recordProgress({ metric, amount, occurredAt });
await s.startAttempt({ metric, attemptId }); await s.failExpiredAttempt({ metric, attemptId });
await s.notify({ title, message, target, showPageOverlay });
await s.customLog({ title, description, role });
// Files:
await s.files.capabilities(); await s.files.list(); await s.files.get(fileId);
await s.files.upload(file, { purpose }); await s.files.delete(fileId);App file staging (appKey + appId)
const f = chastify.appFiles(appId);
await f.capabilities(); await f.stage(file, { purpose, draftId });
await f.listStaged({ purpose, draftId }); await f.get(fileId); await f.deleteStaged(fileId);Errors
Every non-2xx response throws a typed error extending ChastifyError
(.status, .code, .message, .requestId, .body):
| Class | When |
|-------|------|
| AuthenticationError | 401 missing_token / invalid_token / revoked_token |
| AuthorizationError | 403 insufficient_scope / not_authorized |
| NotFoundError | 404 lock_not_found / no_device |
| ConflictError | 409 no_active_lock_session / lock_ended |
| ValidationError | 400 / 422 |
| RateLimitError | 429 (.retryAfterMs) |
| DeviceTimeoutError | 504 device_timeout |
| ServerError | 5xx |
| NetworkError / TimeoutError | transport-level |
import { RateLimitError, AuthenticationError } from "@e-cosplay/chastify-sdk";
try { await chastify.lock.addTime(600); }
catch (e) {
if (e instanceof AuthenticationError) { /* refresh token */ }
if (e instanceof RateLimitError) { /* e.retryAfterMs */ }
}Rate limits (per-minute): read 300, write 120, upload 10, action 30, token 30.
The client retries 429/5xx automatically with exponential backoff.
Webhooks
import { ChastifyWebhooks } from "@e-cosplay/chastify-sdk";
// Express-style handler:
app.post("/chastify/webhook", express.raw({ type: "*/*" }), (req, res) => {
const event = ChastifyWebhooks.constructEvent(req.body, req.headers, process.env.CHASTIFY_WEBHOOK_TOKEN!);
// throws WebhookVerificationError if x-webhook-token-hash doesn't match
switch (event.event) {
case "lock.time_changed": /* ... */ break;
case "task.completed": /* ... */ break;
}
res.sendStatus(200); // delivery is at-least-once → dedupe on event.id
});Browser iframe bridge
import { parseHashPayload, ChastifyBridgeClient } from "@e-cosplay/chastify-sdk/bridge";
const ctx = parseHashPayload(location.hash);
const bridge = new ChastifyBridgeClient(ctx);
const session = await bridge.request("session.get");Testing
Tests run entirely against a bundled mock server (test/mock-server.ts) that
simulates both success and error responses — the live Chastify API is never
contacted.
bun test # run all tests
bun test --watch
bun run coverage # 100% line coverage of src/ is enforced (see bunfig.toml)Runnable usage examples live in examples/ (quickstart, device,
cache+Redis, logging, extensions, webhooks, browser bridge).
Maintenance & future API changes
If the Chastify API changes, this SDK is intended to be updated on a best-effort basis, subject to the e-cosplay.fr association's volunteer time. No delivery guarantee is implied. Reports → [email protected].
License
Source-available under the e-cosplay.fr Source-Available License. © 2026 e-cosplay.fr. Chastify and the Chastify API are property of Chastify.net. No AI/automated ingestion or training without written agreement — see AGENTS.md.
