@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.
- package/LICENSE +21 -0
- package/README.md +266 -0
- package/dist/dispatch-handler-Bnda1Ekq.d.cts +515 -0
- package/dist/dispatch-handler-Bnda1Ekq.d.ts +515 -0
- package/dist/express.cjs +48 -0
- package/dist/express.cjs.map +1 -0
- package/dist/express.d.cts +29 -0
- package/dist/express.d.ts +29 -0
- package/dist/express.js +46 -0
- package/dist/express.js.map +1 -0
- package/dist/index.cjs +908 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +214 -0
- package/dist/index.d.ts +214 -0
- package/dist/index.js +899 -0
- package/dist/index.js.map +1 -0
- package/dist/next.cjs +26 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +28 -0
- package/dist/next.d.ts +28 -0
- package/dist/next.js +24 -0
- package/dist/next.js.map +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cronvello
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# @cronvello/sdk
|
|
2
|
+
|
|
3
|
+
**Code-first cron jobs for [Cronvello](https://cronvello.com).** Define your scheduled jobs in
|
|
4
|
+
your codebase, and the SDK keeps them in sync with Cronvello and runs them — **no dashboard
|
|
5
|
+
required**. Your jobs always follow your code.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i @cronvello/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- ✅ **Zero runtime dependencies** (Node 18+ `fetch`).
|
|
12
|
+
- ✅ **Dual ESM + CJS**, full TypeScript types.
|
|
13
|
+
- ✅ **Base URL baked in** (`https://api.cronvello.com`) — override only for self-hosting.
|
|
14
|
+
- ✅ **Two layers**: a turnkey code-first **registry**, and a typed **low-level client** for the
|
|
15
|
+
full `/v1` API.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## The idea
|
|
20
|
+
|
|
21
|
+
You declare jobs once, in code. On every deploy you call `sync()`, and the SDK reconciles your
|
|
22
|
+
registry into Cronvello: **one Cronvello "job" container for your app, one task per registry
|
|
23
|
+
entry.** When a task is due, Cronvello calls your app back over HTTPS; the SDK's mounted handler
|
|
24
|
+
verifies the call and runs the right job. Add a job → it appears. Remove a job → it's pruned.
|
|
25
|
+
Change a schedule → it's updated. Idempotent, every time.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Quick start (Express)
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// cronvello.ts
|
|
33
|
+
import { defineCronvello } from "@cronvello/sdk";
|
|
34
|
+
|
|
35
|
+
export const cronvello = defineCronvello({
|
|
36
|
+
appName: "my-app", // your app's identity (stable, unique)
|
|
37
|
+
appUrl: process.env.APP_URL!, // public URL of THIS app, e.g. https://my-app.com
|
|
38
|
+
apiKey: process.env.CRONVELLO_API_KEY!, // crn_live_…
|
|
39
|
+
dispatchSecret: process.env.CRONVELLO_DISPATCH_SECRET!, // random 32-byte secret
|
|
40
|
+
|
|
41
|
+
jobs: {
|
|
42
|
+
"send-daily-digest": {
|
|
43
|
+
schedule: "0 8 * * *",
|
|
44
|
+
handler: async () => {
|
|
45
|
+
await sendDigests();
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
"cleanup-temp": {
|
|
49
|
+
schedule: "*/15 * * * *",
|
|
50
|
+
description: "Purge temp files older than an hour",
|
|
51
|
+
handler: async ({ schedule }) => {
|
|
52
|
+
const removed = await purgeTemp();
|
|
53
|
+
return { removed, schedule }; // returned value is recorded on the run
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// server.ts
|
|
62
|
+
import express from "express";
|
|
63
|
+
import { cronvello } from "./cronvello";
|
|
64
|
+
|
|
65
|
+
const app = express();
|
|
66
|
+
|
|
67
|
+
// Mount the dispatch endpoint Cronvello calls when a job is due.
|
|
68
|
+
app.post(cronvello.dispatchPath, express.json(), cronvello.expressHandler());
|
|
69
|
+
|
|
70
|
+
app.listen(3000, async () => {
|
|
71
|
+
// Reconcile the registry → Cronvello. Safe to run on every boot/deploy.
|
|
72
|
+
const result = await cronvello.sync();
|
|
73
|
+
console.log(`Cronvello synced: +${result.created} ~${result.updated} -${result.deleted}`);
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
That's it. No dashboard clicks. The jobs in your code are the jobs that run.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Quick start (Next.js App Router)
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// app/cronvello/dispatch/route.ts
|
|
85
|
+
import { cronvello } from "@/lib/cronvello";
|
|
86
|
+
export const POST = cronvello.nextHandler();
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// scripts/sync-cron.ts (run in CI / on deploy)
|
|
91
|
+
import { cronvello } from "@/lib/cronvello";
|
|
92
|
+
await cronvello.sync();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> On serverless hosts use the default **sync** execution mode so jobs finish within the request.
|
|
96
|
+
> `async_callback` (below) needs a host that keeps running after the HTTP response.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Defining jobs — two styles, your choice
|
|
101
|
+
|
|
102
|
+
**Keyed object** (the key is the stable identity):
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
jobs: {
|
|
106
|
+
"rotate-keys": { schedule: "0 3 * * 0", handler: rotateKeys },
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Array** (each entry carries its own `key`) — handy when jobs live across modules:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { billingJobs } from "./billing/jobs";
|
|
114
|
+
import { reportJobs } from "./reports/jobs";
|
|
115
|
+
|
|
116
|
+
jobs: [...billingJobs, ...reportJobs] // each: { key, schedule, handler, … }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Job options
|
|
120
|
+
|
|
121
|
+
| Field | Default | Notes |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| `schedule` | — | Cron expression, validated by Cronvello on sync. |
|
|
124
|
+
| `handler` | — | `async (ctx) => result`. The return value is recorded on the run. |
|
|
125
|
+
| `description` | — | Stored on the task. |
|
|
126
|
+
| `timeZone` | app default (`Europe/Berlin`) | IANA zone. |
|
|
127
|
+
| `urgency` | — | `low` \| `medium` \| `high` \| `critical`. |
|
|
128
|
+
| `maxRetries` | server default | 0–20. |
|
|
129
|
+
| `executionMode` | `sync` | `async_callback` for long jobs (see below). |
|
|
130
|
+
| `allowConcurrentRuns` | `false` | Allow overlap with an in-flight run. |
|
|
131
|
+
| `payload` | — | Static object merged into the request body and exposed as `ctx.payload`. |
|
|
132
|
+
| `enabled` | `true` | `false` keeps the code but stops scheduling. |
|
|
133
|
+
|
|
134
|
+
### The handler context
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
handler: async (ctx) => {
|
|
138
|
+
ctx.key; // "cleanup-temp"
|
|
139
|
+
ctx.schedule; // "*/15 * * * *"
|
|
140
|
+
ctx.payload; // your static payload, if any
|
|
141
|
+
ctx.body; // full request body Cronvello sent
|
|
142
|
+
ctx.isAsync; // true under async_callback mode
|
|
143
|
+
};
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## `sync()` — idempotent reconcile
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const result = await cronvello.sync();
|
|
152
|
+
// { jobId, jobName, jobCreated, created, updated, unchanged, deleted, skipped, changes[] }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
- Ensures one Cronvello **job container** named `appName`.
|
|
156
|
+
- For each registry job: **creates** it (and starts it — Cronvello creates tasks disabled),
|
|
157
|
+
**patches** it when the schedule/options changed, or leaves it **unchanged**.
|
|
158
|
+
- **Prunes** tasks in the container that are no longer in your registry (`prune: false` to keep them).
|
|
159
|
+
- `dryRun: true` returns the diff without touching anything.
|
|
160
|
+
|
|
161
|
+
Under the hood `sync()` does this in a **single server-side call** (`PUT /v1/registry`) when the
|
|
162
|
+
API supports it, and transparently falls back to a client-side diff against older servers — you
|
|
163
|
+
don't need to care which path ran.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
await cronvello.sync({ dryRun: true }); // preview
|
|
167
|
+
await cronvello.sync({ prune: false }); // never delete
|
|
168
|
+
await cronvello.sync({ rotateSecret: true }); // re-write the dispatch secret on every task
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Only tasks inside **your** app's container are ever touched — anything else in your account is
|
|
172
|
+
left alone.
|
|
173
|
+
|
|
174
|
+
### Trigger a job now
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
await cronvello.run("send-daily-digest"); // manual one-off run via Cronvello
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Security model
|
|
183
|
+
|
|
184
|
+
Cronvello calls your dispatch endpoint with `Authorization: Bearer <dispatchSecret>`. The mounted
|
|
185
|
+
handler verifies it in **constant time** before running anything. Generate a strong secret once:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
node -e "console.log(require('@cronvello/sdk').generateDispatchSecret())"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Store it as `CRONVELLO_DISPATCH_SECRET` in both your app env and nowhere else — `sync()` registers
|
|
192
|
+
it with Cronvello as the task's bearer token (encrypted at rest; never returned on read).
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Long-running jobs — `async_callback`
|
|
197
|
+
|
|
198
|
+
For work that exceeds the request timeout, set `executionMode: "async_callback"`. The handler
|
|
199
|
+
returns immediately with `202`, runs in the background, and the SDK posts the result back to
|
|
200
|
+
Cronvello (HMAC-signed). Use only on a host that keeps executing after the response (a
|
|
201
|
+
long-running Node server — **not** typical serverless).
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
"rebuild-search-index": {
|
|
205
|
+
schedule: "0 4 * * *",
|
|
206
|
+
executionMode: "async_callback",
|
|
207
|
+
callbackTimeoutMs: 600_000,
|
|
208
|
+
handler: async () => { await rebuildIndex(); },
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Low-level client
|
|
215
|
+
|
|
216
|
+
The full typed `/v1` surface, for anything beyond the registry model:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import { CronvelloClient } from "@cronvello/sdk";
|
|
220
|
+
|
|
221
|
+
const cv = new CronvelloClient({ apiKey: process.env.CRONVELLO_API_KEY! });
|
|
222
|
+
|
|
223
|
+
const jobs = await cv.jobs.list();
|
|
224
|
+
const job = await cv.jobs.create({ name: "adhoc" });
|
|
225
|
+
const task = await cv.jobs.createTask(job.id, {
|
|
226
|
+
name: "ping",
|
|
227
|
+
schedule: "* * * * *",
|
|
228
|
+
targetUrl: "https://example.com/ping",
|
|
229
|
+
});
|
|
230
|
+
await cv.tasks.start(task.id);
|
|
231
|
+
|
|
232
|
+
const runs = await cv.runs.list({ limit: 20 });
|
|
233
|
+
const me = await cv.account.me();
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Endpoints not yet wrapped (heartbeat monitors, maintenance windows, DLQ, notification channels,
|
|
237
|
+
audit log, API-key self-service, analytics) are reachable via the escape hatch:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const monitors = await cv.request("GET", "/v1/heartbeat-monitors");
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Errors
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { CronvelloApiError } from "@cronvello/sdk";
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
await cv.jobs.get("missing");
|
|
250
|
+
} catch (e) {
|
|
251
|
+
if (e instanceof CronvelloApiError) {
|
|
252
|
+
e.status; // 404
|
|
253
|
+
e.isNotFound; // true
|
|
254
|
+
e.isRateLimited; // false
|
|
255
|
+
e.retryAfterSeconds; // set on 429
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Transient failures (429 / 5xx / network) are retried automatically with backoff.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## License
|
|
265
|
+
|
|
266
|
+
MIT
|