@cronvello/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,515 @@
1
+ /**
2
+ * Minimal HTTP transport over the global `fetch` (Node 18+). No axios, no node-fetch —
3
+ * zero runtime dependencies. Adds bearer auth, JSON (de)serialization, typed error mapping,
4
+ * bounded retries with backoff for transient failures, and optional idempotency keys.
5
+ */
6
+ type FetchLike = (input: string, init?: {
7
+ method?: string;
8
+ headers?: Record<string, string>;
9
+ body?: string;
10
+ signal?: AbortSignal;
11
+ }) => Promise<{
12
+ ok: boolean;
13
+ status: number;
14
+ headers: {
15
+ get(name: string): string | null;
16
+ };
17
+ text(): Promise<string>;
18
+ }>;
19
+ interface TransportOptions {
20
+ baseUrl: string;
21
+ apiKey: string;
22
+ /** Per-request timeout in ms. Default 30_000. */
23
+ timeoutMs?: number;
24
+ /** Max retry attempts for 429/5xx/network errors. Default 2 (=> up to 3 tries). */
25
+ maxRetries?: number;
26
+ /** Override the fetch implementation (testing / custom agents). */
27
+ fetch?: FetchLike;
28
+ /** Extra headers sent on every request (e.g. a custom User-Agent). */
29
+ defaultHeaders?: Record<string, string>;
30
+ /** Telemetry hook — called once per completed attempt. */
31
+ onRequest?: (info: {
32
+ method: string;
33
+ path: string;
34
+ status: number;
35
+ attempt: number;
36
+ durationMs: number;
37
+ }) => void;
38
+ }
39
+ interface RequestOptions {
40
+ method: string;
41
+ path: string;
42
+ query?: Record<string, string | number | boolean | undefined>;
43
+ body?: unknown;
44
+ /** Idempotency-Key header value — replays of the same key are de-duplicated server-side. */
45
+ idempotencyKey?: string;
46
+ signal?: AbortSignal;
47
+ }
48
+ declare class Transport {
49
+ private readonly baseUrl;
50
+ private readonly apiKey;
51
+ private readonly timeoutMs;
52
+ private readonly maxRetries;
53
+ private readonly fetchImpl;
54
+ private readonly defaultHeaders;
55
+ private readonly onRequest;
56
+ constructor(opts: TransportOptions);
57
+ request<T>(opts: RequestOptions): Promise<T>;
58
+ private buildUrl;
59
+ private withTimeout;
60
+ private backoff;
61
+ }
62
+
63
+ /**
64
+ * Wire types for the Cronvello public `/v1` API.
65
+ *
66
+ * These mirror the server-side Zod DTOs (`src/routes/v1/v1.dto.ts` in node-cron) but are
67
+ * hand-authored as plain TypeScript so the SDK ships with ZERO runtime dependencies. When
68
+ * the server contract changes, update these to match — they are the single source of truth
69
+ * for the SDK's request/response shapes. See `scripts/check-contract.md` for the drift check.
70
+ */
71
+ type Urgency = "low" | "medium" | "high" | "critical";
72
+ type ExecutionMode = "sync" | "async_callback";
73
+ type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
74
+ type JobStatus = "DISABLED" | "ACTIVE" | "ERROR" | "ERROR_MAX_RETRIES";
75
+ type RunStatus = "queued" | "running" | "completed" | "failed";
76
+ type RunType = "scheduled" | "manual";
77
+ type CallbackStatus = "pending" | "acknowledged" | "completed" | "failed" | "timed_out";
78
+ /** Optional success assertions beyond the default 2xx check. */
79
+ interface SuccessCriteria {
80
+ statusCodeMin?: number;
81
+ statusCodeMax?: number;
82
+ bodyContains?: string;
83
+ bodyJsonPath?: string;
84
+ bodyJsonEquals?: string | number | boolean;
85
+ bodyRegex?: string;
86
+ }
87
+ interface PublicJob {
88
+ id: string;
89
+ name: string;
90
+ description: string | null;
91
+ status: JobStatus;
92
+ urgency: Urgency | null;
93
+ createdAt: string;
94
+ updatedAt: string | null;
95
+ taskCount: number;
96
+ activeTaskCount: number;
97
+ errorTaskCount: number;
98
+ }
99
+ interface JobCreateBody {
100
+ name: string;
101
+ description?: string;
102
+ }
103
+ interface JobUpdateBody {
104
+ name?: string;
105
+ description?: string | null;
106
+ }
107
+ interface TaskCreateBody {
108
+ name: string;
109
+ description?: string;
110
+ /** Cron expression (validated server-side). */
111
+ schedule: string;
112
+ /** IANA timezone; defaults to "Europe/Berlin" server-side. */
113
+ timeZone?: string;
114
+ /** HTTPS endpoint Cronvello calls when the task is due. */
115
+ targetUrl: string;
116
+ /** Bearer token Cronvello attaches as `Authorization: Bearer <token>`. Encrypted at rest. */
117
+ targetToken?: string;
118
+ /** HTTP method for the outbound call (default POST). */
119
+ method?: HttpMethod;
120
+ /** Custom request headers (values encrypted at rest, never returned on read). */
121
+ headers?: Record<string, string>;
122
+ /** JSON string forwarded to the target as the request body. */
123
+ requestBody?: string;
124
+ requestTimeoutMs?: number;
125
+ successCriteria?: SuccessCriteria;
126
+ urgency?: Urgency;
127
+ maxRetries?: number;
128
+ executionMode?: ExecutionMode;
129
+ callbackTimeoutMs?: number;
130
+ allowConcurrentRuns?: boolean;
131
+ }
132
+ interface TaskUpdateBody {
133
+ name?: string;
134
+ description?: string | null;
135
+ schedule?: string;
136
+ timeZone?: string;
137
+ targetUrl?: string;
138
+ targetToken?: string | null;
139
+ method?: HttpMethod;
140
+ headers?: Record<string, string> | null;
141
+ requestBody?: string | null;
142
+ requestTimeoutMs?: number | null;
143
+ successCriteria?: SuccessCriteria | null;
144
+ urgency?: Urgency;
145
+ maxRetries?: number;
146
+ executionMode?: ExecutionMode;
147
+ callbackTimeoutMs?: number | null;
148
+ allowConcurrentRuns?: boolean;
149
+ }
150
+ interface PublicTask {
151
+ id: string;
152
+ jobId: string;
153
+ jobName: string;
154
+ name: string;
155
+ description: string | null;
156
+ schedule: string;
157
+ timeZone: string;
158
+ type: string;
159
+ status: JobStatus;
160
+ targetUrl: string | null;
161
+ hasTargetToken: boolean;
162
+ method: HttpMethod;
163
+ customHeaderNames: string[];
164
+ requestBody: string | null;
165
+ requestTimeoutMs: number | null;
166
+ successCriteria: SuccessCriteria | null;
167
+ urgency: Urgency | null;
168
+ maxRetries: number | null;
169
+ executionMode: string | null;
170
+ callbackTimeoutMs: number | null;
171
+ allowConcurrentRuns: boolean | null;
172
+ nextRun: string | null;
173
+ lastRunAt: string | null;
174
+ lastRunStatus: RunStatus | null;
175
+ createdAt: string;
176
+ hasOpenDlq: boolean;
177
+ statusReason: string;
178
+ }
179
+ interface PublicRun {
180
+ id: string;
181
+ jobTaskId: string;
182
+ taskName: string;
183
+ jobId: string;
184
+ jobName: string;
185
+ status: RunStatus;
186
+ runType: RunType;
187
+ httpStatusCode: number | null;
188
+ responseBody: string | null;
189
+ startedAt: string | null;
190
+ endedAt: string | null;
191
+ durationMs: number | null;
192
+ error: string | null;
193
+ executionMode: string | null;
194
+ callbackStatus: CallbackStatus | null;
195
+ }
196
+ interface Pagination {
197
+ page: number;
198
+ limit: number;
199
+ total: number;
200
+ totalPages: number;
201
+ hasNext: boolean;
202
+ hasPrev: boolean;
203
+ }
204
+ interface RunsPage {
205
+ runs: PublicRun[];
206
+ pagination: Pagination;
207
+ }
208
+ interface TasksPage {
209
+ tasks: PublicTask[];
210
+ pagination: Pagination;
211
+ }
212
+ interface RunNowResult {
213
+ success: boolean;
214
+ runId?: string;
215
+ async?: boolean;
216
+ error?: string;
217
+ }
218
+ interface RegistryTaskInput {
219
+ key: string;
220
+ schedule: string;
221
+ targetUrl: string;
222
+ targetToken?: string;
223
+ method?: HttpMethod;
224
+ headers?: Record<string, string>;
225
+ requestBody?: string;
226
+ requestTimeoutMs?: number;
227
+ timeZone?: string;
228
+ description?: string;
229
+ urgency?: Urgency;
230
+ maxRetries?: number;
231
+ executionMode?: ExecutionMode;
232
+ callbackTimeoutMs?: number;
233
+ allowConcurrentRuns?: boolean;
234
+ successCriteria?: SuccessCriteria;
235
+ enabled?: boolean;
236
+ }
237
+ interface RegistryReconcileRequest {
238
+ appName: string;
239
+ description?: string;
240
+ tasks: RegistryTaskInput[];
241
+ prune?: boolean;
242
+ rotateSecret?: boolean;
243
+ }
244
+ interface RegistryReconcileChange {
245
+ key: string;
246
+ action: "created" | "updated" | "unchanged" | "deleted" | "skipped";
247
+ taskId: string | null;
248
+ changedFields?: string[];
249
+ }
250
+ interface RegistryReconcileResponse {
251
+ job: PublicJob;
252
+ jobCreated: boolean;
253
+ created: number;
254
+ updated: number;
255
+ unchanged: number;
256
+ deleted: number;
257
+ skipped: number;
258
+ changes: RegistryReconcileChange[];
259
+ tasks: PublicTask[];
260
+ }
261
+ interface MeResponse {
262
+ accountId: number;
263
+ accountName: string;
264
+ email: string;
265
+ isActive: boolean;
266
+ createdAt: string;
267
+ serverTime: string;
268
+ plan: {
269
+ planName: string;
270
+ displayName: string;
271
+ } | null;
272
+ limits: {
273
+ maxJobs: number | null;
274
+ maxTasksPerJob: number | null;
275
+ minIntervalSeconds: number | null;
276
+ rateLimitPerMinute: number | null;
277
+ rateLimitPerHour: number | null;
278
+ };
279
+ usage: {
280
+ jobsUsed: number;
281
+ period: {
282
+ start: string;
283
+ end: string;
284
+ };
285
+ executionCount: number;
286
+ executionQuota: number | null;
287
+ };
288
+ summary: {
289
+ openDlqCount: number;
290
+ heartbeatNeedsAttention: number;
291
+ activeMaintenanceWindows: number;
292
+ };
293
+ }
294
+ interface UsageResponse {
295
+ accountId: number;
296
+ serverTime: string;
297
+ plan: {
298
+ planName: string;
299
+ displayName: string;
300
+ } | null;
301
+ period: {
302
+ start: string;
303
+ end: string;
304
+ };
305
+ usage: {
306
+ executionCount: number;
307
+ quota: number | null;
308
+ quotaExceeded: boolean;
309
+ quotaUsedPercent: number | null;
310
+ };
311
+ }
312
+ interface RunsQuery {
313
+ page?: number;
314
+ limit?: number;
315
+ status?: RunStatus;
316
+ runType?: RunType;
317
+ }
318
+ interface AccountTasksQuery {
319
+ page?: number;
320
+ limit?: number;
321
+ jobId?: string;
322
+ status?: JobStatus;
323
+ }
324
+ interface AccountRunsQuery {
325
+ page?: number;
326
+ limit?: number;
327
+ taskId?: string;
328
+ jobId?: string;
329
+ status?: RunStatus;
330
+ runType?: RunType;
331
+ }
332
+
333
+ /**
334
+ * Public types for the code-first registry layer (`defineCronvello`).
335
+ */
336
+
337
+ /** Context passed to a job handler when Cronvello fires it. */
338
+ interface CronvelloJobContext<Payload = Record<string, unknown>> {
339
+ /** The stable registry key of the job being run. */
340
+ key: string;
341
+ /** The cron schedule Cronvello fired for (echoed back in the request body). */
342
+ schedule: string;
343
+ /** Static payload configured on the job, if any. */
344
+ payload: Payload;
345
+ /** The full parsed request body Cronvello sent. */
346
+ body: Record<string, unknown>;
347
+ /** Raw request headers (lower-cased keys). */
348
+ headers: Record<string, string>;
349
+ /** True when this is an async_callback run (handler may take longer than the HTTP timeout). */
350
+ isAsync: boolean;
351
+ /** Aborts if the runtime cancels the request. */
352
+ signal: AbortSignal | undefined;
353
+ }
354
+ type CronvelloJobHandler<Payload = Record<string, unknown>> = (ctx: CronvelloJobContext<Payload>) => unknown | Promise<unknown>;
355
+ /** Shape of a single job in the keyed-object form. */
356
+ interface CronvelloJobConfig<Payload = Record<string, unknown>> {
357
+ /** Cron expression, e.g. "0 8 * * *". Validated by Cronvello on sync. */
358
+ schedule: string;
359
+ /** The function that runs when the job fires. */
360
+ handler: CronvelloJobHandler<Payload>;
361
+ /** Human description (stored on the Cronvello task). */
362
+ description?: string;
363
+ /** IANA timezone for this job's schedule. Defaults to the app-level `timeZone`. */
364
+ timeZone?: string;
365
+ urgency?: Urgency;
366
+ /** Max automatic retries on failure (0–20). */
367
+ maxRetries?: number;
368
+ /**
369
+ * "sync" (default): handler runs inline, result returned in the HTTP response.
370
+ * "async_callback": respond immediately, run in the background, post the result back.
371
+ * Use async only for work that exceeds the request timeout AND a long-running (non-serverless) host.
372
+ */
373
+ executionMode?: ExecutionMode;
374
+ /** Async-mode budget before Cronvello marks the run timed out (ms). */
375
+ callbackTimeoutMs?: number;
376
+ /** Allow a new run to start while a previous one is still in flight (default false). */
377
+ allowConcurrentRuns?: boolean;
378
+ /** Optional success assertions beyond the default 2xx. */
379
+ successCriteria?: SuccessCriteria;
380
+ /** Static payload merged into the request body Cronvello stores and sends. */
381
+ payload?: Payload;
382
+ /** Set false to keep the code but stop syncing/scheduling this job. */
383
+ enabled?: boolean;
384
+ }
385
+ /** Array form — each entry carries its own stable `key`. */
386
+ interface CronvelloJobConfigWithKey extends CronvelloJobConfig {
387
+ /** Stable identity. Renaming the display elsewhere never breaks the link. */
388
+ key: string;
389
+ }
390
+ /** Jobs may be declared as a keyed object (key = identity) or an array of keyed entries. */
391
+ type CronvelloJobsInput = Record<string, CronvelloJobConfig> | CronvelloJobConfigWithKey[];
392
+ interface CronvelloLogger {
393
+ debug?(msg: string, meta?: Record<string, unknown>): void;
394
+ info?(msg: string, meta?: Record<string, unknown>): void;
395
+ warn?(msg: string, meta?: Record<string, unknown>): void;
396
+ error?(msg: string, meta?: Record<string, unknown>): void;
397
+ }
398
+ interface CronvelloAppConfig {
399
+ /** Your jobs — keyed object or array form. */
400
+ jobs: CronvelloJobsInput;
401
+ /**
402
+ * App identity. Becomes the name of the single Cronvello Job container that holds all
403
+ * your tasks. Must be stable and unique within your account.
404
+ */
405
+ appName: string;
406
+ /**
407
+ * Public base URL of THIS app, where Cronvello delivers callbacks.
408
+ * e.g. "https://app.example.com". The dispatch handler is mounted under it.
409
+ */
410
+ appUrl: string;
411
+ /** Cronvello account API key (`crn_live_…`). */
412
+ apiKey: string;
413
+ /**
414
+ * Shared secret. Cronvello sends it back as `Authorization: Bearer <secret>` on every
415
+ * dispatch; the mounted handler verifies it in constant time. Generate a strong random
416
+ * value and store it in your env (e.g. `openssl rand -hex 32`).
417
+ */
418
+ dispatchSecret: string;
419
+ /** Path the dispatch handler is mounted at. Default "/cronvello/dispatch". */
420
+ dispatchPath?: string;
421
+ /** Cronvello API base URL. Default "https://api.cronvello.com". */
422
+ baseUrl?: string;
423
+ /** Default timezone for jobs that don't set their own. Default "Europe/Berlin". */
424
+ timeZone?: string;
425
+ /** Per-request API timeout in ms (default 30_000). */
426
+ timeoutMs?: number;
427
+ /** Retry attempts for transient API failures (default 2). */
428
+ maxRetries?: number;
429
+ /** Custom fetch implementation (default: global fetch). */
430
+ fetch?: FetchLike;
431
+ /** Optional structured logger. */
432
+ logger?: CronvelloLogger;
433
+ }
434
+ /** Per-job result of a reconcile. */
435
+ interface ReconcileTaskChange {
436
+ key: string;
437
+ action: "created" | "updated" | "unchanged" | "deleted" | "skipped";
438
+ taskId: string | null;
439
+ /** Field names that changed (for "updated"). */
440
+ changedFields?: string[];
441
+ reason?: string;
442
+ }
443
+ interface ReconcileResult {
444
+ jobId: string;
445
+ jobName: string;
446
+ /** True if the Job container was created during this sync. */
447
+ jobCreated: boolean;
448
+ created: number;
449
+ updated: number;
450
+ unchanged: number;
451
+ deleted: number;
452
+ skipped: number;
453
+ changes: ReconcileTaskChange[];
454
+ /** Resulting tasks — populated when the reconcile ran server-side (PUT /v1/registry). */
455
+ tasks?: PublicTask[];
456
+ }
457
+ interface SyncOptions {
458
+ /**
459
+ * Delete tasks in the app's Job container that are no longer in the registry.
460
+ * Default true — the container is fully SDK-managed. Set false to leave orphans.
461
+ */
462
+ prune?: boolean;
463
+ /** Re-write the dispatch secret on every task even if unchanged (use after rotating). */
464
+ rotateSecret?: boolean;
465
+ /** Compute the diff and return it WITHOUT making any changes. */
466
+ dryRun?: boolean;
467
+ }
468
+
469
+ /**
470
+ * Framework-neutral dispatch core.
471
+ *
472
+ * Cronvello fires a due task by POSTing to the app's dispatch URL with
473
+ * Authorization: Bearer <dispatchSecret>
474
+ * body: { job: "<registry-key>", schedule: "<cron>", ...payload, _callback?: {...} }
475
+ *
476
+ * This module verifies the bearer, selects the job by key, runs its handler, and returns a
477
+ * response. The Express/Next adapters are thin shells that translate their request/response
478
+ * objects to/from the neutral shapes below.
479
+ */
480
+
481
+ interface ResolvedJob {
482
+ key: string;
483
+ config: CronvelloJobConfig;
484
+ }
485
+ interface DispatchRequest {
486
+ method: string;
487
+ /** Value of the Authorization header, if any. */
488
+ authorization: string | undefined;
489
+ /** Raw request body as text. */
490
+ rawBody: string;
491
+ /** Lower-cased request headers. */
492
+ headers: Record<string, string>;
493
+ /** Abort signal for the inbound request, if the host provides one. */
494
+ signal?: AbortSignal;
495
+ /**
496
+ * Optional serverless lifecycle hook (e.g. Vercel/Cloudflare `ctx.waitUntil`). When present,
497
+ * async_callback background work is registered with it so the function stays alive until done.
498
+ */
499
+ waitUntil?: (promise: Promise<unknown>) => void;
500
+ }
501
+ interface DispatchResponse {
502
+ status: number;
503
+ body: Record<string, unknown>;
504
+ }
505
+
506
+ /**
507
+ * The minimal interface the adapters depend on: anything that can turn a neutral
508
+ * `DispatchRequest` into a `DispatchResponse`. `CronvelloApp` implements it.
509
+ */
510
+
511
+ interface DispatchHandler {
512
+ handle(req: DispatchRequest): Promise<DispatchResponse>;
513
+ }
514
+
515
+ export { type AccountTasksQuery as A, type RunStatus as B, type CronvelloAppConfig as C, type DispatchRequest as D, type ExecutionMode as E, type FetchLike as F, type RunType as G, type HttpMethod as H, type SuccessCriteria as I, type JobCreateBody as J, type Urgency as K, type DispatchHandler as L, type MeResponse as M, type PublicJob as P, type RunNowResult as R, type SyncOptions as S, Transport as T, type UsageResponse as U, type JobUpdateBody as a, type PublicTask as b, type TaskCreateBody as c, type TasksPage as d, type TaskUpdateBody as e, type RunsQuery as f, type RunsPage as g, type AccountRunsQuery as h, type PublicRun as i, type RegistryReconcileRequest as j, type RegistryReconcileResponse as k, type ResolvedJob as l, type ReconcileResult as m, type DispatchResponse as n, type CallbackStatus as o, type CronvelloJobConfig as p, type CronvelloJobConfigWithKey as q, type CronvelloJobContext as r, type CronvelloJobHandler as s, type CronvelloJobsInput as t, type CronvelloLogger as u, type JobStatus as v, type Pagination as w, type ReconcileTaskChange as x, type RegistryReconcileChange as y, type RegistryTaskInput as z };