@cronwatch/sdk 0.12.3 → 0.12.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +3 -3
- package/dist/anthropic.cjs +2 -2
- package/dist/anthropic.cjs.map +1 -1
- package/dist/anthropic.d.cts +1 -1
- package/dist/anthropic.d.ts +1 -1
- package/dist/anthropic.js +2 -2
- package/dist/anthropic.js.map +1 -1
- package/dist/bugsnag.cjs.map +1 -1
- package/dist/bugsnag.d.cts +1 -1
- package/dist/bugsnag.d.ts +1 -1
- package/dist/bugsnag.js.map +1 -1
- package/dist/{client-DySsV5T0.d.ts → client-CFJUFEOg.d.cts} +14 -14
- package/dist/{client-BkPMRkDq.d.cts → client-k5UVIH38.d.ts} +14 -14
- package/dist/d1.cjs +1 -1
- package/dist/d1.cjs.map +1 -1
- package/dist/d1.d.cts +4 -4
- package/dist/d1.d.ts +4 -4
- package/dist/d1.js +1 -1
- package/dist/d1.js.map +1 -1
- package/dist/datadog.cjs.map +1 -1
- package/dist/datadog.d.cts +2 -2
- package/dist/datadog.d.ts +2 -2
- package/dist/datadog.js.map +1 -1
- package/dist/discord.cjs.map +1 -1
- package/dist/discord.d.cts +5 -5
- package/dist/discord.d.ts +5 -5
- package/dist/discord.js.map +1 -1
- package/dist/{email-Bx3ehK0O.d.ts → email-CPoxD8--.d.ts} +3 -3
- package/dist/{email-CD6NO8CV.d.cts → email-U0hYRnPa.d.cts} +3 -3
- package/dist/honeybadger.cjs.map +1 -1
- package/dist/honeybadger.d.cts +1 -1
- package/dist/honeybadger.d.ts +1 -1
- package/dist/honeybadger.js.map +1 -1
- package/dist/index.cjs +17 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +17 -13
- package/dist/index.js.map +1 -1
- package/dist/mailgun.cjs.map +1 -1
- package/dist/mailgun.d.cts +2 -2
- package/dist/mailgun.d.ts +2 -2
- package/dist/mailgun.js.map +1 -1
- package/dist/newrelic.cjs.map +1 -1
- package/dist/newrelic.d.cts +1 -1
- package/dist/newrelic.d.ts +1 -1
- package/dist/newrelic.js.map +1 -1
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +3 -3
- package/dist/node.d.ts +3 -3
- package/dist/node.js.map +1 -1
- package/dist/pg-cron.cjs.map +1 -1
- package/dist/pg-cron.d.cts +10 -10
- package/dist/pg-cron.d.ts +10 -10
- package/dist/pg-cron.js.map +1 -1
- package/dist/postgres.cjs +1 -1
- package/dist/postgres.cjs.map +1 -1
- package/dist/postgres.d.cts +3 -3
- package/dist/postgres.d.ts +3 -3
- package/dist/postgres.js +1 -1
- package/dist/postgres.js.map +1 -1
- package/dist/postmark.cjs.map +1 -1
- package/dist/postmark.d.cts +2 -2
- package/dist/postmark.d.ts +2 -2
- package/dist/postmark.js.map +1 -1
- package/dist/resend.cjs.map +1 -1
- package/dist/resend.d.cts +2 -2
- package/dist/resend.d.ts +2 -2
- package/dist/resend.js.map +1 -1
- package/dist/rollbar.cjs.map +1 -1
- package/dist/rollbar.d.cts +1 -1
- package/dist/rollbar.d.ts +1 -1
- package/dist/rollbar.js.map +1 -1
- package/dist/sendgrid.cjs.map +1 -1
- package/dist/sendgrid.d.cts +2 -2
- package/dist/sendgrid.d.ts +2 -2
- package/dist/sendgrid.js.map +1 -1
- package/dist/sentry.cjs.map +1 -1
- package/dist/sentry.d.cts +2 -2
- package/dist/sentry.d.ts +2 -2
- package/dist/sentry.js.map +1 -1
- package/dist/ses.cjs.map +1 -1
- package/dist/ses.d.cts +2 -2
- package/dist/ses.d.ts +2 -2
- package/dist/ses.js.map +1 -1
- package/dist/slack.cjs.map +1 -1
- package/dist/slack.d.cts +1 -1
- package/dist/slack.d.ts +1 -1
- package/dist/slack.js.map +1 -1
- package/dist/sqlite.cjs +1 -1
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +2 -2
- package/dist/sqlite.d.ts +2 -2
- package/dist/sqlite.js +1 -1
- package/dist/sqlite.js.map +1 -1
- package/dist/twilio.cjs.map +1 -1
- package/dist/twilio.d.cts +5 -5
- package/dist/twilio.d.ts +5 -5
- package/dist/twilio.js.map +1 -1
- package/dist/{types-Bm972Uqj.d.cts → types-V8SN54o9.d.cts} +7 -7
- package/dist/{types-Bm972Uqj.d.ts → types-V8SN54o9.d.ts} +7 -7
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.d.cts +2 -2
- package/dist/webhook.d.ts +2 -2
- package/dist/webhook.js.map +1 -1
- package/package.json +3 -3
package/dist/mailgun.cjs.map
CHANGED
|
@@ -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 { 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\" || type === \"under_floor\") 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, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\").replace(/'/g, \"'\");\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\" || type === \"under_floor\") 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, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\").replace(/'/g, \"'\");\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"]}
|
package/dist/mailgun.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as AlertChannel } from './types-
|
|
2
|
-
import { E as EmailOptions } from './email-
|
|
1
|
+
import { a as AlertChannel } from './types-V8SN54o9.cjs';
|
|
2
|
+
import { E as EmailOptions } from './email-U0hYRnPa.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-
|
|
2
|
-
import { E as EmailOptions } from './email-
|
|
1
|
+
import { a as AlertChannel } from './types-V8SN54o9.js';
|
|
2
|
+
import { E as EmailOptions } from './email-CPoxD8--.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
|
package/dist/mailgun.js.map
CHANGED
|
@@ -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 { 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\" || type === \"under_floor\") 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, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\").replace(/'/g, \"'\");\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\" || type === \"under_floor\") 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, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\").replace(/'/g, \"'\");\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"]}
|
package/dist/newrelic.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/newrelic.ts"],"names":[],"mappings":";;;AAQO,IAAM,UAAA,GAAa,GAAA;AAKnB,SAAS,SAAS,IAAA,EAA+B;AACtD,EAAA,IAAI,IAAA,KAAS,aAAa,OAAO,MAAA;AACjC,EAAA,IAAI,SAAS,MAAA,IAAU,IAAA,KAAS,aAAA,IAAiB,IAAA,KAAS,eAAe,OAAO,SAAA;AAChF,EAAA,OAAO,OAAA;AACT;AAGO,SAAS,OAAO,GAAA,EAAqB;AAC1C,EAAA,IAAI;AACF,IAAA,OAAO,IAAI,GAAA,CAAI,GAAG,CAAA,CAAE,MAAA;AAAA,EACtB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,eAAA;AAAA,EACT;AACF;AAOO,SAAS,SAAS,GAAA,EAAqB;AAC5C,EAAA,IAAI,QAAA;AACJ,EAAA,IAAI;AACF,IAAA,QAAA,GAAW,IAAI,GAAA,CAAI,GAAG,CAAA,CAAE,QAAA;AAAA,EAC1B,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,MAAM,yDAAyD,CAAA;AAAA,EAC3E;AACA,EAAA,IAAI,QAAA,KAAa,WAAW,QAAA,KAAa,QAAA,QAAgB,IAAI,KAAA,CAAM,CAAA,+CAAA,EAAkD,QAAQ,CAAA,CAAE,CAAA;AAC/H,EAAA,OAAO,GAAA;AACT;AAGO,IAAM,cAAA,GAAiB,GAAA;AAW9B,eAAsB,KAAK,QAAA,EAAkB,GAAA,EAAa,IAAA,EAAyD,OAAA,GAAkC,EAAC,EAAsB;AAC1K,EAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,QAAA,CAAS,GAAG,CAAA,EAAG,EAAE,MAAA,EAAQ,MAAA,EAAQ,OAAA,EAAS,IAAA,CAAK,SAAS,IAAA,EAAM,IAAA,CAAK,MAAM,QAAA,EAAU,OAAA,EAAS,QAAQ,WAAA,CAAY,OAAA,CAAQ,UAAU,CAAA,EAAG,CAAA;AAClK,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,IAAI,IAAA,GAAO,EAAA;AACX,IAAA,IAAI;AACF,MAAA,IAAA,GAAO,MAAM,SAAS,IAAA,EAAK;AAAA,IAC7B,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,MAAM,IAAI,MAAM,CAAA,EAAG,QAAQ,IAAI,MAAA,CAAO,GAAG,CAAC,CAAA,UAAA,EAAa,QAAA,CAAS,MAAM,CAAA,EAAG,IAAA,GAAO,KAAK,SAAA,CAAU,IAAA,EAAM,OAAO,CAAC,CAAA,CAAA,GAAK,EAAE,CAAA,CAAE,CAAA;AAAA,EACxH;AACA,EAAA,OAAO,QAAA;AACT;AAGO,SAAS,SAAA,CAAU,IAAA,EAAc,OAAA,GAAkC,EAAC,EAAW;AACpF,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAmB,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,CAAE,MAAA,IAAU,CAAC,CAAA;AACtF,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,EAAG,CAAA,KAAM,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,CAAA,CAAE,MAAM,CAAA,EAAG,CAAC,CAAA;AAC9D,EAAA,IAAI,IAAA,GAAO,GAAA,CAAI,IAAA,EAAM,cAAA,GAAiB,OAAO,CAAA;AAC7C,EAAA,KAAA,MAAW,MAAA,IAAU,MAAM,IAAA,GAAO,IAAA,CAAK,MAAM,MAAM,CAAA,CAAE,KAAK,YAAY,CAAA;AACtE,EAAA,OAAO,GAAA,CAAI,MAAM,cAAc,CAAA;AACjC;AAGO,SAAS,QAAQ,KAAA,EAAwB;AAC9C,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,GAAW,KAAA,CAAM,MAAK,GAAI,EAAA;AACpD;AAoCO,SAAS,GAAA,CAAI,MAAc,GAAA,EAAqB;AACrD,EAAA,IAAI,IAAA,CAAK,MAAA,IAAU,GAAA,EAAK,OAAO,IAAA;AAC/B,EAAA,IAAI,GAAA,GAAM,GAAA;AACV,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,UAAA,CAAW,GAAA,GAAM,CAAC,CAAA;AACpC,EAAA,IAAI,IAAA,IAAQ,KAAA,IAAU,IAAA,IAAQ,KAAA,EAAQ,GAAA,IAAO,CAAA;AAC7C,EAAA,OAAO,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA;AAC1B;;;ACrGO,SAAS,SAAS,OAAA,EAAwC;AAE/D,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,MAAM,CAAA;AACrC,EAAA,IAAI,CAAC,MAAA,EAAQ,MAAM,IAAI,MAAM,4BAA4B,CAAA;AACzD,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,SAAA,IAAa,EAAE,CAAA;AAC9C,EAAA,IAAI,CAAC,QAAQ,IAAA,CAAK,OAAO,GAAG,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAClF,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,MAAA,KAAW,IAAA,GAAO,6CAAA,GAAgD,yCAAA;AACvF,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,IAAI,CAAA,aAAA,EAAgB,OAAO,CAAA,OAAA,CAAA;AAC1C,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,gBAAA;AACvC,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,UAAA;AAAA,IACN,MAAM,KAAK,KAAA,EAAO;AAChB,MAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,IAAA,GAAO,KAAK,CAAA;AACjC,MAAA,MAAM,MAAM,KAAA,CAAM,GAAA;AAElB,MAAA,MAAM,KAAA,GAAyC;AAAA,QAC7C,SAAA;AAAA,QACA,WAAW,KAAA,CAAM,EAAA;AAAA,QACjB,GAAA,EAAK,GAAA,CAAI,KAAA,CAAM,GAAA,EAAK,IAAI,CAAA;AAAA,QACxB,WAAW,KAAA,CAAM,IAAA;AAAA,QACjB,QAAA,EAAU,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA;AAAA,QAC7B,KAAA,EAAO,GAAA,CAAI,KAAA,CAAM,KAAA,EAAO,IAAI,CAAA;AAAA,QAC5B,OAAA,EAAS,GAAA,CAAI,KAAA,CAAM,OAAA,EAAS,IAAI;AAAA,OAClC;AACA,MAAA,IAAI,MAAM,MAAA,EAAQ,KAAA,CAAM,SAAS,GAAA,CAAI,KAAA,CAAM,QAAQ,IAAI,CAAA;AACvD,MAAA,IAAI,IAAA,EAAM,KAAA,CAAM,IAAA,GAAO,GAAA,CAAI,MAAM,IAAI,CAAA;AACrC,MAAA,IAAI,GAAA,EAAK;AACP,QAAA,KAAA,CAAM,QAAQ,GAAA,CAAI,EAAA;AAClB,QAAA,KAAA,CAAM,YAAY,GAAA,CAAI,MAAA;AACtB,QAAA,IAAI,GAAA,CAAI,UAAA,KAAe,IAAA,EAAM,KAAA,CAAM,aAAa,GAAA,CAAI,UAAA;AAAA,MACtD;AACA,MAAA,MAAM,IAAA,CAAK,aAAa,GAAA,EAAK;AAAA,QAC3B,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAoB,WAAW,MAAA,EAAO;AAAA,QACjE,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,CAAC,KAAK,CAAC;AAAA,OAC9B,EAAG,CAAC,MAAM,CAAC,CAAA;AAAA,IACb;AAAA,GACF;AACF","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\" || type === \"under_floor\") 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
|
+
{"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\" || type === \"under_floor\") 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/newrelic.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as Alert, a as AlertChannel } from './types-
|
|
1
|
+
import { A as Alert, a as AlertChannel } from './types-V8SN54o9.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/
|
package/dist/newrelic.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as Alert, a as AlertChannel } from './types-
|
|
1
|
+
import { A as Alert, a as AlertChannel } from './types-V8SN54o9.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/
|
package/dist/newrelic.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/newrelic.ts"],"names":[],"mappings":";AAQO,IAAM,UAAA,GAAa,GAAA;AAKnB,SAAS,SAAS,IAAA,EAA+B;AACtD,EAAA,IAAI,IAAA,KAAS,aAAa,OAAO,MAAA;AACjC,EAAA,IAAI,SAAS,MAAA,IAAU,IAAA,KAAS,aAAA,IAAiB,IAAA,KAAS,eAAe,OAAO,SAAA;AAChF,EAAA,OAAO,OAAA;AACT;AAGO,SAAS,OAAO,GAAA,EAAqB;AAC1C,EAAA,IAAI;AACF,IAAA,OAAO,IAAI,GAAA,CAAI,GAAG,CAAA,CAAE,MAAA;AAAA,EACtB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,eAAA;AAAA,EACT;AACF;AAOO,SAAS,SAAS,GAAA,EAAqB;AAC5C,EAAA,IAAI,QAAA;AACJ,EAAA,IAAI;AACF,IAAA,QAAA,GAAW,IAAI,GAAA,CAAI,GAAG,CAAA,CAAE,QAAA;AAAA,EAC1B,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,MAAM,yDAAyD,CAAA;AAAA,EAC3E;AACA,EAAA,IAAI,QAAA,KAAa,WAAW,QAAA,KAAa,QAAA,QAAgB,IAAI,KAAA,CAAM,CAAA,+CAAA,EAAkD,QAAQ,CAAA,CAAE,CAAA;AAC/H,EAAA,OAAO,GAAA;AACT;AAGO,IAAM,cAAA,GAAiB,GAAA;AAW9B,eAAsB,KAAK,QAAA,EAAkB,GAAA,EAAa,IAAA,EAAyD,OAAA,GAAkC,EAAC,EAAsB;AAC1K,EAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,QAAA,CAAS,GAAG,CAAA,EAAG,EAAE,MAAA,EAAQ,MAAA,EAAQ,OAAA,EAAS,IAAA,CAAK,SAAS,IAAA,EAAM,IAAA,CAAK,MAAM,QAAA,EAAU,OAAA,EAAS,QAAQ,WAAA,CAAY,OAAA,CAAQ,UAAU,CAAA,EAAG,CAAA;AAClK,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,IAAI,IAAA,GAAO,EAAA;AACX,IAAA,IAAI;AACF,MAAA,IAAA,GAAO,MAAM,SAAS,IAAA,EAAK;AAAA,IAC7B,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,MAAM,IAAI,MAAM,CAAA,EAAG,QAAQ,IAAI,MAAA,CAAO,GAAG,CAAC,CAAA,UAAA,EAAa,QAAA,CAAS,MAAM,CAAA,EAAG,IAAA,GAAO,KAAK,SAAA,CAAU,IAAA,EAAM,OAAO,CAAC,CAAA,CAAA,GAAK,EAAE,CAAA,CAAE,CAAA;AAAA,EACxH;AACA,EAAA,OAAO,QAAA;AACT;AAGO,SAAS,SAAA,CAAU,IAAA,EAAc,OAAA,GAAkC,EAAC,EAAW;AACpF,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAmB,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,CAAE,MAAA,IAAU,CAAC,CAAA;AACtF,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,EAAG,CAAA,KAAM,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,CAAA,CAAE,MAAM,CAAA,EAAG,CAAC,CAAA;AAC9D,EAAA,IAAI,IAAA,GAAO,GAAA,CAAI,IAAA,EAAM,cAAA,GAAiB,OAAO,CAAA;AAC7C,EAAA,KAAA,MAAW,MAAA,IAAU,MAAM,IAAA,GAAO,IAAA,CAAK,MAAM,MAAM,CAAA,CAAE,KAAK,YAAY,CAAA;AACtE,EAAA,OAAO,GAAA,CAAI,MAAM,cAAc,CAAA;AACjC;AAGO,SAAS,QAAQ,KAAA,EAAwB;AAC9C,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,GAAW,KAAA,CAAM,MAAK,GAAI,EAAA;AACpD;AAoCO,SAAS,GAAA,CAAI,MAAc,GAAA,EAAqB;AACrD,EAAA,IAAI,IAAA,CAAK,MAAA,IAAU,GAAA,EAAK,OAAO,IAAA;AAC/B,EAAA,IAAI,GAAA,GAAM,GAAA;AACV,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,UAAA,CAAW,GAAA,GAAM,CAAC,CAAA;AACpC,EAAA,IAAI,IAAA,IAAQ,KAAA,IAAU,IAAA,IAAQ,KAAA,EAAQ,GAAA,IAAO,CAAA;AAC7C,EAAA,OAAO,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA;AAC1B;;;ACrGO,SAAS,SAAS,OAAA,EAAwC;AAE/D,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,MAAM,CAAA;AACrC,EAAA,IAAI,CAAC,MAAA,EAAQ,MAAM,IAAI,MAAM,4BAA4B,CAAA;AACzD,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,SAAA,IAAa,EAAE,CAAA;AAC9C,EAAA,IAAI,CAAC,QAAQ,IAAA,CAAK,OAAO,GAAG,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAClF,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,MAAA,KAAW,IAAA,GAAO,6CAAA,GAAgD,yCAAA;AACvF,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,IAAI,CAAA,aAAA,EAAgB,OAAO,CAAA,OAAA,CAAA;AAC1C,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,gBAAA;AACvC,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,UAAA;AAAA,IACN,MAAM,KAAK,KAAA,EAAO;AAChB,MAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,IAAA,GAAO,KAAK,CAAA;AACjC,MAAA,MAAM,MAAM,KAAA,CAAM,GAAA;AAElB,MAAA,MAAM,KAAA,GAAyC;AAAA,QAC7C,SAAA;AAAA,QACA,WAAW,KAAA,CAAM,EAAA;AAAA,QACjB,GAAA,EAAK,GAAA,CAAI,KAAA,CAAM,GAAA,EAAK,IAAI,CAAA;AAAA,QACxB,WAAW,KAAA,CAAM,IAAA;AAAA,QACjB,QAAA,EAAU,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA;AAAA,QAC7B,KAAA,EAAO,GAAA,CAAI,KAAA,CAAM,KAAA,EAAO,IAAI,CAAA;AAAA,QAC5B,OAAA,EAAS,GAAA,CAAI,KAAA,CAAM,OAAA,EAAS,IAAI;AAAA,OAClC;AACA,MAAA,IAAI,MAAM,MAAA,EAAQ,KAAA,CAAM,SAAS,GAAA,CAAI,KAAA,CAAM,QAAQ,IAAI,CAAA;AACvD,MAAA,IAAI,IAAA,EAAM,KAAA,CAAM,IAAA,GAAO,GAAA,CAAI,MAAM,IAAI,CAAA;AACrC,MAAA,IAAI,GAAA,EAAK;AACP,QAAA,KAAA,CAAM,QAAQ,GAAA,CAAI,EAAA;AAClB,QAAA,KAAA,CAAM,YAAY,GAAA,CAAI,MAAA;AACtB,QAAA,IAAI,GAAA,CAAI,UAAA,KAAe,IAAA,EAAM,KAAA,CAAM,aAAa,GAAA,CAAI,UAAA;AAAA,MACtD;AACA,MAAA,MAAM,IAAA,CAAK,aAAa,GAAA,EAAK;AAAA,QAC3B,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAoB,WAAW,MAAA,EAAO;AAAA,QACjE,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,CAAC,KAAK,CAAC;AAAA,OAC9B,EAAG,CAAC,MAAM,CAAC,CAAA;AAAA,IACb;AAAA,GACF;AACF","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\" || type === \"under_floor\") 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
|
+
{"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\" || type === \"under_floor\") 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/node.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/node.ts"],"names":["Readable","pipeline"],"mappings":"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","file":"node.cjs","sourcesContent":["/**\n * @cronwatch/sdk/node: serve a fetch-style handler (cw.routes().handler, or\n * a job's handler()) from Node's http module and the frameworks built on it:\n * a plain http server, Express and Connect, NestJS, Koa, Strapi and Firebase\n * onRequest. The SDK core stays free of node: imports so it runs on Workers;\n * this entry point is the only one that uses Node's http and stream types.\n */\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport type { ReadableStream as NodeReadableStream } from \"node:stream/web\";\n\n/** A fetch-style handler: a Request in, a Response out. */\nexport type FetchLike = (request: Request) => Response | Promise<Response>;\n\nexport interface NodeHandlerOptions {\n /**\n * Build the request URL from X-Forwarded-Proto and X-Forwarded-Host (the\n * first value of each) when present, instead of the connection's scheme and\n * the Host header. Only behind a proxy that sets or overwrites both, since\n * a client can send them too. Default false.\n */\n trustProxy?: boolean;\n /**\n * Answer only requests at this path or under it, and pass the rest on: to\n * `next()` as Express or Connect middleware, to the next Koa middleware,\n * and with a 404 when there is no next. The path is read from\n * `req.originalUrl` (so Express mount paths count) or `req.url`. Leave it\n * out to answer every request the handler is given, which suits\n * `app.use(\"/cronwatch\", ...)`, a Nest controller and a Firebase function.\n */\n basePath?: string;\n}\n\n/** Called by Express and Connect for the next middleware, or with an error. */\nexport type NodeNext = (error?: unknown) => void;\n\n/** A handler for http.createServer, Express or Connect, Nest and Firebase onRequest. */\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse, next?: NodeNext) => Promise<void>;\n\n/** The parts of a Koa context the middleware uses, so the SDK needs no Koa types. */\nexport interface KoaContextLike {\n req: IncomingMessage;\n res: ServerResponse;\n respond?: boolean;\n /** Koa's own copy of the URL, kept when koa-mount rewrites req.url. */\n originalUrl?: string;\n /** Set by koa-bodyparser and similar, when they run before this middleware. */\n request?: { body?: unknown; rawBody?: unknown };\n}\n\n/** Where a request body may already be, when a parser has read the stream. */\nexport interface BodySource {\n /** The request target to use instead of req.originalUrl or req.url. */\n url?: string;\n /** The body's bytes as read (Firebase, Nest with rawBody: true, koa-bodyparser). */\n rawBody?: unknown;\n /** A body a parser has decoded (express.json, express.urlencoded). */\n body?: unknown;\n}\n\ntype NodeRequest = IncomingMessage & { originalUrl?: string; rawBody?: unknown; body?: unknown };\n\nfunction header(value: string | string[] | undefined): string | undefined {\n return Array.isArray(value) ? value[0] : value;\n}\n\nfunction firstValue(value: string | string[] | undefined): string | undefined {\n const first = header(value)?.split(\",\")[0]?.trim();\n return first ? first : undefined;\n}\n\n/** An origin from a scheme and host, or null when the host is not a bare host[:port]. */\nfunction originFrom(proto: string, host: string): string | null {\n try {\n const url = new URL(`${proto}://${host}`);\n if (url.pathname !== \"/\" || url.username || url.password || url.search || url.hash) return null;\n return url.origin;\n } catch {\n return null;\n }\n}\n\n/** The path and query of a request target, which may be absolute-form. */\nfunction pathAndQuery(target: string): string {\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(target)) {\n try {\n const url = new URL(target);\n return url.pathname + url.search;\n } catch {\n return \"/\";\n }\n }\n // \"//x\" must stay a path, not become a host, so it is never parsed relative to a base.\n return target.startsWith(\"/\") ? target : `/${target}`;\n}\n\nfunction requestOrigin(req: IncomingMessage, trustProxy: boolean): string {\n const socket = req.socket as { encrypted?: boolean } | undefined;\n let proto = socket?.encrypted === true ? \"https\" : \"http\";\n let host = header(req.headers.host) ?? header(req.headers[\":authority\"] as string | string[] | undefined);\n if (trustProxy) {\n const fp = firstValue(req.headers[\"x-forwarded-proto\"])?.toLowerCase();\n if (fp === \"http\" || fp === \"https\") proto = fp;\n const fh = firstValue(req.headers[\"x-forwarded-host\"]);\n if (fh && originFrom(proto, fh) !== null) host = fh;\n }\n return (host ? originFrom(proto, host) : null) ?? `${proto}://localhost`;\n}\n\n/** The stream has not been read by a body parser, so it can be passed on as is. */\nfunction unread(req: IncomingMessage): boolean {\n if (typeof req.on !== \"function\" || req.readable === false) return false;\n return !req.readableEnded && req.readableDidRead !== true;\n}\n\n/**\n * A body a parser decoded, encoded again for its content type: JSON as JSON,\n * a urlencoded form as a form. Anything else (multipart, unknown types) is\n * dropped, because it cannot be rebuilt faithfully.\n */\nfunction reencode(parsed: unknown, headers: Headers): string | Uint8Array | null {\n if (parsed === undefined || parsed === null) return null;\n if (typeof parsed === \"string\" || parsed instanceof Uint8Array) return parsed;\n if (typeof parsed !== \"object\") return null;\n const type = (headers.get(\"content-type\") ?? \"\").toLowerCase();\n let text: string | null = null;\n if (type.includes(\"application/x-www-form-urlencoded\")) {\n const form = new URLSearchParams();\n for (const [key, value] of Object.entries(parsed)) {\n for (const v of Array.isArray(value) ? value : [value]) {\n if (v !== undefined && v !== null) form.append(key, typeof v === \"object\" ? JSON.stringify(v) : String(v));\n }\n }\n text = form.toString();\n } else if (type.includes(\"json\")) {\n text = JSON.stringify(parsed);\n }\n if (text === null) return null;\n headers.delete(\"content-length\");\n headers.delete(\"transfer-encoding\");\n return text;\n}\n\n/**\n * A fetch Request for a Node request. The URL is the Host header (or the\n * forwarded host and scheme with trustProxy) plus req.originalUrl or req.url.\n * The body, for anything but GET and HEAD, is the first of: `rawBody` (from\n * the source, or req.rawBody), the request stream when nothing has read it,\n * or a body a parser decoded (req.body), re-encoded as JSON or a urlencoded\n * form to match its content type.\n */\nexport function toRequest(req: IncomingMessage, options: { trustProxy?: boolean; signal?: AbortSignal } = {}, source: BodySource = {}): Request {\n const node = req as NodeRequest;\n const target = source.url ?? node.originalUrl ?? req.url ?? \"/\";\n const url = requestOrigin(req, options.trustProxy === true) + pathAndQuery(target);\n\n const headers = new Headers();\n for (const [name, value] of Object.entries(req.headers)) {\n // HTTP/2 pseudo-headers (\":authority\") are not valid header names.\n if (value === undefined || name.startsWith(\":\")) continue;\n if (Array.isArray(value)) for (const v of value) headers.append(name, v);\n else headers.set(name, value);\n }\n\n const method = (req.method ?? \"GET\").toUpperCase();\n const init: RequestInit & { duplex?: \"half\" } = { method, headers };\n if (options.signal) init.signal = options.signal;\n if (method !== \"GET\" && method !== \"HEAD\") {\n const raw = source.rawBody ?? node.rawBody;\n // A Buffer is a Uint8Array view, which fetch reads with its offset.\n if (raw instanceof Uint8Array) init.body = raw as unknown as BodyInit;\n else if (typeof raw === \"string\") init.body = raw;\n else if (unread(req)) {\n init.body = Readable.toWeb(req) as unknown as ReadableStream<Uint8Array>;\n init.duplex = \"half\";\n } else {\n const body = reencode(source.body !== undefined ? source.body : node.body, headers);\n if (body !== null) init.body = body as unknown as BodyInit;\n else {\n // The bytes are gone, so the Request has no body and says so.\n headers.delete(\"content-length\");\n headers.delete(\"transfer-encoding\");\n }\n }\n }\n return new Request(url, init);\n}\n\n/**\n * Writes a fetch Response to a Node response: the status, every header\n * (each Set-Cookie as its own header line), and the body, streamed. A HEAD\n * request gets the headers only.\n */\nexport async function writeResponse(res: ServerResponse, response: Response, method = \"GET\"): Promise<void> {\n res.statusCode = response.status;\n response.headers.forEach((value, name) => {\n if (name !== \"set-cookie\") res.setHeader(name, value);\n });\n const cookies = response.headers.getSetCookie();\n if (cookies.length > 0) res.setHeader(\"set-cookie\", cookies);\n if (response.body === null || method.toUpperCase() === \"HEAD\") {\n if (response.body !== null) await response.body.cancel().catch(() => {});\n res.end();\n return;\n }\n try {\n await pipeline(Readable.fromWeb(response.body as unknown as NodeReadableStream<Uint8Array>), res);\n } catch {\n // The client went away mid-body; there is no one left to tell.\n }\n}\n\nfunction mountOf(basePath: string | undefined): string | null {\n return basePath === undefined ? null : basePath.replace(/\\/+$/, \"\");\n}\n\nfunction inMount(pathname: string, mount: string | null): boolean {\n return mount === null || mount === \"\" || pathname === mount || pathname.startsWith(`${mount}/`);\n}\n\nfunction pathnameOf(target: string): string {\n const path = pathAndQuery(target);\n const q = path.search(/[?#]/);\n return q === -1 ? path : path.slice(0, q);\n}\n\n/** Aborts the Request's signal when the client disconnects before the response is done. */\nfunction disconnectSignal(res: ServerResponse): AbortSignal {\n const controller = new AbortController();\n res.once(\"close\", () => {\n if (!res.writableFinished) controller.abort();\n });\n return controller.signal;\n}\n\n/**\n * A Node handler for a fetch-style handler. Use it as a plain server\n * (`http.createServer(toNodeHandler(routes.handler))`), as Express or Connect\n * middleware (`app.use(\"/cronwatch\", toNodeHandler(routes.handler))`), from a\n * Nest controller, or as a Firebase `onRequest` function.\n *\n * With `basePath`, requests outside it go to `next()` (or get a 404 when\n * there is no next); without it, every request is answered. An error thrown\n * by the fetch handler goes to `next(error)` when there is a next, and is\n * otherwise answered with a plain 500.\n */\nexport function toNodeHandler(handler: FetchLike, options: NodeHandlerOptions = {}): NodeHandler {\n const mount = mountOf(options.basePath);\n const trustProxy = options.trustProxy === true;\n return async (req, res, next) => {\n const node = req as NodeRequest;\n if (!inMount(pathnameOf(node.originalUrl ?? req.url ?? \"/\"), mount)) {\n if (next) next();\n else {\n res.statusCode = 404;\n res.setHeader(\"content-type\", \"text/plain; charset=utf-8\");\n res.end(\"Not found\");\n }\n return;\n }\n try {\n const request = toRequest(req, { trustProxy, signal: disconnectSignal(res) });\n await writeResponse(res, await handler(request), request.method);\n } catch (error) {\n if (next) next(error);\n else if (!res.headersSent) {\n res.statusCode = 500;\n res.setHeader(\"content-type\", \"text/plain; charset=utf-8\");\n res.end(\"Internal Server Error\");\n } else res.destroy(error instanceof Error ? error : undefined);\n }\n };\n}\n\n/**\n * Koa middleware for a fetch-style handler. Requests outside `basePath` (when\n * given) go on to the next middleware. The rest are answered here with\n * `ctx.respond = false`, so Koa leaves the response alone. Place it before\n * any body parser; if one runs first, its `ctx.request.rawBody` or\n * `ctx.request.body` is used. An error thrown by the fetch handler is thrown\n * on to Koa before anything is written.\n */\nexport function toKoaMiddleware(handler: FetchLike, options: NodeHandlerOptions = {}): (ctx: KoaContextLike, next: () => Promise<unknown>) => Promise<void> {\n const mount = mountOf(options.basePath);\n const trustProxy = options.trustProxy === true;\n return async (ctx, next) => {\n const target = ctx.originalUrl ?? ctx.req.url ?? \"/\";\n if (!inMount(pathnameOf(target), mount)) {\n await next();\n return;\n }\n const request = toRequest(ctx.req, { trustProxy, signal: disconnectSignal(ctx.res) }, {\n url: target,\n rawBody: ctx.request?.rawBody,\n body: ctx.request?.body,\n });\n const response = await handler(request);\n ctx.respond = false;\n await writeResponse(ctx.res, response, request.method);\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/node.ts"],"names":["Readable","pipeline"],"mappings":"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","file":"node.cjs","sourcesContent":["/**\n * @cronwatch/sdk/node: serve a fetch-style handler (cw.routes().handler, or\n * a job's handler()) from Node's http module and the frameworks built on it:\n * a plain http server, Express and Connect, NestJS, Koa, Strapi, and Firebase\n * onRequest. The SDK core stays free of node: imports so it runs on Workers;\n * this entry point is the only one that uses Node's http and stream types.\n */\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport type { ReadableStream as NodeReadableStream } from \"node:stream/web\";\n\n/** A fetch-style handler: a Request in, a Response out. */\nexport type FetchLike = (request: Request) => Response | Promise<Response>;\n\nexport interface NodeHandlerOptions {\n /**\n * Build the request URL from X-Forwarded-Proto and X-Forwarded-Host (the\n * first value of each) when present, instead of the connection's scheme and\n * the Host header. Only behind a proxy that sets or overwrites both, since\n * a client can send them too. Default false.\n */\n trustProxy?: boolean;\n /**\n * Answer only requests at this path or under it, and pass the rest on: to\n * `next()` as Express or Connect middleware, to the next Koa middleware,\n * and with a 404 when there is no next. The path is read from\n * `req.originalUrl` (so Express mount paths count) or `req.url`. Leave it\n * out to answer every request the handler is given, which suits\n * `app.use(\"/cronwatch\", ...)`, a Nest controller, and a Firebase function.\n */\n basePath?: string;\n}\n\n/** Called by Express and Connect for the next middleware, or with an error. */\nexport type NodeNext = (error?: unknown) => void;\n\n/** A handler for http.createServer, Express or Connect, Nest, and Firebase onRequest. */\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse, next?: NodeNext) => Promise<void>;\n\n/** The parts of a Koa context the middleware uses, so the SDK needs no Koa types. */\nexport interface KoaContextLike {\n req: IncomingMessage;\n res: ServerResponse;\n respond?: boolean;\n /** Koa's own copy of the URL, kept when koa-mount rewrites req.url. */\n originalUrl?: string;\n /** Set by koa-bodyparser and similar, when they run before this middleware. */\n request?: { body?: unknown; rawBody?: unknown };\n}\n\n/** Where a request body may already be, when a parser has read the stream. */\nexport interface BodySource {\n /** The request target to use instead of req.originalUrl or req.url. */\n url?: string;\n /** The body's bytes as read (Firebase, Nest with rawBody: true, koa-bodyparser). */\n rawBody?: unknown;\n /** A body a parser has decoded (express.json, express.urlencoded). */\n body?: unknown;\n}\n\ntype NodeRequest = IncomingMessage & { originalUrl?: string; rawBody?: unknown; body?: unknown };\n\nfunction header(value: string | string[] | undefined): string | undefined {\n return Array.isArray(value) ? value[0] : value;\n}\n\nfunction firstValue(value: string | string[] | undefined): string | undefined {\n const first = header(value)?.split(\",\")[0]?.trim();\n return first ? first : undefined;\n}\n\n/** An origin from a scheme and host, or null when the host is not a bare host[:port]. */\nfunction originFrom(proto: string, host: string): string | null {\n try {\n const url = new URL(`${proto}://${host}`);\n if (url.pathname !== \"/\" || url.username || url.password || url.search || url.hash) return null;\n return url.origin;\n } catch {\n return null;\n }\n}\n\n/** The path and query of a request target, which may be absolute-form. */\nfunction pathAndQuery(target: string): string {\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(target)) {\n try {\n const url = new URL(target);\n return url.pathname + url.search;\n } catch {\n return \"/\";\n }\n }\n // \"//x\" must stay a path, not become a host, so it is never parsed relative to a base.\n return target.startsWith(\"/\") ? target : `/${target}`;\n}\n\nfunction requestOrigin(req: IncomingMessage, trustProxy: boolean): string {\n const socket = req.socket as { encrypted?: boolean } | undefined;\n let proto = socket?.encrypted === true ? \"https\" : \"http\";\n let host = header(req.headers.host) ?? header(req.headers[\":authority\"] as string | string[] | undefined);\n if (trustProxy) {\n const fp = firstValue(req.headers[\"x-forwarded-proto\"])?.toLowerCase();\n if (fp === \"http\" || fp === \"https\") proto = fp;\n const fh = firstValue(req.headers[\"x-forwarded-host\"]);\n if (fh && originFrom(proto, fh) !== null) host = fh;\n }\n return (host ? originFrom(proto, host) : null) ?? `${proto}://localhost`;\n}\n\n/** The stream has not been read by a body parser, so it can be passed on as is. */\nfunction unread(req: IncomingMessage): boolean {\n if (typeof req.on !== \"function\" || req.readable === false) return false;\n return !req.readableEnded && req.readableDidRead !== true;\n}\n\n/**\n * A body a parser decoded, encoded again for its content type: JSON as JSON,\n * a urlencoded form as a form. Anything else (multipart, unknown types) is\n * dropped, because it cannot be rebuilt faithfully.\n */\nfunction reencode(parsed: unknown, headers: Headers): string | Uint8Array | null {\n if (parsed === undefined || parsed === null) return null;\n if (typeof parsed === \"string\" || parsed instanceof Uint8Array) return parsed;\n if (typeof parsed !== \"object\") return null;\n const type = (headers.get(\"content-type\") ?? \"\").toLowerCase();\n let text: string | null = null;\n if (type.includes(\"application/x-www-form-urlencoded\")) {\n const form = new URLSearchParams();\n for (const [key, value] of Object.entries(parsed)) {\n for (const v of Array.isArray(value) ? value : [value]) {\n if (v !== undefined && v !== null) form.append(key, typeof v === \"object\" ? JSON.stringify(v) : String(v));\n }\n }\n text = form.toString();\n } else if (type.includes(\"json\")) {\n text = JSON.stringify(parsed);\n }\n if (text === null) return null;\n headers.delete(\"content-length\");\n headers.delete(\"transfer-encoding\");\n return text;\n}\n\n/**\n * A fetch Request for a Node request. The URL is the Host header (or the\n * forwarded host and scheme with trustProxy) plus req.originalUrl or req.url.\n * The body, for anything but GET and HEAD, is the first of: `rawBody` (from\n * the source, or req.rawBody), the request stream when nothing has read it,\n * or a body a parser decoded (req.body), re-encoded as JSON or a urlencoded\n * form to match its content type.\n */\nexport function toRequest(req: IncomingMessage, options: { trustProxy?: boolean; signal?: AbortSignal } = {}, source: BodySource = {}): Request {\n const node = req as NodeRequest;\n const target = source.url ?? node.originalUrl ?? req.url ?? \"/\";\n const url = requestOrigin(req, options.trustProxy === true) + pathAndQuery(target);\n\n const headers = new Headers();\n for (const [name, value] of Object.entries(req.headers)) {\n // HTTP/2 pseudo-headers (\":authority\") are not valid header names.\n if (value === undefined || name.startsWith(\":\")) continue;\n if (Array.isArray(value)) for (const v of value) headers.append(name, v);\n else headers.set(name, value);\n }\n\n const method = (req.method ?? \"GET\").toUpperCase();\n const init: RequestInit & { duplex?: \"half\" } = { method, headers };\n if (options.signal) init.signal = options.signal;\n if (method !== \"GET\" && method !== \"HEAD\") {\n const raw = source.rawBody ?? node.rawBody;\n // A Buffer is a Uint8Array view, which fetch reads with its offset.\n if (raw instanceof Uint8Array) init.body = raw as unknown as BodyInit;\n else if (typeof raw === \"string\") init.body = raw;\n else if (unread(req)) {\n init.body = Readable.toWeb(req) as unknown as ReadableStream<Uint8Array>;\n init.duplex = \"half\";\n } else {\n const body = reencode(source.body !== undefined ? source.body : node.body, headers);\n if (body !== null) init.body = body as unknown as BodyInit;\n else {\n // The bytes are gone, so the Request has no body and says so.\n headers.delete(\"content-length\");\n headers.delete(\"transfer-encoding\");\n }\n }\n }\n return new Request(url, init);\n}\n\n/**\n * Writes a fetch Response to a Node response: the status, every header\n * (each Set-Cookie as its own header line), and the body, streamed. A HEAD\n * request gets the headers only.\n */\nexport async function writeResponse(res: ServerResponse, response: Response, method = \"GET\"): Promise<void> {\n res.statusCode = response.status;\n response.headers.forEach((value, name) => {\n if (name !== \"set-cookie\") res.setHeader(name, value);\n });\n const cookies = response.headers.getSetCookie();\n if (cookies.length > 0) res.setHeader(\"set-cookie\", cookies);\n if (response.body === null || method.toUpperCase() === \"HEAD\") {\n if (response.body !== null) await response.body.cancel().catch(() => {});\n res.end();\n return;\n }\n try {\n await pipeline(Readable.fromWeb(response.body as unknown as NodeReadableStream<Uint8Array>), res);\n } catch {\n // The client went away mid-body; there is no one left to tell.\n }\n}\n\nfunction mountOf(basePath: string | undefined): string | null {\n return basePath === undefined ? null : basePath.replace(/\\/+$/, \"\");\n}\n\nfunction inMount(pathname: string, mount: string | null): boolean {\n return mount === null || mount === \"\" || pathname === mount || pathname.startsWith(`${mount}/`);\n}\n\nfunction pathnameOf(target: string): string {\n const path = pathAndQuery(target);\n const q = path.search(/[?#]/);\n return q === -1 ? path : path.slice(0, q);\n}\n\n/** Aborts the Request's signal when the client disconnects before the response is done. */\nfunction disconnectSignal(res: ServerResponse): AbortSignal {\n const controller = new AbortController();\n res.once(\"close\", () => {\n if (!res.writableFinished) controller.abort();\n });\n return controller.signal;\n}\n\n/**\n * A Node handler for a fetch-style handler. Use it as a plain server\n * (`http.createServer(toNodeHandler(routes.handler))`), as Express or Connect\n * middleware (`app.use(\"/cronwatch\", toNodeHandler(routes.handler))`), from a\n * Nest controller, or as a Firebase `onRequest` function.\n *\n * With `basePath`, requests outside it go to `next()` (or get a 404 when\n * there is no next); without it, every request is answered. An error thrown\n * by the fetch handler goes to `next(error)` when there is a next, and is\n * otherwise answered with a plain 500.\n */\nexport function toNodeHandler(handler: FetchLike, options: NodeHandlerOptions = {}): NodeHandler {\n const mount = mountOf(options.basePath);\n const trustProxy = options.trustProxy === true;\n return async (req, res, next) => {\n const node = req as NodeRequest;\n if (!inMount(pathnameOf(node.originalUrl ?? req.url ?? \"/\"), mount)) {\n if (next) next();\n else {\n res.statusCode = 404;\n res.setHeader(\"content-type\", \"text/plain; charset=utf-8\");\n res.end(\"Not found\");\n }\n return;\n }\n try {\n const request = toRequest(req, { trustProxy, signal: disconnectSignal(res) });\n await writeResponse(res, await handler(request), request.method);\n } catch (error) {\n if (next) next(error);\n else if (!res.headersSent) {\n res.statusCode = 500;\n res.setHeader(\"content-type\", \"text/plain; charset=utf-8\");\n res.end(\"Internal Server Error\");\n } else res.destroy(error instanceof Error ? error : undefined);\n }\n };\n}\n\n/**\n * Koa middleware for a fetch-style handler. Requests outside `basePath` (when\n * given) go on to the next middleware. The rest are answered here with\n * `ctx.respond = false`, so Koa leaves the response alone. Place it before\n * any body parser; if one runs first, its `ctx.request.rawBody` or\n * `ctx.request.body` is used. An error thrown by the fetch handler is thrown\n * on to Koa before anything is written.\n */\nexport function toKoaMiddleware(handler: FetchLike, options: NodeHandlerOptions = {}): (ctx: KoaContextLike, next: () => Promise<unknown>) => Promise<void> {\n const mount = mountOf(options.basePath);\n const trustProxy = options.trustProxy === true;\n return async (ctx, next) => {\n const target = ctx.originalUrl ?? ctx.req.url ?? \"/\";\n if (!inMount(pathnameOf(target), mount)) {\n await next();\n return;\n }\n const request = toRequest(ctx.req, { trustProxy, signal: disconnectSignal(ctx.res) }, {\n url: target,\n rawBody: ctx.request?.rawBody,\n body: ctx.request?.body,\n });\n const response = await handler(request);\n ctx.respond = false;\n await writeResponse(ctx.res, response, request.method);\n };\n}\n"]}
|
package/dist/node.d.cts
CHANGED
|
@@ -3,7 +3,7 @@ import { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
3
3
|
/**
|
|
4
4
|
* @cronwatch/sdk/node: serve a fetch-style handler (cw.routes().handler, or
|
|
5
5
|
* a job's handler()) from Node's http module and the frameworks built on it:
|
|
6
|
-
* a plain http server, Express and Connect, NestJS, Koa, Strapi and Firebase
|
|
6
|
+
* a plain http server, Express and Connect, NestJS, Koa, Strapi, and Firebase
|
|
7
7
|
* onRequest. The SDK core stays free of node: imports so it runs on Workers;
|
|
8
8
|
* this entry point is the only one that uses Node's http and stream types.
|
|
9
9
|
*/
|
|
@@ -24,13 +24,13 @@ interface NodeHandlerOptions {
|
|
|
24
24
|
* and with a 404 when there is no next. The path is read from
|
|
25
25
|
* `req.originalUrl` (so Express mount paths count) or `req.url`. Leave it
|
|
26
26
|
* out to answer every request the handler is given, which suits
|
|
27
|
-
* `app.use("/cronwatch", ...)`, a Nest controller and a Firebase function.
|
|
27
|
+
* `app.use("/cronwatch", ...)`, a Nest controller, and a Firebase function.
|
|
28
28
|
*/
|
|
29
29
|
basePath?: string;
|
|
30
30
|
}
|
|
31
31
|
/** Called by Express and Connect for the next middleware, or with an error. */
|
|
32
32
|
type NodeNext = (error?: unknown) => void;
|
|
33
|
-
/** A handler for http.createServer, Express or Connect, Nest and Firebase onRequest. */
|
|
33
|
+
/** A handler for http.createServer, Express or Connect, Nest, and Firebase onRequest. */
|
|
34
34
|
type NodeHandler = (req: IncomingMessage, res: ServerResponse, next?: NodeNext) => Promise<void>;
|
|
35
35
|
/** The parts of a Koa context the middleware uses, so the SDK needs no Koa types. */
|
|
36
36
|
interface KoaContextLike {
|
package/dist/node.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
3
3
|
/**
|
|
4
4
|
* @cronwatch/sdk/node: serve a fetch-style handler (cw.routes().handler, or
|
|
5
5
|
* a job's handler()) from Node's http module and the frameworks built on it:
|
|
6
|
-
* a plain http server, Express and Connect, NestJS, Koa, Strapi and Firebase
|
|
6
|
+
* a plain http server, Express and Connect, NestJS, Koa, Strapi, and Firebase
|
|
7
7
|
* onRequest. The SDK core stays free of node: imports so it runs on Workers;
|
|
8
8
|
* this entry point is the only one that uses Node's http and stream types.
|
|
9
9
|
*/
|
|
@@ -24,13 +24,13 @@ interface NodeHandlerOptions {
|
|
|
24
24
|
* and with a 404 when there is no next. The path is read from
|
|
25
25
|
* `req.originalUrl` (so Express mount paths count) or `req.url`. Leave it
|
|
26
26
|
* out to answer every request the handler is given, which suits
|
|
27
|
-
* `app.use("/cronwatch", ...)`, a Nest controller and a Firebase function.
|
|
27
|
+
* `app.use("/cronwatch", ...)`, a Nest controller, and a Firebase function.
|
|
28
28
|
*/
|
|
29
29
|
basePath?: string;
|
|
30
30
|
}
|
|
31
31
|
/** Called by Express and Connect for the next middleware, or with an error. */
|
|
32
32
|
type NodeNext = (error?: unknown) => void;
|
|
33
|
-
/** A handler for http.createServer, Express or Connect, Nest and Firebase onRequest. */
|
|
33
|
+
/** A handler for http.createServer, Express or Connect, Nest, and Firebase onRequest. */
|
|
34
34
|
type NodeHandler = (req: IncomingMessage, res: ServerResponse, next?: NodeNext) => Promise<void>;
|
|
35
35
|
/** The parts of a Koa context the middleware uses, so the SDK needs no Koa types. */
|
|
36
36
|
interface KoaContextLike {
|