@cronwatch/sdk 0.10.0 → 0.11.1

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.
Files changed (86) hide show
  1. package/README.md +4 -4
  2. package/dist/anthropic.d.cts +1 -1
  3. package/dist/anthropic.d.ts +1 -1
  4. package/dist/bugsnag.d.cts +1 -1
  5. package/dist/bugsnag.d.ts +1 -1
  6. package/dist/{client-DUoSea7X.d.cts → client-CRMfhylq.d.cts} +63 -26
  7. package/dist/{client-BcIjVBiZ.d.ts → client-D2Xk2600.d.ts} +63 -26
  8. package/dist/d1.cjs +29 -11
  9. package/dist/d1.cjs.map +1 -1
  10. package/dist/d1.d.cts +1 -1
  11. package/dist/d1.d.ts +1 -1
  12. package/dist/d1.js +29 -11
  13. package/dist/d1.js.map +1 -1
  14. package/dist/datadog.d.cts +1 -1
  15. package/dist/datadog.d.ts +1 -1
  16. package/dist/discord.cjs +10 -4
  17. package/dist/discord.cjs.map +1 -1
  18. package/dist/discord.d.cts +13 -6
  19. package/dist/discord.d.ts +13 -6
  20. package/dist/discord.js +7 -1
  21. package/dist/discord.js.map +1 -1
  22. package/dist/{email-VrGsVz43.d.ts → email-BWuUiCkw.d.ts} +1 -1
  23. package/dist/{email-JZs3vxwq.d.cts → email-Dcqe3k8n.d.cts} +1 -1
  24. package/dist/honeybadger.d.cts +1 -1
  25. package/dist/honeybadger.d.ts +1 -1
  26. package/dist/index.cjs +194 -40
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +15 -9
  29. package/dist/index.d.ts +15 -9
  30. package/dist/index.js +193 -41
  31. package/dist/index.js.map +1 -1
  32. package/dist/mailgun.d.cts +2 -2
  33. package/dist/mailgun.d.ts +2 -2
  34. package/dist/newrelic.d.cts +1 -1
  35. package/dist/newrelic.d.ts +1 -1
  36. package/dist/pg-cron.cjs +17 -7
  37. package/dist/pg-cron.cjs.map +1 -1
  38. package/dist/pg-cron.d.cts +15 -12
  39. package/dist/pg-cron.d.ts +15 -12
  40. package/dist/pg-cron.js +14 -4
  41. package/dist/pg-cron.js.map +1 -1
  42. package/dist/postgres.cjs +28 -10
  43. package/dist/postgres.cjs.map +1 -1
  44. package/dist/postgres.d.cts +1 -1
  45. package/dist/postgres.d.ts +1 -1
  46. package/dist/postgres.js +28 -10
  47. package/dist/postgres.js.map +1 -1
  48. package/dist/postmark.d.cts +2 -2
  49. package/dist/postmark.d.ts +2 -2
  50. package/dist/resend.d.cts +2 -2
  51. package/dist/resend.d.ts +2 -2
  52. package/dist/rollbar.d.cts +1 -1
  53. package/dist/rollbar.d.ts +1 -1
  54. package/dist/sendgrid.d.cts +2 -2
  55. package/dist/sendgrid.d.ts +2 -2
  56. package/dist/sentry.cjs +4 -1
  57. package/dist/sentry.cjs.map +1 -1
  58. package/dist/sentry.d.cts +5 -2
  59. package/dist/sentry.d.ts +5 -2
  60. package/dist/sentry.js +4 -1
  61. package/dist/sentry.js.map +1 -1
  62. package/dist/ses.d.cts +2 -2
  63. package/dist/ses.d.ts +2 -2
  64. package/dist/slack.d.cts +1 -1
  65. package/dist/slack.d.ts +1 -1
  66. package/dist/sqlite.cjs +29 -11
  67. package/dist/sqlite.cjs.map +1 -1
  68. package/dist/sqlite.d.cts +1 -1
  69. package/dist/sqlite.d.ts +1 -1
  70. package/dist/sqlite.js +29 -11
  71. package/dist/sqlite.js.map +1 -1
  72. package/dist/twilio.cjs +10 -4
  73. package/dist/twilio.cjs.map +1 -1
  74. package/dist/twilio.d.cts +12 -7
  75. package/dist/twilio.d.ts +12 -7
  76. package/dist/twilio.js +7 -1
  77. package/dist/twilio.js.map +1 -1
  78. package/dist/{types-DyWCxlSi.d.cts → types-DLnlUie8.d.cts} +5 -2
  79. package/dist/{types-DyWCxlSi.d.ts → types-DLnlUie8.d.ts} +5 -2
  80. package/dist/webhook.cjs +10 -5
  81. package/dist/webhook.cjs.map +1 -1
  82. package/dist/webhook.d.cts +18 -7
  83. package/dist/webhook.d.ts +18 -7
  84. package/dist/webhook.js +10 -6
  85. package/dist/webhook.js.map +1 -1
  86. package/package.json +1 -1
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { C as CronWatchOptions, a as CronWatch } from './client-DUoSea7X.cjs';
2
- export { F as FetchHandler, H as HandlerFn, b as HandlerOptions, J as JobContext, c as JobFn, d as JobHandle, R as RecordRunOptions, e as Routes, f as RoutesOptions, g as RunHandle, h as RunOutcome, S as Source, i as SourceHost, j as StartOptions, k as consoleChannel, l as createRoutes, m as custom } from './client-DUoSea7X.cjs';
3
- import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-DyWCxlSi.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, J as JobDefinition, h as JobHealth, i 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-DyWCxlSi.cjs';
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.1";
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
- * Parsed once per (schedule, timezone) pair and cached. The croner instance
32
- * gets no callback, so it schedules nothing; it is only used to compute fire
33
- * times. Without a timezone the expression is read in the process timezone,
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?: CronWatchOptions): CronWatch;
56
+ declare function cronwatch(options?: CronwatchOptions): Cronwatch;
51
57
 
52
- export { Alert, AlertDraft, CronWatch, CronWatchOptions, Duration, type ParsedSchedule, Store, StoredJobDefinition, composeAlert, cronwatch, formatDuration, memory, nextFire, parseDuration, parseSchedule };
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 CronWatchOptions, a as CronWatch } from './client-BcIjVBiZ.js';
2
- export { F as FetchHandler, H as HandlerFn, b as HandlerOptions, J as JobContext, c as JobFn, d as JobHandle, R as RecordRunOptions, e as Routes, f as RoutesOptions, g as RunHandle, h as RunOutcome, S as Source, i as SourceHost, j as StartOptions, k as consoleChannel, l as createRoutes, m as custom } from './client-BcIjVBiZ.js';
3
- import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-DyWCxlSi.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, J as JobDefinition, h as JobHealth, i 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-DyWCxlSi.js';
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.1";
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
- * Parsed once per (schedule, timezone) pair and cached. The croner instance
32
- * gets no callback, so it schedules nothing; it is only used to compute fire
33
- * times. Without a timezone the expression is read in the process timezone,
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?: CronWatchOptions): CronWatch;
56
+ declare function cronwatch(options?: CronwatchOptions): Cronwatch;
51
57
 
52
- export { Alert, AlertDraft, CronWatch, CronWatchOptions, Duration, type ParsedSchedule, Store, StoredJobDefinition, composeAlert, cronwatch, formatDuration, memory, nextFire, parseDuration, parseSchedule };
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
- if (typeof process === "undefined" || !process.env) return false;
83
- const mode = process.env.NODE_ENV;
84
- return mode === "development" || mode === "test";
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: { ...state.open },
336
+ open,
291
337
  consecutiveFailures: failureCount(state),
292
- pendingRecovery: [...state.pendingRecovery ?? []],
293
- undelivered: [...state.undelivered ?? []],
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") return alert.details.after.some((condition) => state.open[condition] !== void 0);
498
- return state.open[alert.type] !== alert.at;
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) {
@@ -851,6 +912,21 @@ function manifest(base) {
851
912
  });
852
913
  }
853
914
  var APP_JS = `"use strict";
915
+ (function () {
916
+ var root = document.documentElement;
917
+ try {
918
+ var stored = localStorage.getItem("cronwatch-theme");
919
+ if (stored === "light" || stored === "dark") root.setAttribute("data-theme", stored);
920
+ } catch (e) {}
921
+ document.addEventListener("keydown", function (event) {
922
+ if (!(event.metaKey || event.ctrlKey) || !event.shiftKey || event.altKey || event.code !== "KeyD") return;
923
+ event.preventDefault();
924
+ var shown = root.getAttribute("data-theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
925
+ var next = shown === "dark" ? "light" : "dark";
926
+ root.setAttribute("data-theme", next);
927
+ try { localStorage.setItem("cronwatch-theme", next); } catch (e) {}
928
+ });
929
+ })();
854
930
  (function () {
855
931
  var script = document.currentScript;
856
932
  if (!script || !("serviceWorker" in navigator)) return;
@@ -1227,7 +1303,8 @@ function weekRunsLimit(job, now) {
1227
1303
  var h = escapeHtml;
1228
1304
  var CSS = `
1229
1305
  :root{color-scheme:light dark;--paper:#f4f4f5;--sheet:#fff;--sunk:#fafafa;--rule:#e4e4e7;--rule-2:#d4d4d8;--tick:#909098;--ink:#000;--body:#18181b;--muted:#71717a;--ok:#15803d;--warn:#a16207;--bad:#b91c1c;--serif:"Newsreader",ui-serif,Georgia,Cambria,"Times New Roman",serif;--mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--who:200px}
1230
- @media(prefers-color-scheme:dark){:root{--paper:#09090b;--sheet:#111113;--sunk:#18181b;--rule:#27272a;--rule-2:#3f3f46;--tick:#66666f;--ink:#fff;--body:#e4e4e7;--muted:#a1a1aa;--ok:#4ade80;--warn:#fbbf24;--bad:#f87171}}
1306
+ @media(prefers-color-scheme:dark){:root:not([data-theme=light]){--paper:#09090b;--sheet:#111113;--sunk:#18181b;--rule:#27272a;--rule-2:#3f3f46;--tick:#66666f;--ink:#fff;--body:#e4e4e7;--muted:#a1a1aa;--ok:#4ade80;--warn:#fbbf24;--bad:#f87171}}
1307
+ :root[data-theme=light]{color-scheme:light}:root[data-theme=dark]{color-scheme:dark;--paper:#09090b;--sheet:#111113;--sunk:#18181b;--rule:#27272a;--rule-2:#3f3f46;--tick:#66666f;--ink:#fff;--body:#e4e4e7;--muted:#a1a1aa;--ok:#4ade80;--warn:#fbbf24;--bad:#f87171}
1231
1308
  *{box-sizing:border-box}html{-webkit-text-size-adjust:100%}
1232
1309
  body{margin:0;background:var(--paper);color:var(--ink);font:400 16px/1.55 var(--serif);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
1233
1310
  a{color:inherit;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:.16em;text-decoration-color:var(--rule-2)}a:hover{text-decoration-color:currentColor}
@@ -1579,12 +1656,18 @@ function jobPage(job, runs, now, base, complete = true) {
1579
1656
  return layout(`${job.name}: CronWatch`, body, base, { refresh: 60 });
1580
1657
  }
1581
1658
  function messagePage(title, message, base, signIn = false) {
1582
- const form = signIn ? `<form class="signin" method="get" action="${h(base)}/"><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>` : "";
1659
+ 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
1660
  return layout(title, `<header class="top">${brand(base)}</header><main class="message"><h1>${h(title)}</h1><p>${h(message)}</p>${form}</main>`, base);
1584
1661
  }
1585
1662
 
1663
+ // src/version.ts
1664
+ var VERSION = "0.11.1";
1665
+ var API_VERSION = 1;
1666
+
1586
1667
  // src/routes/index.ts
1587
1668
  var COOKIE = "cronwatch_token";
1669
+ var LIBRARY = "@cronwatch/sdk";
1670
+ var LANGUAGE = "typescript";
1588
1671
  async function cookieValue(token) {
1589
1672
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`cronwatch-cookie:${token}`));
1590
1673
  return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
@@ -1668,6 +1751,20 @@ function forwardedOrigin(request, url) {
1668
1751
  return url.origin;
1669
1752
  }
1670
1753
  }
1754
+ function bearerToken(header) {
1755
+ const match = header === null ? null : /^Bearer\s+([\s\S]*)$/i.exec(header);
1756
+ return match ? match[1] : null;
1757
+ }
1758
+ function signInReturn(referer, publicOrigin, base) {
1759
+ if (referer === null || !referer.startsWith(publicOrigin + "/")) return `${base}/`;
1760
+ let url;
1761
+ try {
1762
+ url = new URL(referer);
1763
+ } catch {
1764
+ return `${base}/`;
1765
+ }
1766
+ return url.searchParams.has("token") ? `${base}/` : referer;
1767
+ }
1671
1768
  function stripBase(pathname, base) {
1672
1769
  let path = pathname.startsWith(base) ? pathname.slice(base.length) : pathname;
1673
1770
  if (path === "") path = "/";
@@ -1710,8 +1807,12 @@ function runsLimit(value) {
1710
1807
  return Number.isFinite(n) ? Math.min(MAX_RUNS, Math.max(1, n)) : DEFAULT_RUNS;
1711
1808
  }
1712
1809
  function createRoutes(cw, options = {}) {
1810
+ return cw.routes(options);
1811
+ }
1812
+ function buildRoutes(cw, options = {}) {
1713
1813
  const optedOut = options.token === null;
1714
- const configured = optedOut ? null : options.token || readEnv("CRONWATCH_TOKEN") || null;
1814
+ const given = secretOption(options.token, "routes: token");
1815
+ const configured = optedOut ? null : given || readSecretEnv("CRONWATCH_TOKEN") || null;
1715
1816
  const base = (options.basePath ?? "/cronwatch").replace(/\/+$/, "");
1716
1817
  const developing = isDevelopment();
1717
1818
  const fixedOrigin = configuredOrigin(options.origin);
@@ -1743,7 +1844,17 @@ function createRoutes(cw, options = {}) {
1743
1844
  if (method !== "GET" && method !== "HEAD" && crossSite(request, publicOrigin)) {
1744
1845
  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
1846
  }
1746
- const bearer = request.headers.get("authorization")?.replace(/^Bearer\s+/i, "") ?? null;
1847
+ const bearer = bearerToken(request.headers.get("authorization"));
1848
+ const signInCookie = async () => {
1849
+ const secure = publicOrigin.startsWith("https:") ? "; Secure" : "";
1850
+ return `${COOKIE}=${await expectedCookie(token)}; Path=${base || "/"}; HttpOnly; SameSite=Lax; Max-Age=${60 * 60 * 24 * 30}${secure}`;
1851
+ };
1852
+ 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);
1853
+ if (token && method === "POST" && path === "/signin") {
1854
+ const sent = (await readBody(request)).token;
1855
+ if (sent === void 0 || !constantTimeEqual(sent, token)) return signInPage();
1856
+ return redirect(signInReturn(request.headers.get("referer"), publicOrigin, base), { "set-cookie": await signInCookie() });
1857
+ }
1747
1858
  if (token) {
1748
1859
  const query = wantsHtml && method === "GET" ? url.searchParams.get("token") : null;
1749
1860
  const sent = readCookie(request, COOKIE);
@@ -1751,17 +1862,12 @@ function createRoutes(cw, options = {}) {
1751
1862
  const cronSecretOk = isCheck && bearer !== null && cw.cronSecret !== null && constantTimeEqual(bearer, cw.cronSecret);
1752
1863
  const tokenOk = bearer !== null ? constantTimeEqual(bearer, token) : query !== null ? constantTimeEqual(query, token) : sent !== null && constantTimeEqual(sent, await expectedCookie(token));
1753
1864
  if (!cronSecretOk && !tokenOk) {
1754
- if (generated) {
1755
- return wantsHtml ? 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 and this browser stays signed in.", base, true), 401) : api({ ok: false, error: "Unauthorized: CRONWATCH_TOKEN is not set, so this development server made a token; it is in the server log" }, 401);
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);
1865
+ if (wantsHtml) return signInPage();
1866
+ 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
1867
  }
1759
1868
  if (query !== null) {
1760
1869
  url.searchParams.delete("token");
1761
- const secure = publicOrigin.startsWith("https:") ? "; Secure" : "";
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
- });
1870
+ return redirect(url.pathname + (url.search || ""), { "set-cookie": await signInCookie() });
1765
1871
  }
1766
1872
  }
1767
1873
  const redirectBack = () => {
@@ -1812,6 +1918,9 @@ function createRoutes(cw, options = {}) {
1812
1918
  }
1813
1919
  if (parts[0] === "api") {
1814
1920
  const rest = parts.slice(1);
1921
+ if (method === "GET" && rest.length === 0) {
1922
+ return api({ ok: true, library: LIBRARY, language: LANGUAGE, version: VERSION, api: API_VERSION });
1923
+ }
1815
1924
  if (method === "GET" && rest[0] === "jobs" && rest.length === 1) {
1816
1925
  return api({ ok: true, jobs: await cw.jobs() });
1817
1926
  }
@@ -1839,9 +1948,13 @@ function createRoutes(cw, options = {}) {
1839
1948
  } catch (e) {
1840
1949
  return api({ ok: false, error: e.message }, 400);
1841
1950
  }
1842
- return api({ ok: true, state: await cw.silence(name, duration) });
1951
+ await cw.silence(name, duration);
1952
+ return api({ ok: true, job: await cw.jobSummary(name) });
1953
+ }
1954
+ if (rest[2] === "unsilence") {
1955
+ await cw.unsilence(name);
1956
+ return api({ ok: true, job: await cw.jobSummary(name) });
1843
1957
  }
1844
- if (rest[2] === "unsilence") return api({ ok: true, state: await cw.unsilence(name) });
1845
1958
  }
1846
1959
  if (rest[0] === "check" && rest.length === 1) {
1847
1960
  if (method === "GET" && bearer === null) {
@@ -2049,7 +2162,7 @@ function validateDefinition(def) {
2049
2162
  function sameState(a, b) {
2050
2163
  return JSON.stringify(a) === JSON.stringify(b);
2051
2164
  }
2052
- var CronWatch = class {
2165
+ var Cronwatch = class {
2053
2166
  store;
2054
2167
  alerts;
2055
2168
  triage;
@@ -2062,7 +2175,7 @@ var CronWatch = class {
2062
2175
  /** cronSecret was passed as null: handlers may run without a secret. */
2063
2176
  secretOptOut;
2064
2177
  redact;
2065
- /** "check": queue alerts for another process's check instead of sending them. See CronWatchOptions.deliver. */
2178
+ /** "check": queue alerts for another process's check instead of sending them. See CronwatchOptions.deliver. */
2066
2179
  deferDelivery;
2067
2180
  defaults;
2068
2181
  definitions = /* @__PURE__ */ new Map();
@@ -2081,12 +2194,15 @@ var CronWatch = class {
2081
2194
  usingDefaultStore = false;
2082
2195
  warnedNoSecret = false;
2083
2196
  warnedDeferredStart = false;
2197
+ /** Stored jobs read with a definition that was not a JSON object (see readJob). */
2198
+ unreadable = /* @__PURE__ */ new WeakSet();
2084
2199
  constructor(options = {}) {
2085
2200
  this.store = options.store ?? (this.usingDefaultStore = true, memory());
2086
2201
  this.alerts = options.alerts ?? [consoleChannel()];
2087
2202
  this.triage = options.triage;
2088
2203
  this.sources = options.sources ?? [];
2089
- const secret = options.cronSecret === void 0 ? readEnv("CRON_SECRET") : options.cronSecret;
2204
+ const given = secretOption(options.cronSecret, "cronSecret");
2205
+ const secret = options.cronSecret === void 0 ? readSecretEnv("CRON_SECRET") : given;
2090
2206
  this.cronSecret = secret ? secret : null;
2091
2207
  this.secretOptOut = options.cronSecret === null;
2092
2208
  this.retentionMs = parseDuration(options.retention ?? "30d", "retention");
@@ -2147,7 +2263,7 @@ var CronWatch = class {
2147
2263
  return outcome.result;
2148
2264
  },
2149
2265
  handler(fn, options) {
2150
- const own = options?.secret;
2266
+ const own = secretOption(options?.secret, "handler: secret");
2151
2267
  const secret = own === null ? null : own ? own : self.cronSecret;
2152
2268
  const optedOut = own === null || !own && self.secretOptOut;
2153
2269
  return async (request) => {
@@ -2199,7 +2315,7 @@ var CronWatch = class {
2199
2315
  if (!this.ready) {
2200
2316
  this.ready = (async () => {
2201
2317
  if (this.store.init) await this.store.init();
2202
- if (this.usingDefaultStore && readEnv("NODE_ENV") === "production") {
2318
+ if (this.usingDefaultStore && isProduction()) {
2203
2319
  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
2320
  }
2205
2321
  })().catch((error) => {
@@ -2249,13 +2365,23 @@ var CronWatch = class {
2249
2365
  const jobs = await this.store.listJobs();
2250
2366
  const listed = new Set(jobs.map((job) => job.name));
2251
2367
  const missing = [...this.definitions.values()].filter((definition) => !listed.has(definition.name));
2252
- if (missing.length === 0) return jobs;
2368
+ if (missing.length === 0) return jobs.map((job) => this.readJob(job));
2253
2369
  for (const definition of missing) {
2254
2370
  if (this.definitions.get(definition.name) !== definition) continue;
2255
2371
  this.synced.delete(definition.name);
2256
2372
  await this.sync(definition);
2257
2373
  }
2258
- return this.store.listJobs();
2374
+ return (await this.store.listJobs()).map((job) => this.readJob(job));
2375
+ }
2376
+ /** A stored job read leniently (readStoredJob); one whose definition is unreadable is remembered, so that evaluating it throws. */
2377
+ readJob(stored) {
2378
+ const { job, readable } = readStoredJob(stored);
2379
+ if (!readable) this.unreadable.add(job);
2380
+ return job;
2381
+ }
2382
+ /** Throws for a job whose stored definition was not a JSON object: reported, and shown as failing, while the others carry on. */
2383
+ evaluable(stored) {
2384
+ if (this.unreadable.has(stored)) throw new Error(`job "${stored.name}": its stored definition is not a JSON object`);
2259
2385
  }
2260
2386
  /**
2261
2387
  * Runs `fn` after every earlier state update for the same job has settled,
@@ -2674,6 +2800,10 @@ ${id}`;
2674
2800
  async recordRun(input, options = {}) {
2675
2801
  const declared = this.definitions.get(input.job);
2676
2802
  if (!declared) throw new Error(`recordRun: job "${input.job}" is not declared; call job() first`);
2803
+ if (typeof input.id !== "string" || input.id.length === 0 || input.id.length > MAX_RUN_ID) {
2804
+ const got = typeof input.id === "string" ? `${input.id.length} characters` : typeof input.id;
2805
+ throw new Error(`recordRun: run ids must be 1 to ${MAX_RUN_ID} characters (got ${got}; job "${input.job}")`);
2806
+ }
2677
2807
  if (input.id.includes("\0")) throw new Error(`recordRun: run ids cannot contain a NUL character (job "${input.job}")`);
2678
2808
  for (const [metric, value] of Object.entries(input.metrics ?? {})) {
2679
2809
  if (typeof value !== "number" || !Number.isFinite(value)) {
@@ -2802,7 +2932,10 @@ ${id}`;
2802
2932
  for (const listed of await this.store.runningRuns()) {
2803
2933
  try {
2804
2934
  const declared = this.definitions.get(listed.job);
2805
- const definition = declared ? toStored(declared) : (await this.store.getJob(listed.job))?.definition;
2935
+ const found = declared ? null : await this.store.getJob(listed.job);
2936
+ const stored = found ? this.readJob(found) : null;
2937
+ if (stored) this.evaluable(stored);
2938
+ const definition = declared ? toStored(declared) : stored?.definition;
2806
2939
  if (!definition || !isStuck(definition, listed, now)) continue;
2807
2940
  const run = await this.store.getRun(listed.id);
2808
2941
  if (!run || run.status !== "running" || run.job !== listed.job) continue;
@@ -2821,6 +2954,7 @@ ${id}`;
2821
2954
  const retries = { spentMs: 0 };
2822
2955
  for (const stored of await this.storedJobs()) {
2823
2956
  try {
2957
+ this.evaluable(stored);
2824
2958
  const recent = await this.store.listRuns(stored.name, BASELINE_WINDOW);
2825
2959
  let nextExpectedAt = null;
2826
2960
  const { state, result: held } = await this.updateState(stored.name, (previous) => {
@@ -2856,6 +2990,7 @@ ${id}`;
2856
2990
  let recent = [];
2857
2991
  try {
2858
2992
  recent = await this.store.listRuns(stored.name, Math.max(runs, BASELINE_WINDOW));
2993
+ this.evaluable(stored);
2859
2994
  const state = await this.readState(stored.name);
2860
2995
  const { nextExpectedAt } = onCheck(stored.definition, stored, recent[0] ?? null, state, now);
2861
2996
  return { job: summarize(stored, recent, state, nextExpectedAt, now), runs: recent.slice(0, runs) };
@@ -2890,7 +3025,7 @@ ${id}`;
2890
3025
  if (definition) await this.sync(definition, true);
2891
3026
  const stored = await this.store.getJob(name);
2892
3027
  if (!stored) return null;
2893
- return (await this.snapshot(stored, this.now(), 0)).job;
3028
+ return (await this.snapshot(this.readJob(stored), this.now(), 0)).job;
2894
3029
  }
2895
3030
  /** A job's runs, newest first. `limit` is a whole number from 1 to 500. */
2896
3031
  async runs(name, limit = 50) {
@@ -2937,21 +3072,27 @@ ${id}`;
2937
3072
  this.synced.delete(name);
2938
3073
  await this.store.deleteJob(name);
2939
3074
  }
2940
- /** The dashboard and JSON API as fetch-style handlers. See createRoutes(). */
3075
+ /**
3076
+ * The dashboard and JSON API as fetch-style handlers: the one way to mount
3077
+ * them. A Next.js app exports them from app/cronwatch/[[...path]]/route.ts:
3078
+ *
3079
+ * export const { GET, POST, DELETE } = cw.routes();
3080
+ */
2941
3081
  routes(options) {
2942
- return createRoutes(this, options);
3082
+ return buildRoutes(this, options);
2943
3083
  }
2944
3084
  /**
2945
3085
  * Check on an interval, for long-running servers. Default every minute.
2946
3086
  * Not for serverless functions or Cloudflare Workers, where nothing runs
2947
- * between requests: call check() from a cron there instead.
3087
+ * between requests: call check() from a cron there instead. (A job's
3088
+ * start() opens a run; this starts the checks.)
2948
3089
  */
2949
- start(every = "1m") {
3090
+ startChecking(every = "1m") {
2950
3091
  if (this.timer) return;
2951
3092
  const ms = Math.min(TIMER_MAX_MS, Math.max(5e3, parseDuration(every, "check interval")));
2952
3093
  if (this.deferDelivery && !this.warnedDeferredStart) {
2953
3094
  this.warnedDeferredStart = true;
2954
- console.warn('[cronwatch] start() was called with deliver: "check", so these checks send no alerts. Another process must run checks with deliver: "now" (the default) to send them.');
3095
+ 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
3096
  }
2956
3097
  const tick = () => this.check().catch((e) => this.onError(e, "check"));
2957
3098
  this.timer = setInterval(tick, ms);
@@ -2962,6 +3103,15 @@ ${id}`;
2962
3103
  }, 1e3);
2963
3104
  if (typeof this.firstTick === "object" && "unref" in this.firstTick) this.firstTick.unref();
2964
3105
  }
3106
+ /**
3107
+ * startChecking(), under its former name.
3108
+ * @deprecated Renamed `startChecking`, since a job's `start()` opens a run.
3109
+ * This name still works through 1.x and goes in 2.0.
3110
+ */
3111
+ start(every = "1m") {
3112
+ this.startChecking(every);
3113
+ }
3114
+ /** Stop the checks startChecking() began. */
2965
3115
  stop() {
2966
3116
  if (this.timer) clearInterval(this.timer);
2967
3117
  if (this.firstTick) clearTimeout(this.firstTick);
@@ -3074,9 +3224,10 @@ ${id}`;
3074
3224
  }
3075
3225
  };
3076
3226
  var RESERVED_RUN_ID_PREFIX = "pgcron:";
3227
+ var MAX_RUN_ID = 200;
3077
3228
  function checkRunId(job, id, method) {
3078
- if (typeof id !== "string" || id.length === 0 || id.length > 200) {
3079
- throw new Error(`job "${job}": ${method}() needs a run id of 1 to 200 characters (got ${typeof id === "string" ? `${id.length} characters` : typeof id})`);
3229
+ if (typeof id !== "string" || id.length === 0 || id.length > MAX_RUN_ID) {
3230
+ 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
3231
  }
3081
3232
  if (id.includes("\0")) throw new Error(`job "${job}": ${method}() cannot take a run id containing a NUL character`);
3082
3233
  if (id.startsWith(RESERVED_RUN_ID_PREFIX)) {
@@ -3137,12 +3288,13 @@ function custom(name, send) {
3137
3288
  await send(alert);
3138
3289
  } };
3139
3290
  }
3291
+ var CronWatch = Cronwatch;
3140
3292
 
3141
3293
  // src/index.ts
3142
3294
  function cronwatch(options = {}) {
3143
- return new CronWatch(options);
3295
+ return new Cronwatch(options);
3144
3296
  }
3145
3297
 
3146
- export { CronWatch, composeAlert, consoleChannel, createRoutes, cronwatch, custom, formatDuration, memory, nextFire, parseDuration, parseSchedule };
3298
+ export { CronWatch, Cronwatch, VERSION, composeAlert, consoleChannel, createRoutes, cronwatch, custom, formatDuration, memory, nextFire, parseDuration, parseSchedule };
3147
3299
  //# sourceMappingURL=index.js.map
3148
3300
  //# sourceMappingURL=index.js.map