@cronvello/sdk 0.2.1 → 0.4.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/CHANGELOG.md +56 -0
- package/README.md +102 -15
- package/dist/cli.cjs +990 -216
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +990 -216
- package/dist/cli.js.map +1 -1
- package/dist/dev.cjs +1001 -217
- package/dist/dev.cjs.map +1 -1
- package/dist/dev.d.cts +5 -0
- package/dist/dev.d.ts +5 -0
- package/dist/dev.js +1001 -217
- package/dist/dev.js.map +1 -1
- package/dist/{dispatch-handler-BoadpsL9.d.cts → dispatch-handler-yHIFEnNG.d.cts} +17 -5
- package/dist/{dispatch-handler-BoadpsL9.d.ts → dispatch-handler-yHIFEnNG.d.ts} +17 -5
- package/dist/express.d.cts +1 -1
- package/dist/express.d.ts +1 -1
- package/dist/index.cjs +191 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +280 -10
- package/dist/index.d.ts +280 -10
- package/dist/index.js +191 -26
- package/dist/index.js.map +1 -1
- package/dist/next.d.cts +1 -1
- package/dist/next.d.ts +1 -1
- package/docs/dashboard.png +0 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,62 @@
|
|
|
3
3
|
All notable changes to `@cronvello/sdk` are documented here. This project adheres to
|
|
4
4
|
[Semantic Versioning](https://semver.org/) (pre-1.0: minor-feature additions ship as patch releases).
|
|
5
5
|
|
|
6
|
+
## 0.3.0
|
|
7
|
+
|
|
8
|
+
**Credentials are no longer part of defining jobs.** 0.2.0 made the SDK run locally with no account,
|
|
9
|
+
but `defineCronvello()` still demanded an `apiKey`, an `appUrl` and a `dispatchSecret` before it
|
|
10
|
+
would construct anything. So the documented way to try the local engine was to invent a fake API
|
|
11
|
+
key for a scheduler that never makes a network call. That was real config standing in for no real
|
|
12
|
+
constraint, and it landed on exactly the people the local mode is meant to attract.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **`apiKey`, `appUrl` and `dispatchSecret` are now optional.** `defineCronvello({ appName, jobs })`
|
|
17
|
+
is a complete, valid app. `cronvello dev`, `dev()` and `trigger()` work on it unchanged.
|
|
18
|
+
- **The hosted side validates where it is used, not at define time.** `sync()`, `run()`, `client`,
|
|
19
|
+
`dispatchUrl`, `expressHandler()` and `nextHandler()` each check what they actually need and throw
|
|
20
|
+
a `CronvelloConfigError` naming every missing field. `client` is now built on first access rather
|
|
21
|
+
than eagerly.
|
|
22
|
+
- A *present* but malformed value is still rejected at define time (`appUrl` must be absolute
|
|
23
|
+
http(s), `dispatchSecret` at least 16 chars). This removes a requirement, not a check.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **`app.isCloudConfigured`** — true when `apiKey`, `appUrl` and `dispatchSecret` are all present.
|
|
28
|
+
- **A rebuilt local dashboard.** The old page answered "what are my jobs" but not "is this healthy",
|
|
29
|
+
so you had to read a log to find out. It now opens on a summary (jobs, runs, succeeded, failed,
|
|
30
|
+
next fire) and a **timeline**: one lane per job, past runs plotted against upcoming fires around a
|
|
31
|
+
moving now-line. A schedule that is wrong shows up there immediately.
|
|
32
|
+
- Run detail now includes the **error message** or the **returned value**, which is the thing you
|
|
33
|
+
opened the panel for.
|
|
34
|
+
- The feed can be paused, and says how many events it is holding back.
|
|
35
|
+
- Rows are keyboard-reachable, Escape closes the drawer and returns focus.
|
|
36
|
+
- The page is driven by one `/api/state` snapshot instead of a fetch per event, and the timeline
|
|
37
|
+
advances by translating a fixed track rather than repositioning every mark each second.
|
|
38
|
+
- **`GET /api/state`** on the dashboard server: jobs (with last run and upcoming fires), run history
|
|
39
|
+
and engine state in one response.
|
|
40
|
+
- **`engine.startedAt`** and **`engine.running`** on `LocalEngine`.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- The dashboard's job table stayed on "Loading" forever when the registry was empty, because
|
|
45
|
+
"no jobs" and "not loaded yet" looked identical to it.
|
|
46
|
+
- A `scheduled` event carries the *next fire time* as its timestamp, which the feed printed in its
|
|
47
|
+
clock column and so appeared to jump forward in time. It now reads "scheduled for HH:MM:SS" and
|
|
48
|
+
the clock column is strictly arrival time.
|
|
49
|
+
|
|
50
|
+
### Security
|
|
51
|
+
|
|
52
|
+
- An app without a `dispatchSecret` never runs a job from an HTTP request. `expressHandler()` and
|
|
53
|
+
`nextHandler()` throw at mount time rather than exposing a route, and `handle()` answers `500`
|
|
54
|
+
("Dispatch is not configured") before touching the body. There is no path in which a missing
|
|
55
|
+
secret degrades into accepting unauthenticated dispatches.
|
|
56
|
+
|
|
57
|
+
### Compatibility
|
|
58
|
+
|
|
59
|
+
Backwards compatible: every config valid in 0.2.x is still valid and behaves identically. Minor
|
|
60
|
+
rather than patch because the public config type widened and `client` / `dispatchUrl` became lazy.
|
|
61
|
+
|
|
6
62
|
## 0.2.1
|
|
7
63
|
|
|
8
64
|
- Move the canonical SDK source to the public
|
package/README.md
CHANGED
|
@@ -4,14 +4,45 @@
|
|
|
4
4
|
[](https://github.com/niccasWilliams/cronvello-sdk/actions/workflows/ci.yml)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
7
|
-
**Code-first cron jobs
|
|
8
|
-
your
|
|
9
|
-
required
|
|
7
|
+
**Code-first cron jobs.** Declare your scheduled jobs in your codebase and run them: with a real
|
|
8
|
+
scheduler on your own machine, and optionally with [Cronvello](https://cronvello.com) hosting them.
|
|
9
|
+
**No dashboard required.** Your jobs always follow your code.
|
|
10
|
+
|
|
11
|
+
## Try it without an account
|
|
12
|
+
|
|
13
|
+
Three steps, no signup, nothing leaves your machine:
|
|
10
14
|
|
|
11
15
|
```bash
|
|
12
16
|
npm i @cronvello/sdk
|
|
13
17
|
```
|
|
14
18
|
|
|
19
|
+
```ts
|
|
20
|
+
// cronvello.config.ts
|
|
21
|
+
import { defineCronvello, every } from "@cronvello/sdk";
|
|
22
|
+
|
|
23
|
+
export default defineCronvello({
|
|
24
|
+
appName: "my-app",
|
|
25
|
+
jobs: {
|
|
26
|
+
heartbeat: { schedule: every("10s"), handler: async () => console.log("beat") },
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx cronvello dev
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
That starts an actual scheduler loop: each next fire time computed from the cron expression,
|
|
36
|
+
timezone- and DST-aware, with overlap protection, per-run timeouts and retry with backoff. It makes
|
|
37
|
+
no network calls. This part is MIT licensed and works standalone. See
|
|
38
|
+
[Local development](#local-development-no-account--cronvello-dev) for the full output.
|
|
39
|
+
|
|
40
|
+
The hosted side adds what a local loop can't: run history, retries you can inspect, alerts when a
|
|
41
|
+
run *doesn't* happen, and replay. It's opt-in, and only the calls that use it (`sync()`, `run()`,
|
|
42
|
+
the dispatch handler) need credentials.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
15
46
|
- ✅ **Runs locally with no account** — `npx cronvello dev` starts a real scheduler loop on your
|
|
16
47
|
machine (timezone/DST, overlap protection, timeouts, retry/backoff). No cloud, no network.
|
|
17
48
|
- ✅ **Zero runtime dependencies** (Node 20+ `fetch` + WebCrypto).
|
|
@@ -27,7 +58,7 @@ npm i @cronvello/sdk
|
|
|
27
58
|
|
|
28
59
|
---
|
|
29
60
|
|
|
30
|
-
## The idea
|
|
61
|
+
## The idea (hosted)
|
|
31
62
|
|
|
32
63
|
You declare jobs once, in code. On every deploy you call `sync()`, and the SDK reconciles your
|
|
33
64
|
registry into Cronvello: **one Cronvello "job" container for your app, one task per registry
|
|
@@ -37,7 +68,10 @@ Change a schedule → it's updated. Idempotent, every time.
|
|
|
37
68
|
|
|
38
69
|
---
|
|
39
70
|
|
|
40
|
-
##
|
|
71
|
+
## Hosted quick start (Express)
|
|
72
|
+
|
|
73
|
+
> Needs a Cronvello account. To stay local, skip to
|
|
74
|
+
> [Local development](#local-development-no-account--cronvello-dev).
|
|
41
75
|
|
|
42
76
|
```ts
|
|
43
77
|
// cronvello.ts
|
|
@@ -89,7 +123,7 @@ That's it. No dashboard clicks. The jobs in your code are the jobs that run.
|
|
|
89
123
|
|
|
90
124
|
---
|
|
91
125
|
|
|
92
|
-
##
|
|
126
|
+
## Hosted quick start (Next.js App Router)
|
|
93
127
|
|
|
94
128
|
```ts
|
|
95
129
|
// app/cronvello/dispatch/route.ts
|
|
@@ -221,15 +255,15 @@ your machine that actually fires the handlers when they're due — computing eac
|
|
|
221
255
|
its cron expression (timezone- and DST-aware) and enforcing the same production policies the cloud
|
|
222
256
|
does: **overlap protection, per-run timeout, and retry with backoff**. No cloud, no network.
|
|
223
257
|
|
|
258
|
+
Jobs are the only required config. `apiKey`, `appUrl` and `dispatchSecret` belong to the hosted
|
|
259
|
+
side, and nothing local asks for them:
|
|
260
|
+
|
|
224
261
|
```ts
|
|
225
262
|
// cronvello.config.ts
|
|
226
263
|
import { defineCronvello, every, daily } from "@cronvello/sdk";
|
|
227
264
|
|
|
228
265
|
export const cronvello = defineCronvello({
|
|
229
266
|
appName: "my-app",
|
|
230
|
-
appUrl: "https://my-app.example.com",
|
|
231
|
-
apiKey: "crn_local_dev", // any non-empty value — the local engine never calls the API
|
|
232
|
-
dispatchSecret: "local-dev-secret-0123456789abcdef",
|
|
233
267
|
timeZone: "Europe/Berlin",
|
|
234
268
|
jobs: {
|
|
235
269
|
heartbeat: { schedule: every("10s"), handler: async () => { console.log("beat"); } },
|
|
@@ -295,15 +329,22 @@ npx cronvello dev --dashboard # → http://127.0.0.1:4747 (override
|
|
|
295
329
|
|
|
296
330
|
Open the printed URL and you get, live:
|
|
297
331
|
|
|
332
|
+
- a **summary** across the session: jobs, runs, how many succeeded, how many failed, and what fires
|
|
333
|
+
next,
|
|
334
|
+
- a **timeline** with one lane per job, plotting the runs that already happened next to the fires
|
|
335
|
+
still to come, around a now-line that moves as you watch. A schedule that is wrong is usually
|
|
336
|
+
obvious here before it is obvious anywhere else,
|
|
298
337
|
- every **job** with its cron expression, timezone, and a **countdown to the next fire**,
|
|
299
|
-
- a **run feed** that streams each fire, success, error, timeout, retry, and skip as it happens,
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
338
|
+
- a **run feed** that streams each fire, success, error, timeout, retry, and skip as it happens, with
|
|
339
|
+
a pause that tells you how much you're missing,
|
|
340
|
+
- a **job drawer** with the next fire times and recent runs, each one carrying its **error message**
|
|
341
|
+
or its **returned value**, plus a **Run now** button that triggers the handler through the engine,
|
|
342
|
+
- light + dark, keyboard-reachable rows, and an honest **reconnecting** state if the engine stops.
|
|
303
343
|
|
|
304
344
|
It binds to `127.0.0.1` by default (it's a dev tool, not a public server) and exposes a tiny
|
|
305
|
-
read-only JSON API plus an SSE stream —
|
|
306
|
-
as NDJSON. You can also start it
|
|
345
|
+
read-only JSON API plus an SSE stream — `GET /api/state` for everything the page draws, and
|
|
346
|
+
`GET /api/runs.ndjson` to pipe the run history out as NDJSON. You can also start it
|
|
347
|
+
programmatically:
|
|
307
348
|
|
|
308
349
|
```ts
|
|
309
350
|
const engine = cronvello.dev({ dashboard: true }); // or { dashboard: { port: 5000 } }
|
|
@@ -423,6 +464,10 @@ node -e "console.log(require('@cronvello/sdk').generateDispatchSecret())"
|
|
|
423
464
|
Store it as `CRONVELLO_DISPATCH_SECRET` in both your app env and nowhere else — `sync()` registers
|
|
424
465
|
it with Cronvello as the task's bearer token (encrypted at rest; never returned on read).
|
|
425
466
|
|
|
467
|
+
An app defined without a `dispatchSecret` has no HTTP entry point to protect, so `expressHandler()`
|
|
468
|
+
and `nextHandler()` refuse to be mounted at all, and a dispatch that somehow reaches `handle()` is
|
|
469
|
+
rejected. There is no configuration in which an unauthenticated request runs a job.
|
|
470
|
+
|
|
426
471
|
---
|
|
427
472
|
|
|
428
473
|
## Long-running jobs — `async_callback`
|
|
@@ -549,6 +594,48 @@ Transient failures (429 / 5xx / network) are retried automatically with backoff.
|
|
|
549
594
|
|
|
550
595
|
---
|
|
551
596
|
|
|
597
|
+
## Operator client
|
|
598
|
+
|
|
599
|
+
You do not need this to *use* Cronvello. It is the backend-to-backend surface for the service
|
|
600
|
+
that **provisions apps into** Cronvello — registering them, checking their registration, minting
|
|
601
|
+
new per-app tokens, removing them.
|
|
602
|
+
|
|
603
|
+
It is a separate class on purpose. It runs against the same host as `/v1`, but it takes
|
|
604
|
+
Cronvello's **service key**, which is authorized across every registered app — far broader than
|
|
605
|
+
an account `apiKey`. Two classes with two differently named options means you cannot send the
|
|
606
|
+
wrong credential by accident.
|
|
607
|
+
|
|
608
|
+
```ts
|
|
609
|
+
import { CronvelloAdminClient } from "@cronvello/sdk";
|
|
610
|
+
|
|
611
|
+
const admin = new CronvelloAdminClient({ serviceKey: process.env.CRONVELLO_SERVICE_KEY! });
|
|
612
|
+
|
|
613
|
+
// Idempotent upsert, keyed on the string appId. Re-run it on every provisioning pass.
|
|
614
|
+
const app = await admin.externalApps.register({
|
|
615
|
+
appId: "node-shop",
|
|
616
|
+
name: "Shop",
|
|
617
|
+
base_url: "https://shop.example.com",
|
|
618
|
+
generateApiKey: true,
|
|
619
|
+
});
|
|
620
|
+
app.generatedApiKey; // plaintext, exactly ONCE, and only for a newly created app
|
|
621
|
+
|
|
622
|
+
const status = await admin.externalApps.status("node-shop");
|
|
623
|
+
// { registered, isActive, isLive, lastSyncedAt, jobCount }
|
|
624
|
+
|
|
625
|
+
// Drift recovery when the current token is lost. Invalidates the old one.
|
|
626
|
+
const { newApiKey } = await admin.externalApps.rotateKey("node-shop");
|
|
627
|
+
|
|
628
|
+
// Takes the NUMERIC app id, not the string appId — an asymmetry in the server contract.
|
|
629
|
+
await admin.externalApps.delete(app.id);
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
Cross-field rules (`base_url` or `targetUrl`; a key or `generateApiKey`; both OAuth credentials)
|
|
633
|
+
are checked before the request leaves, so a bad call raises `CronvelloConfigError` synchronously
|
|
634
|
+
rather than returning an opaque 400. Everything else — errors, retries, envelope handling —
|
|
635
|
+
behaves exactly as the low-level client above.
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
552
639
|
## License
|
|
553
640
|
|
|
554
641
|
MIT
|