@cronwatch/sdk 0.10.0 → 0.11.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/README.md +4 -4
- package/dist/anthropic.d.cts +1 -1
- package/dist/anthropic.d.ts +1 -1
- package/dist/bugsnag.d.cts +1 -1
- package/dist/bugsnag.d.ts +1 -1
- package/dist/{client-DUoSea7X.d.cts → client-CRMfhylq.d.cts} +63 -26
- package/dist/{client-BcIjVBiZ.d.ts → client-D2Xk2600.d.ts} +63 -26
- package/dist/d1.cjs +29 -11
- package/dist/d1.cjs.map +1 -1
- package/dist/d1.d.cts +1 -1
- package/dist/d1.d.ts +1 -1
- package/dist/d1.js +29 -11
- package/dist/d1.js.map +1 -1
- package/dist/datadog.d.cts +1 -1
- package/dist/datadog.d.ts +1 -1
- package/dist/discord.cjs +10 -4
- package/dist/discord.cjs.map +1 -1
- package/dist/discord.d.cts +13 -6
- package/dist/discord.d.ts +13 -6
- package/dist/discord.js +7 -1
- package/dist/discord.js.map +1 -1
- package/dist/{email-VrGsVz43.d.ts → email-BWuUiCkw.d.ts} +1 -1
- package/dist/{email-JZs3vxwq.d.cts → email-Dcqe3k8n.d.cts} +1 -1
- package/dist/honeybadger.d.cts +1 -1
- package/dist/honeybadger.d.ts +1 -1
- package/dist/index.cjs +177 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -9
- package/dist/index.d.ts +15 -9
- package/dist/index.js +176 -40
- package/dist/index.js.map +1 -1
- package/dist/mailgun.d.cts +2 -2
- package/dist/mailgun.d.ts +2 -2
- package/dist/newrelic.d.cts +1 -1
- package/dist/newrelic.d.ts +1 -1
- package/dist/pg-cron.cjs +17 -7
- package/dist/pg-cron.cjs.map +1 -1
- package/dist/pg-cron.d.cts +15 -12
- package/dist/pg-cron.d.ts +15 -12
- package/dist/pg-cron.js +14 -4
- package/dist/pg-cron.js.map +1 -1
- package/dist/postgres.cjs +28 -10
- package/dist/postgres.cjs.map +1 -1
- package/dist/postgres.d.cts +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.js +28 -10
- package/dist/postgres.js.map +1 -1
- package/dist/postmark.d.cts +2 -2
- package/dist/postmark.d.ts +2 -2
- package/dist/resend.d.cts +2 -2
- package/dist/resend.d.ts +2 -2
- package/dist/rollbar.d.cts +1 -1
- package/dist/rollbar.d.ts +1 -1
- package/dist/sendgrid.d.cts +2 -2
- package/dist/sendgrid.d.ts +2 -2
- package/dist/sentry.cjs +4 -1
- package/dist/sentry.cjs.map +1 -1
- package/dist/sentry.d.cts +5 -2
- package/dist/sentry.d.ts +5 -2
- package/dist/sentry.js +4 -1
- package/dist/sentry.js.map +1 -1
- package/dist/ses.d.cts +2 -2
- package/dist/ses.d.ts +2 -2
- package/dist/slack.d.cts +1 -1
- package/dist/slack.d.ts +1 -1
- package/dist/sqlite.cjs +29 -11
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +1 -1
- package/dist/sqlite.d.ts +1 -1
- package/dist/sqlite.js +29 -11
- package/dist/sqlite.js.map +1 -1
- package/dist/twilio.cjs +10 -4
- package/dist/twilio.cjs.map +1 -1
- package/dist/twilio.d.cts +12 -7
- package/dist/twilio.d.ts +12 -7
- package/dist/twilio.js +7 -1
- package/dist/twilio.js.map +1 -1
- package/dist/{types-DyWCxlSi.d.cts → types-DLnlUie8.d.cts} +5 -2
- package/dist/{types-DyWCxlSi.d.ts → types-DLnlUie8.d.ts} +5 -2
- package/dist/webhook.cjs +10 -5
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.d.cts +18 -7
- package/dist/webhook.d.ts +18 -7
- package/dist/webhook.js +10 -6
- package/dist/webhook.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
export { F as FetchHandler, H as HandlerFn,
|
|
3
|
-
import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-
|
|
4
|
-
export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule,
|
|
1
|
+
import { C as CronwatchOptions, a as Cronwatch } from './client-CRMfhylq.cjs';
|
|
2
|
+
export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-CRMfhylq.cjs';
|
|
3
|
+
import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-DLnlUie8.cjs';
|
|
4
|
+
export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-DLnlUie8.cjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Keeps everything in process memory. The default when no store is given,
|
|
@@ -10,6 +10,9 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
|
|
|
10
10
|
*/
|
|
11
11
|
declare function memory(): Store;
|
|
12
12
|
|
|
13
|
+
/** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
|
|
14
|
+
declare const VERSION = "0.11.0";
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* "15m" -> 900000. Accepts a plain number of milliseconds, and compound
|
|
15
18
|
* strings such as "1h30m". Whitespace between parts is fine.
|
|
@@ -28,9 +31,12 @@ interface ParsedSchedule {
|
|
|
28
31
|
}
|
|
29
32
|
/**
|
|
30
33
|
* "0 2 * * *" (cron, five or six fields), "@hourly", or "every 5m".
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
+
* Text with a colon after its first character, which croner would take for
|
|
35
|
+
* a one-time date, is refused. A cron that names a date no month has
|
|
36
|
+
* ("0 0 30 2 *") is read, and never fires: nothing is ever due, so it is
|
|
37
|
+
* never missed. Parsed once per (schedule, timezone) pair and cached. The
|
|
38
|
+
* croner instance gets no callback, so it schedules nothing; it is only used
|
|
39
|
+
* to compute fire times. Without a timezone the expression is read in the process timezone,
|
|
34
40
|
* like crontab. Vercel and GitHub Actions run their crons in UTC, so pass
|
|
35
41
|
* timezone: "UTC" for those.
|
|
36
42
|
*/
|
|
@@ -47,6 +53,6 @@ declare function composeAlert(draft: AlertDraft, def: StoredJobDefinition, now:
|
|
|
47
53
|
* export const cw = cronwatch({ store: sqlite({ path: "./data/cronwatch.db" }), alerts: [slack({ webhookUrl })] });
|
|
48
54
|
* export const nightly = cw.job("nightly-report", { schedule: "0 2 * * *", grace: "15m" });
|
|
49
55
|
*/
|
|
50
|
-
declare function cronwatch(options?:
|
|
56
|
+
declare function cronwatch(options?: CronwatchOptions): Cronwatch;
|
|
51
57
|
|
|
52
|
-
export { Alert, AlertDraft,
|
|
58
|
+
export { Alert, AlertDraft, Cronwatch, CronwatchOptions, Duration, type ParsedSchedule, Store, StoredJobDefinition, VERSION, composeAlert, cronwatch, formatDuration, memory, nextFire, parseDuration, parseSchedule };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
export { F as FetchHandler, H as HandlerFn,
|
|
3
|
-
import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-
|
|
4
|
-
export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule,
|
|
1
|
+
import { C as CronwatchOptions, a as Cronwatch } from './client-D2Xk2600.js';
|
|
2
|
+
export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-D2Xk2600.js';
|
|
3
|
+
import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-DLnlUie8.js';
|
|
4
|
+
export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-DLnlUie8.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Keeps everything in process memory. The default when no store is given,
|
|
@@ -10,6 +10,9 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
|
|
|
10
10
|
*/
|
|
11
11
|
declare function memory(): Store;
|
|
12
12
|
|
|
13
|
+
/** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
|
|
14
|
+
declare const VERSION = "0.11.0";
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* "15m" -> 900000. Accepts a plain number of milliseconds, and compound
|
|
15
18
|
* strings such as "1h30m". Whitespace between parts is fine.
|
|
@@ -28,9 +31,12 @@ interface ParsedSchedule {
|
|
|
28
31
|
}
|
|
29
32
|
/**
|
|
30
33
|
* "0 2 * * *" (cron, five or six fields), "@hourly", or "every 5m".
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
+
* Text with a colon after its first character, which croner would take for
|
|
35
|
+
* a one-time date, is refused. A cron that names a date no month has
|
|
36
|
+
* ("0 0 30 2 *") is read, and never fires: nothing is ever due, so it is
|
|
37
|
+
* never missed. Parsed once per (schedule, timezone) pair and cached. The
|
|
38
|
+
* croner instance gets no callback, so it schedules nothing; it is only used
|
|
39
|
+
* to compute fire times. Without a timezone the expression is read in the process timezone,
|
|
34
40
|
* like crontab. Vercel and GitHub Actions run their crons in UTC, so pass
|
|
35
41
|
* timezone: "UTC" for those.
|
|
36
42
|
*/
|
|
@@ -47,6 +53,6 @@ declare function composeAlert(draft: AlertDraft, def: StoredJobDefinition, now:
|
|
|
47
53
|
* export const cw = cronwatch({ store: sqlite({ path: "./data/cronwatch.db" }), alerts: [slack({ webhookUrl })] });
|
|
48
54
|
* export const nightly = cw.job("nightly-report", { schedule: "0 2 * * *", grace: "15m" });
|
|
49
55
|
*/
|
|
50
|
-
declare function cronwatch(options?:
|
|
56
|
+
declare function cronwatch(options?: CronwatchOptions): Cronwatch;
|
|
51
57
|
|
|
52
|
-
export { Alert, AlertDraft,
|
|
58
|
+
export { Alert, AlertDraft, Cronwatch, CronwatchOptions, Duration, type ParsedSchedule, Store, StoredJobDefinition, VERSION, composeAlert, cronwatch, formatDuration, memory, nextFire, parseDuration, parseSchedule };
|
package/dist/index.js
CHANGED
|
@@ -78,13 +78,46 @@ function beyondDates(at) {
|
|
|
78
78
|
function readEnv(name) {
|
|
79
79
|
return typeof process === "undefined" || !process.env ? void 0 : process.env[name];
|
|
80
80
|
}
|
|
81
|
+
function readSecretEnv(name) {
|
|
82
|
+
const value = readEnv(name);
|
|
83
|
+
return value === void 0 || value.trim() === "" ? void 0 : value;
|
|
84
|
+
}
|
|
85
|
+
function secretOption(value, what) {
|
|
86
|
+
if (value === void 0 || value === null) return value;
|
|
87
|
+
if (typeof value !== "string") {
|
|
88
|
+
throw new TypeError(`${what} must be a string, or null to opt out, not ${Array.isArray(value) ? "an array" : typeof value}`);
|
|
89
|
+
}
|
|
90
|
+
return value.trim() === "" ? void 0 : value;
|
|
91
|
+
}
|
|
92
|
+
function environment() {
|
|
93
|
+
if (typeof process === "undefined" || !process.env) return "";
|
|
94
|
+
for (const value of [process.env.CRONWATCH_ENV, process.env.APP_ENV, process.env.NODE_ENV]) {
|
|
95
|
+
const name = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
96
|
+
if (name === "") continue;
|
|
97
|
+
if (name === "prod") return "production";
|
|
98
|
+
if (name === "dev" || name === "local" || name === "test" || name === "testing") return "development";
|
|
99
|
+
return name;
|
|
100
|
+
}
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
81
103
|
function isDevelopment() {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
return environment() === "development";
|
|
105
|
+
}
|
|
106
|
+
function isProduction() {
|
|
107
|
+
return environment() === "production";
|
|
85
108
|
}
|
|
86
109
|
var cache = /* @__PURE__ */ new Map();
|
|
87
110
|
var crons = /* @__PURE__ */ new WeakMap();
|
|
111
|
+
var never = /* @__PURE__ */ new WeakSet();
|
|
112
|
+
var PROBE_FROM_MS = Date.UTC(2e3, 0, 1);
|
|
113
|
+
function neverFires(cron) {
|
|
114
|
+
try {
|
|
115
|
+
return cron.nextRuns(1, new Date(PROBE_FROM_MS)).length === 0;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return error instanceof RangeError && /call stack/i.test(error.message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
var ONE_TIME_DATE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
|
|
88
121
|
var EARLY_SLACK_MS = 6e4;
|
|
89
122
|
function parseSchedule(schedule, timezone) {
|
|
90
123
|
const key = `${timezone ?? ""}|${schedule}`;
|
|
@@ -98,6 +131,10 @@ function parseSchedule(schedule, timezone) {
|
|
|
98
131
|
if (everyMs < 1e3) throw new Error(`schedule "${schedule}" is shorter than one second`);
|
|
99
132
|
parsed = { kind: "interval", source: text, everyMs };
|
|
100
133
|
} else {
|
|
134
|
+
if (text.slice(1).includes(":")) {
|
|
135
|
+
const reason = ONE_TIME_DATE.test(text) ? "CronPattern: a one-time date is not supported" : "Invalid ISO8601 passed to timezone parser.";
|
|
136
|
+
throw new Error(`schedule "${schedule}" is not a cron expression or "every <duration>": ${reason}`);
|
|
137
|
+
}
|
|
101
138
|
let cron;
|
|
102
139
|
try {
|
|
103
140
|
cron = new Cron(text, timezone ? { timezone } : {});
|
|
@@ -106,6 +143,7 @@ function parseSchedule(schedule, timezone) {
|
|
|
106
143
|
}
|
|
107
144
|
parsed = { kind: "cron", source: text, ...timezone ? { timezone } : {} };
|
|
108
145
|
crons.set(parsed, cron);
|
|
146
|
+
if (neverFires(cron)) never.add(parsed);
|
|
109
147
|
}
|
|
110
148
|
cache.set(key, parsed);
|
|
111
149
|
return parsed;
|
|
@@ -132,6 +170,7 @@ function countFrom(from) {
|
|
|
132
170
|
function fireAfter(parsed, from) {
|
|
133
171
|
const cron = crons.get(parsed);
|
|
134
172
|
if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
|
|
173
|
+
if (never.has(parsed)) return null;
|
|
135
174
|
const start = countFrom(from);
|
|
136
175
|
if (start === null) return null;
|
|
137
176
|
let probe = start;
|
|
@@ -147,6 +186,7 @@ function fireAfter(parsed, from) {
|
|
|
147
186
|
function firesBetween(parsed, from, to, limit) {
|
|
148
187
|
const cron = crons.get(parsed);
|
|
149
188
|
if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
|
|
189
|
+
if (never.has(parsed)) return [];
|
|
150
190
|
const out = [];
|
|
151
191
|
const start = countFrom(from);
|
|
152
192
|
if (start === null) return out;
|
|
@@ -281,16 +321,24 @@ function failureCount(state) {
|
|
|
281
321
|
function emptyState(job) {
|
|
282
322
|
return { job, open: {}, consecutiveFailures: 0, silencedUntil: null, lastAlertAt: null, pendingRecovery: [], undelivered: [] };
|
|
283
323
|
}
|
|
324
|
+
function isObject(value) {
|
|
325
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
326
|
+
}
|
|
284
327
|
function normalizeState(state, job) {
|
|
285
|
-
if (!state) return emptyState(job);
|
|
328
|
+
if (!isObject(state)) return emptyState(job);
|
|
286
329
|
const { sending, ...rest } = state;
|
|
330
|
+
const open = isObject(state.open) ? Object.fromEntries(Object.entries(state.open).filter(([, at]) => typeof at === "number")) : {};
|
|
331
|
+
const number = (value) => typeof value === "number" ? value : null;
|
|
332
|
+
const list = (value) => Array.isArray(value) ? value : [];
|
|
287
333
|
return {
|
|
288
334
|
...emptyState(job),
|
|
289
335
|
...rest,
|
|
290
|
-
open
|
|
336
|
+
open,
|
|
291
337
|
consecutiveFailures: failureCount(state),
|
|
292
|
-
|
|
293
|
-
|
|
338
|
+
silencedUntil: number(state.silencedUntil),
|
|
339
|
+
lastAlertAt: number(state.lastAlertAt),
|
|
340
|
+
pendingRecovery: list(state.pendingRecovery).filter((c) => typeof c === "string"),
|
|
341
|
+
undelivered: list(state.undelivered).filter((alert) => isObject(alert)),
|
|
294
342
|
...Array.isArray(sending) && sending.length > 0 ? { sending: [...sending] } : {}
|
|
295
343
|
};
|
|
296
344
|
}
|
|
@@ -494,8 +542,12 @@ function applySilence(previous, evaluation, now) {
|
|
|
494
542
|
return { state: muteOpens(previous, evaluation.state), alerts: [] };
|
|
495
543
|
}
|
|
496
544
|
function staleAlert(alert, state) {
|
|
497
|
-
if (alert.type === "recovered")
|
|
498
|
-
|
|
545
|
+
if (alert.type === "recovered") {
|
|
546
|
+
const after2 = isObject(alert.details) ? alert.details.after : void 0;
|
|
547
|
+
if (!Array.isArray(after2)) return true;
|
|
548
|
+
return after2.some((condition) => typeof condition !== "string" || state.open[condition] !== void 0);
|
|
549
|
+
}
|
|
550
|
+
return typeof alert.at !== "number" || state.open[alert.type] !== alert.at;
|
|
499
551
|
}
|
|
500
552
|
function jobHealth(def, lastRun, state, now) {
|
|
501
553
|
const open = openConditions(state);
|
|
@@ -808,6 +860,15 @@ function checkExpectation(expect, output) {
|
|
|
808
860
|
}
|
|
809
861
|
return ok ? null : "Output did not pass the expect() check";
|
|
810
862
|
}
|
|
863
|
+
function readStoredJob(stored) {
|
|
864
|
+
const definition = stored.definition;
|
|
865
|
+
if (typeof definition !== "object" || definition === null || Array.isArray(definition)) {
|
|
866
|
+
return { job: { ...stored, definition: { name: stored.name } }, readable: false };
|
|
867
|
+
}
|
|
868
|
+
const read = { ...definition };
|
|
869
|
+
if ("tags" in read && !(Array.isArray(read.tags) && read.tags.every((tag) => typeof tag === "string"))) delete read.tags;
|
|
870
|
+
return { job: { ...stored, definition: read }, readable: true };
|
|
871
|
+
}
|
|
811
872
|
|
|
812
873
|
// src/routes/escape.ts
|
|
813
874
|
function escapeHtml(value) {
|
|
@@ -1579,12 +1640,18 @@ function jobPage(job, runs, now, base, complete = true) {
|
|
|
1579
1640
|
return layout(`${job.name}: CronWatch`, body, base, { refresh: 60 });
|
|
1580
1641
|
}
|
|
1581
1642
|
function messagePage(title, message, base, signIn = false) {
|
|
1582
|
-
const form = signIn ? `<form class="signin" method="
|
|
1643
|
+
const form = signIn ? `<form class="signin" method="post" action="${h(base)}/signin"><label for="token">Token</label><input id="token" name="token" type="password" autocomplete="current-password" autocapitalize="off" spellcheck="false" required><button class="primary" type="submit">Sign in</button></form>` : "";
|
|
1583
1644
|
return layout(title, `<header class="top">${brand(base)}</header><main class="message"><h1>${h(title)}</h1><p>${h(message)}</p>${form}</main>`, base);
|
|
1584
1645
|
}
|
|
1585
1646
|
|
|
1647
|
+
// src/version.ts
|
|
1648
|
+
var VERSION = "0.11.0";
|
|
1649
|
+
var API_VERSION = 1;
|
|
1650
|
+
|
|
1586
1651
|
// src/routes/index.ts
|
|
1587
1652
|
var COOKIE = "cronwatch_token";
|
|
1653
|
+
var LIBRARY = "@cronwatch/sdk";
|
|
1654
|
+
var LANGUAGE = "typescript";
|
|
1588
1655
|
async function cookieValue(token) {
|
|
1589
1656
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`cronwatch-cookie:${token}`));
|
|
1590
1657
|
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
@@ -1668,6 +1735,20 @@ function forwardedOrigin(request, url) {
|
|
|
1668
1735
|
return url.origin;
|
|
1669
1736
|
}
|
|
1670
1737
|
}
|
|
1738
|
+
function bearerToken(header) {
|
|
1739
|
+
const match = header === null ? null : /^Bearer\s+([\s\S]*)$/i.exec(header);
|
|
1740
|
+
return match ? match[1] : null;
|
|
1741
|
+
}
|
|
1742
|
+
function signInReturn(referer, publicOrigin, base) {
|
|
1743
|
+
if (referer === null || !referer.startsWith(publicOrigin + "/")) return `${base}/`;
|
|
1744
|
+
let url;
|
|
1745
|
+
try {
|
|
1746
|
+
url = new URL(referer);
|
|
1747
|
+
} catch {
|
|
1748
|
+
return `${base}/`;
|
|
1749
|
+
}
|
|
1750
|
+
return url.searchParams.has("token") ? `${base}/` : referer;
|
|
1751
|
+
}
|
|
1671
1752
|
function stripBase(pathname, base) {
|
|
1672
1753
|
let path = pathname.startsWith(base) ? pathname.slice(base.length) : pathname;
|
|
1673
1754
|
if (path === "") path = "/";
|
|
@@ -1710,8 +1791,12 @@ function runsLimit(value) {
|
|
|
1710
1791
|
return Number.isFinite(n) ? Math.min(MAX_RUNS, Math.max(1, n)) : DEFAULT_RUNS;
|
|
1711
1792
|
}
|
|
1712
1793
|
function createRoutes(cw, options = {}) {
|
|
1794
|
+
return cw.routes(options);
|
|
1795
|
+
}
|
|
1796
|
+
function buildRoutes(cw, options = {}) {
|
|
1713
1797
|
const optedOut = options.token === null;
|
|
1714
|
-
const
|
|
1798
|
+
const given = secretOption(options.token, "routes: token");
|
|
1799
|
+
const configured = optedOut ? null : given || readSecretEnv("CRONWATCH_TOKEN") || null;
|
|
1715
1800
|
const base = (options.basePath ?? "/cronwatch").replace(/\/+$/, "");
|
|
1716
1801
|
const developing = isDevelopment();
|
|
1717
1802
|
const fixedOrigin = configuredOrigin(options.origin);
|
|
@@ -1743,7 +1828,17 @@ function createRoutes(cw, options = {}) {
|
|
|
1743
1828
|
if (method !== "GET" && method !== "HEAD" && crossSite(request, publicOrigin)) {
|
|
1744
1829
|
return wantsHtml ? html(messagePage("Cross-site request refused", "Changes can only be made from the dashboard itself.", base), 403) : api({ ok: false, error: "Cross-site request refused" }, 403);
|
|
1745
1830
|
}
|
|
1746
|
-
const bearer = request.headers.get("authorization")
|
|
1831
|
+
const bearer = bearerToken(request.headers.get("authorization"));
|
|
1832
|
+
const signInCookie = async () => {
|
|
1833
|
+
const secure = publicOrigin.startsWith("https:") ? "; Secure" : "";
|
|
1834
|
+
return `${COOKIE}=${await expectedCookie(token)}; Path=${base || "/"}; HttpOnly; SameSite=Lax; Max-Age=${60 * 60 * 24 * 30}${secure}`;
|
|
1835
|
+
};
|
|
1836
|
+
const signInPage = () => generated ? html(messagePage("Sign in", "CRONWATCH_TOKEN is not set, so this development server made a token. The sign-in link is in the server log: open it once, or enter the token from it below, and this browser stays signed in.", base, true), 401) : html(messagePage("Sign in", "Enter your CRONWATCH_TOKEN and this browser stays signed in.", base, true), 401);
|
|
1837
|
+
if (token && method === "POST" && path === "/signin") {
|
|
1838
|
+
const sent = (await readBody(request)).token;
|
|
1839
|
+
if (sent === void 0 || !constantTimeEqual(sent, token)) return signInPage();
|
|
1840
|
+
return redirect(signInReturn(request.headers.get("referer"), publicOrigin, base), { "set-cookie": await signInCookie() });
|
|
1841
|
+
}
|
|
1747
1842
|
if (token) {
|
|
1748
1843
|
const query = wantsHtml && method === "GET" ? url.searchParams.get("token") : null;
|
|
1749
1844
|
const sent = readCookie(request, COOKIE);
|
|
@@ -1751,17 +1846,12 @@ function createRoutes(cw, options = {}) {
|
|
|
1751
1846
|
const cronSecretOk = isCheck && bearer !== null && cw.cronSecret !== null && constantTimeEqual(bearer, cw.cronSecret);
|
|
1752
1847
|
const tokenOk = bearer !== null ? constantTimeEqual(bearer, token) : query !== null ? constantTimeEqual(query, token) : sent !== null && constantTimeEqual(sent, await expectedCookie(token));
|
|
1753
1848
|
if (!cronSecretOk && !tokenOk) {
|
|
1754
|
-
if (
|
|
1755
|
-
|
|
1756
|
-
}
|
|
1757
|
-
return wantsHtml ? html(messagePage("Sign in", `Open this page with ?token=<your CRONWATCH_TOKEN> once and it will stay signed in.`, base, true), 401) : api({ ok: false, error: "Unauthorized" }, 401);
|
|
1849
|
+
if (wantsHtml) return signInPage();
|
|
1850
|
+
return generated ? api({ ok: false, error: "Unauthorized: CRONWATCH_TOKEN is not set, so this development server made a token; it is in the server log" }, 401) : api({ ok: false, error: "Unauthorized" }, 401);
|
|
1758
1851
|
}
|
|
1759
1852
|
if (query !== null) {
|
|
1760
1853
|
url.searchParams.delete("token");
|
|
1761
|
-
|
|
1762
|
-
return redirect(url.pathname + (url.search || ""), {
|
|
1763
|
-
"set-cookie": `${COOKIE}=${await expectedCookie(token)}; Path=${base || "/"}; HttpOnly; SameSite=Lax; Max-Age=${60 * 60 * 24 * 30}${secure}`
|
|
1764
|
-
});
|
|
1854
|
+
return redirect(url.pathname + (url.search || ""), { "set-cookie": await signInCookie() });
|
|
1765
1855
|
}
|
|
1766
1856
|
}
|
|
1767
1857
|
const redirectBack = () => {
|
|
@@ -1812,6 +1902,9 @@ function createRoutes(cw, options = {}) {
|
|
|
1812
1902
|
}
|
|
1813
1903
|
if (parts[0] === "api") {
|
|
1814
1904
|
const rest = parts.slice(1);
|
|
1905
|
+
if (method === "GET" && rest.length === 0) {
|
|
1906
|
+
return api({ ok: true, library: LIBRARY, language: LANGUAGE, version: VERSION, api: API_VERSION });
|
|
1907
|
+
}
|
|
1815
1908
|
if (method === "GET" && rest[0] === "jobs" && rest.length === 1) {
|
|
1816
1909
|
return api({ ok: true, jobs: await cw.jobs() });
|
|
1817
1910
|
}
|
|
@@ -1839,9 +1932,13 @@ function createRoutes(cw, options = {}) {
|
|
|
1839
1932
|
} catch (e) {
|
|
1840
1933
|
return api({ ok: false, error: e.message }, 400);
|
|
1841
1934
|
}
|
|
1842
|
-
|
|
1935
|
+
await cw.silence(name, duration);
|
|
1936
|
+
return api({ ok: true, job: await cw.jobSummary(name) });
|
|
1937
|
+
}
|
|
1938
|
+
if (rest[2] === "unsilence") {
|
|
1939
|
+
await cw.unsilence(name);
|
|
1940
|
+
return api({ ok: true, job: await cw.jobSummary(name) });
|
|
1843
1941
|
}
|
|
1844
|
-
if (rest[2] === "unsilence") return api({ ok: true, state: await cw.unsilence(name) });
|
|
1845
1942
|
}
|
|
1846
1943
|
if (rest[0] === "check" && rest.length === 1) {
|
|
1847
1944
|
if (method === "GET" && bearer === null) {
|
|
@@ -2049,7 +2146,7 @@ function validateDefinition(def) {
|
|
|
2049
2146
|
function sameState(a, b) {
|
|
2050
2147
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
2051
2148
|
}
|
|
2052
|
-
var
|
|
2149
|
+
var Cronwatch = class {
|
|
2053
2150
|
store;
|
|
2054
2151
|
alerts;
|
|
2055
2152
|
triage;
|
|
@@ -2062,7 +2159,7 @@ var CronWatch = class {
|
|
|
2062
2159
|
/** cronSecret was passed as null: handlers may run without a secret. */
|
|
2063
2160
|
secretOptOut;
|
|
2064
2161
|
redact;
|
|
2065
|
-
/** "check": queue alerts for another process's check instead of sending them. See
|
|
2162
|
+
/** "check": queue alerts for another process's check instead of sending them. See CronwatchOptions.deliver. */
|
|
2066
2163
|
deferDelivery;
|
|
2067
2164
|
defaults;
|
|
2068
2165
|
definitions = /* @__PURE__ */ new Map();
|
|
@@ -2081,12 +2178,15 @@ var CronWatch = class {
|
|
|
2081
2178
|
usingDefaultStore = false;
|
|
2082
2179
|
warnedNoSecret = false;
|
|
2083
2180
|
warnedDeferredStart = false;
|
|
2181
|
+
/** Stored jobs read with a definition that was not a JSON object (see readJob). */
|
|
2182
|
+
unreadable = /* @__PURE__ */ new WeakSet();
|
|
2084
2183
|
constructor(options = {}) {
|
|
2085
2184
|
this.store = options.store ?? (this.usingDefaultStore = true, memory());
|
|
2086
2185
|
this.alerts = options.alerts ?? [consoleChannel()];
|
|
2087
2186
|
this.triage = options.triage;
|
|
2088
2187
|
this.sources = options.sources ?? [];
|
|
2089
|
-
const
|
|
2188
|
+
const given = secretOption(options.cronSecret, "cronSecret");
|
|
2189
|
+
const secret = options.cronSecret === void 0 ? readSecretEnv("CRON_SECRET") : given;
|
|
2090
2190
|
this.cronSecret = secret ? secret : null;
|
|
2091
2191
|
this.secretOptOut = options.cronSecret === null;
|
|
2092
2192
|
this.retentionMs = parseDuration(options.retention ?? "30d", "retention");
|
|
@@ -2147,7 +2247,7 @@ var CronWatch = class {
|
|
|
2147
2247
|
return outcome.result;
|
|
2148
2248
|
},
|
|
2149
2249
|
handler(fn, options) {
|
|
2150
|
-
const own = options?.secret;
|
|
2250
|
+
const own = secretOption(options?.secret, "handler: secret");
|
|
2151
2251
|
const secret = own === null ? null : own ? own : self.cronSecret;
|
|
2152
2252
|
const optedOut = own === null || !own && self.secretOptOut;
|
|
2153
2253
|
return async (request) => {
|
|
@@ -2199,7 +2299,7 @@ var CronWatch = class {
|
|
|
2199
2299
|
if (!this.ready) {
|
|
2200
2300
|
this.ready = (async () => {
|
|
2201
2301
|
if (this.store.init) await this.store.init();
|
|
2202
|
-
if (this.usingDefaultStore &&
|
|
2302
|
+
if (this.usingDefaultStore && isProduction()) {
|
|
2203
2303
|
console.warn("[cronwatch] using the in-memory store: runs and state are lost on restart. Pass a store from @cronwatch/sdk/sqlite or @cronwatch/sdk/postgres.");
|
|
2204
2304
|
}
|
|
2205
2305
|
})().catch((error) => {
|
|
@@ -2249,13 +2349,23 @@ var CronWatch = class {
|
|
|
2249
2349
|
const jobs = await this.store.listJobs();
|
|
2250
2350
|
const listed = new Set(jobs.map((job) => job.name));
|
|
2251
2351
|
const missing = [...this.definitions.values()].filter((definition) => !listed.has(definition.name));
|
|
2252
|
-
if (missing.length === 0) return jobs;
|
|
2352
|
+
if (missing.length === 0) return jobs.map((job) => this.readJob(job));
|
|
2253
2353
|
for (const definition of missing) {
|
|
2254
2354
|
if (this.definitions.get(definition.name) !== definition) continue;
|
|
2255
2355
|
this.synced.delete(definition.name);
|
|
2256
2356
|
await this.sync(definition);
|
|
2257
2357
|
}
|
|
2258
|
-
return this.store.listJobs();
|
|
2358
|
+
return (await this.store.listJobs()).map((job) => this.readJob(job));
|
|
2359
|
+
}
|
|
2360
|
+
/** A stored job read leniently (readStoredJob); one whose definition is unreadable is remembered, so that evaluating it throws. */
|
|
2361
|
+
readJob(stored) {
|
|
2362
|
+
const { job, readable } = readStoredJob(stored);
|
|
2363
|
+
if (!readable) this.unreadable.add(job);
|
|
2364
|
+
return job;
|
|
2365
|
+
}
|
|
2366
|
+
/** Throws for a job whose stored definition was not a JSON object: reported, and shown as failing, while the others carry on. */
|
|
2367
|
+
evaluable(stored) {
|
|
2368
|
+
if (this.unreadable.has(stored)) throw new Error(`job "${stored.name}": its stored definition is not a JSON object`);
|
|
2259
2369
|
}
|
|
2260
2370
|
/**
|
|
2261
2371
|
* Runs `fn` after every earlier state update for the same job has settled,
|
|
@@ -2674,6 +2784,10 @@ ${id}`;
|
|
|
2674
2784
|
async recordRun(input, options = {}) {
|
|
2675
2785
|
const declared = this.definitions.get(input.job);
|
|
2676
2786
|
if (!declared) throw new Error(`recordRun: job "${input.job}" is not declared; call job() first`);
|
|
2787
|
+
if (typeof input.id !== "string" || input.id.length === 0 || input.id.length > MAX_RUN_ID) {
|
|
2788
|
+
const got = typeof input.id === "string" ? `${input.id.length} characters` : typeof input.id;
|
|
2789
|
+
throw new Error(`recordRun: run ids must be 1 to ${MAX_RUN_ID} characters (got ${got}; job "${input.job}")`);
|
|
2790
|
+
}
|
|
2677
2791
|
if (input.id.includes("\0")) throw new Error(`recordRun: run ids cannot contain a NUL character (job "${input.job}")`);
|
|
2678
2792
|
for (const [metric, value] of Object.entries(input.metrics ?? {})) {
|
|
2679
2793
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
@@ -2802,7 +2916,10 @@ ${id}`;
|
|
|
2802
2916
|
for (const listed of await this.store.runningRuns()) {
|
|
2803
2917
|
try {
|
|
2804
2918
|
const declared = this.definitions.get(listed.job);
|
|
2805
|
-
const
|
|
2919
|
+
const found = declared ? null : await this.store.getJob(listed.job);
|
|
2920
|
+
const stored = found ? this.readJob(found) : null;
|
|
2921
|
+
if (stored) this.evaluable(stored);
|
|
2922
|
+
const definition = declared ? toStored(declared) : stored?.definition;
|
|
2806
2923
|
if (!definition || !isStuck(definition, listed, now)) continue;
|
|
2807
2924
|
const run = await this.store.getRun(listed.id);
|
|
2808
2925
|
if (!run || run.status !== "running" || run.job !== listed.job) continue;
|
|
@@ -2821,6 +2938,7 @@ ${id}`;
|
|
|
2821
2938
|
const retries = { spentMs: 0 };
|
|
2822
2939
|
for (const stored of await this.storedJobs()) {
|
|
2823
2940
|
try {
|
|
2941
|
+
this.evaluable(stored);
|
|
2824
2942
|
const recent = await this.store.listRuns(stored.name, BASELINE_WINDOW);
|
|
2825
2943
|
let nextExpectedAt = null;
|
|
2826
2944
|
const { state, result: held } = await this.updateState(stored.name, (previous) => {
|
|
@@ -2856,6 +2974,7 @@ ${id}`;
|
|
|
2856
2974
|
let recent = [];
|
|
2857
2975
|
try {
|
|
2858
2976
|
recent = await this.store.listRuns(stored.name, Math.max(runs, BASELINE_WINDOW));
|
|
2977
|
+
this.evaluable(stored);
|
|
2859
2978
|
const state = await this.readState(stored.name);
|
|
2860
2979
|
const { nextExpectedAt } = onCheck(stored.definition, stored, recent[0] ?? null, state, now);
|
|
2861
2980
|
return { job: summarize(stored, recent, state, nextExpectedAt, now), runs: recent.slice(0, runs) };
|
|
@@ -2890,7 +3009,7 @@ ${id}`;
|
|
|
2890
3009
|
if (definition) await this.sync(definition, true);
|
|
2891
3010
|
const stored = await this.store.getJob(name);
|
|
2892
3011
|
if (!stored) return null;
|
|
2893
|
-
return (await this.snapshot(stored, this.now(), 0)).job;
|
|
3012
|
+
return (await this.snapshot(this.readJob(stored), this.now(), 0)).job;
|
|
2894
3013
|
}
|
|
2895
3014
|
/** A job's runs, newest first. `limit` is a whole number from 1 to 500. */
|
|
2896
3015
|
async runs(name, limit = 50) {
|
|
@@ -2937,21 +3056,27 @@ ${id}`;
|
|
|
2937
3056
|
this.synced.delete(name);
|
|
2938
3057
|
await this.store.deleteJob(name);
|
|
2939
3058
|
}
|
|
2940
|
-
/**
|
|
3059
|
+
/**
|
|
3060
|
+
* The dashboard and JSON API as fetch-style handlers: the one way to mount
|
|
3061
|
+
* them. A Next.js app exports them from app/cronwatch/[[...path]]/route.ts:
|
|
3062
|
+
*
|
|
3063
|
+
* export const { GET, POST, DELETE } = cw.routes();
|
|
3064
|
+
*/
|
|
2941
3065
|
routes(options) {
|
|
2942
|
-
return
|
|
3066
|
+
return buildRoutes(this, options);
|
|
2943
3067
|
}
|
|
2944
3068
|
/**
|
|
2945
3069
|
* Check on an interval, for long-running servers. Default every minute.
|
|
2946
3070
|
* Not for serverless functions or Cloudflare Workers, where nothing runs
|
|
2947
|
-
* between requests: call check() from a cron there instead.
|
|
3071
|
+
* between requests: call check() from a cron there instead. (A job's
|
|
3072
|
+
* start() opens a run; this starts the checks.)
|
|
2948
3073
|
*/
|
|
2949
|
-
|
|
3074
|
+
startChecking(every = "1m") {
|
|
2950
3075
|
if (this.timer) return;
|
|
2951
3076
|
const ms = Math.min(TIMER_MAX_MS, Math.max(5e3, parseDuration(every, "check interval")));
|
|
2952
3077
|
if (this.deferDelivery && !this.warnedDeferredStart) {
|
|
2953
3078
|
this.warnedDeferredStart = true;
|
|
2954
|
-
console.warn('[cronwatch]
|
|
3079
|
+
console.warn('[cronwatch] startChecking() was called with deliver: "check", so these checks send no alerts. Another process must run checks with deliver: "now" (the default) to send them.');
|
|
2955
3080
|
}
|
|
2956
3081
|
const tick = () => this.check().catch((e) => this.onError(e, "check"));
|
|
2957
3082
|
this.timer = setInterval(tick, ms);
|
|
@@ -2962,6 +3087,15 @@ ${id}`;
|
|
|
2962
3087
|
}, 1e3);
|
|
2963
3088
|
if (typeof this.firstTick === "object" && "unref" in this.firstTick) this.firstTick.unref();
|
|
2964
3089
|
}
|
|
3090
|
+
/**
|
|
3091
|
+
* startChecking(), under its former name.
|
|
3092
|
+
* @deprecated Renamed `startChecking`, since a job's `start()` opens a run.
|
|
3093
|
+
* This name still works through 1.x and goes in 2.0.
|
|
3094
|
+
*/
|
|
3095
|
+
start(every = "1m") {
|
|
3096
|
+
this.startChecking(every);
|
|
3097
|
+
}
|
|
3098
|
+
/** Stop the checks startChecking() began. */
|
|
2965
3099
|
stop() {
|
|
2966
3100
|
if (this.timer) clearInterval(this.timer);
|
|
2967
3101
|
if (this.firstTick) clearTimeout(this.firstTick);
|
|
@@ -3074,9 +3208,10 @@ ${id}`;
|
|
|
3074
3208
|
}
|
|
3075
3209
|
};
|
|
3076
3210
|
var RESERVED_RUN_ID_PREFIX = "pgcron:";
|
|
3211
|
+
var MAX_RUN_ID = 200;
|
|
3077
3212
|
function checkRunId(job, id, method) {
|
|
3078
|
-
if (typeof id !== "string" || id.length === 0 || id.length >
|
|
3079
|
-
throw new Error(`job "${job}": ${method}() needs a run id of 1 to
|
|
3213
|
+
if (typeof id !== "string" || id.length === 0 || id.length > MAX_RUN_ID) {
|
|
3214
|
+
throw new Error(`job "${job}": ${method}() needs a run id of 1 to ${MAX_RUN_ID} characters (got ${typeof id === "string" ? `${id.length} characters` : typeof id})`);
|
|
3080
3215
|
}
|
|
3081
3216
|
if (id.includes("\0")) throw new Error(`job "${job}": ${method}() cannot take a run id containing a NUL character`);
|
|
3082
3217
|
if (id.startsWith(RESERVED_RUN_ID_PREFIX)) {
|
|
@@ -3137,12 +3272,13 @@ function custom(name, send) {
|
|
|
3137
3272
|
await send(alert);
|
|
3138
3273
|
} };
|
|
3139
3274
|
}
|
|
3275
|
+
var CronWatch = Cronwatch;
|
|
3140
3276
|
|
|
3141
3277
|
// src/index.ts
|
|
3142
3278
|
function cronwatch(options = {}) {
|
|
3143
|
-
return new
|
|
3279
|
+
return new Cronwatch(options);
|
|
3144
3280
|
}
|
|
3145
3281
|
|
|
3146
|
-
export { CronWatch, composeAlert, consoleChannel, createRoutes, cronwatch, custom, formatDuration, memory, nextFire, parseDuration, parseSchedule };
|
|
3282
|
+
export { CronWatch, Cronwatch, VERSION, composeAlert, consoleChannel, createRoutes, cronwatch, custom, formatDuration, memory, nextFire, parseDuration, parseSchedule };
|
|
3147
3283
|
//# sourceMappingURL=index.js.map
|
|
3148
3284
|
//# sourceMappingURL=index.js.map
|