@cronwatch/sdk 0.7.0 → 0.9.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.
Files changed (111) hide show
  1. package/dist/anthropic.cjs +13 -2
  2. package/dist/anthropic.cjs.map +1 -1
  3. package/dist/anthropic.d.cts +1 -1
  4. package/dist/anthropic.d.ts +1 -1
  5. package/dist/anthropic.js +13 -2
  6. package/dist/anthropic.js.map +1 -1
  7. package/dist/bugsnag.cjs +8 -1
  8. package/dist/bugsnag.cjs.map +1 -1
  9. package/dist/bugsnag.d.cts +1 -1
  10. package/dist/bugsnag.d.ts +1 -1
  11. package/dist/bugsnag.js +8 -1
  12. package/dist/bugsnag.js.map +1 -1
  13. package/dist/{client-B5hM4l-D.d.ts → client-CQm-8-mR.d.ts} +13 -2
  14. package/dist/{client-Di5HAWQz.d.cts → client-D35cfnNV.d.cts} +13 -2
  15. package/dist/d1.cjs +33 -20
  16. package/dist/d1.cjs.map +1 -1
  17. package/dist/d1.d.cts +1 -1
  18. package/dist/d1.d.ts +1 -1
  19. package/dist/d1.js +33 -20
  20. package/dist/d1.js.map +1 -1
  21. package/dist/datadog.cjs.map +1 -1
  22. package/dist/datadog.d.cts +1 -1
  23. package/dist/datadog.d.ts +1 -1
  24. package/dist/datadog.js.map +1 -1
  25. package/dist/discord.cjs.map +1 -1
  26. package/dist/discord.d.cts +1 -1
  27. package/dist/discord.d.ts +1 -1
  28. package/dist/discord.js.map +1 -1
  29. package/dist/{email-Ciq7GRn4.d.ts → email-BxCp9RQV.d.ts} +1 -1
  30. package/dist/{email-DX6gkd5H.d.cts → email-D1d5niMy.d.cts} +1 -1
  31. package/dist/honeybadger.cjs +8 -1
  32. package/dist/honeybadger.cjs.map +1 -1
  33. package/dist/honeybadger.d.cts +1 -1
  34. package/dist/honeybadger.d.ts +1 -1
  35. package/dist/honeybadger.js +8 -1
  36. package/dist/honeybadger.js.map +1 -1
  37. package/dist/index.cjs +119 -35
  38. package/dist/index.cjs.map +1 -1
  39. package/dist/index.d.cts +4 -4
  40. package/dist/index.d.ts +4 -4
  41. package/dist/index.js +119 -35
  42. package/dist/index.js.map +1 -1
  43. package/dist/mailgun.cjs.map +1 -1
  44. package/dist/mailgun.d.cts +2 -2
  45. package/dist/mailgun.d.ts +2 -2
  46. package/dist/mailgun.js.map +1 -1
  47. package/dist/newrelic.cjs.map +1 -1
  48. package/dist/newrelic.d.cts +1 -1
  49. package/dist/newrelic.d.ts +1 -1
  50. package/dist/newrelic.js.map +1 -1
  51. package/dist/pg-cron.cjs +53 -5
  52. package/dist/pg-cron.cjs.map +1 -1
  53. package/dist/pg-cron.d.cts +5 -2
  54. package/dist/pg-cron.d.ts +5 -2
  55. package/dist/pg-cron.js +53 -5
  56. package/dist/pg-cron.js.map +1 -1
  57. package/dist/postgres.cjs +28 -15
  58. package/dist/postgres.cjs.map +1 -1
  59. package/dist/postgres.d.cts +1 -1
  60. package/dist/postgres.d.ts +1 -1
  61. package/dist/postgres.js +28 -15
  62. package/dist/postgres.js.map +1 -1
  63. package/dist/postmark.cjs.map +1 -1
  64. package/dist/postmark.d.cts +2 -2
  65. package/dist/postmark.d.ts +2 -2
  66. package/dist/postmark.js.map +1 -1
  67. package/dist/resend.cjs.map +1 -1
  68. package/dist/resend.d.cts +2 -2
  69. package/dist/resend.d.ts +2 -2
  70. package/dist/resend.js.map +1 -1
  71. package/dist/rollbar.cjs +8 -1
  72. package/dist/rollbar.cjs.map +1 -1
  73. package/dist/rollbar.d.cts +1 -1
  74. package/dist/rollbar.d.ts +1 -1
  75. package/dist/rollbar.js +8 -1
  76. package/dist/rollbar.js.map +1 -1
  77. package/dist/sendgrid.cjs.map +1 -1
  78. package/dist/sendgrid.d.cts +2 -2
  79. package/dist/sendgrid.d.ts +2 -2
  80. package/dist/sendgrid.js.map +1 -1
  81. package/dist/sentry.cjs +8 -1
  82. package/dist/sentry.cjs.map +1 -1
  83. package/dist/sentry.d.cts +1 -1
  84. package/dist/sentry.d.ts +1 -1
  85. package/dist/sentry.js +8 -1
  86. package/dist/sentry.js.map +1 -1
  87. package/dist/ses.cjs.map +1 -1
  88. package/dist/ses.d.cts +2 -2
  89. package/dist/ses.d.ts +2 -2
  90. package/dist/ses.js.map +1 -1
  91. package/dist/slack.cjs.map +1 -1
  92. package/dist/slack.d.cts +1 -1
  93. package/dist/slack.d.ts +1 -1
  94. package/dist/slack.js.map +1 -1
  95. package/dist/sqlite.cjs +30 -19
  96. package/dist/sqlite.cjs.map +1 -1
  97. package/dist/sqlite.d.cts +1 -1
  98. package/dist/sqlite.d.ts +1 -1
  99. package/dist/sqlite.js +30 -19
  100. package/dist/sqlite.js.map +1 -1
  101. package/dist/twilio.cjs.map +1 -1
  102. package/dist/twilio.d.cts +1 -1
  103. package/dist/twilio.d.ts +1 -1
  104. package/dist/twilio.js.map +1 -1
  105. package/dist/{types-BjBbCGeJ.d.cts → types-Cz47Boks.d.cts} +6 -0
  106. package/dist/{types-BjBbCGeJ.d.ts → types-Cz47Boks.d.ts} +6 -0
  107. package/dist/webhook.cjs.map +1 -1
  108. package/dist/webhook.d.cts +1 -1
  109. package/dist/webhook.d.ts +1 -1
  110. package/dist/webhook.js.map +1 -1
  111. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/email.ts","../src/alerts/mailgun.ts"],"names":[],"mappings":"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","file":"mailgun.cjs","sourcesContent":["/**\n * Helpers shared by the provider channels. Only fetch and Web Crypto, so the\n * channels run on Node, Cloudflare Workers, Deno and Bun alike. Not an entry\n * point: each channel bundles its own copy.\n */\nimport type { Alert } from \"../types.js\";\n\nexport const TIMEOUT_MS = 10_000;\n\n/** Severity for trackers that have levels. Recovered is informational. */\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport function severity(type: Alert[\"type\"]): Severity {\n if (type === \"recovered\") return \"info\";\n if (type === \"slow\" || type === \"over_budget\") return \"warning\";\n return \"error\";\n}\n\n/** The scheme, host and port only. A URL's path or query can hold a credential. */\nexport function origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"(invalid URL)\";\n }\n}\n\n/**\n * The URL, once it is one fetch can post to. Refused without quoting it:\n * fetch's own error for a URL it cannot parse (\"Failed to parse URL from ...\")\n * quotes the whole URL, and a webhook URL's path is its credential.\n */\nexport function postable(url: string): string {\n let protocol: string;\n try {\n protocol = new URL(url).protocol;\n } catch {\n throw new Error(\"only http and https URLs can be posted to, not this URL\");\n }\n if (protocol !== \"http:\" && protocol !== \"https:\") throw new Error(`only http and https URLs can be posted to, not ${protocol}`);\n return url;\n}\n\n/** How much of a provider's error body goes into the error message. */\nexport const ERROR_BODY_MAX = 200;\n\n/**\n * POSTs and throws on a non-2xx answer. The error names the provider and the\n * URL's origin, plus the start of the response body with every secret the\n * channel holds cut out, in case a provider echoes one back. Secrets are cut\n * from a prefix long enough to hold one that starts inside the first\n * ERROR_BODY_MAX characters, and only then is it cut to that length, so no\n * part of a secret survives at the edge. A redirect is refused rather than\n * followed: the credential headers would go with it to wherever it points.\n */\nexport async function post(provider: string, url: string, init: { headers: Record<string, string>; body: string }, secrets: (string | undefined)[] = []): Promise<Response> {\n const response = await fetch(postable(url), { method: \"POST\", headers: init.headers, body: init.body, redirect: \"error\", signal: AbortSignal.timeout(TIMEOUT_MS) });\n if (!response.ok) {\n let text = \"\";\n try {\n text = await response.text();\n } catch {\n // The status is enough.\n }\n throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : \"\"}`);\n }\n return response;\n}\n\n/** The start of an error body, secrets cut out first, then cut to ERROR_BODY_MAX on a code point. */\nexport function errorBody(text: string, secrets: (string | undefined)[] = []): string {\n const kept = secrets.filter((s): s is string => typeof s === \"string\" && s.length >= 4);\n const longest = kept.reduce((n, s) => Math.max(n, s.length), 0);\n let head = cut(text, ERROR_BODY_MAX + longest);\n for (const secret of kept) head = head.split(secret).join(\"[redacted]\");\n return cut(head, ERROR_BODY_MAX);\n}\n\n/** A credential with the spaces and newlines a paste leaves around it taken off. Anything not a string is empty. */\nexport function trimmed(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Base64 of UTF-8, without Buffer. */\nexport function base64(text: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nexport function basicAuth(user: string, password: string): string {\n return `Basic ${base64(`${user}:${password}`)}`;\n}\n\nexport function hex(bytes: ArrayBuffer | Uint8Array): string {\n return Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport async function sha256Hex(text: string): Promise<string> {\n return hex(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text)));\n}\n\n/**\n * A stable 32 hex character id for one alert: the same job, type and time\n * always give the same id, so a provider that deduplicates on it drops a\n * resend of an alert it already took.\n */\nexport async function alertId(alert: Alert): Promise<string> {\n return (await sha256Hex(`${alert.job}\\n${alert.type}\\n${alert.at}`)).slice(0, 32);\n}\n\n/** The same id laid out as a UUID, for APIs that ask for one. */\nexport function asUuid(id: string): string {\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20, 32)}`;\n}\n\n/** Cuts to at most `max` UTF-16 units without splitting a surrogate pair. */\nexport function cut(text: string, max: number): string {\n if (text.length <= max) return text;\n let end = max;\n const code = text.charCodeAt(end - 1);\n if (code >= 0xd800 && code <= 0xdbff) end -= 1;\n return text.slice(0, end);\n}\n\n/** The run fields worth attaching to a tracker event. */\nexport function runSummary(alert: Alert): Record<string, unknown> | null {\n const run = alert.run;\n if (!run) return null;\n return { id: run.id, status: run.status, startedAt: new Date(run.startedAt).toISOString(), durationMs: run.durationMs, trigger: run.trigger };\n}\n\n/** Title, message, triage and link as one plain text block, the way every channel reads. */\nexport function plainText(alert: Alert, link: string | undefined): string {\n return [alert.title, \"\", alert.message, ...(alert.triage ? [\"\", `Triage: ${alert.triage}`] : []), ...(link ? [\"\", `Open: ${link}`] : [])].join(\"\\n\");\n}\n","/**\n * What every email channel sends: one subject, a plain text body and a small\n * HTML body, so an alert reads the same whichever provider carries it. Not an\n * entry point; resend, postmark, sendgrid, mailgun and ses each bundle it.\n */\nimport type { Alert } from \"../types.js\";\nimport { cut, plainText } from \"./shared.js\";\n\n/** The options every email channel takes. */\nexport interface EmailOptions {\n /** The sender, \"alerts@example.com\" or \"CronWatch <alerts@example.com>\". The provider must allow it. */\n from: string;\n /** One address or several. */\n to: string | string[];\n /** Put in front of the title in the subject, \"[prod]\" say. */\n subjectPrefix?: string;\n /** Link back to the job in your dashboard: (alert) => `https://app.example.com/cronwatch/jobs/${alert.job}`. */\n link?: (alert: Alert) => string;\n}\n\nexport interface Email {\n from: string;\n to: string[];\n subject: string;\n text: string;\n html: string;\n}\n\n/** Checks the shared options once, when the channel is made. */\nexport function recipients(name: string, options: EmailOptions): string[] {\n if (!options.from) throw new Error(`${name}() needs a from address`);\n const to = (Array.isArray(options.to) ? options.to : [options.to]).filter((a) => typeof a === \"string\" && a.trim() !== \"\");\n if (to.length === 0) throw new Error(`${name}() needs at least one to address`);\n return to.map((a) => a.trim());\n}\n\nexport function composeEmail(alert: Alert, options: EmailOptions, to: string[]): Email {\n const link = safeLink(options.link?.(alert));\n // One line: a newline in a subject is a header injection or a rejected send.\n const subject = cut(`${options.subjectPrefix ? `${options.subjectPrefix} ` : \"\"}${alert.title}`.replace(/[\\r\\n]+/g, \" \"), 250);\n return { from: options.from, to, subject, text: plainText(alert, link), html: html(alert, link) };\n}\n\n/** Escapes text for HTML content and double quoted attributes. */\nexport function escapeHtml(text: string): string {\n return text.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\").replace(/'/g, \"&#39;\");\n}\n\n/** Only http and https links are put in a mail; anything else is dropped. */\nfunction safeLink(link: string | undefined): string | undefined {\n if (!link) return undefined;\n return /^https?:\\/\\//i.test(link) ? link : undefined;\n}\n\nfunction html(alert: Alert, link: string | undefined): string {\n const parts = [\n `<!doctype html>`,\n `<html><body style=\"margin:0;padding:16px;font-family:Georgia,serif;color:#1d1b16;background:#ffffff\">`,\n `<p style=\"margin:0 0 12px;font-size:18px\"><strong>${escapeHtml(alert.title)}</strong></p>`,\n `<pre style=\"margin:0 0 12px;padding:12px;background:#f6f3ec;white-space:pre-wrap;word-break:break-word;font:13px/1.45 Menlo,Consolas,monospace\">${escapeHtml(alert.message)}</pre>`,\n ];\n if (alert.triage) parts.push(`<p style=\"margin:0 0 12px\"><em>Triage:</em> ${escapeHtml(alert.triage)}</p>`);\n if (link) parts.push(`<p style=\"margin:0\"><a href=\"${escapeHtml(link)}\">Open ${escapeHtml(alert.job)}</a></p>`);\n parts.push(`</body></html>`);\n return parts.join(\"\\n\");\n}\n\n/** Splits \"Name <a@b.c>\" into its parts; a bare address has no name. */\nexport function parseAddress(address: string): { email: string; name?: string } {\n const match = /^\\s*(.*?)\\s*<([^<>]+)>\\s*$/.exec(address);\n if (!match) return { email: address.trim() };\n const name = match[1]!.replace(/^\"(.*)\"$/, \"$1\");\n return name ? { email: match[2]!.trim(), name } : { email: match[2]!.trim() };\n}\n","/**\n * Mailgun. API reference: https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/messages/post-v3--domain-name--messages\n * POST https://api.mailgun.net/v3/<domain>/messages (api.eu.mailgun.net for\n * the EU region), form encoded, with basic auth \"api:<key>\".\n */\nimport type { AlertChannel } from \"../types.js\";\nimport { composeEmail, recipients, type EmailOptions } from \"./email.js\";\nimport { basicAuth, post, trimmed } from \"./shared.js\";\n\nexport interface MailgunOptions extends EmailOptions {\n /** A sending or account API key. */\n apiKey: string;\n /** The sending domain, \"mg.example.com\". */\n domain: string;\n /** \"eu\" for a domain in the EU region. Defaults to \"us\". */\n region?: \"us\" | \"eu\";\n}\n\n/** Sends alerts as email through Mailgun. */\nexport function mailgun(options: MailgunOptions): AlertChannel {\n // A pasted credential often carries a stray space or newline, which a header would refuse or send.\n const apiKey = trimmed(options.apiKey);\n if (!apiKey) throw new Error(\"mailgun() needs an apiKey\");\n if (!options.domain) throw new Error(\"mailgun() needs a domain\");\n const to = recipients(\"mailgun\", options);\n const host = options.region === \"eu\" ? \"https://api.eu.mailgun.net\" : \"https://api.mailgun.net\";\n const url = `${host}/v3/${encodeURIComponent(options.domain)}/messages`;\n return {\n name: \"mailgun\",\n async send(alert) {\n const email = composeEmail(alert, options, to);\n const form = new URLSearchParams();\n form.append(\"from\", email.from);\n for (const address of email.to) form.append(\"to\", address);\n form.append(\"subject\", email.subject);\n form.append(\"text\", email.text);\n form.append(\"html\", email.html);\n form.append(\"o:tag\", \"cronwatch\");\n await post(\"Mailgun\", url, {\n headers: { \"content-type\": \"application/x-www-form-urlencoded\", authorization: basicAuth(\"api\", apiKey) },\n body: form.toString(),\n }, [apiKey]);\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/email.ts","../src/alerts/mailgun.ts"],"names":[],"mappings":"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","file":"mailgun.cjs","sourcesContent":["/**\n * Helpers shared by the provider channels. Only fetch and Web Crypto, so the\n * channels run on Node, Cloudflare Workers, Deno and Bun alike. Not an entry\n * point: each channel bundles its own copy.\n */\nimport { isoTime } from \"../duration.js\";\nimport type { Alert } from \"../types.js\";\n\nexport const TIMEOUT_MS = 10_000;\n\n/** Severity for trackers that have levels. Recovered is informational. */\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport function severity(type: Alert[\"type\"]): Severity {\n if (type === \"recovered\") return \"info\";\n if (type === \"slow\" || type === \"over_budget\") return \"warning\";\n return \"error\";\n}\n\n/** The scheme, host and port only. A URL's path or query can hold a credential. */\nexport function origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"(invalid URL)\";\n }\n}\n\n/**\n * The URL, once it is one fetch can post to. Refused without quoting it:\n * fetch's own error for a URL it cannot parse (\"Failed to parse URL from ...\")\n * quotes the whole URL, and a webhook URL's path is its credential.\n */\nexport function postable(url: string): string {\n let protocol: string;\n try {\n protocol = new URL(url).protocol;\n } catch {\n throw new Error(\"only http and https URLs can be posted to, not this URL\");\n }\n if (protocol !== \"http:\" && protocol !== \"https:\") throw new Error(`only http and https URLs can be posted to, not ${protocol}`);\n return url;\n}\n\n/** How much of a provider's error body goes into the error message. */\nexport const ERROR_BODY_MAX = 200;\n\n/**\n * POSTs and throws on a non-2xx answer. The error names the provider and the\n * URL's origin, plus the start of the response body with every secret the\n * channel holds cut out, in case a provider echoes one back. Secrets are cut\n * from a prefix long enough to hold one that starts inside the first\n * ERROR_BODY_MAX characters, and only then is it cut to that length, so no\n * part of a secret survives at the edge. A redirect is refused rather than\n * followed: the credential headers would go with it to wherever it points.\n */\nexport async function post(provider: string, url: string, init: { headers: Record<string, string>; body: string }, secrets: (string | undefined)[] = []): Promise<Response> {\n const response = await fetch(postable(url), { method: \"POST\", headers: init.headers, body: init.body, redirect: \"error\", signal: AbortSignal.timeout(TIMEOUT_MS) });\n if (!response.ok) {\n let text = \"\";\n try {\n text = await response.text();\n } catch {\n // The status is enough.\n }\n throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : \"\"}`);\n }\n return response;\n}\n\n/** The start of an error body, secrets cut out first, then cut to ERROR_BODY_MAX on a code point. */\nexport function errorBody(text: string, secrets: (string | undefined)[] = []): string {\n const kept = secrets.filter((s): s is string => typeof s === \"string\" && s.length >= 4);\n const longest = kept.reduce((n, s) => Math.max(n, s.length), 0);\n let head = cut(text, ERROR_BODY_MAX + longest);\n for (const secret of kept) head = head.split(secret).join(\"[redacted]\");\n return cut(head, ERROR_BODY_MAX);\n}\n\n/** A credential with the spaces and newlines a paste leaves around it taken off. Anything not a string is empty. */\nexport function trimmed(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Base64 of UTF-8, without Buffer. */\nexport function base64(text: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nexport function basicAuth(user: string, password: string): string {\n return `Basic ${base64(`${user}:${password}`)}`;\n}\n\nexport function hex(bytes: ArrayBuffer | Uint8Array): string {\n return Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport async function sha256Hex(text: string): Promise<string> {\n return hex(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text)));\n}\n\n/**\n * A stable 32 hex character id for one alert: the same job, type and time\n * always give the same id, so a provider that deduplicates on it drops a\n * resend of an alert it already took.\n */\nexport async function alertId(alert: Alert): Promise<string> {\n return (await sha256Hex(`${alert.job}\\n${alert.type}\\n${alert.at}`)).slice(0, 32);\n}\n\n/** The same id laid out as a UUID, for APIs that ask for one. */\nexport function asUuid(id: string): string {\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20, 32)}`;\n}\n\n/** Cuts to at most `max` UTF-16 units without splitting a surrogate pair. */\nexport function cut(text: string, max: number): string {\n if (text.length <= max) return text;\n let end = max;\n const code = text.charCodeAt(end - 1);\n if (code >= 0xd800 && code <= 0xdbff) end -= 1;\n return text.slice(0, end);\n}\n\n/** The run fields worth attaching to a tracker event. A start before the year 1 or after 9999 is null. */\nexport function runSummary(alert: Alert): Record<string, unknown> | null {\n const run = alert.run;\n if (!run) return null;\n return { id: run.id, status: run.status, startedAt: isoTime(run.startedAt), durationMs: run.durationMs, trigger: run.trigger };\n}\n\n/** Title, message, triage and link as one plain text block, the way every channel reads. */\nexport function plainText(alert: Alert, link: string | undefined): string {\n return [alert.title, \"\", alert.message, ...(alert.triage ? [\"\", `Triage: ${alert.triage}`] : []), ...(link ? [\"\", `Open: ${link}`] : [])].join(\"\\n\");\n}\n","/**\n * What every email channel sends: one subject, a plain text body and a small\n * HTML body, so an alert reads the same whichever provider carries it. Not an\n * entry point; resend, postmark, sendgrid, mailgun and ses each bundle it.\n */\nimport type { Alert } from \"../types.js\";\nimport { cut, plainText } from \"./shared.js\";\n\n/** The options every email channel takes. */\nexport interface EmailOptions {\n /** The sender, \"alerts@example.com\" or \"CronWatch <alerts@example.com>\". The provider must allow it. */\n from: string;\n /** One address or several. */\n to: string | string[];\n /** Put in front of the title in the subject, \"[prod]\" say. */\n subjectPrefix?: string;\n /** Link back to the job in your dashboard: (alert) => `https://app.example.com/cronwatch/jobs/${alert.job}`. */\n link?: (alert: Alert) => string;\n}\n\nexport interface Email {\n from: string;\n to: string[];\n subject: string;\n text: string;\n html: string;\n}\n\n/** Checks the shared options once, when the channel is made. */\nexport function recipients(name: string, options: EmailOptions): string[] {\n if (!options.from) throw new Error(`${name}() needs a from address`);\n const to = (Array.isArray(options.to) ? options.to : [options.to]).filter((a) => typeof a === \"string\" && a.trim() !== \"\");\n if (to.length === 0) throw new Error(`${name}() needs at least one to address`);\n return to.map((a) => a.trim());\n}\n\nexport function composeEmail(alert: Alert, options: EmailOptions, to: string[]): Email {\n const link = safeLink(options.link?.(alert));\n // One line: a newline in a subject is a header injection or a rejected send.\n const subject = cut(`${options.subjectPrefix ? `${options.subjectPrefix} ` : \"\"}${alert.title}`.replace(/[\\r\\n]+/g, \" \"), 250);\n return { from: options.from, to, subject, text: plainText(alert, link), html: html(alert, link) };\n}\n\n/** Escapes text for HTML content and double quoted attributes. */\nexport function escapeHtml(text: string): string {\n return text.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\").replace(/'/g, \"&#39;\");\n}\n\n/** Only http and https links are put in a mail; anything else is dropped. */\nfunction safeLink(link: string | undefined): string | undefined {\n if (!link) return undefined;\n return /^https?:\\/\\//i.test(link) ? link : undefined;\n}\n\nfunction html(alert: Alert, link: string | undefined): string {\n const parts = [\n `<!doctype html>`,\n `<html><body style=\"margin:0;padding:16px;font-family:Georgia,serif;color:#1d1b16;background:#ffffff\">`,\n `<p style=\"margin:0 0 12px;font-size:18px\"><strong>${escapeHtml(alert.title)}</strong></p>`,\n `<pre style=\"margin:0 0 12px;padding:12px;background:#f6f3ec;white-space:pre-wrap;word-break:break-word;font:13px/1.45 Menlo,Consolas,monospace\">${escapeHtml(alert.message)}</pre>`,\n ];\n if (alert.triage) parts.push(`<p style=\"margin:0 0 12px\"><em>Triage:</em> ${escapeHtml(alert.triage)}</p>`);\n if (link) parts.push(`<p style=\"margin:0\"><a href=\"${escapeHtml(link)}\">Open ${escapeHtml(alert.job)}</a></p>`);\n parts.push(`</body></html>`);\n return parts.join(\"\\n\");\n}\n\n/** Splits \"Name <a@b.c>\" into its parts; a bare address has no name. */\nexport function parseAddress(address: string): { email: string; name?: string } {\n const match = /^\\s*(.*?)\\s*<([^<>]+)>\\s*$/.exec(address);\n if (!match) return { email: address.trim() };\n const name = match[1]!.replace(/^\"(.*)\"$/, \"$1\");\n return name ? { email: match[2]!.trim(), name } : { email: match[2]!.trim() };\n}\n","/**\n * Mailgun. API reference: https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/messages/post-v3--domain-name--messages\n * POST https://api.mailgun.net/v3/<domain>/messages (api.eu.mailgun.net for\n * the EU region), form encoded, with basic auth \"api:<key>\".\n */\nimport type { AlertChannel } from \"../types.js\";\nimport { composeEmail, recipients, type EmailOptions } from \"./email.js\";\nimport { basicAuth, post, trimmed } from \"./shared.js\";\n\nexport interface MailgunOptions extends EmailOptions {\n /** A sending or account API key. */\n apiKey: string;\n /** The sending domain, \"mg.example.com\". */\n domain: string;\n /** \"eu\" for a domain in the EU region. Defaults to \"us\". */\n region?: \"us\" | \"eu\";\n}\n\n/** Sends alerts as email through Mailgun. */\nexport function mailgun(options: MailgunOptions): AlertChannel {\n // A pasted credential often carries a stray space or newline, which a header would refuse or send.\n const apiKey = trimmed(options.apiKey);\n if (!apiKey) throw new Error(\"mailgun() needs an apiKey\");\n if (!options.domain) throw new Error(\"mailgun() needs a domain\");\n const to = recipients(\"mailgun\", options);\n const host = options.region === \"eu\" ? \"https://api.eu.mailgun.net\" : \"https://api.mailgun.net\";\n const url = `${host}/v3/${encodeURIComponent(options.domain)}/messages`;\n return {\n name: \"mailgun\",\n async send(alert) {\n const email = composeEmail(alert, options, to);\n const form = new URLSearchParams();\n form.append(\"from\", email.from);\n for (const address of email.to) form.append(\"to\", address);\n form.append(\"subject\", email.subject);\n form.append(\"text\", email.text);\n form.append(\"html\", email.html);\n form.append(\"o:tag\", \"cronwatch\");\n await post(\"Mailgun\", url, {\n headers: { \"content-type\": \"application/x-www-form-urlencoded\", authorization: basicAuth(\"api\", apiKey) },\n body: form.toString(),\n }, [apiKey]);\n },\n };\n}\n"]}
@@ -1,5 +1,5 @@
1
- import { a as AlertChannel } from './types-BjBbCGeJ.cjs';
2
- import { E as EmailOptions } from './email-DX6gkd5H.cjs';
1
+ import { a as AlertChannel } from './types-Cz47Boks.cjs';
2
+ import { E as EmailOptions } from './email-D1d5niMy.cjs';
3
3
 
4
4
  /**
5
5
  * Mailgun. API reference: https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/messages/post-v3--domain-name--messages
package/dist/mailgun.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as AlertChannel } from './types-BjBbCGeJ.js';
2
- import { E as EmailOptions } from './email-Ciq7GRn4.js';
1
+ import { a as AlertChannel } from './types-Cz47Boks.js';
2
+ import { E as EmailOptions } from './email-BxCp9RQV.js';
3
3
 
4
4
  /**
5
5
  * Mailgun. API reference: https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/messages/post-v3--domain-name--messages
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/email.ts","../src/alerts/mailgun.ts"],"names":[],"mappings":"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","file":"mailgun.js","sourcesContent":["/**\n * Helpers shared by the provider channels. Only fetch and Web Crypto, so the\n * channels run on Node, Cloudflare Workers, Deno and Bun alike. Not an entry\n * point: each channel bundles its own copy.\n */\nimport type { Alert } from \"../types.js\";\n\nexport const TIMEOUT_MS = 10_000;\n\n/** Severity for trackers that have levels. Recovered is informational. */\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport function severity(type: Alert[\"type\"]): Severity {\n if (type === \"recovered\") return \"info\";\n if (type === \"slow\" || type === \"over_budget\") return \"warning\";\n return \"error\";\n}\n\n/** The scheme, host and port only. A URL's path or query can hold a credential. */\nexport function origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"(invalid URL)\";\n }\n}\n\n/**\n * The URL, once it is one fetch can post to. Refused without quoting it:\n * fetch's own error for a URL it cannot parse (\"Failed to parse URL from ...\")\n * quotes the whole URL, and a webhook URL's path is its credential.\n */\nexport function postable(url: string): string {\n let protocol: string;\n try {\n protocol = new URL(url).protocol;\n } catch {\n throw new Error(\"only http and https URLs can be posted to, not this URL\");\n }\n if (protocol !== \"http:\" && protocol !== \"https:\") throw new Error(`only http and https URLs can be posted to, not ${protocol}`);\n return url;\n}\n\n/** How much of a provider's error body goes into the error message. */\nexport const ERROR_BODY_MAX = 200;\n\n/**\n * POSTs and throws on a non-2xx answer. The error names the provider and the\n * URL's origin, plus the start of the response body with every secret the\n * channel holds cut out, in case a provider echoes one back. Secrets are cut\n * from a prefix long enough to hold one that starts inside the first\n * ERROR_BODY_MAX characters, and only then is it cut to that length, so no\n * part of a secret survives at the edge. A redirect is refused rather than\n * followed: the credential headers would go with it to wherever it points.\n */\nexport async function post(provider: string, url: string, init: { headers: Record<string, string>; body: string }, secrets: (string | undefined)[] = []): Promise<Response> {\n const response = await fetch(postable(url), { method: \"POST\", headers: init.headers, body: init.body, redirect: \"error\", signal: AbortSignal.timeout(TIMEOUT_MS) });\n if (!response.ok) {\n let text = \"\";\n try {\n text = await response.text();\n } catch {\n // The status is enough.\n }\n throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : \"\"}`);\n }\n return response;\n}\n\n/** The start of an error body, secrets cut out first, then cut to ERROR_BODY_MAX on a code point. */\nexport function errorBody(text: string, secrets: (string | undefined)[] = []): string {\n const kept = secrets.filter((s): s is string => typeof s === \"string\" && s.length >= 4);\n const longest = kept.reduce((n, s) => Math.max(n, s.length), 0);\n let head = cut(text, ERROR_BODY_MAX + longest);\n for (const secret of kept) head = head.split(secret).join(\"[redacted]\");\n return cut(head, ERROR_BODY_MAX);\n}\n\n/** A credential with the spaces and newlines a paste leaves around it taken off. Anything not a string is empty. */\nexport function trimmed(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Base64 of UTF-8, without Buffer. */\nexport function base64(text: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nexport function basicAuth(user: string, password: string): string {\n return `Basic ${base64(`${user}:${password}`)}`;\n}\n\nexport function hex(bytes: ArrayBuffer | Uint8Array): string {\n return Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport async function sha256Hex(text: string): Promise<string> {\n return hex(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text)));\n}\n\n/**\n * A stable 32 hex character id for one alert: the same job, type and time\n * always give the same id, so a provider that deduplicates on it drops a\n * resend of an alert it already took.\n */\nexport async function alertId(alert: Alert): Promise<string> {\n return (await sha256Hex(`${alert.job}\\n${alert.type}\\n${alert.at}`)).slice(0, 32);\n}\n\n/** The same id laid out as a UUID, for APIs that ask for one. */\nexport function asUuid(id: string): string {\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20, 32)}`;\n}\n\n/** Cuts to at most `max` UTF-16 units without splitting a surrogate pair. */\nexport function cut(text: string, max: number): string {\n if (text.length <= max) return text;\n let end = max;\n const code = text.charCodeAt(end - 1);\n if (code >= 0xd800 && code <= 0xdbff) end -= 1;\n return text.slice(0, end);\n}\n\n/** The run fields worth attaching to a tracker event. */\nexport function runSummary(alert: Alert): Record<string, unknown> | null {\n const run = alert.run;\n if (!run) return null;\n return { id: run.id, status: run.status, startedAt: new Date(run.startedAt).toISOString(), durationMs: run.durationMs, trigger: run.trigger };\n}\n\n/** Title, message, triage and link as one plain text block, the way every channel reads. */\nexport function plainText(alert: Alert, link: string | undefined): string {\n return [alert.title, \"\", alert.message, ...(alert.triage ? [\"\", `Triage: ${alert.triage}`] : []), ...(link ? [\"\", `Open: ${link}`] : [])].join(\"\\n\");\n}\n","/**\n * What every email channel sends: one subject, a plain text body and a small\n * HTML body, so an alert reads the same whichever provider carries it. Not an\n * entry point; resend, postmark, sendgrid, mailgun and ses each bundle it.\n */\nimport type { Alert } from \"../types.js\";\nimport { cut, plainText } from \"./shared.js\";\n\n/** The options every email channel takes. */\nexport interface EmailOptions {\n /** The sender, \"alerts@example.com\" or \"CronWatch <alerts@example.com>\". The provider must allow it. */\n from: string;\n /** One address or several. */\n to: string | string[];\n /** Put in front of the title in the subject, \"[prod]\" say. */\n subjectPrefix?: string;\n /** Link back to the job in your dashboard: (alert) => `https://app.example.com/cronwatch/jobs/${alert.job}`. */\n link?: (alert: Alert) => string;\n}\n\nexport interface Email {\n from: string;\n to: string[];\n subject: string;\n text: string;\n html: string;\n}\n\n/** Checks the shared options once, when the channel is made. */\nexport function recipients(name: string, options: EmailOptions): string[] {\n if (!options.from) throw new Error(`${name}() needs a from address`);\n const to = (Array.isArray(options.to) ? options.to : [options.to]).filter((a) => typeof a === \"string\" && a.trim() !== \"\");\n if (to.length === 0) throw new Error(`${name}() needs at least one to address`);\n return to.map((a) => a.trim());\n}\n\nexport function composeEmail(alert: Alert, options: EmailOptions, to: string[]): Email {\n const link = safeLink(options.link?.(alert));\n // One line: a newline in a subject is a header injection or a rejected send.\n const subject = cut(`${options.subjectPrefix ? `${options.subjectPrefix} ` : \"\"}${alert.title}`.replace(/[\\r\\n]+/g, \" \"), 250);\n return { from: options.from, to, subject, text: plainText(alert, link), html: html(alert, link) };\n}\n\n/** Escapes text for HTML content and double quoted attributes. */\nexport function escapeHtml(text: string): string {\n return text.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\").replace(/'/g, \"&#39;\");\n}\n\n/** Only http and https links are put in a mail; anything else is dropped. */\nfunction safeLink(link: string | undefined): string | undefined {\n if (!link) return undefined;\n return /^https?:\\/\\//i.test(link) ? link : undefined;\n}\n\nfunction html(alert: Alert, link: string | undefined): string {\n const parts = [\n `<!doctype html>`,\n `<html><body style=\"margin:0;padding:16px;font-family:Georgia,serif;color:#1d1b16;background:#ffffff\">`,\n `<p style=\"margin:0 0 12px;font-size:18px\"><strong>${escapeHtml(alert.title)}</strong></p>`,\n `<pre style=\"margin:0 0 12px;padding:12px;background:#f6f3ec;white-space:pre-wrap;word-break:break-word;font:13px/1.45 Menlo,Consolas,monospace\">${escapeHtml(alert.message)}</pre>`,\n ];\n if (alert.triage) parts.push(`<p style=\"margin:0 0 12px\"><em>Triage:</em> ${escapeHtml(alert.triage)}</p>`);\n if (link) parts.push(`<p style=\"margin:0\"><a href=\"${escapeHtml(link)}\">Open ${escapeHtml(alert.job)}</a></p>`);\n parts.push(`</body></html>`);\n return parts.join(\"\\n\");\n}\n\n/** Splits \"Name <a@b.c>\" into its parts; a bare address has no name. */\nexport function parseAddress(address: string): { email: string; name?: string } {\n const match = /^\\s*(.*?)\\s*<([^<>]+)>\\s*$/.exec(address);\n if (!match) return { email: address.trim() };\n const name = match[1]!.replace(/^\"(.*)\"$/, \"$1\");\n return name ? { email: match[2]!.trim(), name } : { email: match[2]!.trim() };\n}\n","/**\n * Mailgun. API reference: https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/messages/post-v3--domain-name--messages\n * POST https://api.mailgun.net/v3/<domain>/messages (api.eu.mailgun.net for\n * the EU region), form encoded, with basic auth \"api:<key>\".\n */\nimport type { AlertChannel } from \"../types.js\";\nimport { composeEmail, recipients, type EmailOptions } from \"./email.js\";\nimport { basicAuth, post, trimmed } from \"./shared.js\";\n\nexport interface MailgunOptions extends EmailOptions {\n /** A sending or account API key. */\n apiKey: string;\n /** The sending domain, \"mg.example.com\". */\n domain: string;\n /** \"eu\" for a domain in the EU region. Defaults to \"us\". */\n region?: \"us\" | \"eu\";\n}\n\n/** Sends alerts as email through Mailgun. */\nexport function mailgun(options: MailgunOptions): AlertChannel {\n // A pasted credential often carries a stray space or newline, which a header would refuse or send.\n const apiKey = trimmed(options.apiKey);\n if (!apiKey) throw new Error(\"mailgun() needs an apiKey\");\n if (!options.domain) throw new Error(\"mailgun() needs a domain\");\n const to = recipients(\"mailgun\", options);\n const host = options.region === \"eu\" ? \"https://api.eu.mailgun.net\" : \"https://api.mailgun.net\";\n const url = `${host}/v3/${encodeURIComponent(options.domain)}/messages`;\n return {\n name: \"mailgun\",\n async send(alert) {\n const email = composeEmail(alert, options, to);\n const form = new URLSearchParams();\n form.append(\"from\", email.from);\n for (const address of email.to) form.append(\"to\", address);\n form.append(\"subject\", email.subject);\n form.append(\"text\", email.text);\n form.append(\"html\", email.html);\n form.append(\"o:tag\", \"cronwatch\");\n await post(\"Mailgun\", url, {\n headers: { \"content-type\": \"application/x-www-form-urlencoded\", authorization: basicAuth(\"api\", apiKey) },\n body: form.toString(),\n }, [apiKey]);\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/email.ts","../src/alerts/mailgun.ts"],"names":[],"mappings":"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","file":"mailgun.js","sourcesContent":["/**\n * Helpers shared by the provider channels. Only fetch and Web Crypto, so the\n * channels run on Node, Cloudflare Workers, Deno and Bun alike. Not an entry\n * point: each channel bundles its own copy.\n */\nimport { isoTime } from \"../duration.js\";\nimport type { Alert } from \"../types.js\";\n\nexport const TIMEOUT_MS = 10_000;\n\n/** Severity for trackers that have levels. Recovered is informational. */\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport function severity(type: Alert[\"type\"]): Severity {\n if (type === \"recovered\") return \"info\";\n if (type === \"slow\" || type === \"over_budget\") return \"warning\";\n return \"error\";\n}\n\n/** The scheme, host and port only. A URL's path or query can hold a credential. */\nexport function origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"(invalid URL)\";\n }\n}\n\n/**\n * The URL, once it is one fetch can post to. Refused without quoting it:\n * fetch's own error for a URL it cannot parse (\"Failed to parse URL from ...\")\n * quotes the whole URL, and a webhook URL's path is its credential.\n */\nexport function postable(url: string): string {\n let protocol: string;\n try {\n protocol = new URL(url).protocol;\n } catch {\n throw new Error(\"only http and https URLs can be posted to, not this URL\");\n }\n if (protocol !== \"http:\" && protocol !== \"https:\") throw new Error(`only http and https URLs can be posted to, not ${protocol}`);\n return url;\n}\n\n/** How much of a provider's error body goes into the error message. */\nexport const ERROR_BODY_MAX = 200;\n\n/**\n * POSTs and throws on a non-2xx answer. The error names the provider and the\n * URL's origin, plus the start of the response body with every secret the\n * channel holds cut out, in case a provider echoes one back. Secrets are cut\n * from a prefix long enough to hold one that starts inside the first\n * ERROR_BODY_MAX characters, and only then is it cut to that length, so no\n * part of a secret survives at the edge. A redirect is refused rather than\n * followed: the credential headers would go with it to wherever it points.\n */\nexport async function post(provider: string, url: string, init: { headers: Record<string, string>; body: string }, secrets: (string | undefined)[] = []): Promise<Response> {\n const response = await fetch(postable(url), { method: \"POST\", headers: init.headers, body: init.body, redirect: \"error\", signal: AbortSignal.timeout(TIMEOUT_MS) });\n if (!response.ok) {\n let text = \"\";\n try {\n text = await response.text();\n } catch {\n // The status is enough.\n }\n throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : \"\"}`);\n }\n return response;\n}\n\n/** The start of an error body, secrets cut out first, then cut to ERROR_BODY_MAX on a code point. */\nexport function errorBody(text: string, secrets: (string | undefined)[] = []): string {\n const kept = secrets.filter((s): s is string => typeof s === \"string\" && s.length >= 4);\n const longest = kept.reduce((n, s) => Math.max(n, s.length), 0);\n let head = cut(text, ERROR_BODY_MAX + longest);\n for (const secret of kept) head = head.split(secret).join(\"[redacted]\");\n return cut(head, ERROR_BODY_MAX);\n}\n\n/** A credential with the spaces and newlines a paste leaves around it taken off. Anything not a string is empty. */\nexport function trimmed(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Base64 of UTF-8, without Buffer. */\nexport function base64(text: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nexport function basicAuth(user: string, password: string): string {\n return `Basic ${base64(`${user}:${password}`)}`;\n}\n\nexport function hex(bytes: ArrayBuffer | Uint8Array): string {\n return Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport async function sha256Hex(text: string): Promise<string> {\n return hex(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text)));\n}\n\n/**\n * A stable 32 hex character id for one alert: the same job, type and time\n * always give the same id, so a provider that deduplicates on it drops a\n * resend of an alert it already took.\n */\nexport async function alertId(alert: Alert): Promise<string> {\n return (await sha256Hex(`${alert.job}\\n${alert.type}\\n${alert.at}`)).slice(0, 32);\n}\n\n/** The same id laid out as a UUID, for APIs that ask for one. */\nexport function asUuid(id: string): string {\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20, 32)}`;\n}\n\n/** Cuts to at most `max` UTF-16 units without splitting a surrogate pair. */\nexport function cut(text: string, max: number): string {\n if (text.length <= max) return text;\n let end = max;\n const code = text.charCodeAt(end - 1);\n if (code >= 0xd800 && code <= 0xdbff) end -= 1;\n return text.slice(0, end);\n}\n\n/** The run fields worth attaching to a tracker event. A start before the year 1 or after 9999 is null. */\nexport function runSummary(alert: Alert): Record<string, unknown> | null {\n const run = alert.run;\n if (!run) return null;\n return { id: run.id, status: run.status, startedAt: isoTime(run.startedAt), durationMs: run.durationMs, trigger: run.trigger };\n}\n\n/** Title, message, triage and link as one plain text block, the way every channel reads. */\nexport function plainText(alert: Alert, link: string | undefined): string {\n return [alert.title, \"\", alert.message, ...(alert.triage ? [\"\", `Triage: ${alert.triage}`] : []), ...(link ? [\"\", `Open: ${link}`] : [])].join(\"\\n\");\n}\n","/**\n * What every email channel sends: one subject, a plain text body and a small\n * HTML body, so an alert reads the same whichever provider carries it. Not an\n * entry point; resend, postmark, sendgrid, mailgun and ses each bundle it.\n */\nimport type { Alert } from \"../types.js\";\nimport { cut, plainText } from \"./shared.js\";\n\n/** The options every email channel takes. */\nexport interface EmailOptions {\n /** The sender, \"alerts@example.com\" or \"CronWatch <alerts@example.com>\". The provider must allow it. */\n from: string;\n /** One address or several. */\n to: string | string[];\n /** Put in front of the title in the subject, \"[prod]\" say. */\n subjectPrefix?: string;\n /** Link back to the job in your dashboard: (alert) => `https://app.example.com/cronwatch/jobs/${alert.job}`. */\n link?: (alert: Alert) => string;\n}\n\nexport interface Email {\n from: string;\n to: string[];\n subject: string;\n text: string;\n html: string;\n}\n\n/** Checks the shared options once, when the channel is made. */\nexport function recipients(name: string, options: EmailOptions): string[] {\n if (!options.from) throw new Error(`${name}() needs a from address`);\n const to = (Array.isArray(options.to) ? options.to : [options.to]).filter((a) => typeof a === \"string\" && a.trim() !== \"\");\n if (to.length === 0) throw new Error(`${name}() needs at least one to address`);\n return to.map((a) => a.trim());\n}\n\nexport function composeEmail(alert: Alert, options: EmailOptions, to: string[]): Email {\n const link = safeLink(options.link?.(alert));\n // One line: a newline in a subject is a header injection or a rejected send.\n const subject = cut(`${options.subjectPrefix ? `${options.subjectPrefix} ` : \"\"}${alert.title}`.replace(/[\\r\\n]+/g, \" \"), 250);\n return { from: options.from, to, subject, text: plainText(alert, link), html: html(alert, link) };\n}\n\n/** Escapes text for HTML content and double quoted attributes. */\nexport function escapeHtml(text: string): string {\n return text.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\").replace(/'/g, \"&#39;\");\n}\n\n/** Only http and https links are put in a mail; anything else is dropped. */\nfunction safeLink(link: string | undefined): string | undefined {\n if (!link) return undefined;\n return /^https?:\\/\\//i.test(link) ? link : undefined;\n}\n\nfunction html(alert: Alert, link: string | undefined): string {\n const parts = [\n `<!doctype html>`,\n `<html><body style=\"margin:0;padding:16px;font-family:Georgia,serif;color:#1d1b16;background:#ffffff\">`,\n `<p style=\"margin:0 0 12px;font-size:18px\"><strong>${escapeHtml(alert.title)}</strong></p>`,\n `<pre style=\"margin:0 0 12px;padding:12px;background:#f6f3ec;white-space:pre-wrap;word-break:break-word;font:13px/1.45 Menlo,Consolas,monospace\">${escapeHtml(alert.message)}</pre>`,\n ];\n if (alert.triage) parts.push(`<p style=\"margin:0 0 12px\"><em>Triage:</em> ${escapeHtml(alert.triage)}</p>`);\n if (link) parts.push(`<p style=\"margin:0\"><a href=\"${escapeHtml(link)}\">Open ${escapeHtml(alert.job)}</a></p>`);\n parts.push(`</body></html>`);\n return parts.join(\"\\n\");\n}\n\n/** Splits \"Name <a@b.c>\" into its parts; a bare address has no name. */\nexport function parseAddress(address: string): { email: string; name?: string } {\n const match = /^\\s*(.*?)\\s*<([^<>]+)>\\s*$/.exec(address);\n if (!match) return { email: address.trim() };\n const name = match[1]!.replace(/^\"(.*)\"$/, \"$1\");\n return name ? { email: match[2]!.trim(), name } : { email: match[2]!.trim() };\n}\n","/**\n * Mailgun. API reference: https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/messages/post-v3--domain-name--messages\n * POST https://api.mailgun.net/v3/<domain>/messages (api.eu.mailgun.net for\n * the EU region), form encoded, with basic auth \"api:<key>\".\n */\nimport type { AlertChannel } from \"../types.js\";\nimport { composeEmail, recipients, type EmailOptions } from \"./email.js\";\nimport { basicAuth, post, trimmed } from \"./shared.js\";\n\nexport interface MailgunOptions extends EmailOptions {\n /** A sending or account API key. */\n apiKey: string;\n /** The sending domain, \"mg.example.com\". */\n domain: string;\n /** \"eu\" for a domain in the EU region. Defaults to \"us\". */\n region?: \"us\" | \"eu\";\n}\n\n/** Sends alerts as email through Mailgun. */\nexport function mailgun(options: MailgunOptions): AlertChannel {\n // A pasted credential often carries a stray space or newline, which a header would refuse or send.\n const apiKey = trimmed(options.apiKey);\n if (!apiKey) throw new Error(\"mailgun() needs an apiKey\");\n if (!options.domain) throw new Error(\"mailgun() needs a domain\");\n const to = recipients(\"mailgun\", options);\n const host = options.region === \"eu\" ? \"https://api.eu.mailgun.net\" : \"https://api.mailgun.net\";\n const url = `${host}/v3/${encodeURIComponent(options.domain)}/messages`;\n return {\n name: \"mailgun\",\n async send(alert) {\n const email = composeEmail(alert, options, to);\n const form = new URLSearchParams();\n form.append(\"from\", email.from);\n for (const address of email.to) form.append(\"to\", address);\n form.append(\"subject\", email.subject);\n form.append(\"text\", email.text);\n form.append(\"html\", email.html);\n form.append(\"o:tag\", \"cronwatch\");\n await post(\"Mailgun\", url, {\n headers: { \"content-type\": \"application/x-www-form-urlencoded\", authorization: basicAuth(\"api\", apiKey) },\n body: form.toString(),\n }, [apiKey]);\n },\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/newrelic.ts"],"names":[],"mappings":"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","file":"newrelic.cjs","sourcesContent":["/**\n * Helpers shared by the provider channels. Only fetch and Web Crypto, so the\n * channels run on Node, Cloudflare Workers, Deno and Bun alike. Not an entry\n * point: each channel bundles its own copy.\n */\nimport type { Alert } from \"../types.js\";\n\nexport const TIMEOUT_MS = 10_000;\n\n/** Severity for trackers that have levels. Recovered is informational. */\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport function severity(type: Alert[\"type\"]): Severity {\n if (type === \"recovered\") return \"info\";\n if (type === \"slow\" || type === \"over_budget\") return \"warning\";\n return \"error\";\n}\n\n/** The scheme, host and port only. A URL's path or query can hold a credential. */\nexport function origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"(invalid URL)\";\n }\n}\n\n/**\n * The URL, once it is one fetch can post to. Refused without quoting it:\n * fetch's own error for a URL it cannot parse (\"Failed to parse URL from ...\")\n * quotes the whole URL, and a webhook URL's path is its credential.\n */\nexport function postable(url: string): string {\n let protocol: string;\n try {\n protocol = new URL(url).protocol;\n } catch {\n throw new Error(\"only http and https URLs can be posted to, not this URL\");\n }\n if (protocol !== \"http:\" && protocol !== \"https:\") throw new Error(`only http and https URLs can be posted to, not ${protocol}`);\n return url;\n}\n\n/** How much of a provider's error body goes into the error message. */\nexport const ERROR_BODY_MAX = 200;\n\n/**\n * POSTs and throws on a non-2xx answer. The error names the provider and the\n * URL's origin, plus the start of the response body with every secret the\n * channel holds cut out, in case a provider echoes one back. Secrets are cut\n * from a prefix long enough to hold one that starts inside the first\n * ERROR_BODY_MAX characters, and only then is it cut to that length, so no\n * part of a secret survives at the edge. A redirect is refused rather than\n * followed: the credential headers would go with it to wherever it points.\n */\nexport async function post(provider: string, url: string, init: { headers: Record<string, string>; body: string }, secrets: (string | undefined)[] = []): Promise<Response> {\n const response = await fetch(postable(url), { method: \"POST\", headers: init.headers, body: init.body, redirect: \"error\", signal: AbortSignal.timeout(TIMEOUT_MS) });\n if (!response.ok) {\n let text = \"\";\n try {\n text = await response.text();\n } catch {\n // The status is enough.\n }\n throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : \"\"}`);\n }\n return response;\n}\n\n/** The start of an error body, secrets cut out first, then cut to ERROR_BODY_MAX on a code point. */\nexport function errorBody(text: string, secrets: (string | undefined)[] = []): string {\n const kept = secrets.filter((s): s is string => typeof s === \"string\" && s.length >= 4);\n const longest = kept.reduce((n, s) => Math.max(n, s.length), 0);\n let head = cut(text, ERROR_BODY_MAX + longest);\n for (const secret of kept) head = head.split(secret).join(\"[redacted]\");\n return cut(head, ERROR_BODY_MAX);\n}\n\n/** A credential with the spaces and newlines a paste leaves around it taken off. Anything not a string is empty. */\nexport function trimmed(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Base64 of UTF-8, without Buffer. */\nexport function base64(text: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nexport function basicAuth(user: string, password: string): string {\n return `Basic ${base64(`${user}:${password}`)}`;\n}\n\nexport function hex(bytes: ArrayBuffer | Uint8Array): string {\n return Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport async function sha256Hex(text: string): Promise<string> {\n return hex(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text)));\n}\n\n/**\n * A stable 32 hex character id for one alert: the same job, type and time\n * always give the same id, so a provider that deduplicates on it drops a\n * resend of an alert it already took.\n */\nexport async function alertId(alert: Alert): Promise<string> {\n return (await sha256Hex(`${alert.job}\\n${alert.type}\\n${alert.at}`)).slice(0, 32);\n}\n\n/** The same id laid out as a UUID, for APIs that ask for one. */\nexport function asUuid(id: string): string {\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20, 32)}`;\n}\n\n/** Cuts to at most `max` UTF-16 units without splitting a surrogate pair. */\nexport function cut(text: string, max: number): string {\n if (text.length <= max) return text;\n let end = max;\n const code = text.charCodeAt(end - 1);\n if (code >= 0xd800 && code <= 0xdbff) end -= 1;\n return text.slice(0, end);\n}\n\n/** The run fields worth attaching to a tracker event. */\nexport function runSummary(alert: Alert): Record<string, unknown> | null {\n const run = alert.run;\n if (!run) return null;\n return { id: run.id, status: run.status, startedAt: new Date(run.startedAt).toISOString(), durationMs: run.durationMs, trigger: run.trigger };\n}\n\n/** Title, message, triage and link as one plain text block, the way every channel reads. */\nexport function plainText(alert: Alert, link: string | undefined): string {\n return [alert.title, \"\", alert.message, ...(alert.triage ? [\"\", `Triage: ${alert.triage}`] : []), ...(link ? [\"\", `Open: ${link}`] : [])].join(\"\\n\");\n}\n","/**\n * New Relic Event API. API reference: https://docs.newrelic.com/docs/data-apis/ingest-apis/event-api/introduction-event-api/\n * POST https://insights-collector.newrelic.com/v1/accounts/<id>/events\n * (insights-collector.eu01.nr-data.net for EU accounts) with Api-Key.\n * Each alert is one custom event of type CronWatchAlert, queryable with NRQL:\n * SELECT * FROM CronWatchAlert WHERE job = 'nightly'.\n */\nimport type { Alert, AlertChannel } from \"../types.js\";\nimport { cut, post, severity, trimmed } from \"./shared.js\";\n\nexport interface NewRelicOptions {\n /** The account id, the number in your New Relic URLs. */\n accountId: string | number;\n /** A license key (INGEST - LICENSE). */\n apiKey: string;\n /** \"eu\" for an account in the EU data center. Defaults to \"us\". */\n region?: \"us\" | \"eu\";\n /** Defaults to \"CronWatchAlert\". */\n eventType?: string;\n link?: (alert: Alert) => string;\n}\n\n/** Records alerts as New Relic custom events. */\nexport function newrelic(options: NewRelicOptions): AlertChannel {\n // A pasted credential often carries a stray space or newline, which a header would refuse or send.\n const apiKey = trimmed(options.apiKey);\n if (!apiKey) throw new Error(\"newrelic() needs an apiKey\");\n const account = String(options.accountId ?? \"\");\n if (!/^\\d+$/.test(account)) throw new Error(\"newrelic() needs a numeric accountId\");\n const host = options.region === \"eu\" ? \"https://insights-collector.eu01.nr-data.net\" : \"https://insights-collector.newrelic.com\";\n const url = `${host}/v1/accounts/${account}/events`;\n const eventType = options.eventType ?? \"CronWatchAlert\";\n return {\n name: \"newrelic\",\n async send(alert) {\n const link = options.link?.(alert);\n const run = alert.run;\n // Flat attributes only, strings under 4096 characters.\n const event: Record<string, string | number> = {\n eventType,\n timestamp: alert.at,\n job: cut(alert.job, 4095),\n alertType: alert.type,\n severity: severity(alert.type),\n title: cut(alert.title, 4095),\n message: cut(alert.message, 4095),\n };\n if (alert.triage) event.triage = cut(alert.triage, 4095);\n if (link) event.link = cut(link, 4095);\n if (run) {\n event.runId = run.id;\n event.runStatus = run.status;\n if (run.durationMs !== null) event.durationMs = run.durationMs;\n }\n await post(\"New Relic\", url, {\n headers: { \"content-type\": \"application/json\", \"api-key\": apiKey },\n body: JSON.stringify([event]),\n }, [apiKey]);\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/newrelic.ts"],"names":[],"mappings":"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","file":"newrelic.cjs","sourcesContent":["/**\n * Helpers shared by the provider channels. Only fetch and Web Crypto, so the\n * channels run on Node, Cloudflare Workers, Deno and Bun alike. Not an entry\n * point: each channel bundles its own copy.\n */\nimport { isoTime } from \"../duration.js\";\nimport type { Alert } from \"../types.js\";\n\nexport const TIMEOUT_MS = 10_000;\n\n/** Severity for trackers that have levels. Recovered is informational. */\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport function severity(type: Alert[\"type\"]): Severity {\n if (type === \"recovered\") return \"info\";\n if (type === \"slow\" || type === \"over_budget\") return \"warning\";\n return \"error\";\n}\n\n/** The scheme, host and port only. A URL's path or query can hold a credential. */\nexport function origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"(invalid URL)\";\n }\n}\n\n/**\n * The URL, once it is one fetch can post to. Refused without quoting it:\n * fetch's own error for a URL it cannot parse (\"Failed to parse URL from ...\")\n * quotes the whole URL, and a webhook URL's path is its credential.\n */\nexport function postable(url: string): string {\n let protocol: string;\n try {\n protocol = new URL(url).protocol;\n } catch {\n throw new Error(\"only http and https URLs can be posted to, not this URL\");\n }\n if (protocol !== \"http:\" && protocol !== \"https:\") throw new Error(`only http and https URLs can be posted to, not ${protocol}`);\n return url;\n}\n\n/** How much of a provider's error body goes into the error message. */\nexport const ERROR_BODY_MAX = 200;\n\n/**\n * POSTs and throws on a non-2xx answer. The error names the provider and the\n * URL's origin, plus the start of the response body with every secret the\n * channel holds cut out, in case a provider echoes one back. Secrets are cut\n * from a prefix long enough to hold one that starts inside the first\n * ERROR_BODY_MAX characters, and only then is it cut to that length, so no\n * part of a secret survives at the edge. A redirect is refused rather than\n * followed: the credential headers would go with it to wherever it points.\n */\nexport async function post(provider: string, url: string, init: { headers: Record<string, string>; body: string }, secrets: (string | undefined)[] = []): Promise<Response> {\n const response = await fetch(postable(url), { method: \"POST\", headers: init.headers, body: init.body, redirect: \"error\", signal: AbortSignal.timeout(TIMEOUT_MS) });\n if (!response.ok) {\n let text = \"\";\n try {\n text = await response.text();\n } catch {\n // The status is enough.\n }\n throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : \"\"}`);\n }\n return response;\n}\n\n/** The start of an error body, secrets cut out first, then cut to ERROR_BODY_MAX on a code point. */\nexport function errorBody(text: string, secrets: (string | undefined)[] = []): string {\n const kept = secrets.filter((s): s is string => typeof s === \"string\" && s.length >= 4);\n const longest = kept.reduce((n, s) => Math.max(n, s.length), 0);\n let head = cut(text, ERROR_BODY_MAX + longest);\n for (const secret of kept) head = head.split(secret).join(\"[redacted]\");\n return cut(head, ERROR_BODY_MAX);\n}\n\n/** A credential with the spaces and newlines a paste leaves around it taken off. Anything not a string is empty. */\nexport function trimmed(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Base64 of UTF-8, without Buffer. */\nexport function base64(text: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nexport function basicAuth(user: string, password: string): string {\n return `Basic ${base64(`${user}:${password}`)}`;\n}\n\nexport function hex(bytes: ArrayBuffer | Uint8Array): string {\n return Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport async function sha256Hex(text: string): Promise<string> {\n return hex(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text)));\n}\n\n/**\n * A stable 32 hex character id for one alert: the same job, type and time\n * always give the same id, so a provider that deduplicates on it drops a\n * resend of an alert it already took.\n */\nexport async function alertId(alert: Alert): Promise<string> {\n return (await sha256Hex(`${alert.job}\\n${alert.type}\\n${alert.at}`)).slice(0, 32);\n}\n\n/** The same id laid out as a UUID, for APIs that ask for one. */\nexport function asUuid(id: string): string {\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20, 32)}`;\n}\n\n/** Cuts to at most `max` UTF-16 units without splitting a surrogate pair. */\nexport function cut(text: string, max: number): string {\n if (text.length <= max) return text;\n let end = max;\n const code = text.charCodeAt(end - 1);\n if (code >= 0xd800 && code <= 0xdbff) end -= 1;\n return text.slice(0, end);\n}\n\n/** The run fields worth attaching to a tracker event. A start before the year 1 or after 9999 is null. */\nexport function runSummary(alert: Alert): Record<string, unknown> | null {\n const run = alert.run;\n if (!run) return null;\n return { id: run.id, status: run.status, startedAt: isoTime(run.startedAt), durationMs: run.durationMs, trigger: run.trigger };\n}\n\n/** Title, message, triage and link as one plain text block, the way every channel reads. */\nexport function plainText(alert: Alert, link: string | undefined): string {\n return [alert.title, \"\", alert.message, ...(alert.triage ? [\"\", `Triage: ${alert.triage}`] : []), ...(link ? [\"\", `Open: ${link}`] : [])].join(\"\\n\");\n}\n","/**\n * New Relic Event API. API reference: https://docs.newrelic.com/docs/data-apis/ingest-apis/event-api/introduction-event-api/\n * POST https://insights-collector.newrelic.com/v1/accounts/<id>/events\n * (insights-collector.eu01.nr-data.net for EU accounts) with Api-Key.\n * Each alert is one custom event of type CronWatchAlert, queryable with NRQL:\n * SELECT * FROM CronWatchAlert WHERE job = 'nightly'.\n */\nimport type { Alert, AlertChannel } from \"../types.js\";\nimport { cut, post, severity, trimmed } from \"./shared.js\";\n\nexport interface NewRelicOptions {\n /** The account id, the number in your New Relic URLs. */\n accountId: string | number;\n /** A license key (INGEST - LICENSE). */\n apiKey: string;\n /** \"eu\" for an account in the EU data center. Defaults to \"us\". */\n region?: \"us\" | \"eu\";\n /** Defaults to \"CronWatchAlert\". */\n eventType?: string;\n link?: (alert: Alert) => string;\n}\n\n/** Records alerts as New Relic custom events. */\nexport function newrelic(options: NewRelicOptions): AlertChannel {\n // A pasted credential often carries a stray space or newline, which a header would refuse or send.\n const apiKey = trimmed(options.apiKey);\n if (!apiKey) throw new Error(\"newrelic() needs an apiKey\");\n const account = String(options.accountId ?? \"\");\n if (!/^\\d+$/.test(account)) throw new Error(\"newrelic() needs a numeric accountId\");\n const host = options.region === \"eu\" ? \"https://insights-collector.eu01.nr-data.net\" : \"https://insights-collector.newrelic.com\";\n const url = `${host}/v1/accounts/${account}/events`;\n const eventType = options.eventType ?? \"CronWatchAlert\";\n return {\n name: \"newrelic\",\n async send(alert) {\n const link = options.link?.(alert);\n const run = alert.run;\n // Flat attributes only, strings under 4096 characters.\n const event: Record<string, string | number> = {\n eventType,\n timestamp: alert.at,\n job: cut(alert.job, 4095),\n alertType: alert.type,\n severity: severity(alert.type),\n title: cut(alert.title, 4095),\n message: cut(alert.message, 4095),\n };\n if (alert.triage) event.triage = cut(alert.triage, 4095);\n if (link) event.link = cut(link, 4095);\n if (run) {\n event.runId = run.id;\n event.runStatus = run.status;\n if (run.durationMs !== null) event.durationMs = run.durationMs;\n }\n await post(\"New Relic\", url, {\n headers: { \"content-type\": \"application/json\", \"api-key\": apiKey },\n body: JSON.stringify([event]),\n }, [apiKey]);\n },\n };\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { A as Alert, a as AlertChannel } from './types-BjBbCGeJ.cjs';
1
+ import { A as Alert, a as AlertChannel } from './types-Cz47Boks.cjs';
2
2
 
3
3
  /**
4
4
  * New Relic Event API. API reference: https://docs.newrelic.com/docs/data-apis/ingest-apis/event-api/introduction-event-api/
@@ -1,4 +1,4 @@
1
- import { A as Alert, a as AlertChannel } from './types-BjBbCGeJ.js';
1
+ import { A as Alert, a as AlertChannel } from './types-Cz47Boks.js';
2
2
 
3
3
  /**
4
4
  * New Relic Event API. API reference: https://docs.newrelic.com/docs/data-apis/ingest-apis/event-api/introduction-event-api/
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/newrelic.ts"],"names":[],"mappings":"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","file":"newrelic.js","sourcesContent":["/**\n * Helpers shared by the provider channels. Only fetch and Web Crypto, so the\n * channels run on Node, Cloudflare Workers, Deno and Bun alike. Not an entry\n * point: each channel bundles its own copy.\n */\nimport type { Alert } from \"../types.js\";\n\nexport const TIMEOUT_MS = 10_000;\n\n/** Severity for trackers that have levels. Recovered is informational. */\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport function severity(type: Alert[\"type\"]): Severity {\n if (type === \"recovered\") return \"info\";\n if (type === \"slow\" || type === \"over_budget\") return \"warning\";\n return \"error\";\n}\n\n/** The scheme, host and port only. A URL's path or query can hold a credential. */\nexport function origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"(invalid URL)\";\n }\n}\n\n/**\n * The URL, once it is one fetch can post to. Refused without quoting it:\n * fetch's own error for a URL it cannot parse (\"Failed to parse URL from ...\")\n * quotes the whole URL, and a webhook URL's path is its credential.\n */\nexport function postable(url: string): string {\n let protocol: string;\n try {\n protocol = new URL(url).protocol;\n } catch {\n throw new Error(\"only http and https URLs can be posted to, not this URL\");\n }\n if (protocol !== \"http:\" && protocol !== \"https:\") throw new Error(`only http and https URLs can be posted to, not ${protocol}`);\n return url;\n}\n\n/** How much of a provider's error body goes into the error message. */\nexport const ERROR_BODY_MAX = 200;\n\n/**\n * POSTs and throws on a non-2xx answer. The error names the provider and the\n * URL's origin, plus the start of the response body with every secret the\n * channel holds cut out, in case a provider echoes one back. Secrets are cut\n * from a prefix long enough to hold one that starts inside the first\n * ERROR_BODY_MAX characters, and only then is it cut to that length, so no\n * part of a secret survives at the edge. A redirect is refused rather than\n * followed: the credential headers would go with it to wherever it points.\n */\nexport async function post(provider: string, url: string, init: { headers: Record<string, string>; body: string }, secrets: (string | undefined)[] = []): Promise<Response> {\n const response = await fetch(postable(url), { method: \"POST\", headers: init.headers, body: init.body, redirect: \"error\", signal: AbortSignal.timeout(TIMEOUT_MS) });\n if (!response.ok) {\n let text = \"\";\n try {\n text = await response.text();\n } catch {\n // The status is enough.\n }\n throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : \"\"}`);\n }\n return response;\n}\n\n/** The start of an error body, secrets cut out first, then cut to ERROR_BODY_MAX on a code point. */\nexport function errorBody(text: string, secrets: (string | undefined)[] = []): string {\n const kept = secrets.filter((s): s is string => typeof s === \"string\" && s.length >= 4);\n const longest = kept.reduce((n, s) => Math.max(n, s.length), 0);\n let head = cut(text, ERROR_BODY_MAX + longest);\n for (const secret of kept) head = head.split(secret).join(\"[redacted]\");\n return cut(head, ERROR_BODY_MAX);\n}\n\n/** A credential with the spaces and newlines a paste leaves around it taken off. Anything not a string is empty. */\nexport function trimmed(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Base64 of UTF-8, without Buffer. */\nexport function base64(text: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nexport function basicAuth(user: string, password: string): string {\n return `Basic ${base64(`${user}:${password}`)}`;\n}\n\nexport function hex(bytes: ArrayBuffer | Uint8Array): string {\n return Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport async function sha256Hex(text: string): Promise<string> {\n return hex(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text)));\n}\n\n/**\n * A stable 32 hex character id for one alert: the same job, type and time\n * always give the same id, so a provider that deduplicates on it drops a\n * resend of an alert it already took.\n */\nexport async function alertId(alert: Alert): Promise<string> {\n return (await sha256Hex(`${alert.job}\\n${alert.type}\\n${alert.at}`)).slice(0, 32);\n}\n\n/** The same id laid out as a UUID, for APIs that ask for one. */\nexport function asUuid(id: string): string {\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20, 32)}`;\n}\n\n/** Cuts to at most `max` UTF-16 units without splitting a surrogate pair. */\nexport function cut(text: string, max: number): string {\n if (text.length <= max) return text;\n let end = max;\n const code = text.charCodeAt(end - 1);\n if (code >= 0xd800 && code <= 0xdbff) end -= 1;\n return text.slice(0, end);\n}\n\n/** The run fields worth attaching to a tracker event. */\nexport function runSummary(alert: Alert): Record<string, unknown> | null {\n const run = alert.run;\n if (!run) return null;\n return { id: run.id, status: run.status, startedAt: new Date(run.startedAt).toISOString(), durationMs: run.durationMs, trigger: run.trigger };\n}\n\n/** Title, message, triage and link as one plain text block, the way every channel reads. */\nexport function plainText(alert: Alert, link: string | undefined): string {\n return [alert.title, \"\", alert.message, ...(alert.triage ? [\"\", `Triage: ${alert.triage}`] : []), ...(link ? [\"\", `Open: ${link}`] : [])].join(\"\\n\");\n}\n","/**\n * New Relic Event API. API reference: https://docs.newrelic.com/docs/data-apis/ingest-apis/event-api/introduction-event-api/\n * POST https://insights-collector.newrelic.com/v1/accounts/<id>/events\n * (insights-collector.eu01.nr-data.net for EU accounts) with Api-Key.\n * Each alert is one custom event of type CronWatchAlert, queryable with NRQL:\n * SELECT * FROM CronWatchAlert WHERE job = 'nightly'.\n */\nimport type { Alert, AlertChannel } from \"../types.js\";\nimport { cut, post, severity, trimmed } from \"./shared.js\";\n\nexport interface NewRelicOptions {\n /** The account id, the number in your New Relic URLs. */\n accountId: string | number;\n /** A license key (INGEST - LICENSE). */\n apiKey: string;\n /** \"eu\" for an account in the EU data center. Defaults to \"us\". */\n region?: \"us\" | \"eu\";\n /** Defaults to \"CronWatchAlert\". */\n eventType?: string;\n link?: (alert: Alert) => string;\n}\n\n/** Records alerts as New Relic custom events. */\nexport function newrelic(options: NewRelicOptions): AlertChannel {\n // A pasted credential often carries a stray space or newline, which a header would refuse or send.\n const apiKey = trimmed(options.apiKey);\n if (!apiKey) throw new Error(\"newrelic() needs an apiKey\");\n const account = String(options.accountId ?? \"\");\n if (!/^\\d+$/.test(account)) throw new Error(\"newrelic() needs a numeric accountId\");\n const host = options.region === \"eu\" ? \"https://insights-collector.eu01.nr-data.net\" : \"https://insights-collector.newrelic.com\";\n const url = `${host}/v1/accounts/${account}/events`;\n const eventType = options.eventType ?? \"CronWatchAlert\";\n return {\n name: \"newrelic\",\n async send(alert) {\n const link = options.link?.(alert);\n const run = alert.run;\n // Flat attributes only, strings under 4096 characters.\n const event: Record<string, string | number> = {\n eventType,\n timestamp: alert.at,\n job: cut(alert.job, 4095),\n alertType: alert.type,\n severity: severity(alert.type),\n title: cut(alert.title, 4095),\n message: cut(alert.message, 4095),\n };\n if (alert.triage) event.triage = cut(alert.triage, 4095);\n if (link) event.link = cut(link, 4095);\n if (run) {\n event.runId = run.id;\n event.runStatus = run.status;\n if (run.durationMs !== null) event.durationMs = run.durationMs;\n }\n await post(\"New Relic\", url, {\n headers: { \"content-type\": \"application/json\", \"api-key\": apiKey },\n body: JSON.stringify([event]),\n }, [apiKey]);\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/newrelic.ts"],"names":[],"mappings":"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","file":"newrelic.js","sourcesContent":["/**\n * Helpers shared by the provider channels. Only fetch and Web Crypto, so the\n * channels run on Node, Cloudflare Workers, Deno and Bun alike. Not an entry\n * point: each channel bundles its own copy.\n */\nimport { isoTime } from \"../duration.js\";\nimport type { Alert } from \"../types.js\";\n\nexport const TIMEOUT_MS = 10_000;\n\n/** Severity for trackers that have levels. Recovered is informational. */\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport function severity(type: Alert[\"type\"]): Severity {\n if (type === \"recovered\") return \"info\";\n if (type === \"slow\" || type === \"over_budget\") return \"warning\";\n return \"error\";\n}\n\n/** The scheme, host and port only. A URL's path or query can hold a credential. */\nexport function origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"(invalid URL)\";\n }\n}\n\n/**\n * The URL, once it is one fetch can post to. Refused without quoting it:\n * fetch's own error for a URL it cannot parse (\"Failed to parse URL from ...\")\n * quotes the whole URL, and a webhook URL's path is its credential.\n */\nexport function postable(url: string): string {\n let protocol: string;\n try {\n protocol = new URL(url).protocol;\n } catch {\n throw new Error(\"only http and https URLs can be posted to, not this URL\");\n }\n if (protocol !== \"http:\" && protocol !== \"https:\") throw new Error(`only http and https URLs can be posted to, not ${protocol}`);\n return url;\n}\n\n/** How much of a provider's error body goes into the error message. */\nexport const ERROR_BODY_MAX = 200;\n\n/**\n * POSTs and throws on a non-2xx answer. The error names the provider and the\n * URL's origin, plus the start of the response body with every secret the\n * channel holds cut out, in case a provider echoes one back. Secrets are cut\n * from a prefix long enough to hold one that starts inside the first\n * ERROR_BODY_MAX characters, and only then is it cut to that length, so no\n * part of a secret survives at the edge. A redirect is refused rather than\n * followed: the credential headers would go with it to wherever it points.\n */\nexport async function post(provider: string, url: string, init: { headers: Record<string, string>; body: string }, secrets: (string | undefined)[] = []): Promise<Response> {\n const response = await fetch(postable(url), { method: \"POST\", headers: init.headers, body: init.body, redirect: \"error\", signal: AbortSignal.timeout(TIMEOUT_MS) });\n if (!response.ok) {\n let text = \"\";\n try {\n text = await response.text();\n } catch {\n // The status is enough.\n }\n throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : \"\"}`);\n }\n return response;\n}\n\n/** The start of an error body, secrets cut out first, then cut to ERROR_BODY_MAX on a code point. */\nexport function errorBody(text: string, secrets: (string | undefined)[] = []): string {\n const kept = secrets.filter((s): s is string => typeof s === \"string\" && s.length >= 4);\n const longest = kept.reduce((n, s) => Math.max(n, s.length), 0);\n let head = cut(text, ERROR_BODY_MAX + longest);\n for (const secret of kept) head = head.split(secret).join(\"[redacted]\");\n return cut(head, ERROR_BODY_MAX);\n}\n\n/** A credential with the spaces and newlines a paste leaves around it taken off. Anything not a string is empty. */\nexport function trimmed(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Base64 of UTF-8, without Buffer. */\nexport function base64(text: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nexport function basicAuth(user: string, password: string): string {\n return `Basic ${base64(`${user}:${password}`)}`;\n}\n\nexport function hex(bytes: ArrayBuffer | Uint8Array): string {\n return Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport async function sha256Hex(text: string): Promise<string> {\n return hex(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text)));\n}\n\n/**\n * A stable 32 hex character id for one alert: the same job, type and time\n * always give the same id, so a provider that deduplicates on it drops a\n * resend of an alert it already took.\n */\nexport async function alertId(alert: Alert): Promise<string> {\n return (await sha256Hex(`${alert.job}\\n${alert.type}\\n${alert.at}`)).slice(0, 32);\n}\n\n/** The same id laid out as a UUID, for APIs that ask for one. */\nexport function asUuid(id: string): string {\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20, 32)}`;\n}\n\n/** Cuts to at most `max` UTF-16 units without splitting a surrogate pair. */\nexport function cut(text: string, max: number): string {\n if (text.length <= max) return text;\n let end = max;\n const code = text.charCodeAt(end - 1);\n if (code >= 0xd800 && code <= 0xdbff) end -= 1;\n return text.slice(0, end);\n}\n\n/** The run fields worth attaching to a tracker event. A start before the year 1 or after 9999 is null. */\nexport function runSummary(alert: Alert): Record<string, unknown> | null {\n const run = alert.run;\n if (!run) return null;\n return { id: run.id, status: run.status, startedAt: isoTime(run.startedAt), durationMs: run.durationMs, trigger: run.trigger };\n}\n\n/** Title, message, triage and link as one plain text block, the way every channel reads. */\nexport function plainText(alert: Alert, link: string | undefined): string {\n return [alert.title, \"\", alert.message, ...(alert.triage ? [\"\", `Triage: ${alert.triage}`] : []), ...(link ? [\"\", `Open: ${link}`] : [])].join(\"\\n\");\n}\n","/**\n * New Relic Event API. API reference: https://docs.newrelic.com/docs/data-apis/ingest-apis/event-api/introduction-event-api/\n * POST https://insights-collector.newrelic.com/v1/accounts/<id>/events\n * (insights-collector.eu01.nr-data.net for EU accounts) with Api-Key.\n * Each alert is one custom event of type CronWatchAlert, queryable with NRQL:\n * SELECT * FROM CronWatchAlert WHERE job = 'nightly'.\n */\nimport type { Alert, AlertChannel } from \"../types.js\";\nimport { cut, post, severity, trimmed } from \"./shared.js\";\n\nexport interface NewRelicOptions {\n /** The account id, the number in your New Relic URLs. */\n accountId: string | number;\n /** A license key (INGEST - LICENSE). */\n apiKey: string;\n /** \"eu\" for an account in the EU data center. Defaults to \"us\". */\n region?: \"us\" | \"eu\";\n /** Defaults to \"CronWatchAlert\". */\n eventType?: string;\n link?: (alert: Alert) => string;\n}\n\n/** Records alerts as New Relic custom events. */\nexport function newrelic(options: NewRelicOptions): AlertChannel {\n // A pasted credential often carries a stray space or newline, which a header would refuse or send.\n const apiKey = trimmed(options.apiKey);\n if (!apiKey) throw new Error(\"newrelic() needs an apiKey\");\n const account = String(options.accountId ?? \"\");\n if (!/^\\d+$/.test(account)) throw new Error(\"newrelic() needs a numeric accountId\");\n const host = options.region === \"eu\" ? \"https://insights-collector.eu01.nr-data.net\" : \"https://insights-collector.newrelic.com\";\n const url = `${host}/v1/accounts/${account}/events`;\n const eventType = options.eventType ?? \"CronWatchAlert\";\n return {\n name: \"newrelic\",\n async send(alert) {\n const link = options.link?.(alert);\n const run = alert.run;\n // Flat attributes only, strings under 4096 characters.\n const event: Record<string, string | number> = {\n eventType,\n timestamp: alert.at,\n job: cut(alert.job, 4095),\n alertType: alert.type,\n severity: severity(alert.type),\n title: cut(alert.title, 4095),\n message: cut(alert.message, 4095),\n };\n if (alert.triage) event.triage = cut(alert.triage, 4095);\n if (link) event.link = cut(link, 4095);\n if (run) {\n event.runId = run.id;\n event.runStatus = run.status;\n if (run.durationMs !== null) event.durationMs = run.durationMs;\n }\n await post(\"New Relic\", url, {\n headers: { \"content-type\": \"application/json\", \"api-key\": apiKey },\n body: JSON.stringify([event]),\n }, [apiKey]);\n },\n };\n}\n"]}
package/dist/pg-cron.cjs CHANGED
@@ -1,5 +1,12 @@
1
1
  'use strict';
2
2
 
3
+ // src/evaluate.ts
4
+ var MAX_DURATION_MS = Number.MAX_SAFE_INTEGER;
5
+ function runDuration(startedAt, finishedAt) {
6
+ const ms = finishedAt - startedAt;
7
+ return ms > 0 ? Math.min(ms, MAX_DURATION_MS) : 0;
8
+ }
9
+
3
10
  // src/sources/pgcron.ts
4
11
  var BACKFILL = 20;
5
12
  var PAGE = 500;
@@ -43,7 +50,7 @@ function pgCronRun(row, job, idPrefix, fallbackAt = Date.now()) {
43
50
  status,
44
51
  startedAt,
45
52
  finishedAt: end,
46
- durationMs: end === null ? null : end - startedAt,
53
+ durationMs: end === null ? null : runDuration(startedAt, end),
47
54
  error: status === "failed" ? message ?? "pg_cron reported the run as failed" : null,
48
55
  output: status === "ok" ? message : null,
49
56
  metrics: {},
@@ -75,6 +82,7 @@ function pgCron(db, options = {}) {
75
82
  const retired = /* @__PURE__ */ new Set();
76
83
  let scanned = false;
77
84
  const warned = /* @__PURE__ */ new Set();
85
+ const failing = /* @__PURE__ */ new Set();
78
86
  const warnOnce = (host, key, message) => {
79
87
  if (warned.has(key)) return;
80
88
  warned.add(key);
@@ -128,15 +136,55 @@ function pgCron(db, options = {}) {
128
136
  warnOnce(host, "empty", "cron.job shows no jobs. pg_cron's row level security shows a role only the jobs it scheduled: connect as that role, or give this one BYPASSRLS.");
129
137
  }
130
138
  const all = rows.map((r) => ({ ...r, jobid: Number(r.jobid) }));
131
- const jobs = all.filter(picks);
132
139
  const names = /* @__PURE__ */ new Map();
133
140
  const definitions = /* @__PURE__ */ new Map();
134
141
  const used = /* @__PURE__ */ new Set();
135
- for (const job of jobs) {
136
- let name = prefix + (options.jobName ? options.jobName(job) : pgCronJobName(job));
142
+ const trouble = (job, what) => {
143
+ if (!failing.has(job.jobid)) {
144
+ failing.add(job.jobid);
145
+ host.onError(new Error(`pg_cron job ${job.jobid}: ${what}; it keeps its last declaration until that works`), "source pg_cron");
146
+ }
147
+ const last = known.get(job.jobid);
148
+ if (!last || used.has(last.name)) return;
149
+ names.set(job.jobid, last.name);
150
+ definitions.set(job.jobid, last.definition);
151
+ used.add(last.name);
152
+ };
153
+ const threw = (e) => e instanceof Error ? `${e.name}: ${e.message}` : String(e);
154
+ for (const job of all) {
155
+ let picked;
156
+ try {
157
+ picked = picks(job);
158
+ } catch (e) {
159
+ trouble(job, `the jobs callback threw ${threw(e)}`);
160
+ continue;
161
+ }
162
+ if (!picked) {
163
+ failing.delete(job.jobid);
164
+ continue;
165
+ }
166
+ let base;
167
+ let extra;
168
+ try {
169
+ base = options.jobName ? options.jobName(job) : pgCronJobName(job);
170
+ } catch (e) {
171
+ trouble(job, `jobName threw ${threw(e)}`);
172
+ continue;
173
+ }
174
+ if (typeof base !== "string") {
175
+ trouble(job, `jobName returned ${base === null ? "null" : typeof base}, not a name`);
176
+ continue;
177
+ }
178
+ try {
179
+ extra = typeof options.options === "function" ? options.options(job) : options.options ?? {};
180
+ } catch (e) {
181
+ trouble(job, `the options callback threw ${threw(e)}`);
182
+ continue;
183
+ }
184
+ failing.delete(job.jobid);
185
+ let name = prefix + base;
137
186
  if (used.has(name)) name = `${name}:${job.jobid}`;
138
187
  used.add(name);
139
- const extra = typeof options.options === "function" ? options.options(job) : options.options ?? {};
140
188
  const schedule = job.active && recording ? pgCronSchedule(job.schedule) : null;
141
189
  let definition = {
142
190
  description: `pg_cron job ${job.jobid} in ${job.database} as ${job.username}${job.active ? "" : " (paused)"}`,