Skip to content

Node.js SDK

Terminal window
pnpm add @chronos.sh/sdk
  • Requires Node.js 20+
  • Dual module: ESM (import) and CJS (require) both supported
  • Zero runtime dependencies

The top-level client. The only class you construct directly.

import { Chronos } from '@chronos.sh/sdk';
const chronos = new Chronos({
apiKey: process.env.CHRONOS_API_KEY!,
});
new Chronos(options: ChronosOptions)

Throws ChronosConfigError if any option fails validation.

Property Type Description
worker Worker Read-only. The long-poll worker instance. All functionality lives here.

None. Chronos is a composition root. Use chronos.worker for all operations.

Accessed via chronos.worker. Not directly importable.

handle<TPayload = unknown>(name: string, handler: ChronosHandler<TPayload>): this

Register a handler for a named job type. Returns this for chaining.

Parameter Type Description
name string Job type name. Trimmed internally. Must be 1–255 characters.
handler ChronosHandler<TPayload> Async or sync function that processes the job.

Throws ChronosError:

  • 'Handler name is required': empty or whitespace-only name
  • 'Handler name must be 255 characters or fewer'
  • 'Handler "{name}" is already registered': duplicate name
  • 'Handler "{name}" must be a function': non-function passed
start(): Promise<void>

Begin long-polling for jobs. The returned promise resolves when stop() is called and all in-flight work completes, or rejects if the API rejects the credentials (401 unauthorized).

Throws ChronosError (synchronously):

  • 'Chronos worker is already started'
  • 'Register at least one handler before starting Chronos'
stop(): Promise<void>

Request graceful shutdown.

  1. Aborts the current long-poll immediately
  2. Waits for any in-flight handler to finish (or hit its timeout) and report its result
  3. Resolves when complete

Safe to call at any time and never rejects: a credential rejection (401 unauthorized) surfaces on start() instead. Bounded by the handler timeout plus timeoutMs per report attempt; see Graceful Shutdown for the arithmetic.

type ChronosOptions = {
apiKey: string;
baseUrl?: string;
fetch?: FetchLike;
logger?: ChronosLogger;
pollWaitTimeSeconds?: number;
timeoutMs?: number;
retryDelayMs?: number;
workerId?: string;
};
Option Type Default Validation
apiKey string — (required) Trimmed. Throws ChronosConfigError if empty.
baseUrl string 'https://api.chronos.sh' Trimmed, trailing slashes stripped. Throws if empty after trim.
fetch FetchLike globalThis.fetch None.
logger ChronosLogger Console-backed logger None.
pollWaitTimeSeconds number 20 Integer, 0–20 inclusive. Throws ChronosConfigError.
timeoutMs number 30000 Positive integer up to 2147483647. Throws ChronosConfigError.
retryDelayMs number 1000 Finite, non-negative. Throws ChronosConfigError.
workerId string Auto-generated Trimmed. 1–255 characters after trim. Throws ChronosConfigError.

timeoutMs bounds every HTTP request the SDK makes; a request that exceeds it fails with ChronosTimeoutError. Long-poll claim requests get pollWaitTimeSeconds × 1000 of extra headroom so the intentional wait never counts against the bound. workerId is the identity reported to the API on every claim; set it to a stable value when you want the same worker identity across restarts.

type ChronosContext<TPayload = unknown> = {
jobId: string;
executionId: string;
handler: string;
payload: TPayload;
scheduledFor: Date;
attempt: number;
timeout: number;
signal: AbortSignal;
schedule: ChronosSchedule | null;
};
Field Type Description
jobId string Stable job identifier. Same across retries. Use for idempotency keys in downstream calls.
executionId string Unique to this execution attempt. Use for per-attempt logs, metrics, and correlation.
handler string Handler name that matched this job.
payload TPayload Job payload. Typed via the generic on .handle<TPayload>().
scheduledFor Date When the job was originally scheduled. Parsed from ISO string.
attempt number Attempt number. 1 on first try, increments on retries.
timeout number Timeout for the handler in seconds. The SDK races the handler against this duration and aborts ctx.signal when it elapses.
signal AbortSignal Fires when timeout elapses. Pass to fetch, AbortSignal.any(), etc.
schedule ChronosSchedule | null Parent schedule, or null for one-off jobs.
type ChronosSchedule = {
id: string;
name: string;
};
type RateLimitInfo = {
limit: number;
remaining: number;
reset: number;
};
Field Type Description
limit number Maximum requests allowed per window.
remaining number Requests remaining in the current window.
reset number Seconds until the current window resets.

Attached to ChronosApiError.rateLimit on non-429 error responses. All three X-RateLimit-* headers must be present; if any is missing, rateLimit is undefined.

type ChronosHandler<TPayload = unknown> = (
ctx: ChronosContext<TPayload>,
) => ChronosHandlerResult | Promise<ChronosHandlerResult>;

Can be sync or async.

type ChronosHandlerResult = Record<string, unknown> | void;
Return value Effect
Plain object Recorded as execution result. Must be JSON-serializable.
undefined / void Execution marked completed, no result data.

Rejected at runtime (throws ChronosError):

  • Arrays
  • Class instances (non-plain-object prototypes)
  • Non-JSON-serializable values
type ChronosLogger = {
debug(message: string, meta?: Record<string, unknown>): void;
info(message: string, meta?: Record<string, unknown>): void;
warn(message: string, meta?: Record<string, unknown>): void;
error(message: string, meta?: Record<string, unknown>): void;
};

Signature is (message, meta?): message first, metadata second.

Adapting pino (which uses obj, message order):

import pino from 'pino';
const log = pino();
const chronos = new Chronos({
apiKey: process.env.CHRONOS_API_KEY!,
logger: {
debug: (msg, meta) => log.debug(meta ?? {}, msg),
info: (msg, meta) => log.info(meta ?? {}, msg),
warn: (msg, meta) => log.warn(meta ?? {}, msg),
error: (msg, meta) => log.error(meta ?? {}, msg),
},
});
type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;

Compatible with globalThis.fetch. Use cases: inject tracing headers, test mocking. Implementations must forward init.signal to the underlying transport; dropping it disables the SDK’s transport timeout and graceful abort.

Verifies signatures on push deliveries. Standalone: does not require a Chronos client or API key.

import { Webhook } from '@chronos.sh/sdk';
const wh = new Webhook(process.env.CHRONOS_SIGNING_SECRET!);
const payload = await wh.verify(rawBody, req.headers);
new Webhook(secret: string | string[], options?: WebhookOptions)
Parameter Type Description
secret string | string[] Signing secret or array of secrets for key rotation. Blank/whitespace strings are filtered out.
options WebhookOptions Optional configuration.

Throws ChronosWebhookVerificationError if no valid (non-empty) secret is provided, or if toleranceSeconds is invalid.

verify(body: string, headers: Record<string, string | undefined> | Headers): Promise<unknown>

Verifies the signature and returns the parsed JSON body on success. Throws ChronosWebhookVerificationError on any failure.

Parameter Type Description
body string Raw request body. Must be the exact bytes received, not re-serialized.
headers Record<string, string | undefined> | Headers Request headers. Keys are matched case-insensitively. Accepts plain objects or the Web standard Headers class.

The method is async because it uses the Web Crypto API (crypto.subtle). Works in Node.js 18+, Bun, Deno, and edge runtimes.

Crypto keys are lazily imported on the first verify() call and cached for subsequent calls.

type WebhookOptions = {
toleranceSeconds?: number;
};
Option Type Default Description
toleranceSeconds number 300 Timestamp tolerance in seconds. Rejects requests with timestamps more than this many seconds from now (in either direction). Set to 0 to disable replay protection entirely. Must be finite and non-negative.
const DEFAULT_TIMESTAMP_TOLERANCE_SECONDS = 300;

The default tolerance applied when toleranceSeconds is not specified.

Error
└── ChronosError
├── ChronosConfigError
├── ChronosApiError
│ └── ChronosRateLimitError
├── ChronosNetworkError
│ └── ChronosTimeoutError
├── ChronosHandlerError
└── ChronosWebhookVerificationError
class ChronosError extends Error {
readonly cause?: unknown;
constructor(message: string, options?: { cause?: unknown });
}

Base class for all SDK errors. Catch this to handle any SDK error generically.

Thrown for operational issues: duplicate handler names, invalid return values, calling start() twice.

class ChronosConfigError extends ChronosError {
constructor(message: string);
}

Invalid options passed to new Chronos(). Only thrown during construction. If the constructor succeeds, config is valid.

Messages:

  • 'Chronos apiKey is required'
  • 'Chronos baseUrl is required'
  • 'pollWaitTimeSeconds must be an integer between 0 and 20'
  • 'retryDelayMs must be a non-negative number'
  • 'timeoutMs must be a positive integer no greater than 2147483647'
  • 'workerId must be 1-255 characters'
class ChronosApiError extends ChronosError {
readonly status: number;
readonly code?: string;
readonly body?: unknown;
readonly requestId?: string;
readonly retryAfterSeconds?: number;
readonly rateLimit?: RateLimitInfo;
constructor(message: string, options: ChronosApiErrorOptions);
}
Property Type Description
status number HTTP status code. Can be 200 if the API returned { success: false }.
code string | undefined Application error code (e.g., 'rate_limit_exceeded').
body unknown Full parsed response body.
requestId string | undefined Value of the X-Request-Id response header.
retryAfterSeconds number | undefined Seconds to wait before retrying. Parsed from the Retry-After header on 429 and 5xx responses. Integer seconds only; undefined for HTTP-date values.
rateLimit RateLimitInfo | undefined Rate-limit budget from X-RateLimit-* headers. Present on non-429 error responses when all three headers exist.

Thrown when the API responds with a non-2xx status or a 2xx response with { success: false }. For 429 specifically, the SDK throws the ChronosRateLimitError subclass instead.

try {
await chronos.worker.start();
} catch (err) {
if (err instanceof ChronosApiError) {
console.error(`API error ${err.status}: ${err.code}`);
}
}
class ChronosRateLimitError extends ChronosApiError {
constructor(message: string, options: ChronosApiErrorOptions);
}

Thrown on 429 Too Many Requests. Subclass of ChronosApiError, so existing instanceof ChronosApiError catches still work.

retryAfterSeconds is set when the response includes a Retry-After header; otherwise undefined. rateLimit is always undefined on 429.

import { ChronosRateLimitError } from '@chronos.sh/sdk';
try {
await chronos.worker.start();
} catch (err) {
if (err instanceof ChronosRateLimitError && err.retryAfterSeconds) {
console.warn(`Rate limited. Retry after ${err.retryAfterSeconds}s`);
}
}

The worker handles 429s automatically. See Retries & Error Handling for details.

class ChronosNetworkError extends ChronosError {
readonly cause: unknown;
constructor(message: string, options: { cause: unknown });
}

Thrown when the request fails at the transport layer: DNS resolution failure, TCP connection refused, TLS errors, or a connection dropped mid-response.

cause contains the original error from fetch.

Not thrown for abort signals. If stop() aborts a poll, the raw abort error propagates (not wrapped in ChronosNetworkError). The SDK’s own transport timeout throws the ChronosTimeoutError subclass.

class ChronosTimeoutError extends ChronosNetworkError {
constructor(message: string, options: { cause: unknown });
}

Thrown when a request exceeds timeoutMs before the response completes, including a stall during the body read. Subclass of ChronosNetworkError, so generic transport-failure handling still catches it. cause is the underlying abort rejection (a TimeoutError DOMException on Node).

Message: 'Chronos API request timed out after {timeoutMs}ms', where the value is the effective timeout for that request. Long-poll claims report timeoutMs + pollWaitTimeSeconds × 1000 (50000ms at the defaults).

The worker treats it like any other transport failure: the poll loop waits retryDelayMs and resumes, and result reports get up to 3 attempts.

class ChronosHandlerError extends ChronosError {
readonly cause: unknown;
constructor(message: string, options: { cause: unknown });
}

Wraps any exception thrown inside your handler. The message is copied from the original error (or stringified). If empty, defaults to 'Chronos handler failed'.

This error is never rethrown to your code. It’s logged internally and the failure is reported to the API. The error message (truncated to 4KB) is sent as the execution failure reason.

class ChronosWebhookVerificationError extends ChronosError {
constructor(message: string);
}

Thrown by Webhook when any verification step fails: missing headers, invalid timestamp, bad signature format, wrong secret, or non-JSON body. Also thrown by the Webhook constructor if no valid secret is provided.

The message indicates the specific failure. See Signature Verification for the full list of messages.

import { Webhook, ChronosWebhookVerificationError } from '@chronos.sh/sdk';
const wh = new Webhook(process.env.CHRONOS_SIGNING_SECRET!);
try {
const payload = await wh.verify(rawBody, headers);
} catch (err) {
if (err instanceof ChronosWebhookVerificationError) {
console.error('Verification failed:', err.message);
}
}

Every named export from @chronos.sh/sdk:

Export Kind Description
Chronos Class Top-level client. The only class you construct.
ChronosError Class Base error class for all SDK errors.
ChronosConfigError Class Invalid constructor options.
ChronosApiError Class API responded with an error.
ChronosRateLimitError Class API responded with 429. Subclass of ChronosApiError.
ChronosApiErrorOptions Type Options for constructing ChronosApiError.
RateLimitInfo Type Rate-limit budget (limit, remaining, reset).
ChronosNetworkError Class Network/transport failure.
ChronosTimeoutError Class Request exceeded timeoutMs. Subclass of ChronosNetworkError.
ChronosHandlerError Class Handler threw an exception.
ChronosWebhookVerificationError Class Webhook signature verification failed.
Webhook Class Webhook signature verifier for push deliveries.
ChronosContext Type Context object passed to handlers.
ChronosHandler Type Handler function signature.
ChronosHandlerResult Type Valid handler return types.
ChronosLogger Type Logger interface.
ChronosOptions Type Constructor options.
ChronosSchedule Type Schedule reference on context.
FetchLike Type Custom fetch signature.
WebhookOptions Type Options for Webhook constructor.
DEFAULT_BASE_URL Constant 'https://api.chronos.sh'
DEFAULT_POLL_WAIT_TIME_SECONDS Constant 20
DEFAULT_TIMEOUT_MS Constant 30000
SDK_VERSION Constant Package version string, reported to the API on every claim.
DEFAULT_TIMESTAMP_TOLERANCE_SECONDS Constant 300