@cronwatch/sdk 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/anthropic.d.cts +1 -1
- package/dist/anthropic.d.ts +1 -1
- package/dist/bugsnag.cjs +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 +1 -1
- package/dist/bugsnag.js.map +1 -1
- package/dist/{client-CRMfhylq.d.cts → client-BkPMRkDq.d.cts} +1 -1
- package/dist/{client-D2Xk2600.d.ts → client-DySsV5T0.d.ts} +1 -1
- package/dist/d1.d.cts +1 -1
- package/dist/d1.d.ts +1 -1
- package/dist/datadog.cjs +1 -0
- package/dist/datadog.cjs.map +1 -1
- package/dist/datadog.d.cts +1 -1
- package/dist/datadog.d.ts +1 -1
- package/dist/datadog.js +1 -0
- package/dist/datadog.js.map +1 -1
- package/dist/discord.cjs +1 -0
- package/dist/discord.cjs.map +1 -1
- package/dist/discord.d.cts +1 -1
- package/dist/discord.d.ts +1 -1
- package/dist/discord.js +1 -0
- package/dist/discord.js.map +1 -1
- package/dist/{email-BWuUiCkw.d.ts → email-Bx3ehK0O.d.ts} +1 -1
- package/dist/{email-Dcqe3k8n.d.cts → email-CD6NO8CV.d.cts} +1 -1
- package/dist/honeybadger.cjs +1 -0
- 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 +1 -0
- package/dist/honeybadger.js.map +1 -1
- package/dist/index.cjs +83 -7
- 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 +83 -7
- 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 +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 +1 -1
- package/dist/newrelic.js.map +1 -1
- package/dist/pg-cron.cjs +2 -1
- package/dist/pg-cron.cjs.map +1 -1
- package/dist/pg-cron.d.cts +2 -2
- package/dist/pg-cron.d.ts +2 -2
- package/dist/pg-cron.js +2 -1
- package/dist/pg-cron.js.map +1 -1
- package/dist/postgres.d.cts +1 -1
- package/dist/postgres.d.ts +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 +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 +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 +1 -1
- package/dist/sentry.cjs.map +1 -1
- package/dist/sentry.d.cts +1 -1
- package/dist/sentry.d.ts +1 -1
- package/dist/sentry.js +1 -1
- 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 +1 -0
- 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 +1 -0
- package/dist/slack.js.map +1 -1
- package/dist/sqlite.d.cts +1 -1
- package/dist/sqlite.d.ts +1 -1
- package/dist/twilio.cjs.map +1 -1
- package/dist/twilio.d.cts +1 -1
- package/dist/twilio.d.ts +1 -1
- package/dist/twilio.js.map +1 -1
- package/dist/{types-DLnlUie8.d.cts → types-Bm972Uqj.d.cts} +19 -1
- package/dist/{types-DLnlUie8.d.ts → types-Bm972Uqj.d.ts} +19 -1
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.d.cts +1 -1
- package/dist/webhook.d.ts +1 -1
- package/dist/webhook.js.map +1 -1
- package/package.json +1 -1
package/dist/ses.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/email.ts","../src/alerts/sigv4.ts","../src/alerts/ses.ts"],"names":[],"mappings":"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","file":"ses.js","sourcesContent":["/**\n * Helpers shared by the provider channels. Only fetch and Web Crypto, so the\n * channels run on Node, Cloudflare Workers, Deno and Bun alike. Not an entry\n * point: each channel bundles its own copy.\n */\nimport { isoTime } from \"../duration.js\";\nimport type { Alert } from \"../types.js\";\n\nexport const TIMEOUT_MS = 10_000;\n\n/** Severity for trackers that have levels. Recovered is informational. */\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport function severity(type: Alert[\"type\"]): Severity {\n if (type === \"recovered\") return \"info\";\n if (type === \"slow\" || type === \"over_budget\") return \"warning\";\n return \"error\";\n}\n\n/** The scheme, host and port only. A URL's path or query can hold a credential. */\nexport function origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"(invalid URL)\";\n }\n}\n\n/**\n * The URL, once it is one fetch can post to. Refused without quoting it:\n * fetch's own error for a URL it cannot parse (\"Failed to parse URL from ...\")\n * quotes the whole URL, and a webhook URL's path is its credential.\n */\nexport function postable(url: string): string {\n let protocol: string;\n try {\n protocol = new URL(url).protocol;\n } catch {\n throw new Error(\"only http and https URLs can be posted to, not this URL\");\n }\n if (protocol !== \"http:\" && protocol !== \"https:\") throw new Error(`only http and https URLs can be posted to, not ${protocol}`);\n return url;\n}\n\n/** How much of a provider's error body goes into the error message. */\nexport const ERROR_BODY_MAX = 200;\n\n/**\n * POSTs and throws on a non-2xx answer. The error names the provider and the\n * URL's origin, plus the start of the response body with every secret the\n * channel holds cut out, in case a provider echoes one back. Secrets are cut\n * from a prefix long enough to hold one that starts inside the first\n * ERROR_BODY_MAX characters, and only then is it cut to that length, so no\n * part of a secret survives at the edge. A redirect is refused rather than\n * followed: the credential headers would go with it to wherever it points.\n */\nexport async function post(provider: string, url: string, init: { headers: Record<string, string>; body: string }, secrets: (string | undefined)[] = []): Promise<Response> {\n const response = await fetch(postable(url), { method: \"POST\", headers: init.headers, body: init.body, redirect: \"error\", signal: AbortSignal.timeout(TIMEOUT_MS) });\n if (!response.ok) {\n let text = \"\";\n try {\n text = await response.text();\n } catch {\n // The status is enough.\n }\n throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : \"\"}`);\n }\n return response;\n}\n\n/** The start of an error body, secrets cut out first, then cut to ERROR_BODY_MAX on a code point. */\nexport function errorBody(text: string, secrets: (string | undefined)[] = []): string {\n const kept = secrets.filter((s): s is string => typeof s === \"string\" && s.length >= 4);\n const longest = kept.reduce((n, s) => Math.max(n, s.length), 0);\n let head = cut(text, ERROR_BODY_MAX + longest);\n for (const secret of kept) head = head.split(secret).join(\"[redacted]\");\n return cut(head, ERROR_BODY_MAX);\n}\n\n/** A credential with the spaces and newlines a paste leaves around it taken off. Anything not a string is empty. */\nexport function trimmed(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Base64 of UTF-8, without Buffer. */\nexport function base64(text: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nexport function basicAuth(user: string, password: string): string {\n return `Basic ${base64(`${user}:${password}`)}`;\n}\n\nexport function hex(bytes: ArrayBuffer | Uint8Array): string {\n return Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport async function sha256Hex(text: string): Promise<string> {\n return hex(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text)));\n}\n\n/**\n * A stable 32 hex character id for one alert: the same job, type and time\n * always give the same id, so a provider that deduplicates on it drops a\n * resend of an alert it already took.\n */\nexport async function alertId(alert: Alert): Promise<string> {\n return (await sha256Hex(`${alert.job}\\n${alert.type}\\n${alert.at}`)).slice(0, 32);\n}\n\n/** The same id laid out as a UUID, for APIs that ask for one. */\nexport function asUuid(id: string): string {\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20, 32)}`;\n}\n\n/** Cuts to at most `max` UTF-16 units without splitting a surrogate pair. */\nexport function cut(text: string, max: number): string {\n if (text.length <= max) return text;\n let end = max;\n const code = text.charCodeAt(end - 1);\n if (code >= 0xd800 && code <= 0xdbff) end -= 1;\n return text.slice(0, end);\n}\n\n/** The run fields worth attaching to a tracker event. A start before the year 1 or after 9999 is null. */\nexport function runSummary(alert: Alert): Record<string, unknown> | null {\n const run = alert.run;\n if (!run) return null;\n return { id: run.id, status: run.status, startedAt: isoTime(run.startedAt), durationMs: run.durationMs, trigger: run.trigger };\n}\n\n/** Title, message, triage and link as one plain text block, the way every channel reads. */\nexport function plainText(alert: Alert, link: string | undefined): string {\n return [alert.title, \"\", alert.message, ...(alert.triage ? [\"\", `Triage: ${alert.triage}`] : []), ...(link ? [\"\", `Open: ${link}`] : [])].join(\"\\n\");\n}\n","/**\n * What every email channel sends: one subject, a plain text body and a small\n * HTML body, so an alert reads the same whichever provider carries it. Not an\n * entry point; resend, postmark, sendgrid, mailgun and ses each bundle it.\n */\nimport type { Alert } from \"../types.js\";\nimport { cut, plainText } from \"./shared.js\";\n\n/** The options every email channel takes. */\nexport interface EmailOptions {\n /** The sender, \"alerts@example.com\" or \"CronWatch <alerts@example.com>\". The provider must allow it. */\n from: string;\n /** One address or several. */\n to: string | string[];\n /** Put in front of the title in the subject, \"[prod]\" say. */\n subjectPrefix?: string;\n /** Link back to the job in your dashboard: (alert) => `https://app.example.com/cronwatch/jobs/${alert.job}`. */\n link?: (alert: Alert) => string;\n}\n\nexport interface Email {\n from: string;\n to: string[];\n subject: string;\n text: string;\n html: string;\n}\n\n/** Checks the shared options once, when the channel is made. */\nexport function recipients(name: string, options: EmailOptions): string[] {\n if (!options.from) throw new Error(`${name}() needs a from address`);\n const to = (Array.isArray(options.to) ? options.to : [options.to]).filter((a) => typeof a === \"string\" && a.trim() !== \"\");\n if (to.length === 0) throw new Error(`${name}() needs at least one to address`);\n return to.map((a) => a.trim());\n}\n\nexport function composeEmail(alert: Alert, options: EmailOptions, to: string[]): Email {\n const link = safeLink(options.link?.(alert));\n // One line: a newline in a subject is a header injection or a rejected send.\n const subject = cut(`${options.subjectPrefix ? `${options.subjectPrefix} ` : \"\"}${alert.title}`.replace(/[\\r\\n]+/g, \" \"), 250);\n return { from: options.from, to, subject, text: plainText(alert, link), html: html(alert, link) };\n}\n\n/** Escapes text for HTML content and double quoted attributes. */\nexport function escapeHtml(text: string): string {\n return text.replace(/&/g, \"&\").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 * AWS Signature Version 4 with Web Crypto, for the SES channel.\n * Spec: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html\n * Checked against the AWS SigV4 test suite (test/sigv4.test.ts).\n */\nimport { hex, sha256Hex } from \"./shared.js\";\n\nexport interface SigV4Credentials {\n accessKeyId: string;\n secretAccessKey: string;\n /** For temporary credentials (STS, an IAM role). Sent and signed as X-Amz-Security-Token. */\n sessionToken?: string;\n}\n\nexport interface SigV4Request {\n method: string;\n url: string;\n /** Headers to send and sign. Host is taken from the URL, and X-Amz-Date from `now`. */\n headers: Record<string, string>;\n body: string;\n region: string;\n service: string;\n /** Epoch milliseconds. */\n now: number;\n}\n\n/**\n * Returns the headers to send: the given ones plus x-amz-date, the session\n * token when there is one, and authorization. Host is signed but not\n * returned, because fetch sets it.\n */\nexport async function signV4(request: SigV4Request, credentials: SigV4Credentials): Promise<Record<string, string>> {\n const url = new URL(request.url);\n const amzDate = new Date(request.now).toISOString().replace(/[-:]/g, \"\").replace(/\\.\\d{3}/, \"\");\n const day = amzDate.slice(0, 8);\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(request.headers)) headers[name.toLowerCase()] = value;\n headers[\"x-amz-date\"] = amzDate;\n if (credentials.sessionToken) headers[\"x-amz-security-token\"] = credentials.sessionToken;\n\n const signed: Record<string, string> = { ...headers, host: url.host };\n const names = Object.keys(signed).sort();\n const canonicalHeaders = names.map((n) => `${n}:${signed[n]!.trim().replace(/\\s+/g, \" \")}\\n`).join(\"\");\n const signedHeaders = names.join(\";\");\n const canonicalRequest = [\n request.method.toUpperCase(),\n canonicalUri(url.pathname),\n canonicalQuery(url.searchParams),\n canonicalHeaders,\n signedHeaders,\n await sha256Hex(request.body),\n ].join(\"\\n\");\n const scope = `${day}/${request.region}/${request.service}/aws4_request`;\n const stringToSign = [\"AWS4-HMAC-SHA256\", amzDate, scope, await sha256Hex(canonicalRequest)].join(\"\\n\");\n\n let key = await hmac(new TextEncoder().encode(`AWS4${credentials.secretAccessKey}`), day);\n key = await hmac(key, request.region);\n key = await hmac(key, request.service);\n key = await hmac(key, \"aws4_request\");\n const signature = hex(await hmac(key, stringToSign));\n\n headers.authorization = `AWS4-HMAC-SHA256 Credential=${credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;\n return headers;\n}\n\nasync function hmac(key: Uint8Array, data: string): Promise<Uint8Array> {\n const cryptoKey = await crypto.subtle.importKey(\"raw\", key as Uint8Array<ArrayBuffer>, { name: \"HMAC\", hash: \"SHA-256\" }, false, [\"sign\"]);\n return new Uint8Array(await crypto.subtle.sign(\"HMAC\", cryptoKey, new TextEncoder().encode(data)));\n}\n\n/** RFC 3986 encoding of every byte but the unreserved characters. */\nfunction uriEncode(text: string): string {\n return encodeURIComponent(text).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);\n}\n\nfunction canonicalUri(path: string): string {\n if (!path) return \"/\";\n // URL.pathname is already encoded once; every AWS service but S3 expects each segment encoded again.\n return path.split(\"/\").map(uriEncode).join(\"/\");\n}\n\nfunction canonicalQuery(params: URLSearchParams): string {\n const pairs: [string, string][] = [];\n params.forEach((value, name) => pairs.push([uriEncode(name), uriEncode(value)]));\n pairs.sort(([a, av], [b, bv]) => (a < b ? -1 : a > b ? 1 : av < bv ? -1 : av > bv ? 1 : 0));\n return pairs.map(([n, v]) => `${n}=${v}`).join(\"&\");\n}\n","/**\n * Amazon SES, API v2 SendEmail. API reference: https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.html\n * POST https://email.<region>.amazonaws.com/v2/email/outbound-emails, signed\n * with AWS Signature Version 4 (see sigv4.ts), so no AWS SDK is needed.\n */\nimport type { AlertChannel } from \"../types.js\";\nimport { composeEmail, recipients, type EmailOptions } from \"./email.js\";\nimport { post, trimmed } from \"./shared.js\";\nimport { signV4 } from \"./sigv4.js\";\n\nexport interface SesOptions extends EmailOptions {\n /** The SES region, \"us-east-1\" say. The from identity must be verified there. */\n region: string;\n accessKeyId: string;\n secretAccessKey: string;\n /** For temporary credentials, an assumed role say. */\n sessionToken?: string;\n /** A configuration set for event publishing, if you use one. */\n configurationSetName?: string;\n /** The clock used to sign requests, in epoch milliseconds. For tests. */\n now?: () => number;\n}\n\n/** Sends alerts as email through Amazon SES. */\nexport function ses(options: SesOptions): AlertChannel {\n if (!options.region) throw new Error(\"ses() needs a region\");\n if (!/^[a-z0-9-]+$/.test(options.region)) throw new Error(\"ses() needs a region like us-east-1\");\n // A pasted credential often carries a stray space or newline, which would spoil the signature.\n const credentials = { accessKeyId: trimmed(options.accessKeyId), secretAccessKey: trimmed(options.secretAccessKey), sessionToken: trimmed(options.sessionToken) || undefined };\n if (!credentials.accessKeyId || !credentials.secretAccessKey) throw new Error(\"ses() needs an accessKeyId and secretAccessKey\");\n const to = recipients(\"ses\", options);\n const url = `https://email.${options.region}.amazonaws.com/v2/email/outbound-emails`;\n const now = options.now ?? Date.now;\n return {\n name: \"ses\",\n async send(alert) {\n const email = composeEmail(alert, options, to);\n const body = JSON.stringify({\n FromEmailAddress: email.from,\n Destination: { ToAddresses: email.to },\n Content: {\n Simple: {\n Subject: { Data: email.subject, Charset: \"UTF-8\" },\n Body: { Text: { Data: email.text, Charset: \"UTF-8\" }, Html: { Data: email.html, Charset: \"UTF-8\" } },\n },\n },\n ...(options.configurationSetName ? { ConfigurationSetName: options.configurationSetName } : {}),\n EmailTags: [{ Name: \"source\", Value: \"cronwatch\" }],\n });\n const headers = await signV4(\n { method: \"POST\", url, headers: { \"content-type\": \"application/json\" }, body, region: options.region, service: \"ses\", now: now() },\n credentials,\n );\n await post(\"SES\", url, { headers, body }, [credentials.secretAccessKey, credentials.sessionToken]);\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/email.ts","../src/alerts/sigv4.ts","../src/alerts/ses.ts"],"names":[],"mappings":"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","file":"ses.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 * AWS Signature Version 4 with Web Crypto, for the SES channel.\n * Spec: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html\n * Checked against the AWS SigV4 test suite (test/sigv4.test.ts).\n */\nimport { hex, sha256Hex } from \"./shared.js\";\n\nexport interface SigV4Credentials {\n accessKeyId: string;\n secretAccessKey: string;\n /** For temporary credentials (STS, an IAM role). Sent and signed as X-Amz-Security-Token. */\n sessionToken?: string;\n}\n\nexport interface SigV4Request {\n method: string;\n url: string;\n /** Headers to send and sign. Host is taken from the URL, and X-Amz-Date from `now`. */\n headers: Record<string, string>;\n body: string;\n region: string;\n service: string;\n /** Epoch milliseconds. */\n now: number;\n}\n\n/**\n * Returns the headers to send: the given ones plus x-amz-date, the session\n * token when there is one, and authorization. Host is signed but not\n * returned, because fetch sets it.\n */\nexport async function signV4(request: SigV4Request, credentials: SigV4Credentials): Promise<Record<string, string>> {\n const url = new URL(request.url);\n const amzDate = new Date(request.now).toISOString().replace(/[-:]/g, \"\").replace(/\\.\\d{3}/, \"\");\n const day = amzDate.slice(0, 8);\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(request.headers)) headers[name.toLowerCase()] = value;\n headers[\"x-amz-date\"] = amzDate;\n if (credentials.sessionToken) headers[\"x-amz-security-token\"] = credentials.sessionToken;\n\n const signed: Record<string, string> = { ...headers, host: url.host };\n const names = Object.keys(signed).sort();\n const canonicalHeaders = names.map((n) => `${n}:${signed[n]!.trim().replace(/\\s+/g, \" \")}\\n`).join(\"\");\n const signedHeaders = names.join(\";\");\n const canonicalRequest = [\n request.method.toUpperCase(),\n canonicalUri(url.pathname),\n canonicalQuery(url.searchParams),\n canonicalHeaders,\n signedHeaders,\n await sha256Hex(request.body),\n ].join(\"\\n\");\n const scope = `${day}/${request.region}/${request.service}/aws4_request`;\n const stringToSign = [\"AWS4-HMAC-SHA256\", amzDate, scope, await sha256Hex(canonicalRequest)].join(\"\\n\");\n\n let key = await hmac(new TextEncoder().encode(`AWS4${credentials.secretAccessKey}`), day);\n key = await hmac(key, request.region);\n key = await hmac(key, request.service);\n key = await hmac(key, \"aws4_request\");\n const signature = hex(await hmac(key, stringToSign));\n\n headers.authorization = `AWS4-HMAC-SHA256 Credential=${credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;\n return headers;\n}\n\nasync function hmac(key: Uint8Array, data: string): Promise<Uint8Array> {\n const cryptoKey = await crypto.subtle.importKey(\"raw\", key as Uint8Array<ArrayBuffer>, { name: \"HMAC\", hash: \"SHA-256\" }, false, [\"sign\"]);\n return new Uint8Array(await crypto.subtle.sign(\"HMAC\", cryptoKey, new TextEncoder().encode(data)));\n}\n\n/** RFC 3986 encoding of every byte but the unreserved characters. */\nfunction uriEncode(text: string): string {\n return encodeURIComponent(text).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);\n}\n\nfunction canonicalUri(path: string): string {\n if (!path) return \"/\";\n // URL.pathname is already encoded once; every AWS service but S3 expects each segment encoded again.\n return path.split(\"/\").map(uriEncode).join(\"/\");\n}\n\nfunction canonicalQuery(params: URLSearchParams): string {\n const pairs: [string, string][] = [];\n params.forEach((value, name) => pairs.push([uriEncode(name), uriEncode(value)]));\n pairs.sort(([a, av], [b, bv]) => (a < b ? -1 : a > b ? 1 : av < bv ? -1 : av > bv ? 1 : 0));\n return pairs.map(([n, v]) => `${n}=${v}`).join(\"&\");\n}\n","/**\n * Amazon SES, API v2 SendEmail. API reference: https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.html\n * POST https://email.<region>.amazonaws.com/v2/email/outbound-emails, signed\n * with AWS Signature Version 4 (see sigv4.ts), so no AWS SDK is needed.\n */\nimport type { AlertChannel } from \"../types.js\";\nimport { composeEmail, recipients, type EmailOptions } from \"./email.js\";\nimport { post, trimmed } from \"./shared.js\";\nimport { signV4 } from \"./sigv4.js\";\n\nexport interface SesOptions extends EmailOptions {\n /** The SES region, \"us-east-1\" say. The from identity must be verified there. */\n region: string;\n accessKeyId: string;\n secretAccessKey: string;\n /** For temporary credentials, an assumed role say. */\n sessionToken?: string;\n /** A configuration set for event publishing, if you use one. */\n configurationSetName?: string;\n /** The clock used to sign requests, in epoch milliseconds. For tests. */\n now?: () => number;\n}\n\n/** Sends alerts as email through Amazon SES. */\nexport function ses(options: SesOptions): AlertChannel {\n if (!options.region) throw new Error(\"ses() needs a region\");\n if (!/^[a-z0-9-]+$/.test(options.region)) throw new Error(\"ses() needs a region like us-east-1\");\n // A pasted credential often carries a stray space or newline, which would spoil the signature.\n const credentials = { accessKeyId: trimmed(options.accessKeyId), secretAccessKey: trimmed(options.secretAccessKey), sessionToken: trimmed(options.sessionToken) || undefined };\n if (!credentials.accessKeyId || !credentials.secretAccessKey) throw new Error(\"ses() needs an accessKeyId and secretAccessKey\");\n const to = recipients(\"ses\", options);\n const url = `https://email.${options.region}.amazonaws.com/v2/email/outbound-emails`;\n const now = options.now ?? Date.now;\n return {\n name: \"ses\",\n async send(alert) {\n const email = composeEmail(alert, options, to);\n const body = JSON.stringify({\n FromEmailAddress: email.from,\n Destination: { ToAddresses: email.to },\n Content: {\n Simple: {\n Subject: { Data: email.subject, Charset: \"UTF-8\" },\n Body: { Text: { Data: email.text, Charset: \"UTF-8\" }, Html: { Data: email.html, Charset: \"UTF-8\" } },\n },\n },\n ...(options.configurationSetName ? { ConfigurationSetName: options.configurationSetName } : {}),\n EmailTags: [{ Name: \"source\", Value: \"cronwatch\" }],\n });\n const headers = await signV4(\n { method: \"POST\", url, headers: { \"content-type\": \"application/json\" }, body, region: options.region, service: \"ses\", now: now() },\n credentials,\n );\n await post(\"SES\", url, { headers, body }, [credentials.secretAccessKey, credentials.sessionToken]);\n },\n };\n}\n"]}
|
package/dist/slack.cjs
CHANGED
package/dist/slack.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/slack.ts"],"names":[],"mappings":"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","file":"slack.cjs","sourcesContent":["/**\n * Helpers shared by the provider channels. Only fetch and Web Crypto, so the\n * channels run on Node, Cloudflare Workers, Deno and Bun alike. Not an entry\n * point: each channel bundles its own copy.\n */\nimport { isoTime } from \"../duration.js\";\nimport type { Alert } from \"../types.js\";\n\nexport const TIMEOUT_MS = 10_000;\n\n/** Severity for trackers that have levels. Recovered is informational. */\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport function severity(type: Alert[\"type\"]): Severity {\n if (type === \"recovered\") return \"info\";\n if (type === \"slow\" || type === \"over_budget\") return \"warning\";\n return \"error\";\n}\n\n/** The scheme, host and port only. A URL's path or query can hold a credential. */\nexport function origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"(invalid URL)\";\n }\n}\n\n/**\n * The URL, once it is one fetch can post to. Refused without quoting it:\n * fetch's own error for a URL it cannot parse (\"Failed to parse URL from ...\")\n * quotes the whole URL, and a webhook URL's path is its credential.\n */\nexport function postable(url: string): string {\n let protocol: string;\n try {\n protocol = new URL(url).protocol;\n } catch {\n throw new Error(\"only http and https URLs can be posted to, not this URL\");\n }\n if (protocol !== \"http:\" && protocol !== \"https:\") throw new Error(`only http and https URLs can be posted to, not ${protocol}`);\n return url;\n}\n\n/** How much of a provider's error body goes into the error message. */\nexport const ERROR_BODY_MAX = 200;\n\n/**\n * POSTs and throws on a non-2xx answer. The error names the provider and the\n * URL's origin, plus the start of the response body with every secret the\n * channel holds cut out, in case a provider echoes one back. Secrets are cut\n * from a prefix long enough to hold one that starts inside the first\n * ERROR_BODY_MAX characters, and only then is it cut to that length, so no\n * part of a secret survives at the edge. A redirect is refused rather than\n * followed: the credential headers would go with it to wherever it points.\n */\nexport async function post(provider: string, url: string, init: { headers: Record<string, string>; body: string }, secrets: (string | undefined)[] = []): Promise<Response> {\n const response = await fetch(postable(url), { method: \"POST\", headers: init.headers, body: init.body, redirect: \"error\", signal: AbortSignal.timeout(TIMEOUT_MS) });\n if (!response.ok) {\n let text = \"\";\n try {\n text = await response.text();\n } catch {\n // The status is enough.\n }\n throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : \"\"}`);\n }\n return response;\n}\n\n/** The start of an error body, secrets cut out first, then cut to ERROR_BODY_MAX on a code point. */\nexport function errorBody(text: string, secrets: (string | undefined)[] = []): string {\n const kept = secrets.filter((s): s is string => typeof s === \"string\" && s.length >= 4);\n const longest = kept.reduce((n, s) => Math.max(n, s.length), 0);\n let head = cut(text, ERROR_BODY_MAX + longest);\n for (const secret of kept) head = head.split(secret).join(\"[redacted]\");\n return cut(head, ERROR_BODY_MAX);\n}\n\n/** A credential with the spaces and newlines a paste leaves around it taken off. Anything not a string is empty. */\nexport function trimmed(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Base64 of UTF-8, without Buffer. */\nexport function base64(text: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nexport function basicAuth(user: string, password: string): string {\n return `Basic ${base64(`${user}:${password}`)}`;\n}\n\nexport function hex(bytes: ArrayBuffer | Uint8Array): string {\n return Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport async function sha256Hex(text: string): Promise<string> {\n return hex(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text)));\n}\n\n/**\n * A stable 32 hex character id for one alert: the same job, type and time\n * always give the same id, so a provider that deduplicates on it drops a\n * resend of an alert it already took.\n */\nexport async function alertId(alert: Alert): Promise<string> {\n return (await sha256Hex(`${alert.job}\\n${alert.type}\\n${alert.at}`)).slice(0, 32);\n}\n\n/** The same id laid out as a UUID, for APIs that ask for one. */\nexport function asUuid(id: string): string {\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20, 32)}`;\n}\n\n/** Cuts to at most `max` UTF-16 units without splitting a surrogate pair. */\nexport function cut(text: string, max: number): string {\n if (text.length <= max) return text;\n let end = max;\n const code = text.charCodeAt(end - 1);\n if (code >= 0xd800 && code <= 0xdbff) end -= 1;\n return text.slice(0, end);\n}\n\n/** The run fields worth attaching to a tracker event. A start before the year 1 or after 9999 is null. */\nexport function runSummary(alert: Alert): Record<string, unknown> | null {\n const run = alert.run;\n if (!run) return null;\n return { id: run.id, status: run.status, startedAt: isoTime(run.startedAt), durationMs: run.durationMs, trigger: run.trigger };\n}\n\n/** Title, message, triage and link as one plain text block, the way every channel reads. */\nexport function plainText(alert: Alert, link: string | undefined): string {\n return [alert.title, \"\", alert.message, ...(alert.triage ? [\"\", `Triage: ${alert.triage}`] : []), ...(link ? [\"\", `Open: ${link}`] : [])].join(\"\\n\");\n}\n","import type { Alert, AlertChannel } from \"../types.js\";\nimport { postable } from \"./shared.js\";\n\nexport interface SlackOptions {\n /** An incoming webhook URL from api.slack.com/messaging/webhooks. */\n webhookUrl: 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\nconst EMOJI: Record<Alert[\"type\"], string> = {\n missed: \":hourglass_flowing_sand:\",\n failed: \":x:\",\n stuck: \":no_entry:\",\n slow: \":turtle:\",\n over_budget: \":moneybag:\",\n recovered: \":white_check_mark:\",\n};\n\nconst TIMEOUT_MS = 10_000;\n\n/** Sends alerts to a Slack channel through an incoming webhook. */\nexport function slack(options: SlackOptions): AlertChannel {\n if (!options.webhookUrl) throw new Error(\"slack() needs a webhookUrl\");\n return {\n name: \"slack\",\n async send(alert) {\n const url = options.link?.(alert);\n const title = `${EMOJI[alert.type]} *${escape(alert.title)}*${url ? ` (<${url}|open>)` : \"\"}`;\n const body = codeBlockSafe(escape(alert.message)).slice(0, 2900);\n const response = await fetch(postable(options.webhookUrl), {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n // Refused, not followed: a webhook URL is its own credential.\n redirect: \"error\",\n signal: AbortSignal.timeout(TIMEOUT_MS),\n body: JSON.stringify({\n // The notification fallback is parsed as mrkdwn too, so it is escaped like the blocks.\n text: escape(`${alert.title}\\n${alert.message}`),\n blocks: [\n { type: \"section\", text: { type: \"mrkdwn\", text: title } },\n { type: \"section\", text: { type: \"mrkdwn\", text: \"```\" + body + \"```\" } },\n // Its own block, so a long diagnosis cannot push a block past Slack's 3000 character limit.\n ...(alert.triage ? [{ type: \"section\", text: { type: \"mrkdwn\", text: `_Triage:_ ${escape(alert.triage)}`.slice(0, 3000) } }] : []),\n ],\n }),\n });\n if (!response.ok) throw new Error(`Slack webhook answered ${response.status}: ${(await response.text()).slice(0, 200)}`);\n },\n };\n}\n\n/** Slack's three control characters. Escaping < and > also stops <!channel> and <url|links>. */\nfunction escape(text: string): string {\n return text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\n/** Breaks up ``` so text inside a code block cannot close it. */\nfunction codeBlockSafe(text: string): string {\n return text.replace(/```/g, \"```\");\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/slack.ts"],"names":[],"mappings":"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","file":"slack.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","import type { Alert, AlertChannel } from \"../types.js\";\nimport { postable } from \"./shared.js\";\n\nexport interface SlackOptions {\n /** An incoming webhook URL from api.slack.com/messaging/webhooks. */\n webhookUrl: 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\nconst EMOJI: Record<Alert[\"type\"], string> = {\n missed: \":hourglass_flowing_sand:\",\n failed: \":x:\",\n stuck: \":no_entry:\",\n slow: \":turtle:\",\n over_budget: \":moneybag:\",\n under_floor: \":chart_with_downwards_trend:\",\n recovered: \":white_check_mark:\",\n};\n\nconst TIMEOUT_MS = 10_000;\n\n/** Sends alerts to a Slack channel through an incoming webhook. */\nexport function slack(options: SlackOptions): AlertChannel {\n if (!options.webhookUrl) throw new Error(\"slack() needs a webhookUrl\");\n return {\n name: \"slack\",\n async send(alert) {\n const url = options.link?.(alert);\n const title = `${EMOJI[alert.type]} *${escape(alert.title)}*${url ? ` (<${url}|open>)` : \"\"}`;\n const body = codeBlockSafe(escape(alert.message)).slice(0, 2900);\n const response = await fetch(postable(options.webhookUrl), {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n // Refused, not followed: a webhook URL is its own credential.\n redirect: \"error\",\n signal: AbortSignal.timeout(TIMEOUT_MS),\n body: JSON.stringify({\n // The notification fallback is parsed as mrkdwn too, so it is escaped like the blocks.\n text: escape(`${alert.title}\\n${alert.message}`),\n blocks: [\n { type: \"section\", text: { type: \"mrkdwn\", text: title } },\n { type: \"section\", text: { type: \"mrkdwn\", text: \"```\" + body + \"```\" } },\n // Its own block, so a long diagnosis cannot push a block past Slack's 3000 character limit.\n ...(alert.triage ? [{ type: \"section\", text: { type: \"mrkdwn\", text: `_Triage:_ ${escape(alert.triage)}`.slice(0, 3000) } }] : []),\n ],\n }),\n });\n if (!response.ok) throw new Error(`Slack webhook answered ${response.status}: ${(await response.text()).slice(0, 200)}`);\n },\n };\n}\n\n/** Slack's three control characters. Escaping < and > also stops <!channel> and <url|links>. */\nfunction escape(text: string): string {\n return text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\n/** Breaks up ``` so text inside a code block cannot close it. */\nfunction codeBlockSafe(text: string): string {\n return text.replace(/```/g, \"```\");\n}\n"]}
|
package/dist/slack.d.cts
CHANGED
package/dist/slack.d.ts
CHANGED
package/dist/slack.js
CHANGED
package/dist/slack.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/slack.ts"],"names":[],"mappings":"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","file":"slack.js","sourcesContent":["/**\n * Helpers shared by the provider channels. Only fetch and Web Crypto, so the\n * channels run on Node, Cloudflare Workers, Deno and Bun alike. Not an entry\n * point: each channel bundles its own copy.\n */\nimport { isoTime } from \"../duration.js\";\nimport type { Alert } from \"../types.js\";\n\nexport const TIMEOUT_MS = 10_000;\n\n/** Severity for trackers that have levels. Recovered is informational. */\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport function severity(type: Alert[\"type\"]): Severity {\n if (type === \"recovered\") return \"info\";\n if (type === \"slow\" || type === \"over_budget\") return \"warning\";\n return \"error\";\n}\n\n/** The scheme, host and port only. A URL's path or query can hold a credential. */\nexport function origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"(invalid URL)\";\n }\n}\n\n/**\n * The URL, once it is one fetch can post to. Refused without quoting it:\n * fetch's own error for a URL it cannot parse (\"Failed to parse URL from ...\")\n * quotes the whole URL, and a webhook URL's path is its credential.\n */\nexport function postable(url: string): string {\n let protocol: string;\n try {\n protocol = new URL(url).protocol;\n } catch {\n throw new Error(\"only http and https URLs can be posted to, not this URL\");\n }\n if (protocol !== \"http:\" && protocol !== \"https:\") throw new Error(`only http and https URLs can be posted to, not ${protocol}`);\n return url;\n}\n\n/** How much of a provider's error body goes into the error message. */\nexport const ERROR_BODY_MAX = 200;\n\n/**\n * POSTs and throws on a non-2xx answer. The error names the provider and the\n * URL's origin, plus the start of the response body with every secret the\n * channel holds cut out, in case a provider echoes one back. Secrets are cut\n * from a prefix long enough to hold one that starts inside the first\n * ERROR_BODY_MAX characters, and only then is it cut to that length, so no\n * part of a secret survives at the edge. A redirect is refused rather than\n * followed: the credential headers would go with it to wherever it points.\n */\nexport async function post(provider: string, url: string, init: { headers: Record<string, string>; body: string }, secrets: (string | undefined)[] = []): Promise<Response> {\n const response = await fetch(postable(url), { method: \"POST\", headers: init.headers, body: init.body, redirect: \"error\", signal: AbortSignal.timeout(TIMEOUT_MS) });\n if (!response.ok) {\n let text = \"\";\n try {\n text = await response.text();\n } catch {\n // The status is enough.\n }\n throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : \"\"}`);\n }\n return response;\n}\n\n/** The start of an error body, secrets cut out first, then cut to ERROR_BODY_MAX on a code point. */\nexport function errorBody(text: string, secrets: (string | undefined)[] = []): string {\n const kept = secrets.filter((s): s is string => typeof s === \"string\" && s.length >= 4);\n const longest = kept.reduce((n, s) => Math.max(n, s.length), 0);\n let head = cut(text, ERROR_BODY_MAX + longest);\n for (const secret of kept) head = head.split(secret).join(\"[redacted]\");\n return cut(head, ERROR_BODY_MAX);\n}\n\n/** A credential with the spaces and newlines a paste leaves around it taken off. Anything not a string is empty. */\nexport function trimmed(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Base64 of UTF-8, without Buffer. */\nexport function base64(text: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nexport function basicAuth(user: string, password: string): string {\n return `Basic ${base64(`${user}:${password}`)}`;\n}\n\nexport function hex(bytes: ArrayBuffer | Uint8Array): string {\n return Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport async function sha256Hex(text: string): Promise<string> {\n return hex(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text)));\n}\n\n/**\n * A stable 32 hex character id for one alert: the same job, type and time\n * always give the same id, so a provider that deduplicates on it drops a\n * resend of an alert it already took.\n */\nexport async function alertId(alert: Alert): Promise<string> {\n return (await sha256Hex(`${alert.job}\\n${alert.type}\\n${alert.at}`)).slice(0, 32);\n}\n\n/** The same id laid out as a UUID, for APIs that ask for one. */\nexport function asUuid(id: string): string {\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20, 32)}`;\n}\n\n/** Cuts to at most `max` UTF-16 units without splitting a surrogate pair. */\nexport function cut(text: string, max: number): string {\n if (text.length <= max) return text;\n let end = max;\n const code = text.charCodeAt(end - 1);\n if (code >= 0xd800 && code <= 0xdbff) end -= 1;\n return text.slice(0, end);\n}\n\n/** The run fields worth attaching to a tracker event. A start before the year 1 or after 9999 is null. */\nexport function runSummary(alert: Alert): Record<string, unknown> | null {\n const run = alert.run;\n if (!run) return null;\n return { id: run.id, status: run.status, startedAt: isoTime(run.startedAt), durationMs: run.durationMs, trigger: run.trigger };\n}\n\n/** Title, message, triage and link as one plain text block, the way every channel reads. */\nexport function plainText(alert: Alert, link: string | undefined): string {\n return [alert.title, \"\", alert.message, ...(alert.triage ? [\"\", `Triage: ${alert.triage}`] : []), ...(link ? [\"\", `Open: ${link}`] : [])].join(\"\\n\");\n}\n","import type { Alert, AlertChannel } from \"../types.js\";\nimport { postable } from \"./shared.js\";\n\nexport interface SlackOptions {\n /** An incoming webhook URL from api.slack.com/messaging/webhooks. */\n webhookUrl: 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\nconst EMOJI: Record<Alert[\"type\"], string> = {\n missed: \":hourglass_flowing_sand:\",\n failed: \":x:\",\n stuck: \":no_entry:\",\n slow: \":turtle:\",\n over_budget: \":moneybag:\",\n recovered: \":white_check_mark:\",\n};\n\nconst TIMEOUT_MS = 10_000;\n\n/** Sends alerts to a Slack channel through an incoming webhook. */\nexport function slack(options: SlackOptions): AlertChannel {\n if (!options.webhookUrl) throw new Error(\"slack() needs a webhookUrl\");\n return {\n name: \"slack\",\n async send(alert) {\n const url = options.link?.(alert);\n const title = `${EMOJI[alert.type]} *${escape(alert.title)}*${url ? ` (<${url}|open>)` : \"\"}`;\n const body = codeBlockSafe(escape(alert.message)).slice(0, 2900);\n const response = await fetch(postable(options.webhookUrl), {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n // Refused, not followed: a webhook URL is its own credential.\n redirect: \"error\",\n signal: AbortSignal.timeout(TIMEOUT_MS),\n body: JSON.stringify({\n // The notification fallback is parsed as mrkdwn too, so it is escaped like the blocks.\n text: escape(`${alert.title}\\n${alert.message}`),\n blocks: [\n { type: \"section\", text: { type: \"mrkdwn\", text: title } },\n { type: \"section\", text: { type: \"mrkdwn\", text: \"```\" + body + \"```\" } },\n // Its own block, so a long diagnosis cannot push a block past Slack's 3000 character limit.\n ...(alert.triage ? [{ type: \"section\", text: { type: \"mrkdwn\", text: `_Triage:_ ${escape(alert.triage)}`.slice(0, 3000) } }] : []),\n ],\n }),\n });\n if (!response.ok) throw new Error(`Slack webhook answered ${response.status}: ${(await response.text()).slice(0, 200)}`);\n },\n };\n}\n\n/** Slack's three control characters. Escaping < and > also stops <!channel> and <url|links>. */\nfunction escape(text: string): string {\n return text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\n/** Breaks up ``` so text inside a code block cannot close it. */\nfunction codeBlockSafe(text: string): string {\n return text.replace(/```/g, \"```\");\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/slack.ts"],"names":[],"mappings":"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","file":"slack.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","import type { Alert, AlertChannel } from \"../types.js\";\nimport { postable } from \"./shared.js\";\n\nexport interface SlackOptions {\n /** An incoming webhook URL from api.slack.com/messaging/webhooks. */\n webhookUrl: 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\nconst EMOJI: Record<Alert[\"type\"], string> = {\n missed: \":hourglass_flowing_sand:\",\n failed: \":x:\",\n stuck: \":no_entry:\",\n slow: \":turtle:\",\n over_budget: \":moneybag:\",\n under_floor: \":chart_with_downwards_trend:\",\n recovered: \":white_check_mark:\",\n};\n\nconst TIMEOUT_MS = 10_000;\n\n/** Sends alerts to a Slack channel through an incoming webhook. */\nexport function slack(options: SlackOptions): AlertChannel {\n if (!options.webhookUrl) throw new Error(\"slack() needs a webhookUrl\");\n return {\n name: \"slack\",\n async send(alert) {\n const url = options.link?.(alert);\n const title = `${EMOJI[alert.type]} *${escape(alert.title)}*${url ? ` (<${url}|open>)` : \"\"}`;\n const body = codeBlockSafe(escape(alert.message)).slice(0, 2900);\n const response = await fetch(postable(options.webhookUrl), {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n // Refused, not followed: a webhook URL is its own credential.\n redirect: \"error\",\n signal: AbortSignal.timeout(TIMEOUT_MS),\n body: JSON.stringify({\n // The notification fallback is parsed as mrkdwn too, so it is escaped like the blocks.\n text: escape(`${alert.title}\\n${alert.message}`),\n blocks: [\n { type: \"section\", text: { type: \"mrkdwn\", text: title } },\n { type: \"section\", text: { type: \"mrkdwn\", text: \"```\" + body + \"```\" } },\n // Its own block, so a long diagnosis cannot push a block past Slack's 3000 character limit.\n ...(alert.triage ? [{ type: \"section\", text: { type: \"mrkdwn\", text: `_Triage:_ ${escape(alert.triage)}`.slice(0, 3000) } }] : []),\n ],\n }),\n });\n if (!response.ok) throw new Error(`Slack webhook answered ${response.status}: ${(await response.text()).slice(0, 200)}`);\n },\n };\n}\n\n/** Slack's three control characters. Escaping < and > also stops <!channel> and <url|links>. */\nfunction escape(text: string): string {\n return text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\n/** Breaks up ``` so text inside a code block cannot close it. */\nfunction codeBlockSafe(text: string): string {\n return text.replace(/```/g, \"```\");\n}\n"]}
|
package/dist/sqlite.d.cts
CHANGED
package/dist/sqlite.d.ts
CHANGED
package/dist/twilio.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/twilio.ts","../src/entries/twilio.ts"],"names":["MAX_SEGMENTS","MAX_BODY","smsSegments","smsBody"],"mappings":"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","file":"twilio.cjs","sourcesContent":["/**\n * Helpers shared by the provider channels. Only fetch and Web Crypto, so the\n * channels run on Node, Cloudflare Workers, Deno and Bun alike. Not an entry\n * point: each channel bundles its own copy.\n */\nimport { isoTime } from \"../duration.js\";\nimport type { Alert } from \"../types.js\";\n\nexport const TIMEOUT_MS = 10_000;\n\n/** Severity for trackers that have levels. Recovered is informational. */\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport function severity(type: Alert[\"type\"]): Severity {\n if (type === \"recovered\") return \"info\";\n if (type === \"slow\" || type === \"over_budget\") return \"warning\";\n return \"error\";\n}\n\n/** The scheme, host and port only. A URL's path or query can hold a credential. */\nexport function origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"(invalid URL)\";\n }\n}\n\n/**\n * The URL, once it is one fetch can post to. Refused without quoting it:\n * fetch's own error for a URL it cannot parse (\"Failed to parse URL from ...\")\n * quotes the whole URL, and a webhook URL's path is its credential.\n */\nexport function postable(url: string): string {\n let protocol: string;\n try {\n protocol = new URL(url).protocol;\n } catch {\n throw new Error(\"only http and https URLs can be posted to, not this URL\");\n }\n if (protocol !== \"http:\" && protocol !== \"https:\") throw new Error(`only http and https URLs can be posted to, not ${protocol}`);\n return url;\n}\n\n/** How much of a provider's error body goes into the error message. */\nexport const ERROR_BODY_MAX = 200;\n\n/**\n * POSTs and throws on a non-2xx answer. The error names the provider and the\n * URL's origin, plus the start of the response body with every secret the\n * channel holds cut out, in case a provider echoes one back. Secrets are cut\n * from a prefix long enough to hold one that starts inside the first\n * ERROR_BODY_MAX characters, and only then is it cut to that length, so no\n * part of a secret survives at the edge. A redirect is refused rather than\n * followed: the credential headers would go with it to wherever it points.\n */\nexport async function post(provider: string, url: string, init: { headers: Record<string, string>; body: string }, secrets: (string | undefined)[] = []): Promise<Response> {\n const response = await fetch(postable(url), { method: \"POST\", headers: init.headers, body: init.body, redirect: \"error\", signal: AbortSignal.timeout(TIMEOUT_MS) });\n if (!response.ok) {\n let text = \"\";\n try {\n text = await response.text();\n } catch {\n // The status is enough.\n }\n throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : \"\"}`);\n }\n return response;\n}\n\n/** The start of an error body, secrets cut out first, then cut to ERROR_BODY_MAX on a code point. */\nexport function errorBody(text: string, secrets: (string | undefined)[] = []): string {\n const kept = secrets.filter((s): s is string => typeof s === \"string\" && s.length >= 4);\n const longest = kept.reduce((n, s) => Math.max(n, s.length), 0);\n let head = cut(text, ERROR_BODY_MAX + longest);\n for (const secret of kept) head = head.split(secret).join(\"[redacted]\");\n return cut(head, ERROR_BODY_MAX);\n}\n\n/** A credential with the spaces and newlines a paste leaves around it taken off. Anything not a string is empty. */\nexport function trimmed(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Base64 of UTF-8, without Buffer. */\nexport function base64(text: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nexport function basicAuth(user: string, password: string): string {\n return `Basic ${base64(`${user}:${password}`)}`;\n}\n\nexport function hex(bytes: ArrayBuffer | Uint8Array): string {\n return Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport async function sha256Hex(text: string): Promise<string> {\n return hex(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text)));\n}\n\n/**\n * A stable 32 hex character id for one alert: the same job, type and time\n * always give the same id, so a provider that deduplicates on it drops a\n * resend of an alert it already took.\n */\nexport async function alertId(alert: Alert): Promise<string> {\n return (await sha256Hex(`${alert.job}\\n${alert.type}\\n${alert.at}`)).slice(0, 32);\n}\n\n/** The same id laid out as a UUID, for APIs that ask for one. */\nexport function asUuid(id: string): string {\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20, 32)}`;\n}\n\n/** Cuts to at most `max` UTF-16 units without splitting a surrogate pair. */\nexport function cut(text: string, max: number): string {\n if (text.length <= max) return text;\n let end = max;\n const code = text.charCodeAt(end - 1);\n if (code >= 0xd800 && code <= 0xdbff) end -= 1;\n return text.slice(0, end);\n}\n\n/** The run fields worth attaching to a tracker event. A start before the year 1 or after 9999 is null. */\nexport function runSummary(alert: Alert): Record<string, unknown> | null {\n const run = alert.run;\n if (!run) return null;\n return { id: run.id, status: run.status, startedAt: isoTime(run.startedAt), durationMs: run.durationMs, trigger: run.trigger };\n}\n\n/** Title, message, triage and link as one plain text block, the way every channel reads. */\nexport function plainText(alert: Alert, link: string | undefined): string {\n return [alert.title, \"\", alert.message, ...(alert.triage ? [\"\", `Triage: ${alert.triage}`] : []), ...(link ? [\"\", `Open: ${link}`] : [])].join(\"\\n\");\n}\n","/**\n * Twilio SMS. API reference: https://www.twilio.com/docs/messaging/api/message-resource\n * POST https://api.twilio.com/2010-04-01/Accounts/<AccountSid>/Messages.json,\n * form encoded, with basic auth. One recipient per request.\n */\nimport type { Alert, AlertChannel } from \"../types.js\";\nimport { basicAuth, cut, post, trimmed } from \"./shared.js\";\n\nexport interface TwilioOptions {\n /** The account SID, \"AC...\". It is in the URL whichever credentials sign the request. */\n accountSid: string;\n /** The account's auth token. Or pass apiKeySid and apiKeySecret instead. */\n authToken?: string;\n /** An API key SID, \"SK...\", with apiKeySecret, in place of the auth token. */\n apiKeySid?: string;\n apiKeySecret?: string;\n /** A Twilio number in E.164 form, \"+15005550006\". Or pass messagingServiceSid. */\n from?: string;\n /** A messaging service SID, \"MG...\", in place of from. */\n messagingServiceSid?: string;\n /** One number in E.164 form, or several; each gets its own message. */\n to: string | string[];\n /** Also text when a job recovers. Defaults to false: a text is for what needs a person. */\n recovered?: boolean;\n /** How many SMS segments a message may use, 1 to 10. Defaults to 3. */\n segments?: number;\n link?: (alert: Alert) => string;\n}\n\n/** The most segments a message may use, which keeps it inside Twilio's 1600 character Body limit. */\nexport const MAX_SEGMENTS = 10;\n/** Twilio refuses a Body longer than this. */\nexport const MAX_BODY = 1600;\n\n/**\n * Texts alerts through Twilio, to every number at once. The alert counts as\n * delivered when any number took it; each number that refused it is\n * reported to the client's onError. It fails only when every number did.\n */\nexport function twilio(options: TwilioOptions): AlertChannel {\n // A pasted credential often carries a stray space or newline, which the Authorization header would refuse or send.\n const accountSid = trimmed(options.accountSid);\n if (!accountSid) throw new Error(\"twilio() needs an accountSid\");\n const apiKeySid = trimmed(options.apiKeySid);\n const user = apiKeySid || accountSid;\n const password = apiKeySid ? trimmed(options.apiKeySecret) : trimmed(options.authToken);\n if (!password) throw new Error(\"twilio() needs an authToken, or an apiKeySid and apiKeySecret\");\n if (!options.from && !options.messagingServiceSid) throw new Error(\"twilio() needs a from number or a messagingServiceSid\");\n const to = (Array.isArray(options.to) ? options.to : [options.to]).filter((n) => typeof n === \"string\" && n.trim() !== \"\").map((n) => n.trim());\n if (to.length === 0) throw new Error(\"twilio() needs at least one to number\");\n const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(accountSid)}/Messages.json`;\n const authorization = basicAuth(user, password);\n const segments = segmentBudget(options.segments);\n return {\n name: \"twilio\",\n async send(alert, context) {\n if (alert.type === \"recovered\" && !options.recovered) return;\n const body = smsBody(alert, options.link?.(alert), segments);\n const results = await Promise.allSettled(to.map((number) => {\n const form = new URLSearchParams();\n form.append(\"To\", number);\n if (options.messagingServiceSid) form.append(\"MessagingServiceSid\", options.messagingServiceSid);\n else form.append(\"From\", options.from!);\n form.append(\"Body\", body);\n return post(\"Twilio\", url, { headers: { \"content-type\": \"application/x-www-form-urlencoded\", authorization }, body: form.toString() }, [password]);\n }));\n const failed = results.flatMap((r, i) => (r.status === \"rejected\" ? [{ number: to[i]!, error: r.reason as Error }] : []));\n if (failed.length === 0) return;\n if (failed.length === to.length) {\n const message = failed[0]!.error.message;\n throw new Error(to.length > 1 ? `${message} (${failed.length} of ${to.length} numbers failed)` : message);\n }\n // Delivered to someone: counted as sent, so a retry never texts the numbers that took it again.\n for (const { number, error } of failed) {\n const report = new Error(`${error.message} (to ${maskNumber(number)}; ${to.length - failed.length} of ${to.length} numbers took the alert)`);\n if (context) context.onError(report);\n else console.error(\"[cronwatch] alert channel twilio:\", report);\n }\n },\n };\n}\n\n/** A number with all but its last four digits hidden, for an error message. */\nfunction maskNumber(number: string): string {\n return number.length <= 4 ? number : `${\"*\".repeat(Math.min(number.length - 4, 8))}${number.slice(-4)}`;\n}\n\n// The GSM 03.38 alphabet: a message in it takes 153 characters a segment\n// (when split), anything else is UCS-2 at 67. The extension table costs two.\nconst GSM = \"@£$¥èéùìòÇ\\nØø\\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\\\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà\";\nconst GSM_EXTENDED = \"^{}\\\\[~]|€\\f\";\n\n/**\n * The segments `text` takes. A character is never split across two: an\n * extension character (two septets) or a surrogate pair (two UCS-2 units)\n * that would straddle a boundary starts the next segment, as phones pack them.\n */\nexport function smsSegments(text: string): number {\n const units: number[] = [];\n let gsm = true;\n for (const ch of text) {\n if (GSM.includes(ch)) units.push(1);\n else if (GSM_EXTENDED.includes(ch)) units.push(2);\n else {\n gsm = false;\n break;\n }\n }\n const [single, per, sizes] = gsm ? [160, 153, units] : [70, 67, Array.from(text, (ch) => ch.length)];\n const total = sizes.reduce((n, u) => n + u, 0);\n if (total <= single) return 1;\n let count = 1;\n let used = 0;\n for (const u of sizes) {\n if (used + u > per) {\n count += 1;\n used = 0;\n }\n used += u;\n }\n return count;\n}\n\n/** Fits `text` within `segments` SMS segments and Twilio's Body limit. */\nfunction fits(text: string, segments: number): boolean {\n return text.length <= MAX_BODY && smsSegments(text) <= segments;\n}\n\n/**\n * The title, then as many lines of the message (and the triage) as fit, then\n * the link. The link is kept whole; the text before it is cut to make room.\n * `segments` is clamped to 1 to 10.\n */\nexport function smsBody(alert: Alert, link: string | undefined, segments = 3): string {\n const budget = segmentBudget(segments);\n const tail = link ? `\\n${link}` : \"\";\n const lines = [alert.title, ...alert.message.split(\"\\n\").filter((l) => l.trim() !== \"\"), ...(alert.triage ? [`Triage: ${alert.triage}`] : [])];\n let text = \"\";\n for (const line of lines) {\n const next = text ? `${text}\\n${line}` : line;\n if (fits(next + tail, budget)) {\n text = next;\n continue;\n }\n // Part of this line, cut on a code point and marked.\n const chars = Array.from(line);\n let lo = 0;\n let hi = chars.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n const candidate = (text ? `${text}\\n` : \"\") + chars.slice(0, mid).join(\"\") + \"...\";\n if (fits(candidate + tail, budget)) lo = mid;\n else hi = mid - 1;\n }\n if (lo > 0) text = (text ? `${text}\\n` : \"\") + chars.slice(0, lo).join(\"\") + \"...\";\n break;\n }\n // Only a link too long for any budget gets here too long; Twilio would refuse it whole.\n return cut(text + tail, MAX_BODY);\n}\n\n/** A segment count clamped to 1 to MAX_SEGMENTS; 3 for anything not a number. */\nfunction segmentBudget(segments: number | undefined): number {\n const n = typeof segments === \"number\" && Number.isFinite(segments) ? Math.floor(segments) : 3;\n return Math.min(MAX_SEGMENTS, Math.max(1, n));\n}\n","// @cronwatch/sdk/twilio: what the entry point exports. The helpers the\n// channel is built from are internal; the names below are kept, deprecated,\n// for anyone who imported them, and are no longer exported from 1.0.\nimport * as internal from \"../alerts/twilio.js\";\n\nexport { twilio } from \"../alerts/twilio.js\";\nexport type { TwilioOptions } from \"../alerts/twilio.js\";\n\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const MAX_SEGMENTS = internal.MAX_SEGMENTS;\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const MAX_BODY = internal.MAX_BODY;\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const smsSegments = internal.smsSegments;\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const smsBody = internal.smsBody;\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/twilio.ts","../src/entries/twilio.ts"],"names":["MAX_SEGMENTS","MAX_BODY","smsSegments","smsBody"],"mappings":"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","file":"twilio.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 * Twilio SMS. API reference: https://www.twilio.com/docs/messaging/api/message-resource\n * POST https://api.twilio.com/2010-04-01/Accounts/<AccountSid>/Messages.json,\n * form encoded, with basic auth. One recipient per request.\n */\nimport type { Alert, AlertChannel } from \"../types.js\";\nimport { basicAuth, cut, post, trimmed } from \"./shared.js\";\n\nexport interface TwilioOptions {\n /** The account SID, \"AC...\". It is in the URL whichever credentials sign the request. */\n accountSid: string;\n /** The account's auth token. Or pass apiKeySid and apiKeySecret instead. */\n authToken?: string;\n /** An API key SID, \"SK...\", with apiKeySecret, in place of the auth token. */\n apiKeySid?: string;\n apiKeySecret?: string;\n /** A Twilio number in E.164 form, \"+15005550006\". Or pass messagingServiceSid. */\n from?: string;\n /** A messaging service SID, \"MG...\", in place of from. */\n messagingServiceSid?: string;\n /** One number in E.164 form, or several; each gets its own message. */\n to: string | string[];\n /** Also text when a job recovers. Defaults to false: a text is for what needs a person. */\n recovered?: boolean;\n /** How many SMS segments a message may use, 1 to 10. Defaults to 3. */\n segments?: number;\n link?: (alert: Alert) => string;\n}\n\n/** The most segments a message may use, which keeps it inside Twilio's 1600 character Body limit. */\nexport const MAX_SEGMENTS = 10;\n/** Twilio refuses a Body longer than this. */\nexport const MAX_BODY = 1600;\n\n/**\n * Texts alerts through Twilio, to every number at once. The alert counts as\n * delivered when any number took it; each number that refused it is\n * reported to the client's onError. It fails only when every number did.\n */\nexport function twilio(options: TwilioOptions): AlertChannel {\n // A pasted credential often carries a stray space or newline, which the Authorization header would refuse or send.\n const accountSid = trimmed(options.accountSid);\n if (!accountSid) throw new Error(\"twilio() needs an accountSid\");\n const apiKeySid = trimmed(options.apiKeySid);\n const user = apiKeySid || accountSid;\n const password = apiKeySid ? trimmed(options.apiKeySecret) : trimmed(options.authToken);\n if (!password) throw new Error(\"twilio() needs an authToken, or an apiKeySid and apiKeySecret\");\n if (!options.from && !options.messagingServiceSid) throw new Error(\"twilio() needs a from number or a messagingServiceSid\");\n const to = (Array.isArray(options.to) ? options.to : [options.to]).filter((n) => typeof n === \"string\" && n.trim() !== \"\").map((n) => n.trim());\n if (to.length === 0) throw new Error(\"twilio() needs at least one to number\");\n const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(accountSid)}/Messages.json`;\n const authorization = basicAuth(user, password);\n const segments = segmentBudget(options.segments);\n return {\n name: \"twilio\",\n async send(alert, context) {\n if (alert.type === \"recovered\" && !options.recovered) return;\n const body = smsBody(alert, options.link?.(alert), segments);\n const results = await Promise.allSettled(to.map((number) => {\n const form = new URLSearchParams();\n form.append(\"To\", number);\n if (options.messagingServiceSid) form.append(\"MessagingServiceSid\", options.messagingServiceSid);\n else form.append(\"From\", options.from!);\n form.append(\"Body\", body);\n return post(\"Twilio\", url, { headers: { \"content-type\": \"application/x-www-form-urlencoded\", authorization }, body: form.toString() }, [password]);\n }));\n const failed = results.flatMap((r, i) => (r.status === \"rejected\" ? [{ number: to[i]!, error: r.reason as Error }] : []));\n if (failed.length === 0) return;\n if (failed.length === to.length) {\n const message = failed[0]!.error.message;\n throw new Error(to.length > 1 ? `${message} (${failed.length} of ${to.length} numbers failed)` : message);\n }\n // Delivered to someone: counted as sent, so a retry never texts the numbers that took it again.\n for (const { number, error } of failed) {\n const report = new Error(`${error.message} (to ${maskNumber(number)}; ${to.length - failed.length} of ${to.length} numbers took the alert)`);\n if (context) context.onError(report);\n else console.error(\"[cronwatch] alert channel twilio:\", report);\n }\n },\n };\n}\n\n/** A number with all but its last four digits hidden, for an error message. */\nfunction maskNumber(number: string): string {\n return number.length <= 4 ? number : `${\"*\".repeat(Math.min(number.length - 4, 8))}${number.slice(-4)}`;\n}\n\n// The GSM 03.38 alphabet: a message in it takes 153 characters a segment\n// (when split), anything else is UCS-2 at 67. The extension table costs two.\nconst GSM = \"@£$¥èéùìòÇ\\nØø\\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\\\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà\";\nconst GSM_EXTENDED = \"^{}\\\\[~]|€\\f\";\n\n/**\n * The segments `text` takes. A character is never split across two: an\n * extension character (two septets) or a surrogate pair (two UCS-2 units)\n * that would straddle a boundary starts the next segment, as phones pack them.\n */\nexport function smsSegments(text: string): number {\n const units: number[] = [];\n let gsm = true;\n for (const ch of text) {\n if (GSM.includes(ch)) units.push(1);\n else if (GSM_EXTENDED.includes(ch)) units.push(2);\n else {\n gsm = false;\n break;\n }\n }\n const [single, per, sizes] = gsm ? [160, 153, units] : [70, 67, Array.from(text, (ch) => ch.length)];\n const total = sizes.reduce((n, u) => n + u, 0);\n if (total <= single) return 1;\n let count = 1;\n let used = 0;\n for (const u of sizes) {\n if (used + u > per) {\n count += 1;\n used = 0;\n }\n used += u;\n }\n return count;\n}\n\n/** Fits `text` within `segments` SMS segments and Twilio's Body limit. */\nfunction fits(text: string, segments: number): boolean {\n return text.length <= MAX_BODY && smsSegments(text) <= segments;\n}\n\n/**\n * The title, then as many lines of the message (and the triage) as fit, then\n * the link. The link is kept whole; the text before it is cut to make room.\n * `segments` is clamped to 1 to 10.\n */\nexport function smsBody(alert: Alert, link: string | undefined, segments = 3): string {\n const budget = segmentBudget(segments);\n const tail = link ? `\\n${link}` : \"\";\n const lines = [alert.title, ...alert.message.split(\"\\n\").filter((l) => l.trim() !== \"\"), ...(alert.triage ? [`Triage: ${alert.triage}`] : [])];\n let text = \"\";\n for (const line of lines) {\n const next = text ? `${text}\\n${line}` : line;\n if (fits(next + tail, budget)) {\n text = next;\n continue;\n }\n // Part of this line, cut on a code point and marked.\n const chars = Array.from(line);\n let lo = 0;\n let hi = chars.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n const candidate = (text ? `${text}\\n` : \"\") + chars.slice(0, mid).join(\"\") + \"...\";\n if (fits(candidate + tail, budget)) lo = mid;\n else hi = mid - 1;\n }\n if (lo > 0) text = (text ? `${text}\\n` : \"\") + chars.slice(0, lo).join(\"\") + \"...\";\n break;\n }\n // Only a link too long for any budget gets here too long; Twilio would refuse it whole.\n return cut(text + tail, MAX_BODY);\n}\n\n/** A segment count clamped to 1 to MAX_SEGMENTS; 3 for anything not a number. */\nfunction segmentBudget(segments: number | undefined): number {\n const n = typeof segments === \"number\" && Number.isFinite(segments) ? Math.floor(segments) : 3;\n return Math.min(MAX_SEGMENTS, Math.max(1, n));\n}\n","// @cronwatch/sdk/twilio: what the entry point exports. The helpers the\n// channel is built from are internal; the names below are kept, deprecated,\n// for anyone who imported them, and are no longer exported from 1.0.\nimport * as internal from \"../alerts/twilio.js\";\n\nexport { twilio } from \"../alerts/twilio.js\";\nexport type { TwilioOptions } from \"../alerts/twilio.js\";\n\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const MAX_SEGMENTS = internal.MAX_SEGMENTS;\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const MAX_BODY = internal.MAX_BODY;\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const smsSegments = internal.smsSegments;\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const smsBody = internal.smsBody;\n"]}
|
package/dist/twilio.d.cts
CHANGED
package/dist/twilio.d.ts
CHANGED
package/dist/twilio.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/twilio.ts","../src/entries/twilio.ts"],"names":["MAX_SEGMENTS","MAX_BODY","smsSegments","smsBody"],"mappings":"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","file":"twilio.js","sourcesContent":["/**\n * Helpers shared by the provider channels. Only fetch and Web Crypto, so the\n * channels run on Node, Cloudflare Workers, Deno and Bun alike. Not an entry\n * point: each channel bundles its own copy.\n */\nimport { isoTime } from \"../duration.js\";\nimport type { Alert } from \"../types.js\";\n\nexport const TIMEOUT_MS = 10_000;\n\n/** Severity for trackers that have levels. Recovered is informational. */\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport function severity(type: Alert[\"type\"]): Severity {\n if (type === \"recovered\") return \"info\";\n if (type === \"slow\" || type === \"over_budget\") return \"warning\";\n return \"error\";\n}\n\n/** The scheme, host and port only. A URL's path or query can hold a credential. */\nexport function origin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"(invalid URL)\";\n }\n}\n\n/**\n * The URL, once it is one fetch can post to. Refused without quoting it:\n * fetch's own error for a URL it cannot parse (\"Failed to parse URL from ...\")\n * quotes the whole URL, and a webhook URL's path is its credential.\n */\nexport function postable(url: string): string {\n let protocol: string;\n try {\n protocol = new URL(url).protocol;\n } catch {\n throw new Error(\"only http and https URLs can be posted to, not this URL\");\n }\n if (protocol !== \"http:\" && protocol !== \"https:\") throw new Error(`only http and https URLs can be posted to, not ${protocol}`);\n return url;\n}\n\n/** How much of a provider's error body goes into the error message. */\nexport const ERROR_BODY_MAX = 200;\n\n/**\n * POSTs and throws on a non-2xx answer. The error names the provider and the\n * URL's origin, plus the start of the response body with every secret the\n * channel holds cut out, in case a provider echoes one back. Secrets are cut\n * from a prefix long enough to hold one that starts inside the first\n * ERROR_BODY_MAX characters, and only then is it cut to that length, so no\n * part of a secret survives at the edge. A redirect is refused rather than\n * followed: the credential headers would go with it to wherever it points.\n */\nexport async function post(provider: string, url: string, init: { headers: Record<string, string>; body: string }, secrets: (string | undefined)[] = []): Promise<Response> {\n const response = await fetch(postable(url), { method: \"POST\", headers: init.headers, body: init.body, redirect: \"error\", signal: AbortSignal.timeout(TIMEOUT_MS) });\n if (!response.ok) {\n let text = \"\";\n try {\n text = await response.text();\n } catch {\n // The status is enough.\n }\n throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : \"\"}`);\n }\n return response;\n}\n\n/** The start of an error body, secrets cut out first, then cut to ERROR_BODY_MAX on a code point. */\nexport function errorBody(text: string, secrets: (string | undefined)[] = []): string {\n const kept = secrets.filter((s): s is string => typeof s === \"string\" && s.length >= 4);\n const longest = kept.reduce((n, s) => Math.max(n, s.length), 0);\n let head = cut(text, ERROR_BODY_MAX + longest);\n for (const secret of kept) head = head.split(secret).join(\"[redacted]\");\n return cut(head, ERROR_BODY_MAX);\n}\n\n/** A credential with the spaces and newlines a paste leaves around it taken off. Anything not a string is empty. */\nexport function trimmed(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\n/** Base64 of UTF-8, without Buffer. */\nexport function base64(text: string): string {\n let binary = \"\";\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nexport function basicAuth(user: string, password: string): string {\n return `Basic ${base64(`${user}:${password}`)}`;\n}\n\nexport function hex(bytes: ArrayBuffer | Uint8Array): string {\n return Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport async function sha256Hex(text: string): Promise<string> {\n return hex(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(text)));\n}\n\n/**\n * A stable 32 hex character id for one alert: the same job, type and time\n * always give the same id, so a provider that deduplicates on it drops a\n * resend of an alert it already took.\n */\nexport async function alertId(alert: Alert): Promise<string> {\n return (await sha256Hex(`${alert.job}\\n${alert.type}\\n${alert.at}`)).slice(0, 32);\n}\n\n/** The same id laid out as a UUID, for APIs that ask for one. */\nexport function asUuid(id: string): string {\n return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20, 32)}`;\n}\n\n/** Cuts to at most `max` UTF-16 units without splitting a surrogate pair. */\nexport function cut(text: string, max: number): string {\n if (text.length <= max) return text;\n let end = max;\n const code = text.charCodeAt(end - 1);\n if (code >= 0xd800 && code <= 0xdbff) end -= 1;\n return text.slice(0, end);\n}\n\n/** The run fields worth attaching to a tracker event. A start before the year 1 or after 9999 is null. */\nexport function runSummary(alert: Alert): Record<string, unknown> | null {\n const run = alert.run;\n if (!run) return null;\n return { id: run.id, status: run.status, startedAt: isoTime(run.startedAt), durationMs: run.durationMs, trigger: run.trigger };\n}\n\n/** Title, message, triage and link as one plain text block, the way every channel reads. */\nexport function plainText(alert: Alert, link: string | undefined): string {\n return [alert.title, \"\", alert.message, ...(alert.triage ? [\"\", `Triage: ${alert.triage}`] : []), ...(link ? [\"\", `Open: ${link}`] : [])].join(\"\\n\");\n}\n","/**\n * Twilio SMS. API reference: https://www.twilio.com/docs/messaging/api/message-resource\n * POST https://api.twilio.com/2010-04-01/Accounts/<AccountSid>/Messages.json,\n * form encoded, with basic auth. One recipient per request.\n */\nimport type { Alert, AlertChannel } from \"../types.js\";\nimport { basicAuth, cut, post, trimmed } from \"./shared.js\";\n\nexport interface TwilioOptions {\n /** The account SID, \"AC...\". It is in the URL whichever credentials sign the request. */\n accountSid: string;\n /** The account's auth token. Or pass apiKeySid and apiKeySecret instead. */\n authToken?: string;\n /** An API key SID, \"SK...\", with apiKeySecret, in place of the auth token. */\n apiKeySid?: string;\n apiKeySecret?: string;\n /** A Twilio number in E.164 form, \"+15005550006\". Or pass messagingServiceSid. */\n from?: string;\n /** A messaging service SID, \"MG...\", in place of from. */\n messagingServiceSid?: string;\n /** One number in E.164 form, or several; each gets its own message. */\n to: string | string[];\n /** Also text when a job recovers. Defaults to false: a text is for what needs a person. */\n recovered?: boolean;\n /** How many SMS segments a message may use, 1 to 10. Defaults to 3. */\n segments?: number;\n link?: (alert: Alert) => string;\n}\n\n/** The most segments a message may use, which keeps it inside Twilio's 1600 character Body limit. */\nexport const MAX_SEGMENTS = 10;\n/** Twilio refuses a Body longer than this. */\nexport const MAX_BODY = 1600;\n\n/**\n * Texts alerts through Twilio, to every number at once. The alert counts as\n * delivered when any number took it; each number that refused it is\n * reported to the client's onError. It fails only when every number did.\n */\nexport function twilio(options: TwilioOptions): AlertChannel {\n // A pasted credential often carries a stray space or newline, which the Authorization header would refuse or send.\n const accountSid = trimmed(options.accountSid);\n if (!accountSid) throw new Error(\"twilio() needs an accountSid\");\n const apiKeySid = trimmed(options.apiKeySid);\n const user = apiKeySid || accountSid;\n const password = apiKeySid ? trimmed(options.apiKeySecret) : trimmed(options.authToken);\n if (!password) throw new Error(\"twilio() needs an authToken, or an apiKeySid and apiKeySecret\");\n if (!options.from && !options.messagingServiceSid) throw new Error(\"twilio() needs a from number or a messagingServiceSid\");\n const to = (Array.isArray(options.to) ? options.to : [options.to]).filter((n) => typeof n === \"string\" && n.trim() !== \"\").map((n) => n.trim());\n if (to.length === 0) throw new Error(\"twilio() needs at least one to number\");\n const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(accountSid)}/Messages.json`;\n const authorization = basicAuth(user, password);\n const segments = segmentBudget(options.segments);\n return {\n name: \"twilio\",\n async send(alert, context) {\n if (alert.type === \"recovered\" && !options.recovered) return;\n const body = smsBody(alert, options.link?.(alert), segments);\n const results = await Promise.allSettled(to.map((number) => {\n const form = new URLSearchParams();\n form.append(\"To\", number);\n if (options.messagingServiceSid) form.append(\"MessagingServiceSid\", options.messagingServiceSid);\n else form.append(\"From\", options.from!);\n form.append(\"Body\", body);\n return post(\"Twilio\", url, { headers: { \"content-type\": \"application/x-www-form-urlencoded\", authorization }, body: form.toString() }, [password]);\n }));\n const failed = results.flatMap((r, i) => (r.status === \"rejected\" ? [{ number: to[i]!, error: r.reason as Error }] : []));\n if (failed.length === 0) return;\n if (failed.length === to.length) {\n const message = failed[0]!.error.message;\n throw new Error(to.length > 1 ? `${message} (${failed.length} of ${to.length} numbers failed)` : message);\n }\n // Delivered to someone: counted as sent, so a retry never texts the numbers that took it again.\n for (const { number, error } of failed) {\n const report = new Error(`${error.message} (to ${maskNumber(number)}; ${to.length - failed.length} of ${to.length} numbers took the alert)`);\n if (context) context.onError(report);\n else console.error(\"[cronwatch] alert channel twilio:\", report);\n }\n },\n };\n}\n\n/** A number with all but its last four digits hidden, for an error message. */\nfunction maskNumber(number: string): string {\n return number.length <= 4 ? number : `${\"*\".repeat(Math.min(number.length - 4, 8))}${number.slice(-4)}`;\n}\n\n// The GSM 03.38 alphabet: a message in it takes 153 characters a segment\n// (when split), anything else is UCS-2 at 67. The extension table costs two.\nconst GSM = \"@£$¥èéùìòÇ\\nØø\\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\\\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà\";\nconst GSM_EXTENDED = \"^{}\\\\[~]|€\\f\";\n\n/**\n * The segments `text` takes. A character is never split across two: an\n * extension character (two septets) or a surrogate pair (two UCS-2 units)\n * that would straddle a boundary starts the next segment, as phones pack them.\n */\nexport function smsSegments(text: string): number {\n const units: number[] = [];\n let gsm = true;\n for (const ch of text) {\n if (GSM.includes(ch)) units.push(1);\n else if (GSM_EXTENDED.includes(ch)) units.push(2);\n else {\n gsm = false;\n break;\n }\n }\n const [single, per, sizes] = gsm ? [160, 153, units] : [70, 67, Array.from(text, (ch) => ch.length)];\n const total = sizes.reduce((n, u) => n + u, 0);\n if (total <= single) return 1;\n let count = 1;\n let used = 0;\n for (const u of sizes) {\n if (used + u > per) {\n count += 1;\n used = 0;\n }\n used += u;\n }\n return count;\n}\n\n/** Fits `text` within `segments` SMS segments and Twilio's Body limit. */\nfunction fits(text: string, segments: number): boolean {\n return text.length <= MAX_BODY && smsSegments(text) <= segments;\n}\n\n/**\n * The title, then as many lines of the message (and the triage) as fit, then\n * the link. The link is kept whole; the text before it is cut to make room.\n * `segments` is clamped to 1 to 10.\n */\nexport function smsBody(alert: Alert, link: string | undefined, segments = 3): string {\n const budget = segmentBudget(segments);\n const tail = link ? `\\n${link}` : \"\";\n const lines = [alert.title, ...alert.message.split(\"\\n\").filter((l) => l.trim() !== \"\"), ...(alert.triage ? [`Triage: ${alert.triage}`] : [])];\n let text = \"\";\n for (const line of lines) {\n const next = text ? `${text}\\n${line}` : line;\n if (fits(next + tail, budget)) {\n text = next;\n continue;\n }\n // Part of this line, cut on a code point and marked.\n const chars = Array.from(line);\n let lo = 0;\n let hi = chars.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n const candidate = (text ? `${text}\\n` : \"\") + chars.slice(0, mid).join(\"\") + \"...\";\n if (fits(candidate + tail, budget)) lo = mid;\n else hi = mid - 1;\n }\n if (lo > 0) text = (text ? `${text}\\n` : \"\") + chars.slice(0, lo).join(\"\") + \"...\";\n break;\n }\n // Only a link too long for any budget gets here too long; Twilio would refuse it whole.\n return cut(text + tail, MAX_BODY);\n}\n\n/** A segment count clamped to 1 to MAX_SEGMENTS; 3 for anything not a number. */\nfunction segmentBudget(segments: number | undefined): number {\n const n = typeof segments === \"number\" && Number.isFinite(segments) ? Math.floor(segments) : 3;\n return Math.min(MAX_SEGMENTS, Math.max(1, n));\n}\n","// @cronwatch/sdk/twilio: what the entry point exports. The helpers the\n// channel is built from are internal; the names below are kept, deprecated,\n// for anyone who imported them, and are no longer exported from 1.0.\nimport * as internal from \"../alerts/twilio.js\";\n\nexport { twilio } from \"../alerts/twilio.js\";\nexport type { TwilioOptions } from \"../alerts/twilio.js\";\n\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const MAX_SEGMENTS = internal.MAX_SEGMENTS;\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const MAX_BODY = internal.MAX_BODY;\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const smsSegments = internal.smsSegments;\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const smsBody = internal.smsBody;\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/twilio.ts","../src/entries/twilio.ts"],"names":["MAX_SEGMENTS","MAX_BODY","smsSegments","smsBody"],"mappings":"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","file":"twilio.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 * Twilio SMS. API reference: https://www.twilio.com/docs/messaging/api/message-resource\n * POST https://api.twilio.com/2010-04-01/Accounts/<AccountSid>/Messages.json,\n * form encoded, with basic auth. One recipient per request.\n */\nimport type { Alert, AlertChannel } from \"../types.js\";\nimport { basicAuth, cut, post, trimmed } from \"./shared.js\";\n\nexport interface TwilioOptions {\n /** The account SID, \"AC...\". It is in the URL whichever credentials sign the request. */\n accountSid: string;\n /** The account's auth token. Or pass apiKeySid and apiKeySecret instead. */\n authToken?: string;\n /** An API key SID, \"SK...\", with apiKeySecret, in place of the auth token. */\n apiKeySid?: string;\n apiKeySecret?: string;\n /** A Twilio number in E.164 form, \"+15005550006\". Or pass messagingServiceSid. */\n from?: string;\n /** A messaging service SID, \"MG...\", in place of from. */\n messagingServiceSid?: string;\n /** One number in E.164 form, or several; each gets its own message. */\n to: string | string[];\n /** Also text when a job recovers. Defaults to false: a text is for what needs a person. */\n recovered?: boolean;\n /** How many SMS segments a message may use, 1 to 10. Defaults to 3. */\n segments?: number;\n link?: (alert: Alert) => string;\n}\n\n/** The most segments a message may use, which keeps it inside Twilio's 1600 character Body limit. */\nexport const MAX_SEGMENTS = 10;\n/** Twilio refuses a Body longer than this. */\nexport const MAX_BODY = 1600;\n\n/**\n * Texts alerts through Twilio, to every number at once. The alert counts as\n * delivered when any number took it; each number that refused it is\n * reported to the client's onError. It fails only when every number did.\n */\nexport function twilio(options: TwilioOptions): AlertChannel {\n // A pasted credential often carries a stray space or newline, which the Authorization header would refuse or send.\n const accountSid = trimmed(options.accountSid);\n if (!accountSid) throw new Error(\"twilio() needs an accountSid\");\n const apiKeySid = trimmed(options.apiKeySid);\n const user = apiKeySid || accountSid;\n const password = apiKeySid ? trimmed(options.apiKeySecret) : trimmed(options.authToken);\n if (!password) throw new Error(\"twilio() needs an authToken, or an apiKeySid and apiKeySecret\");\n if (!options.from && !options.messagingServiceSid) throw new Error(\"twilio() needs a from number or a messagingServiceSid\");\n const to = (Array.isArray(options.to) ? options.to : [options.to]).filter((n) => typeof n === \"string\" && n.trim() !== \"\").map((n) => n.trim());\n if (to.length === 0) throw new Error(\"twilio() needs at least one to number\");\n const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(accountSid)}/Messages.json`;\n const authorization = basicAuth(user, password);\n const segments = segmentBudget(options.segments);\n return {\n name: \"twilio\",\n async send(alert, context) {\n if (alert.type === \"recovered\" && !options.recovered) return;\n const body = smsBody(alert, options.link?.(alert), segments);\n const results = await Promise.allSettled(to.map((number) => {\n const form = new URLSearchParams();\n form.append(\"To\", number);\n if (options.messagingServiceSid) form.append(\"MessagingServiceSid\", options.messagingServiceSid);\n else form.append(\"From\", options.from!);\n form.append(\"Body\", body);\n return post(\"Twilio\", url, { headers: { \"content-type\": \"application/x-www-form-urlencoded\", authorization }, body: form.toString() }, [password]);\n }));\n const failed = results.flatMap((r, i) => (r.status === \"rejected\" ? [{ number: to[i]!, error: r.reason as Error }] : []));\n if (failed.length === 0) return;\n if (failed.length === to.length) {\n const message = failed[0]!.error.message;\n throw new Error(to.length > 1 ? `${message} (${failed.length} of ${to.length} numbers failed)` : message);\n }\n // Delivered to someone: counted as sent, so a retry never texts the numbers that took it again.\n for (const { number, error } of failed) {\n const report = new Error(`${error.message} (to ${maskNumber(number)}; ${to.length - failed.length} of ${to.length} numbers took the alert)`);\n if (context) context.onError(report);\n else console.error(\"[cronwatch] alert channel twilio:\", report);\n }\n },\n };\n}\n\n/** A number with all but its last four digits hidden, for an error message. */\nfunction maskNumber(number: string): string {\n return number.length <= 4 ? number : `${\"*\".repeat(Math.min(number.length - 4, 8))}${number.slice(-4)}`;\n}\n\n// The GSM 03.38 alphabet: a message in it takes 153 characters a segment\n// (when split), anything else is UCS-2 at 67. The extension table costs two.\nconst GSM = \"@£$¥èéùìòÇ\\nØø\\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\\\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà\";\nconst GSM_EXTENDED = \"^{}\\\\[~]|€\\f\";\n\n/**\n * The segments `text` takes. A character is never split across two: an\n * extension character (two septets) or a surrogate pair (two UCS-2 units)\n * that would straddle a boundary starts the next segment, as phones pack them.\n */\nexport function smsSegments(text: string): number {\n const units: number[] = [];\n let gsm = true;\n for (const ch of text) {\n if (GSM.includes(ch)) units.push(1);\n else if (GSM_EXTENDED.includes(ch)) units.push(2);\n else {\n gsm = false;\n break;\n }\n }\n const [single, per, sizes] = gsm ? [160, 153, units] : [70, 67, Array.from(text, (ch) => ch.length)];\n const total = sizes.reduce((n, u) => n + u, 0);\n if (total <= single) return 1;\n let count = 1;\n let used = 0;\n for (const u of sizes) {\n if (used + u > per) {\n count += 1;\n used = 0;\n }\n used += u;\n }\n return count;\n}\n\n/** Fits `text` within `segments` SMS segments and Twilio's Body limit. */\nfunction fits(text: string, segments: number): boolean {\n return text.length <= MAX_BODY && smsSegments(text) <= segments;\n}\n\n/**\n * The title, then as many lines of the message (and the triage) as fit, then\n * the link. The link is kept whole; the text before it is cut to make room.\n * `segments` is clamped to 1 to 10.\n */\nexport function smsBody(alert: Alert, link: string | undefined, segments = 3): string {\n const budget = segmentBudget(segments);\n const tail = link ? `\\n${link}` : \"\";\n const lines = [alert.title, ...alert.message.split(\"\\n\").filter((l) => l.trim() !== \"\"), ...(alert.triage ? [`Triage: ${alert.triage}`] : [])];\n let text = \"\";\n for (const line of lines) {\n const next = text ? `${text}\\n${line}` : line;\n if (fits(next + tail, budget)) {\n text = next;\n continue;\n }\n // Part of this line, cut on a code point and marked.\n const chars = Array.from(line);\n let lo = 0;\n let hi = chars.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n const candidate = (text ? `${text}\\n` : \"\") + chars.slice(0, mid).join(\"\") + \"...\";\n if (fits(candidate + tail, budget)) lo = mid;\n else hi = mid - 1;\n }\n if (lo > 0) text = (text ? `${text}\\n` : \"\") + chars.slice(0, lo).join(\"\") + \"...\";\n break;\n }\n // Only a link too long for any budget gets here too long; Twilio would refuse it whole.\n return cut(text + tail, MAX_BODY);\n}\n\n/** A segment count clamped to 1 to MAX_SEGMENTS; 3 for anything not a number. */\nfunction segmentBudget(segments: number | undefined): number {\n const n = typeof segments === \"number\" && Number.isFinite(segments) ? Math.floor(segments) : 3;\n return Math.min(MAX_SEGMENTS, Math.max(1, n));\n}\n","// @cronwatch/sdk/twilio: what the entry point exports. The helpers the\n// channel is built from are internal; the names below are kept, deprecated,\n// for anyone who imported them, and are no longer exported from 1.0.\nimport * as internal from \"../alerts/twilio.js\";\n\nexport { twilio } from \"../alerts/twilio.js\";\nexport type { TwilioOptions } from \"../alerts/twilio.js\";\n\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const MAX_SEGMENTS = internal.MAX_SEGMENTS;\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const MAX_BODY = internal.MAX_BODY;\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const smsSegments = internal.smsSegments;\n/** @deprecated Internal to the Twilio channel; no longer exported from 1.0. */\nexport const smsBody = internal.smsBody;\n"]}
|
|
@@ -33,6 +33,14 @@ interface JobOptions {
|
|
|
33
33
|
* five runs to compare against.
|
|
34
34
|
*/
|
|
35
35
|
budget?: Record<string, number>;
|
|
36
|
+
/**
|
|
37
|
+
* Floors for metrics reported with job.metric(). { rows: 1 } alerts when a
|
|
38
|
+
* run reports rows below 1. Metrics without a floor alert when a run
|
|
39
|
+
* reports 0 or less and the five to twenty successful runs before it all
|
|
40
|
+
* reported more than 0. Catches the job that ran cleanly and wrote nothing.
|
|
41
|
+
* A floor of 0 lets a metric reach 0 without alerting.
|
|
42
|
+
*/
|
|
43
|
+
floor?: Record<string, number>;
|
|
36
44
|
/**
|
|
37
45
|
* A successful run must produce output that satisfies this, or it counts as
|
|
38
46
|
* failed. A string must appear in the output, a RegExp must match it, and a
|
|
@@ -83,7 +91,7 @@ interface Run {
|
|
|
83
91
|
*/
|
|
84
92
|
trigger: string;
|
|
85
93
|
}
|
|
86
|
-
type Condition = "missed" | "failed" | "stuck" | "slow" | "over_budget";
|
|
94
|
+
type Condition = "missed" | "failed" | "stuck" | "slow" | "over_budget" | "under_floor";
|
|
87
95
|
interface JobState {
|
|
88
96
|
job: string;
|
|
89
97
|
/** Conditions currently open, with the time each one opened. */
|
|
@@ -108,6 +116,12 @@ interface JobState {
|
|
|
108
116
|
* Absent when empty, and in state written before this field existed.
|
|
109
117
|
*/
|
|
110
118
|
sending?: SendingAlert[];
|
|
119
|
+
/**
|
|
120
|
+
* The metrics under their floor at the job's last successful run (see
|
|
121
|
+
* floorBreaches). Absent when none, and in state written before this field
|
|
122
|
+
* existed.
|
|
123
|
+
*/
|
|
124
|
+
underFloor?: string[];
|
|
111
125
|
/**
|
|
112
126
|
* Goes up by one on every write, so a store can refuse a write made from a
|
|
113
127
|
* stale read (see Store.compareAndSetState). Absent counts as 0.
|
|
@@ -145,6 +159,10 @@ interface AlertDetails {
|
|
|
145
159
|
over_budget: {
|
|
146
160
|
breaches: BudgetBreach[];
|
|
147
161
|
};
|
|
162
|
+
/** `limit` is the floor, or for a metric without one the lowest of the earlier runs it was judged against. */
|
|
163
|
+
under_floor: {
|
|
164
|
+
breaches: BudgetBreach[];
|
|
165
|
+
};
|
|
148
166
|
/**
|
|
149
167
|
* `after` names the conditions that closed. A recovery with `reason`
|
|
150
168
|
* "unscheduled" closes missed alone because the job no longer has a
|