@cronwatch/sdk 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/dist/anthropic.cjs +3 -3
  2. package/dist/anthropic.cjs.map +1 -1
  3. package/dist/anthropic.d.cts +1 -1
  4. package/dist/anthropic.d.ts +1 -1
  5. package/dist/anthropic.js +3 -3
  6. package/dist/anthropic.js.map +1 -1
  7. package/dist/bugsnag.cjs +28 -5
  8. package/dist/bugsnag.cjs.map +1 -1
  9. package/dist/bugsnag.d.cts +1 -1
  10. package/dist/bugsnag.d.ts +1 -1
  11. package/dist/bugsnag.js +28 -5
  12. package/dist/bugsnag.js.map +1 -1
  13. package/dist/{client-DAJH7YiG.d.ts → client-C8j7B2Bo.d.ts} +31 -19
  14. package/dist/{client-z1LdR_m2.d.cts → client-CHjrNHH-.d.cts} +31 -19
  15. package/dist/d1.cjs.map +1 -1
  16. package/dist/d1.d.cts +2 -2
  17. package/dist/d1.d.ts +2 -2
  18. package/dist/d1.js.map +1 -1
  19. package/dist/datadog.cjs +28 -5
  20. package/dist/datadog.cjs.map +1 -1
  21. package/dist/datadog.d.cts +1 -1
  22. package/dist/datadog.d.ts +1 -1
  23. package/dist/datadog.js +28 -5
  24. package/dist/datadog.js.map +1 -1
  25. package/dist/discord.cjs +28 -1
  26. package/dist/discord.cjs.map +1 -1
  27. package/dist/discord.d.cts +1 -1
  28. package/dist/discord.d.ts +1 -1
  29. package/dist/discord.js +28 -1
  30. package/dist/discord.js.map +1 -1
  31. package/dist/{email-CCUzlLe9.d.cts → email-D7SfsU0F.d.ts} +2 -2
  32. package/dist/{email-CFtDqlvZ.d.ts → email-zSazJClH.d.cts} +2 -2
  33. package/dist/honeybadger.cjs +28 -5
  34. package/dist/honeybadger.cjs.map +1 -1
  35. package/dist/honeybadger.d.cts +1 -1
  36. package/dist/honeybadger.d.ts +1 -1
  37. package/dist/honeybadger.js +28 -5
  38. package/dist/honeybadger.js.map +1 -1
  39. package/dist/index.cjs +249 -84
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +11 -7
  42. package/dist/index.d.ts +11 -7
  43. package/dist/index.js +249 -84
  44. package/dist/index.js.map +1 -1
  45. package/dist/mailgun.cjs +28 -5
  46. package/dist/mailgun.cjs.map +1 -1
  47. package/dist/mailgun.d.cts +2 -2
  48. package/dist/mailgun.d.ts +2 -2
  49. package/dist/mailgun.js +28 -5
  50. package/dist/mailgun.js.map +1 -1
  51. package/dist/newrelic.cjs +28 -5
  52. package/dist/newrelic.cjs.map +1 -1
  53. package/dist/newrelic.d.cts +1 -1
  54. package/dist/newrelic.d.ts +1 -1
  55. package/dist/newrelic.js +28 -5
  56. package/dist/newrelic.js.map +1 -1
  57. package/dist/pg-cron.cjs.map +1 -1
  58. package/dist/pg-cron.d.cts +2 -2
  59. package/dist/pg-cron.d.ts +2 -2
  60. package/dist/pg-cron.js.map +1 -1
  61. package/dist/postgres.cjs.map +1 -1
  62. package/dist/postgres.d.cts +2 -2
  63. package/dist/postgres.d.ts +2 -2
  64. package/dist/postgres.js.map +1 -1
  65. package/dist/postmark.cjs +28 -5
  66. package/dist/postmark.cjs.map +1 -1
  67. package/dist/postmark.d.cts +2 -2
  68. package/dist/postmark.d.ts +2 -2
  69. package/dist/postmark.js +28 -5
  70. package/dist/postmark.js.map +1 -1
  71. package/dist/resend.cjs +28 -5
  72. package/dist/resend.cjs.map +1 -1
  73. package/dist/resend.d.cts +2 -2
  74. package/dist/resend.d.ts +2 -2
  75. package/dist/resend.js +28 -5
  76. package/dist/resend.js.map +1 -1
  77. package/dist/rollbar.cjs +28 -5
  78. package/dist/rollbar.cjs.map +1 -1
  79. package/dist/rollbar.d.cts +1 -1
  80. package/dist/rollbar.d.ts +1 -1
  81. package/dist/rollbar.js +28 -5
  82. package/dist/rollbar.js.map +1 -1
  83. package/dist/sendgrid.cjs +28 -5
  84. package/dist/sendgrid.cjs.map +1 -1
  85. package/dist/sendgrid.d.cts +2 -2
  86. package/dist/sendgrid.d.ts +2 -2
  87. package/dist/sendgrid.js +28 -5
  88. package/dist/sendgrid.js.map +1 -1
  89. package/dist/sentry.cjs +28 -5
  90. package/dist/sentry.cjs.map +1 -1
  91. package/dist/sentry.d.cts +1 -1
  92. package/dist/sentry.d.ts +1 -1
  93. package/dist/sentry.js +28 -5
  94. package/dist/sentry.js.map +1 -1
  95. package/dist/ses.cjs +28 -5
  96. package/dist/ses.cjs.map +1 -1
  97. package/dist/ses.d.cts +2 -2
  98. package/dist/ses.d.ts +2 -2
  99. package/dist/ses.js +28 -5
  100. package/dist/ses.js.map +1 -1
  101. package/dist/slack.cjs +32 -2
  102. package/dist/slack.cjs.map +1 -1
  103. package/dist/slack.d.cts +6 -2
  104. package/dist/slack.d.ts +6 -2
  105. package/dist/slack.js +32 -2
  106. package/dist/slack.js.map +1 -1
  107. package/dist/sqlite.cjs.map +1 -1
  108. package/dist/sqlite.d.cts +2 -2
  109. package/dist/sqlite.d.ts +2 -2
  110. package/dist/sqlite.js.map +1 -1
  111. package/dist/twilio.cjs +28 -5
  112. package/dist/twilio.cjs.map +1 -1
  113. package/dist/twilio.d.cts +1 -1
  114. package/dist/twilio.d.ts +1 -1
  115. package/dist/twilio.js +28 -5
  116. package/dist/twilio.js.map +1 -1
  117. package/dist/{types-D6t0Z846.d.cts → types-B8IFVK5k.d.cts} +23 -7
  118. package/dist/{types-D6t0Z846.d.ts → types-B8IFVK5k.d.ts} +23 -7
  119. package/dist/webhook.cjs +10 -2
  120. package/dist/webhook.cjs.map +1 -1
  121. package/dist/webhook.d.cts +24 -6
  122. package/dist/webhook.d.ts +24 -6
  123. package/dist/webhook.js +10 -2
  124. package/dist/webhook.js.map +1 -1
  125. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { J as JobOptions, R as Run, A as Alert, S as Store, a as AlertChannel, o as TriageFn, D as Duration, h as JobDefinition, f as CheckResult, k as JobSummary, j as JobState } from './types-D6t0Z846.cjs';
1
+ import { J as JobOptions, R as Run, A as Alert, S as Store, a as AlertChannel, o as TriageFn, D as Duration, h as JobDefinition, f as CheckResult, k as JobSummary, j as JobState } from './types-B8IFVK5k.cjs';
2
2
 
3
3
  /** What a job function receives. */
4
4
  interface JobContext {
@@ -23,10 +23,10 @@ interface RoutesOptions {
23
23
  * only whitespace, given here or in the variable, counts as unset, and any
24
24
  * other type (false, a number) throws.
25
25
  * With no token in development (the first of CRONWATCH_ENV, APP_ENV, and
26
- * NODE_ENV that is set names "development", "dev", "local", "test", or
27
- * "testing"), the routes make a random one and print a sign-in link to the
28
- * server log on their first request; with no token otherwise they answer
29
- * 503. Pass `null` to opt out
26
+ * NODE_ENV that is set names "development", "dev", or "local"; "test" and
27
+ * "testing" are not development), the routes make a random one and print a
28
+ * sign-in link to the server log on their first request; with no token
29
+ * otherwise they answer 503. Pass `null` to opt out
30
30
  * and serve them open everywhere, for example behind your own auth.
31
31
  *
32
32
  * The check endpoint (/api/check) also accepts the client's cronSecret, so
@@ -82,8 +82,9 @@ interface HandlerOptions {
82
82
  * whitespace, counts as unset; any other type (false, a number) throws.
83
83
  * With no secret at all the handler answers 503 unless the app is in
84
84
  * development (the first of CRONWATCH_ENV, APP_ENV, and NODE_ENV that is
85
- * set names "development", "dev", "local", "test", or "testing"). Pass
86
- * null to allow anyone.
85
+ * set names "development", "dev", or "local"; "test" and "testing" are not
86
+ * development, so a test run or CI needs a secret too). Pass null to allow
87
+ * anyone.
87
88
  */
88
89
  secret?: string | null;
89
90
  }
@@ -448,7 +449,7 @@ declare class Cronwatch {
448
449
  private history;
449
450
  /**
450
451
  * Look for missed and stuck runs across every job, send alerts, retry
451
- * alerts no channel accepted, and prune old runs. Call it from an interval
452
+ * alerts to the channels that refused them, and prune old runs. Call it from an interval
452
453
  * (start()), a cron hitting the mounted routes, or by hand. Concurrent
453
454
  * calls share one check.
454
455
  */
@@ -536,24 +537,35 @@ declare class Cronwatch {
536
537
  */
537
538
  private dispatch;
538
539
  /**
539
- * Send the alerts that no channel accepted last time, once each, oldest
540
- * first. They are claimed first (claimUndelivered): moved into `sending`
541
- * with a lease, in one conditional write, so when several processes check
542
- * at once only the one whose write lands sends them. An alert that no
543
- * longer describes the job (staleAlert) is dropped instead. Retries across
544
- * a check share RETRY_BUDGET_MS of wall-clock time; claimed alerts it has
545
- * no time left for go back to the queue for the next check.
540
+ * Send the alerts some channel refused last time, once each, oldest
541
+ * first, to the channels that still owe them (HeldAlert.owed) and no
542
+ * others, from the copy held in state, its run trimmed (heldRun). They
543
+ * are claimed first (claimUndelivered): moved into `sending` with a lease,
544
+ * in one conditional write, so when several processes check at once only
545
+ * the one whose write lands sends them. An alert that no longer describes
546
+ * the job (staleAlert) is dropped instead. Retries across a check share
547
+ * RETRY_BUDGET_MS of wall-clock time; claimed alerts it has no time left
548
+ * for go back to the queue for the next check. Returns the alerts some
549
+ * channel took on this attempt.
546
550
  */
547
551
  private retryUndelivered;
548
552
  /**
549
553
  * Mark delivered alerts done, drop stale ones, and keep failed ones for the
550
554
  * next check, taking them all out of `sending` (recordSent). A failed alert
551
- * replaces its stored copy, so a triage made on this attempt is kept.
552
- * lastAlertAt moves only on a delivery. When this write fails, alerts
553
- * still in `sending` are retried once their lease runs out.
555
+ * replaces its stored copy, so a triage made on this attempt and the
556
+ * channels that still owe it are kept. lastAlertAt moves when `reached`,
557
+ * some channel accepted an alert on this attempt. When this write fails,
558
+ * alerts still in `sending` are retried once their lease runs out.
554
559
  */
555
560
  private recordDelivery;
556
- /** Send to every channel at once. True when at least one accepted it, or there are none. */
561
+ /**
562
+ * Send to every channel that still owes the alert (owedChannels: all of
563
+ * them for an alert not sent before), at once, each failure reported under
564
+ * its channel's name. `left` is the alert with `owed` cut to the channels
565
+ * that refused it, or null when none did (or none is left to owe it);
566
+ * `reached` is whether any channel accepted it. With no channels at all it
567
+ * counts as delivered, as it always has.
568
+ */
557
569
  private deliver;
558
570
  /** Sets alert.triage to the diagnosis, or to null when there is none, so it is tried once per alert. */
559
571
  private addTriage;
package/dist/d1.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/output.ts","../src/stores/sql.ts","../src/stores/d1.ts"],"names":["text","json"],"mappings":"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","file":"d1.cjs","sourcesContent":["/** Output is capped so a chatty job cannot fill the store. The tail is kept. */\nexport const OUTPUT_CAP = 16 * 1024;\n\n/**\n * NUL characters are removed first, since Postgres refuses them in TEXT and\n * JSONB and the whole run row would be lost. The cap then applies to what is\n * left.\n */\nexport function capOutput(text: string): string {\n const clean = stripNul(text);\n if (clean.length <= OUTPUT_CAP) return clean;\n return TRIMMED + clean.slice(clean.length - OUTPUT_CAP);\n}\n\nconst TRIMMED = \"[earlier output trimmed]\\n\";\n\n/**\n * How much text before the kept tail redaction reads, and never keeps: three\n * times the longest secret a default pattern can match (a PEM key's 16 KB body\n * with its header and footer, under OUTPUT_CAP + 1024), since a replacement\n * grows what it replaces at most threefold.\n */\nexport const REDACT_EDGE = 3 * (OUTPUT_CAP + 1024);\n\n/**\n * Output or an error as it is stored: redacted, then capped like capOutput,\n * so the cut cannot fall inside a secret and keep what follows its label.\n * Text of at most OUTPUT_CAP + REDACT_EDGE is redacted whole. Longer text is\n * cut to that many units from its end first, and after redacting, the first\n * REDACT_EDGE units are never kept: a secret whose label fell before that cut\n * is left out with them. NULs go before and after `redact`.\n */\nexport function redactAndCap(text: string, redact: (text: string) => string): string {\n const clean = stripNul(text);\n const from = clean.length - (OUTPUT_CAP + REDACT_EDGE);\n if (from <= 0) return capOutput(redact(clean));\n const redacted = stripNul(redact(clean.slice(from)));\n return TRIMMED + redacted.slice(Math.max(redacted.length - OUTPUT_CAP, REDACT_EDGE));\n}\n\n/** Removes every U+0000. */\nexport function stripNul(text: string): string {\n return text.includes(\"\\u0000\") ? text.replace(/\\u0000/g, \"\") : text;\n}\n\n/**\n * Removes every U+0000 from JSON text, keys and strings alike, by dropping\n * each \\u0000 escape (a NUL can appear in JSON no other way). Escapes are\n * read left to right in pairs, so an escaped backslash followed by \"u0000\"\n * is left as it is.\n */\nexport function stripJsonNul(json: string): string {\n return json.includes(\"\\\\u0000\") ? json.replace(/\\\\(u0000|[\\s\\S])/g, (escape, next: string) => (next === \"u0000\" ? \"\" : escape)) : json;\n}\n\n/** \"Name: message\" and the first five stack frames, capped like output. */\nexport function errorMessage(error: unknown): string {\n return capOutput(describeError(error));\n}\n\n/** \"Name: message\" and the first five stack frames, not capped: see redactAndCap. */\nexport function describeError(error: unknown): string {\n if (error instanceof Error) {\n // The stack repeats the header (over several lines when the message has\n // newlines), so take only its frames.\n const frames = (error.stack ?? \"\").split(\"\\n\").filter((line) => /^\\s+at /.test(line)).slice(0, 5);\n return `${error.name}: ${error.message}${frames.length ? `\\n${frames.join(\"\\n\")}` : \"\"}`;\n }\n if (typeof error === \"string\") return error;\n try {\n return JSON.stringify(error) ?? String(error);\n } catch {\n return String(error);\n }\n}\n\nconst REDACTED = \"[redacted]\";\n\n// Bounded quantifiers throughout, so a long line cannot make these backtrack.\n// They apply in this order, each to the text the ones before it left.\nconst SECRET_PATTERNS: [RegExp, string | ((match: string, ...groups: string[]) => string)][] = [\n // A PEM private key, header to footer. Without a footer (the output was\n // trimmed) it runs to the end of the base64 body. A \"-\" that starts five\n // dashes ends the body, so the footer is never swallowed into it.\n [\n /-----BEGIN (?:[A-Z0-9]{1,20} ){0,3}PRIVATE KEY-----(?:[A-Za-z0-9+/=\\s,:]|-(?!----)){0,16384}(?:-----END (?:[A-Z0-9]{1,20} ){0,3}PRIVATE KEY-----)?/g,\n REDACTED,\n ],\n // password=..., API_KEY: ..., \"client_secret\": \"...\", TOKEN='...', token=...,\n // :password=>\"...\" (but not max_tokens: 800). A quoted value is blanked to\n // its closing quote, spaces and all, and keeps its quotes.\n [\n /\\b([A-Za-z0-9_-]{0,40}(?:secret|token|passw(?:or)?d|pwd|api[_-]?key|access[_-]?key|private[_-]?key|credential)[A-Za-z0-9_-]{0,40}(?<![Tt][Oo][Kk][Ee][Nn][Ss])\"?\\s{0,3}(?:=>|[=:])\\s{0,3})(?:(\")[^\"\\n]{1,4096}\"|(')[^'\\n]{1,4096}'|[\"']?[^\\s\"',;&]{1,4096})/gi,\n (_match, name: string, double?: string, single?: string) => {\n const quote = double ?? single ?? \"\";\n return `${name}${quote}${REDACTED}${quote}`;\n },\n ],\n // Authorization: Basic <base64> and Authorization: Token <token>, also as a JSON or hash entry.\n [/\\b((?:proxy-)?authorization[\"']?\\s{0,3}(?:=>|[=:])\\s{0,3}[\"']?\\s{0,3}(?:basic|token)\\s{1,3})[A-Za-z0-9._~+/=:-]{1,4096}/gi, `$1${REDACTED}`],\n // Credentials inside a URL: postgres://user:password@host. The password\n // runs to the last \"@\" before a \"/\" or a space, so one that contains \"@\"\n // is blanked whole.\n [/(\\b[a-z][a-z0-9+.-]{0,30}:\\/\\/[^\\s/:@]{0,256}:)[^\\s/]{1,256}@/gi, `$1${REDACTED}@`],\n // Authorization: Bearer <token>\n [/\\b(Bearer\\s{1,3})[A-Za-z0-9._~+/=-]{8,4096}/g, `$1${REDACTED}`],\n // A bare JWT: three base64url segments, the first starting eyJ.\n [/\\beyJ[A-Za-z0-9_-]{4,4096}\\.[A-Za-z0-9_-]{4,4096}\\.[A-Za-z0-9_-]{0,4096}/g, REDACTED],\n // Incoming webhook URLs carry their secret in the path.\n [/(\\bhooks\\.slack\\.com\\/(?:services|workflows|triggers)\\/)[A-Za-z0-9/_-]{1,255}/gi, `$1${REDACTED}`],\n [/(\\bdiscord(?:app)?\\.com\\/api\\/(?:v\\d{1,2}\\/)?webhooks\\/)[A-Za-z0-9/_-]{1,255}/gi, `$1${REDACTED}`],\n // Well-known token shapes: AWS, GitHub, Slack, Stripe, Anthropic, OpenAI, and Google style keys.\n [/\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b/g, REDACTED],\n [/\\b(?:gh[pousr]_[A-Za-z0-9]{30,255}|github_pat_[A-Za-z0-9_]{20,255})\\b/g, REDACTED],\n [/\\bxox[abposr]-[A-Za-z0-9-]{10,255}/g, REDACTED],\n [/\\b[rsp]k_(?:live|test)_[A-Za-z0-9]{10,255}\\b/g, REDACTED],\n [/\\bwhsec_[A-Za-z0-9+/=]{16,255}/g, REDACTED],\n [/\\bsk-[A-Za-z0-9_-]{20,255}/g, REDACTED],\n [/\\bAIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])/g, REDACTED],\n];\n\n/**\n * The default `redact`: blanks values that look like secrets (key=value pairs\n * with secret-ish names, Authorization headers, URL credentials, bearer\n * tokens, JWTs, PEM private keys, webhook URLs, and well-known token formats)\n * before output or an error is stored, shown, or sent anywhere.\n */\nexport function redactSecrets(text: string): string {\n let out = text;\n for (const [pattern, replacement] of SECRET_PATTERNS) out = out.replace(pattern, replacement as string);\n return out;\n}\n","import { stripJsonNul, stripNul } from \"../output.js\";\nimport type { JobState, Run, StoredJob, StoredJobDefinition } from \"../types.js\";\n\n/**\n * The schema, statements, and row mapping shared by the SQLite and Postgres\n * stores. Imports no driver, so either entry point can pull it in alone.\n * Statements are written with `?` placeholders; Postgres numbers them.\n */\n\nexport type Dialect = \"sqlite\" | \"postgres\";\n\nexport const DEFAULT_PREFIX = \"cronwatch_\";\n\n// Postgres truncates identifiers past 63 bytes; the longest name we build is the prefix plus \"runs_job_started\".\nconst MAX_PREFIX = 63 - \"runs_job_started\".length;\n\n/**\n * Table names are built from the prefix, so it must be a plain lowercase\n * identifier. Uppercase is refused rather than folded: Postgres lowercases\n * unquoted names, so \"Monitoring_\" would quietly become \"monitoring_\".\n */\nexport function tablePrefix(prefix: string = DEFAULT_PREFIX): string {\n if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || prefix.length > MAX_PREFIX) {\n throw new Error(\n `cronwatch: invalid table prefix ${JSON.stringify(prefix)}. Use lowercase letters, digits, and underscores, ` +\n `not starting with a digit, at most ${MAX_PREFIX} characters.`,\n );\n }\n return prefix;\n}\n\nexport function schema(dialect: Dialect, p: string): string {\n const pg = dialect === \"postgres\";\n const int = pg ? \"BIGINT\" : \"INTEGER\";\n const json = pg ? \"JSONB\" : \"TEXT\";\n return `\n CREATE TABLE IF NOT EXISTS ${p}jobs (\n name TEXT PRIMARY KEY,\n definition ${json} NOT NULL,\n created_at ${int} NOT NULL,\n updated_at ${int} NOT NULL\n );\n CREATE TABLE IF NOT EXISTS ${p}runs (${pg ? \"\\n seq BIGSERIAL,\" : \"\"}\n id TEXT PRIMARY KEY,\n job TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at ${int} NOT NULL,\n finished_at ${int},\n duration_ms ${int},\n error TEXT,\n output TEXT,\n metrics ${json} NOT NULL DEFAULT '{}',\n trigger TEXT NOT NULL DEFAULT 'run'\n );\n CREATE INDEX IF NOT EXISTS ${p}runs_job_started ON ${p}runs (job, started_at DESC);\n CREATE INDEX IF NOT EXISTS ${p}runs_running ON ${p}runs (status) WHERE status = 'running';\n CREATE TABLE IF NOT EXISTS ${p}state (\n job TEXT PRIMARY KEY,\n state ${json} NOT NULL\n );\n `;\n}\n\nexport function statements(dialect: Dialect, p: string) {\n const pg = dialect === \"postgres\";\n // Insertion order, to break ties between runs that started in the same millisecond.\n const seq = pg ? \"seq\" : \"rowid\";\n // Byte order on both, so names sort the same whatever the database's collation.\n const byName = pg ? `name COLLATE \"C\"` : \"name\";\n // The version inside a state's JSON, as stateVersion() reads it: a whole\n // number from 0 to 2^53 - 1, else 0 (none, or a foreign row's 1.5 or \"x\",\n // which must neither fail the statement nor refuse every write for good;\n // on SQLite, also text that is not JSON).\n // Each CASE tests the JSON type before any cast.\n const version = (column: string) => {\n if (pg) {\n const v = `(${column}->>'version')::numeric`;\n return `CASE WHEN jsonb_typeof(${column}->'version') <> 'number' THEN 0 WHEN ${v} % 1 = 0 AND ${v} BETWEEN 0 AND 9007199254740991 THEN ${v}::bigint ELSE 0 END`;\n }\n const v = `json_extract(${column}, '$.version')`;\n // Text that is not JSON at all (SQLite holds any) counts as 0 too, before json_type could fail on it.\n return `CASE WHEN NOT json_valid(${column}) THEN 0 WHEN json_type(${column}, '$.version') NOT IN ('integer', 'real') THEN 0 WHEN ${v} = CAST(${v} AS INTEGER) AND ${v} BETWEEN 0 AND 9007199254740991 THEN CAST(${v} AS INTEGER) ELSE 0 END`;\n };\n const sql = {\n upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,\n getJob: `SELECT * FROM ${p}jobs WHERE name = ?`,\n listJobs: `SELECT * FROM ${p}jobs ORDER BY ${byName}`,\n deleteRuns: `DELETE FROM ${p}runs WHERE job = ?`,\n deleteState: `DELETE FROM ${p}state WHERE job = ?`,\n deleteJob: `DELETE FROM ${p}jobs WHERE name = ?`,\n insertRun: `INSERT INTO ${p}runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n updateRun: `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?`,\n getRun: `SELECT * FROM ${p}runs WHERE id = ?`,\n listRuns: `SELECT * FROM ${p}runs WHERE job = ? ORDER BY started_at DESC, ${seq} DESC LIMIT ?`,\n runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,\n getState: `SELECT state FROM ${p}state WHERE job = ?`,\n setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,\n // compareAndSetState. Expecting version 0 also matches a missing row, so\n // that case inserts; any other version must find its row.\n casInsert: `INSERT INTO ${p}state (job, state) VALUES (?, ?)\n ON CONFLICT (job) DO UPDATE SET state = excluded.state WHERE ${version(`${p}state.state`)} = 0`,\n casUpdate: `UPDATE ${p}state SET state = ? WHERE job = ? AND ${version(\"state\")} = ?`,\n // Each job's newest run is kept whatever its age: without it, a job that\n // runs less often than the retention looks like it never ran.\n prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?\n AND started_at < (SELECT MAX(r.started_at) FROM ${p}runs r WHERE r.job = ${p}runs.job)`,\n };\n if (pg) {\n for (const key of Object.keys(sql) as (keyof typeof sql)[]) {\n let n = 0;\n sql[key] = sql[key].replace(/\\?/g, () => `$${++n}`);\n }\n }\n return sql;\n}\n\n/**\n * updateRunIf: the update above, only while the stored status is one of\n * `count` statuses. Built per count, since the list is bound value by value.\n */\nexport function updateRunIfSql(dialect: Dialect, p: string, count: number): string {\n const text = `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ? AND status IN (${Array.from({ length: count }, () => \"?\").join(\", \")})`;\n if (dialect !== \"postgres\") return text;\n let n = 0;\n return text.replace(/\\?/g, () => `$${++n}`);\n}\n\n/**\n * Postgres refuses U+0000 in TEXT and JSONB, and a refused write loses the\n * whole row, so every dialect writes text without it: a run's trigger,\n * output, error, and metric names, and every key and string of a definition\n * and a state. Identifiers (a job's name, a run's id) are written as given;\n * the client refuses one with a NUL before it gets here.\n */\nconst text = (value: string | null) => (value === null ? null : stripNul(value));\nconst jsonText = (value: unknown) => stripJsonNul(JSON.stringify(value));\n\n// Parameters in statement order, so both drivers bind the same values.\nexport const params = {\n upsertJob: (definition: StoredJobDefinition, now: number) => [definition.name, jsonText(definition), now, now],\n insertRun: (run: Run) => [\n run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), stripNul(run.trigger),\n ],\n updateRun: (run: Run) => [run.status, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), run.id],\n updateRunIf: (run: Run, fromStatuses: Run[\"status\"][]) => [\n run.status, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), run.id, ...fromStatuses,\n ],\n setState: (state: JobState) => [state.job, jsonText(state)],\n casInsert: (state: JobState) => [state.job, jsonText(state)],\n casUpdate: (state: JobState, expectedVersion: number) => [jsonText(state), state.job, expectedVersion],\n};\n\n// SQLite hands back JSON as TEXT and Postgres as parsed JSONB; Postgres returns BIGINT as a string.\ntype Json<T> = T | string;\ntype Int = number | string;\n\nexport interface JobRow { name: string; definition: Json<StoredJobDefinition>; created_at: Int; updated_at: Int }\nexport interface RunRow {\n id: string; job: string; status: Run[\"status\"]; started_at: Int; finished_at: Int | null;\n duration_ms: Int | null; error: string | null; output: string | null; metrics: Json<Record<string, number>> | null; trigger: string;\n}\nexport interface StateRow { state: Json<JobState> }\n\n/**\n * Rows are read leniently: a foreign, hand-edited, or damaged row (SQLite\n * keeps whatever type it is given, in any column) must affect only its own\n * job, never every read. JSON text that does not parse reads as null, which\n * the client takes as no state, or as an unreadable definition it reports.\n */\nconst json = (v: unknown): unknown => {\n if (typeof v !== \"string\") return v;\n try {\n return JSON.parse(v) as unknown;\n } catch {\n return null;\n }\n};\n/** A time, or a count of milliseconds, as a column holds it (a string from Postgres's BIGINT): NaN when it is not a number. */\nconst number = (v: unknown): number => (typeof v === \"number\" ? v : typeof v === \"string\" && v.trim() !== \"\" ? Number(v) : Number.NaN);\n/** A time that must be there: one that is not a finite number reads as 0. */\nconst time = (v: unknown): number => {\n const n = number(v);\n return Number.isFinite(n) ? n : 0;\n};\n/** A time or duration that may be absent: one that is not a finite number reads as null. */\nconst maybeTime = (v: unknown): number | null => {\n const n = number(v);\n return Number.isFinite(n) ? n : null;\n};\nconst textOrNull = (v: unknown): string | null => (typeof v === \"string\" ? v : null);\nconst isObject = (v: unknown): v is Record<string, number> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n\n/** A job's row. Its definition is as stored, or null when its text does not parse; the client reads the rest (see readStoredJob). */\nexport function rowToJob(r: JobRow): StoredJob {\n return { name: r.name, definition: json(r.definition) as StoredJobDefinition, createdAt: time(r.created_at), updatedAt: time(r.updated_at) };\n}\n\n/**\n * A run's row. A start that is not a finite number reads as 0, a finish or\n * duration as null; an error or output that is not text as null; metrics\n * that do not parse to an object as {}; a trigger that is not text as \"run\".\n */\nexport function rowToRun(r: RunRow): Run {\n const metrics = json(r.metrics);\n return {\n id: r.id, job: r.job, status: r.status, startedAt: time(r.started_at), finishedAt: maybeTime(r.finished_at),\n durationMs: maybeTime(r.duration_ms), error: textOrNull(r.error), output: textOrNull(r.output),\n metrics: isObject(metrics) ? metrics : {}, trigger: typeof r.trigger === \"string\" ? r.trigger : \"run\",\n };\n}\n\n/** A state's row, or null when its text does not parse (normalizeState reads anything else that is not an object as no state). */\nexport function rowToState(r: StateRow): JobState {\n return json(r.state) as JobState;\n}\n","import type { Store, StoredJob } from \"../types.js\";\nimport { params, rowToJob, rowToRun, rowToState, schema, statements, tablePrefix, updateRunIfSql, type JobRow, type RunRow, type StateRow } from \"./sql.js\";\n\n/**\n * The parts of a Cloudflare D1 binding this store uses. Written out here so\n * the SDK needs nothing from @cloudflare/workers-types: a Worker's\n * `env.DB: D1Database` fits it as it is.\n */\nexport interface D1PreparedStatementLike {\n bind(...values: unknown[]): D1PreparedStatementLike;\n first(): Promise<unknown>;\n all(): Promise<{ results?: unknown[] }>;\n run(): Promise<{ meta?: { changes?: number } }>;\n}\n\nexport interface D1DatabaseLike {\n prepare(query: string): D1PreparedStatementLike;\n batch(statements: D1PreparedStatementLike[]): Promise<unknown[]>;\n}\n\nexport interface D1Options {\n /** Table name prefix: lowercase letters, digits, and underscores. Default \"cronwatch_\". */\n prefix?: string;\n /**\n * Create the tables on first use (CREATE TABLE IF NOT EXISTS), once per\n * isolate. Default true. Pass false when a D1 migration creates them\n * instead (see the Cloudflare docs page for the file).\n */\n createTables?: boolean;\n}\n\n/** Databases (and prefixes) whose tables this isolate has already made sure of. */\nconst created = new WeakMap<object, Set<string>>();\n\n/**\n * Keeps everything in Cloudflare D1, for Workers. D1 is SQLite, so this is\n * the SQLite store's schema, statements, and row mapping over D1's\n * prepare, bind, and batch. Create it where the binding is, inside the\n * handler: `d1(env.DB)`.\n */\nexport function d1(database: D1DatabaseLike, options: D1Options = {}): Store {\n if (!database || typeof database.prepare !== \"function\" || typeof database.batch !== \"function\") {\n throw new Error(\"cronwatch: d1() needs a D1 database binding, such as env.DB\");\n }\n const p = tablePrefix(options.prefix);\n // D1 keeps its own tables under _cf_ and refuses writes to them.\n if (p.startsWith(\"_cf_\")) throw new Error(`cronwatch: invalid table prefix ${JSON.stringify(p)}. D1 reserves names starting with _cf_.`);\n const sql = statements(\"sqlite\", p);\n const createTables = options.createTables ?? true;\n\n const stmt = (text: string, values: unknown[] = []) => {\n const s = database.prepare(text);\n return values.length > 0 ? s.bind(...values) : s;\n };\n const first = async <T>(text: string, values: unknown[]): Promise<T | null> => ((await stmt(text, values).first()) as T | null) ?? null;\n const all = async <T>(text: string, values: unknown[] = []): Promise<T[]> => ((await stmt(text, values).all()).results ?? []) as T[];\n const changes = async (text: string, values: unknown[]): Promise<number> => (await stmt(text, values).run()).meta?.changes ?? 0;\n\n return {\n async init() {\n if (!createTables || created.get(database)?.has(p)) return;\n // exec() splits on newlines, so each statement goes in on its own, in one batch.\n const ddl = schema(\"sqlite\", p).split(\";\").map((s) => s.trim()).filter(Boolean);\n await database.batch(ddl.map((s) => database.prepare(s)));\n // Remembered only once it worked, and as a flag rather than a promise,\n // so no invocation ever waits on another's unfinished I/O.\n let prefixes = created.get(database);\n if (!prefixes) created.set(database, (prefixes = new Set()));\n prefixes.add(p);\n },\n async upsertJob(definition, now) {\n await stmt(sql.upsertJob, params.upsertJob(definition, now)).run();\n },\n async getJob(name) {\n const r = await first<JobRow>(sql.getJob, [name]);\n return r ? rowToJob(r) : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n return (await all<JobRow>(sql.listJobs)).map(rowToJob);\n },\n async deleteJob(name) {\n // A batch is one transaction.\n await database.batch([stmt(sql.deleteRuns, [name]), stmt(sql.deleteState, [name]), stmt(sql.deleteJob, [name])]);\n },\n async insertRun(run) {\n await stmt(sql.insertRun, params.insertRun(run)).run();\n },\n async updateRun(run) {\n await stmt(sql.updateRun, params.updateRun(run)).run();\n },\n async updateRunIf(run, fromStatuses) {\n if (fromStatuses.length === 0) return false;\n return (await changes(updateRunIfSql(\"sqlite\", p, fromStatuses.length), params.updateRunIf(run, fromStatuses))) > 0;\n },\n async getRun(id) {\n const r = await first<RunRow>(sql.getRun, [id]);\n return r ? rowToRun(r) : null;\n },\n async listRuns(job, limit) {\n return (await all<RunRow>(sql.listRuns, [job, limit])).map(rowToRun);\n },\n async lastRun(job) {\n const r = await first<RunRow>(sql.listRuns, [job, 1]);\n return r ? rowToRun(r) : null;\n },\n async runningRuns() {\n return (await all<RunRow>(sql.runningRuns)).map(rowToRun);\n },\n async getState(job) {\n const r = await first<StateRow>(sql.getState, [job]);\n return r ? rowToState(r) : null;\n },\n async setState(state) {\n await stmt(sql.setState, params.setState(state)).run();\n },\n async compareAndSetState(state, expectedVersion) {\n const written = expectedVersion === 0\n ? await changes(sql.casInsert, params.casInsert(state))\n : await changes(sql.casUpdate, params.casUpdate(state, expectedVersion));\n return written > 0;\n },\n async prune(before) {\n return changes(sql.prune, [before]);\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/output.ts","../src/stores/sql.ts","../src/stores/d1.ts"],"names":["text","json"],"mappings":"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","file":"d1.cjs","sourcesContent":["/** Output is capped so a chatty job cannot fill the store. The tail is kept. */\nexport const OUTPUT_CAP = 16 * 1024;\n\n/**\n * NUL characters are removed first, since Postgres refuses them in TEXT and\n * JSONB and the whole run row would be lost. The cap then applies to what is\n * left.\n */\nexport function capOutput(text: string): string {\n const clean = stripNul(text);\n if (clean.length <= OUTPUT_CAP) return clean;\n return TRIMMED + clean.slice(clean.length - OUTPUT_CAP);\n}\n\nconst TRIMMED = \"[earlier output trimmed]\\n\";\n\n/**\n * How much text before the kept tail redaction reads, and never keeps: three\n * times the longest secret a default pattern can match (a PEM key's 16 KB body\n * with its header and footer, under OUTPUT_CAP + 1024), since a replacement\n * grows what it replaces at most threefold.\n */\nexport const REDACT_EDGE = 3 * (OUTPUT_CAP + 1024);\n\n/**\n * Output or an error as it is stored: redacted, then capped like capOutput,\n * so the cut cannot fall inside a secret and keep what follows its label.\n * Text of at most OUTPUT_CAP + REDACT_EDGE is redacted whole. Longer text is\n * cut to that many units from its end first, and after redacting, the first\n * REDACT_EDGE units are never kept: a secret whose label fell before that cut\n * is left out with them. NULs go before and after `redact`.\n */\nexport function redactAndCap(text: string, redact: (text: string) => string): string {\n const clean = stripNul(text);\n const from = clean.length - (OUTPUT_CAP + REDACT_EDGE);\n if (from <= 0) return capOutput(redact(clean));\n const redacted = stripNul(redact(clean.slice(from)));\n return TRIMMED + redacted.slice(Math.max(redacted.length - OUTPUT_CAP, REDACT_EDGE));\n}\n\n/** Removes every U+0000. */\nexport function stripNul(text: string): string {\n return text.includes(\"\\u0000\") ? text.replace(/\\u0000/g, \"\") : text;\n}\n\n/**\n * Removes every U+0000 from JSON text, keys and strings alike, by dropping\n * each \\u0000 escape (a NUL can appear in JSON no other way). Escapes are\n * read left to right in pairs, so an escaped backslash followed by \"u0000\"\n * is left as it is.\n */\nexport function stripJsonNul(json: string): string {\n return json.includes(\"\\\\u0000\") ? json.replace(/\\\\(u0000|[\\s\\S])/g, (escape, next: string) => (next === \"u0000\" ? \"\" : escape)) : json;\n}\n\n/** \"Name: message\" and the first five stack frames, capped like output. */\nexport function errorMessage(error: unknown): string {\n return capOutput(describeError(error));\n}\n\n/** \"Name: message\" and the first five stack frames, not capped: see redactAndCap. */\nexport function describeError(error: unknown): string {\n if (error instanceof Error) {\n // The stack repeats the header (over several lines when the message has\n // newlines), so take only its frames.\n const frames = (error.stack ?? \"\").split(\"\\n\").filter((line) => /^\\s+at /.test(line)).slice(0, 5);\n return `${error.name}: ${error.message}${frames.length ? `\\n${frames.join(\"\\n\")}` : \"\"}`;\n }\n if (typeof error === \"string\") return error;\n try {\n return JSON.stringify(error) ?? String(error);\n } catch {\n return String(error);\n }\n}\n\nconst REDACTED = \"[redacted]\";\n\n// Bounded quantifiers throughout, so a long line cannot make these backtrack.\n// They apply in this order, each to the text the ones before it left.\nconst SECRET_PATTERNS: [RegExp, string | ((match: string, ...groups: string[]) => string)][] = [\n // A PEM private key, header to footer. Without a footer (the output was\n // trimmed) it runs to the end of the base64 body. A \"-\" that starts five\n // dashes ends the body, so the footer is never swallowed into it.\n [\n /-----BEGIN (?:[A-Z0-9]{1,20} ){0,3}PRIVATE KEY-----(?:[A-Za-z0-9+/=\\s,:]|-(?!----)){0,16384}(?:-----END (?:[A-Z0-9]{1,20} ){0,3}PRIVATE KEY-----)?/g,\n REDACTED,\n ],\n // password=..., API_KEY: ..., \"client_secret\": \"...\", TOKEN='...', token=...,\n // :password=>\"...\" (but not max_tokens: 800). A quoted value is blanked to\n // its closing quote, spaces and all, and keeps its quotes.\n [\n /\\b([A-Za-z0-9_-]{0,40}(?:secret|token|passw(?:or)?d|pwd|api[_-]?key|access[_-]?key|private[_-]?key|credential)[A-Za-z0-9_-]{0,40}(?<![Tt][Oo][Kk][Ee][Nn][Ss])\"?\\s{0,3}(?:=>|[=:])\\s{0,3})(?:(\")[^\"\\n]{1,4096}\"|(')[^'\\n]{1,4096}'|[\"']?[^\\s\"',;&]{1,4096})/gi,\n (_match, name: string, double?: string, single?: string) => {\n const quote = double ?? single ?? \"\";\n return `${name}${quote}${REDACTED}${quote}`;\n },\n ],\n // Authorization: Basic <base64> and Authorization: Token <token>, also as a JSON or hash entry.\n [/\\b((?:proxy-)?authorization[\"']?\\s{0,3}(?:=>|[=:])\\s{0,3}[\"']?\\s{0,3}(?:basic|token)\\s{1,3})[A-Za-z0-9._~+/=:-]{1,4096}/gi, `$1${REDACTED}`],\n // Credentials inside a URL: postgres://user:password@host. The password\n // runs to the last \"@\" before a \"/\" or a space, so one that contains \"@\"\n // is blanked whole.\n [/(\\b[a-z][a-z0-9+.-]{0,30}:\\/\\/[^\\s/:@]{0,256}:)[^\\s/]{1,256}@/gi, `$1${REDACTED}@`],\n // Authorization: Bearer <token>\n [/\\b(Bearer\\s{1,3})[A-Za-z0-9._~+/=-]{8,4096}/g, `$1${REDACTED}`],\n // A bare JWT: three base64url segments, the first starting eyJ.\n [/\\beyJ[A-Za-z0-9_-]{4,4096}\\.[A-Za-z0-9_-]{4,4096}\\.[A-Za-z0-9_-]{0,4096}/g, REDACTED],\n // Incoming webhook URLs carry their secret in the path.\n [/(\\bhooks\\.slack\\.com\\/(?:services|workflows|triggers)\\/)[A-Za-z0-9/_-]{1,255}/gi, `$1${REDACTED}`],\n [/(\\bdiscord(?:app)?\\.com\\/api\\/(?:v\\d{1,2}\\/)?webhooks\\/)[A-Za-z0-9/_-]{1,255}/gi, `$1${REDACTED}`],\n // Well-known token shapes: AWS, GitHub, Slack, Stripe, Anthropic, OpenAI, and Google style keys.\n [/\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b/g, REDACTED],\n [/\\b(?:gh[pousr]_[A-Za-z0-9]{30,255}|github_pat_[A-Za-z0-9_]{20,255})\\b/g, REDACTED],\n [/\\bxox[abposr]-[A-Za-z0-9-]{10,255}/g, REDACTED],\n [/\\b[rsp]k_(?:live|test)_[A-Za-z0-9]{10,255}\\b/g, REDACTED],\n [/\\bwhsec_[A-Za-z0-9+/=]{16,255}/g, REDACTED],\n [/\\bsk-[A-Za-z0-9_-]{20,255}/g, REDACTED],\n [/\\bAIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])/g, REDACTED],\n];\n\n/**\n * The default `redact`: blanks values that look like secrets (key=value pairs\n * with secret-ish names, Authorization headers, URL credentials, bearer\n * tokens, JWTs, PEM private keys, webhook URLs, and well-known token formats)\n * before output or an error is stored, shown, or sent anywhere.\n */\nexport function redactSecrets(text: string): string {\n let out = text;\n for (const [pattern, replacement] of SECRET_PATTERNS) out = out.replace(pattern, replacement as string);\n return out;\n}\n","import { stripJsonNul, stripNul } from \"../output.js\";\nimport type { JobState, Run, StoredJob, StoredJobDefinition } from \"../types.js\";\n\n/**\n * The schema, statements, and row mapping shared by the SQLite and Postgres\n * stores. Imports no driver, so either entry point can pull it in alone.\n * Statements are written with `?` placeholders; Postgres numbers them.\n */\n\nexport type Dialect = \"sqlite\" | \"postgres\";\n\nexport const DEFAULT_PREFIX = \"cronwatch_\";\n\n// Postgres truncates identifiers past 63 bytes; the longest name we build is the prefix plus \"runs_job_started\".\nconst MAX_PREFIX = 63 - \"runs_job_started\".length;\n\n/**\n * Table names are built from the prefix, so it must be a plain lowercase\n * identifier. Uppercase is refused rather than folded: Postgres lowercases\n * unquoted names, so \"Monitoring_\" would quietly become \"monitoring_\".\n */\nexport function tablePrefix(prefix: string = DEFAULT_PREFIX): string {\n if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || prefix.length > MAX_PREFIX) {\n throw new Error(\n `cronwatch: invalid table prefix ${JSON.stringify(prefix)}. Use lowercase letters, digits, and underscores, ` +\n `not starting with a digit, at most ${MAX_PREFIX} characters.`,\n );\n }\n return prefix;\n}\n\nexport function schema(dialect: Dialect, p: string): string {\n const pg = dialect === \"postgres\";\n const int = pg ? \"BIGINT\" : \"INTEGER\";\n const json = pg ? \"JSONB\" : \"TEXT\";\n return `\n CREATE TABLE IF NOT EXISTS ${p}jobs (\n name TEXT PRIMARY KEY,\n definition ${json} NOT NULL,\n created_at ${int} NOT NULL,\n updated_at ${int} NOT NULL\n );\n CREATE TABLE IF NOT EXISTS ${p}runs (${pg ? \"\\n seq BIGSERIAL,\" : \"\"}\n id TEXT PRIMARY KEY,\n job TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at ${int} NOT NULL,\n finished_at ${int},\n duration_ms ${int},\n error TEXT,\n output TEXT,\n metrics ${json} NOT NULL DEFAULT '{}',\n trigger TEXT NOT NULL DEFAULT 'run'\n );\n CREATE INDEX IF NOT EXISTS ${p}runs_job_started ON ${p}runs (job, started_at DESC);\n CREATE INDEX IF NOT EXISTS ${p}runs_running ON ${p}runs (status) WHERE status = 'running';\n CREATE TABLE IF NOT EXISTS ${p}state (\n job TEXT PRIMARY KEY,\n state ${json} NOT NULL\n );\n `;\n}\n\nexport function statements(dialect: Dialect, p: string) {\n const pg = dialect === \"postgres\";\n // Insertion order, to break ties between runs that started in the same millisecond.\n const seq = pg ? \"seq\" : \"rowid\";\n // Byte order on both, so names sort the same whatever the database's collation.\n const byName = pg ? `name COLLATE \"C\"` : \"name\";\n // The version inside a state's JSON, as stateVersion() reads it: a whole\n // number from 0 to 2^53 - 1, else 0 (none, or a foreign row's 1.5 or \"x\",\n // which must neither fail the statement nor refuse every write for good;\n // on SQLite, also text that is not JSON).\n // Each CASE tests the JSON type before any cast.\n const version = (column: string) => {\n if (pg) {\n const v = `(${column}->>'version')::numeric`;\n return `CASE WHEN jsonb_typeof(${column}->'version') <> 'number' THEN 0 WHEN ${v} % 1 = 0 AND ${v} BETWEEN 0 AND 9007199254740991 THEN ${v}::bigint ELSE 0 END`;\n }\n const v = `json_extract(${column}, '$.version')`;\n // Text that is not JSON at all (SQLite holds any) counts as 0 too, before json_type could fail on it.\n return `CASE WHEN NOT json_valid(${column}) THEN 0 WHEN json_type(${column}, '$.version') NOT IN ('integer', 'real') THEN 0 WHEN ${v} = CAST(${v} AS INTEGER) AND ${v} BETWEEN 0 AND 9007199254740991 THEN CAST(${v} AS INTEGER) ELSE 0 END`;\n };\n const sql = {\n upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,\n getJob: `SELECT * FROM ${p}jobs WHERE name = ?`,\n listJobs: `SELECT * FROM ${p}jobs ORDER BY ${byName}`,\n deleteRuns: `DELETE FROM ${p}runs WHERE job = ?`,\n deleteState: `DELETE FROM ${p}state WHERE job = ?`,\n deleteJob: `DELETE FROM ${p}jobs WHERE name = ?`,\n insertRun: `INSERT INTO ${p}runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n updateRun: `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?`,\n getRun: `SELECT * FROM ${p}runs WHERE id = ?`,\n listRuns: `SELECT * FROM ${p}runs WHERE job = ? ORDER BY started_at DESC, ${seq} DESC LIMIT ?`,\n runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,\n getState: `SELECT state FROM ${p}state WHERE job = ?`,\n setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,\n // compareAndSetState. Expecting version 0 also matches a missing row, so\n // that case inserts; any other version must find its row.\n casInsert: `INSERT INTO ${p}state (job, state) VALUES (?, ?)\n ON CONFLICT (job) DO UPDATE SET state = excluded.state WHERE ${version(`${p}state.state`)} = 0`,\n casUpdate: `UPDATE ${p}state SET state = ? WHERE job = ? AND ${version(\"state\")} = ?`,\n // Each job's newest run is kept whatever its age: without it, a job that\n // runs less often than the retention looks like it never ran.\n prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?\n AND started_at < (SELECT MAX(r.started_at) FROM ${p}runs r WHERE r.job = ${p}runs.job)`,\n };\n if (pg) {\n for (const key of Object.keys(sql) as (keyof typeof sql)[]) {\n let n = 0;\n sql[key] = sql[key].replace(/\\?/g, () => `$${++n}`);\n }\n }\n return sql;\n}\n\n/**\n * updateRunIf: the update above, only while the stored status is one of\n * `count` statuses. Built per count, since the list is bound value by value.\n */\nexport function updateRunIfSql(dialect: Dialect, p: string, count: number): string {\n const text = `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ? AND status IN (${Array.from({ length: count }, () => \"?\").join(\", \")})`;\n if (dialect !== \"postgres\") return text;\n let n = 0;\n return text.replace(/\\?/g, () => `$${++n}`);\n}\n\n/**\n * Postgres refuses U+0000 in TEXT and JSONB, and a refused write loses the\n * whole row, so every dialect writes text without it: a run's trigger,\n * output, error, and metric names, and every key and string of a definition\n * and a state. Identifiers (a job's name, a run's id) are written as given;\n * the client refuses one with a NUL before it gets here.\n */\nconst text = (value: string | null) => (value === null ? null : stripNul(value));\nconst jsonText = (value: unknown) => stripJsonNul(JSON.stringify(value));\n\n// Parameters in statement order, so both drivers bind the same values.\nexport const params = {\n upsertJob: (definition: StoredJobDefinition, now: number) => [definition.name, jsonText(definition), now, now],\n insertRun: (run: Run) => [\n run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), stripNul(run.trigger),\n ],\n updateRun: (run: Run) => [run.status, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), run.id],\n updateRunIf: (run: Run, fromStatuses: Run[\"status\"][]) => [\n run.status, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), run.id, ...fromStatuses,\n ],\n setState: (state: JobState) => [state.job, jsonText(state)],\n casInsert: (state: JobState) => [state.job, jsonText(state)],\n casUpdate: (state: JobState, expectedVersion: number) => [jsonText(state), state.job, expectedVersion],\n};\n\n// SQLite hands back JSON as TEXT and Postgres as parsed JSONB; Postgres returns BIGINT as a string.\ntype Json<T> = T | string;\ntype Int = number | string;\n\nexport interface JobRow { name: string; definition: Json<StoredJobDefinition>; created_at: Int; updated_at: Int }\nexport interface RunRow {\n id: string; job: string; status: Run[\"status\"]; started_at: Int; finished_at: Int | null;\n duration_ms: Int | null; error: string | null; output: string | null; metrics: Json<Record<string, number>> | null; trigger: string;\n}\nexport interface StateRow { state: Json<JobState> }\n\n/**\n * Rows are read leniently: a foreign, hand-edited, or damaged row (SQLite\n * keeps whatever type it is given, in any column) must affect only its own\n * job, never every read. JSON text that does not parse reads as null, which\n * the client takes as no state, or as an unreadable definition it reports.\n */\nconst json = (v: unknown): unknown => {\n if (typeof v !== \"string\") return v;\n try {\n return JSON.parse(v) as unknown;\n } catch {\n return null;\n }\n};\n/** A time, or a count of milliseconds, as a column holds it (a string from Postgres's BIGINT): NaN when it is not a number. */\nconst number = (v: unknown): number => (typeof v === \"number\" ? v : typeof v === \"string\" && v.trim() !== \"\" ? Number(v) : Number.NaN);\n/** A time that must be there: one that is not a finite number reads as 0. */\nconst time = (v: unknown): number => {\n const n = number(v);\n return Number.isFinite(n) ? n : 0;\n};\n/** A time or duration that may be absent: one that is not a finite number reads as null. */\nconst maybeTime = (v: unknown): number | null => {\n const n = number(v);\n return Number.isFinite(n) ? n : null;\n};\nconst textOrNull = (v: unknown): string | null => (typeof v === \"string\" ? v : null);\nconst isObject = (v: unknown): v is Record<string, number> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n\n/** A job's row. Its definition is as stored, or null when its text does not parse; the client reads the rest (see readStoredJob). */\nexport function rowToJob(r: JobRow): StoredJob {\n return { name: r.name, definition: json(r.definition) as StoredJobDefinition, createdAt: time(r.created_at), updatedAt: time(r.updated_at) };\n}\n\n/**\n * A run's row. A start that is not a finite number reads as 0, a finish or\n * duration as null; an error or output that is not text as null; metrics\n * that do not parse to an object as {}; a trigger that is not text as \"run\".\n */\nexport function rowToRun(r: RunRow): Run {\n const metrics = json(r.metrics);\n return {\n id: r.id, job: r.job, status: r.status, startedAt: time(r.started_at), finishedAt: maybeTime(r.finished_at),\n durationMs: maybeTime(r.duration_ms), error: textOrNull(r.error), output: textOrNull(r.output),\n metrics: isObject(metrics) ? metrics : {}, trigger: typeof r.trigger === \"string\" ? r.trigger : \"run\",\n };\n}\n\n/** A state's row, or null when its text does not parse (normalizeState reads anything else that is not an object as no state). */\nexport function rowToState(r: StateRow): JobState {\n return json(r.state) as JobState;\n}\n","import type { Store, StoredJob } from \"../types.js\";\nimport { params, rowToJob, rowToRun, rowToState, schema, statements, tablePrefix, updateRunIfSql, type JobRow, type RunRow, type StateRow } from \"./sql.js\";\n\n/**\n * The parts of a Cloudflare D1 binding this store uses. Written out here so\n * the SDK needs nothing from @cloudflare/workers-types: a Worker's\n * `env.DB: D1Database` fits it as it is.\n */\nexport interface D1PreparedStatementLike {\n bind(...values: unknown[]): D1PreparedStatementLike;\n first(): Promise<unknown>;\n all(): Promise<{ results?: unknown[] }>;\n run(): Promise<{ meta?: { changes?: number } }>;\n}\n\nexport interface D1DatabaseLike {\n prepare(query: string): D1PreparedStatementLike;\n batch(statements: D1PreparedStatementLike[]): Promise<unknown[]>;\n}\n\nexport interface D1Options {\n /** Table name prefix: lowercase letters, digits, and underscores. Default \"cronwatch_\". */\n prefix?: string;\n /**\n * Create the tables on first use (CREATE TABLE IF NOT EXISTS), once per\n * isolate. Default true. Pass false when a D1 migration creates them\n * instead (see the Cloudflare docs page for the file).\n */\n createTables?: boolean;\n}\n\n/** Databases (and prefixes) whose tables this isolate has already made sure of. */\nconst created = new WeakMap<object, Set<string>>();\n\n/**\n * Keeps everything in Cloudflare D1, for Workers. D1 is SQLite, so this is\n * the SQLite store's schema, statements, and row mapping over D1's\n * prepare, bind, and batch. Create it where the binding is, inside the\n * handler: `d1(env.DB)`.\n */\nexport function d1(database: D1DatabaseLike, options: D1Options = {}): Store & Required<Pick<Store, \"lastRun\">> {\n if (!database || typeof database.prepare !== \"function\" || typeof database.batch !== \"function\") {\n throw new Error(\"cronwatch: d1() needs a D1 database binding, such as env.DB\");\n }\n const p = tablePrefix(options.prefix);\n // D1 keeps its own tables under _cf_ and refuses writes to them.\n if (p.startsWith(\"_cf_\")) throw new Error(`cronwatch: invalid table prefix ${JSON.stringify(p)}. D1 reserves names starting with _cf_.`);\n const sql = statements(\"sqlite\", p);\n const createTables = options.createTables ?? true;\n\n const stmt = (text: string, values: unknown[] = []) => {\n const s = database.prepare(text);\n return values.length > 0 ? s.bind(...values) : s;\n };\n const first = async <T>(text: string, values: unknown[]): Promise<T | null> => ((await stmt(text, values).first()) as T | null) ?? null;\n const all = async <T>(text: string, values: unknown[] = []): Promise<T[]> => ((await stmt(text, values).all()).results ?? []) as T[];\n const changes = async (text: string, values: unknown[]): Promise<number> => (await stmt(text, values).run()).meta?.changes ?? 0;\n\n return {\n async init() {\n if (!createTables || created.get(database)?.has(p)) return;\n // exec() splits on newlines, so each statement goes in on its own, in one batch.\n const ddl = schema(\"sqlite\", p).split(\";\").map((s) => s.trim()).filter(Boolean);\n await database.batch(ddl.map((s) => database.prepare(s)));\n // Remembered only once it worked, and as a flag rather than a promise,\n // so no invocation ever waits on another's unfinished I/O.\n let prefixes = created.get(database);\n if (!prefixes) created.set(database, (prefixes = new Set()));\n prefixes.add(p);\n },\n async upsertJob(definition, now) {\n await stmt(sql.upsertJob, params.upsertJob(definition, now)).run();\n },\n async getJob(name) {\n const r = await first<JobRow>(sql.getJob, [name]);\n return r ? rowToJob(r) : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n return (await all<JobRow>(sql.listJobs)).map(rowToJob);\n },\n async deleteJob(name) {\n // A batch is one transaction.\n await database.batch([stmt(sql.deleteRuns, [name]), stmt(sql.deleteState, [name]), stmt(sql.deleteJob, [name])]);\n },\n async insertRun(run) {\n await stmt(sql.insertRun, params.insertRun(run)).run();\n },\n async updateRun(run) {\n await stmt(sql.updateRun, params.updateRun(run)).run();\n },\n async updateRunIf(run, fromStatuses) {\n if (fromStatuses.length === 0) return false;\n return (await changes(updateRunIfSql(\"sqlite\", p, fromStatuses.length), params.updateRunIf(run, fromStatuses))) > 0;\n },\n async getRun(id) {\n const r = await first<RunRow>(sql.getRun, [id]);\n return r ? rowToRun(r) : null;\n },\n async listRuns(job, limit) {\n return (await all<RunRow>(sql.listRuns, [job, limit])).map(rowToRun);\n },\n async lastRun(job) {\n const r = await first<RunRow>(sql.listRuns, [job, 1]);\n return r ? rowToRun(r) : null;\n },\n async runningRuns() {\n return (await all<RunRow>(sql.runningRuns)).map(rowToRun);\n },\n async getState(job) {\n const r = await first<StateRow>(sql.getState, [job]);\n return r ? rowToState(r) : null;\n },\n async setState(state) {\n await stmt(sql.setState, params.setState(state)).run();\n },\n async compareAndSetState(state, expectedVersion) {\n const written = expectedVersion === 0\n ? await changes(sql.casInsert, params.casInsert(state))\n : await changes(sql.casUpdate, params.casUpdate(state, expectedVersion));\n return written > 0;\n },\n async prune(before) {\n return changes(sql.prune, [before]);\n },\n };\n}\n"]}
package/dist/d1.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { S as Store } from './types-D6t0Z846.cjs';
1
+ import { S as Store } from './types-B8IFVK5k.cjs';
2
2
 
3
3
  /**
4
4
  * The parts of a Cloudflare D1 binding this store uses. Written out here so
@@ -37,6 +37,6 @@ interface D1Options {
37
37
  * prepare, bind, and batch. Create it where the binding is, inside the
38
38
  * handler: `d1(env.DB)`.
39
39
  */
40
- declare function d1(database: D1DatabaseLike, options?: D1Options): Store;
40
+ declare function d1(database: D1DatabaseLike, options?: D1Options): Store & Required<Pick<Store, "lastRun">>;
41
41
 
42
42
  export { type D1DatabaseLike, type D1Options, type D1PreparedStatementLike, d1 };
package/dist/d1.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { S as Store } from './types-D6t0Z846.js';
1
+ import { S as Store } from './types-B8IFVK5k.js';
2
2
 
3
3
  /**
4
4
  * The parts of a Cloudflare D1 binding this store uses. Written out here so
@@ -37,6 +37,6 @@ interface D1Options {
37
37
  * prepare, bind, and batch. Create it where the binding is, inside the
38
38
  * handler: `d1(env.DB)`.
39
39
  */
40
- declare function d1(database: D1DatabaseLike, options?: D1Options): Store;
40
+ declare function d1(database: D1DatabaseLike, options?: D1Options): Store & Required<Pick<Store, "lastRun">>;
41
41
 
42
42
  export { type D1DatabaseLike, type D1Options, type D1PreparedStatementLike, d1 };
package/dist/d1.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/output.ts","../src/stores/sql.ts","../src/stores/d1.ts"],"names":["text","json"],"mappings":"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","file":"d1.js","sourcesContent":["/** Output is capped so a chatty job cannot fill the store. The tail is kept. */\nexport const OUTPUT_CAP = 16 * 1024;\n\n/**\n * NUL characters are removed first, since Postgres refuses them in TEXT and\n * JSONB and the whole run row would be lost. The cap then applies to what is\n * left.\n */\nexport function capOutput(text: string): string {\n const clean = stripNul(text);\n if (clean.length <= OUTPUT_CAP) return clean;\n return TRIMMED + clean.slice(clean.length - OUTPUT_CAP);\n}\n\nconst TRIMMED = \"[earlier output trimmed]\\n\";\n\n/**\n * How much text before the kept tail redaction reads, and never keeps: three\n * times the longest secret a default pattern can match (a PEM key's 16 KB body\n * with its header and footer, under OUTPUT_CAP + 1024), since a replacement\n * grows what it replaces at most threefold.\n */\nexport const REDACT_EDGE = 3 * (OUTPUT_CAP + 1024);\n\n/**\n * Output or an error as it is stored: redacted, then capped like capOutput,\n * so the cut cannot fall inside a secret and keep what follows its label.\n * Text of at most OUTPUT_CAP + REDACT_EDGE is redacted whole. Longer text is\n * cut to that many units from its end first, and after redacting, the first\n * REDACT_EDGE units are never kept: a secret whose label fell before that cut\n * is left out with them. NULs go before and after `redact`.\n */\nexport function redactAndCap(text: string, redact: (text: string) => string): string {\n const clean = stripNul(text);\n const from = clean.length - (OUTPUT_CAP + REDACT_EDGE);\n if (from <= 0) return capOutput(redact(clean));\n const redacted = stripNul(redact(clean.slice(from)));\n return TRIMMED + redacted.slice(Math.max(redacted.length - OUTPUT_CAP, REDACT_EDGE));\n}\n\n/** Removes every U+0000. */\nexport function stripNul(text: string): string {\n return text.includes(\"\\u0000\") ? text.replace(/\\u0000/g, \"\") : text;\n}\n\n/**\n * Removes every U+0000 from JSON text, keys and strings alike, by dropping\n * each \\u0000 escape (a NUL can appear in JSON no other way). Escapes are\n * read left to right in pairs, so an escaped backslash followed by \"u0000\"\n * is left as it is.\n */\nexport function stripJsonNul(json: string): string {\n return json.includes(\"\\\\u0000\") ? json.replace(/\\\\(u0000|[\\s\\S])/g, (escape, next: string) => (next === \"u0000\" ? \"\" : escape)) : json;\n}\n\n/** \"Name: message\" and the first five stack frames, capped like output. */\nexport function errorMessage(error: unknown): string {\n return capOutput(describeError(error));\n}\n\n/** \"Name: message\" and the first five stack frames, not capped: see redactAndCap. */\nexport function describeError(error: unknown): string {\n if (error instanceof Error) {\n // The stack repeats the header (over several lines when the message has\n // newlines), so take only its frames.\n const frames = (error.stack ?? \"\").split(\"\\n\").filter((line) => /^\\s+at /.test(line)).slice(0, 5);\n return `${error.name}: ${error.message}${frames.length ? `\\n${frames.join(\"\\n\")}` : \"\"}`;\n }\n if (typeof error === \"string\") return error;\n try {\n return JSON.stringify(error) ?? String(error);\n } catch {\n return String(error);\n }\n}\n\nconst REDACTED = \"[redacted]\";\n\n// Bounded quantifiers throughout, so a long line cannot make these backtrack.\n// They apply in this order, each to the text the ones before it left.\nconst SECRET_PATTERNS: [RegExp, string | ((match: string, ...groups: string[]) => string)][] = [\n // A PEM private key, header to footer. Without a footer (the output was\n // trimmed) it runs to the end of the base64 body. A \"-\" that starts five\n // dashes ends the body, so the footer is never swallowed into it.\n [\n /-----BEGIN (?:[A-Z0-9]{1,20} ){0,3}PRIVATE KEY-----(?:[A-Za-z0-9+/=\\s,:]|-(?!----)){0,16384}(?:-----END (?:[A-Z0-9]{1,20} ){0,3}PRIVATE KEY-----)?/g,\n REDACTED,\n ],\n // password=..., API_KEY: ..., \"client_secret\": \"...\", TOKEN='...', token=...,\n // :password=>\"...\" (but not max_tokens: 800). A quoted value is blanked to\n // its closing quote, spaces and all, and keeps its quotes.\n [\n /\\b([A-Za-z0-9_-]{0,40}(?:secret|token|passw(?:or)?d|pwd|api[_-]?key|access[_-]?key|private[_-]?key|credential)[A-Za-z0-9_-]{0,40}(?<![Tt][Oo][Kk][Ee][Nn][Ss])\"?\\s{0,3}(?:=>|[=:])\\s{0,3})(?:(\")[^\"\\n]{1,4096}\"|(')[^'\\n]{1,4096}'|[\"']?[^\\s\"',;&]{1,4096})/gi,\n (_match, name: string, double?: string, single?: string) => {\n const quote = double ?? single ?? \"\";\n return `${name}${quote}${REDACTED}${quote}`;\n },\n ],\n // Authorization: Basic <base64> and Authorization: Token <token>, also as a JSON or hash entry.\n [/\\b((?:proxy-)?authorization[\"']?\\s{0,3}(?:=>|[=:])\\s{0,3}[\"']?\\s{0,3}(?:basic|token)\\s{1,3})[A-Za-z0-9._~+/=:-]{1,4096}/gi, `$1${REDACTED}`],\n // Credentials inside a URL: postgres://user:password@host. The password\n // runs to the last \"@\" before a \"/\" or a space, so one that contains \"@\"\n // is blanked whole.\n [/(\\b[a-z][a-z0-9+.-]{0,30}:\\/\\/[^\\s/:@]{0,256}:)[^\\s/]{1,256}@/gi, `$1${REDACTED}@`],\n // Authorization: Bearer <token>\n [/\\b(Bearer\\s{1,3})[A-Za-z0-9._~+/=-]{8,4096}/g, `$1${REDACTED}`],\n // A bare JWT: three base64url segments, the first starting eyJ.\n [/\\beyJ[A-Za-z0-9_-]{4,4096}\\.[A-Za-z0-9_-]{4,4096}\\.[A-Za-z0-9_-]{0,4096}/g, REDACTED],\n // Incoming webhook URLs carry their secret in the path.\n [/(\\bhooks\\.slack\\.com\\/(?:services|workflows|triggers)\\/)[A-Za-z0-9/_-]{1,255}/gi, `$1${REDACTED}`],\n [/(\\bdiscord(?:app)?\\.com\\/api\\/(?:v\\d{1,2}\\/)?webhooks\\/)[A-Za-z0-9/_-]{1,255}/gi, `$1${REDACTED}`],\n // Well-known token shapes: AWS, GitHub, Slack, Stripe, Anthropic, OpenAI, and Google style keys.\n [/\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b/g, REDACTED],\n [/\\b(?:gh[pousr]_[A-Za-z0-9]{30,255}|github_pat_[A-Za-z0-9_]{20,255})\\b/g, REDACTED],\n [/\\bxox[abposr]-[A-Za-z0-9-]{10,255}/g, REDACTED],\n [/\\b[rsp]k_(?:live|test)_[A-Za-z0-9]{10,255}\\b/g, REDACTED],\n [/\\bwhsec_[A-Za-z0-9+/=]{16,255}/g, REDACTED],\n [/\\bsk-[A-Za-z0-9_-]{20,255}/g, REDACTED],\n [/\\bAIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])/g, REDACTED],\n];\n\n/**\n * The default `redact`: blanks values that look like secrets (key=value pairs\n * with secret-ish names, Authorization headers, URL credentials, bearer\n * tokens, JWTs, PEM private keys, webhook URLs, and well-known token formats)\n * before output or an error is stored, shown, or sent anywhere.\n */\nexport function redactSecrets(text: string): string {\n let out = text;\n for (const [pattern, replacement] of SECRET_PATTERNS) out = out.replace(pattern, replacement as string);\n return out;\n}\n","import { stripJsonNul, stripNul } from \"../output.js\";\nimport type { JobState, Run, StoredJob, StoredJobDefinition } from \"../types.js\";\n\n/**\n * The schema, statements, and row mapping shared by the SQLite and Postgres\n * stores. Imports no driver, so either entry point can pull it in alone.\n * Statements are written with `?` placeholders; Postgres numbers them.\n */\n\nexport type Dialect = \"sqlite\" | \"postgres\";\n\nexport const DEFAULT_PREFIX = \"cronwatch_\";\n\n// Postgres truncates identifiers past 63 bytes; the longest name we build is the prefix plus \"runs_job_started\".\nconst MAX_PREFIX = 63 - \"runs_job_started\".length;\n\n/**\n * Table names are built from the prefix, so it must be a plain lowercase\n * identifier. Uppercase is refused rather than folded: Postgres lowercases\n * unquoted names, so \"Monitoring_\" would quietly become \"monitoring_\".\n */\nexport function tablePrefix(prefix: string = DEFAULT_PREFIX): string {\n if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || prefix.length > MAX_PREFIX) {\n throw new Error(\n `cronwatch: invalid table prefix ${JSON.stringify(prefix)}. Use lowercase letters, digits, and underscores, ` +\n `not starting with a digit, at most ${MAX_PREFIX} characters.`,\n );\n }\n return prefix;\n}\n\nexport function schema(dialect: Dialect, p: string): string {\n const pg = dialect === \"postgres\";\n const int = pg ? \"BIGINT\" : \"INTEGER\";\n const json = pg ? \"JSONB\" : \"TEXT\";\n return `\n CREATE TABLE IF NOT EXISTS ${p}jobs (\n name TEXT PRIMARY KEY,\n definition ${json} NOT NULL,\n created_at ${int} NOT NULL,\n updated_at ${int} NOT NULL\n );\n CREATE TABLE IF NOT EXISTS ${p}runs (${pg ? \"\\n seq BIGSERIAL,\" : \"\"}\n id TEXT PRIMARY KEY,\n job TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at ${int} NOT NULL,\n finished_at ${int},\n duration_ms ${int},\n error TEXT,\n output TEXT,\n metrics ${json} NOT NULL DEFAULT '{}',\n trigger TEXT NOT NULL DEFAULT 'run'\n );\n CREATE INDEX IF NOT EXISTS ${p}runs_job_started ON ${p}runs (job, started_at DESC);\n CREATE INDEX IF NOT EXISTS ${p}runs_running ON ${p}runs (status) WHERE status = 'running';\n CREATE TABLE IF NOT EXISTS ${p}state (\n job TEXT PRIMARY KEY,\n state ${json} NOT NULL\n );\n `;\n}\n\nexport function statements(dialect: Dialect, p: string) {\n const pg = dialect === \"postgres\";\n // Insertion order, to break ties between runs that started in the same millisecond.\n const seq = pg ? \"seq\" : \"rowid\";\n // Byte order on both, so names sort the same whatever the database's collation.\n const byName = pg ? `name COLLATE \"C\"` : \"name\";\n // The version inside a state's JSON, as stateVersion() reads it: a whole\n // number from 0 to 2^53 - 1, else 0 (none, or a foreign row's 1.5 or \"x\",\n // which must neither fail the statement nor refuse every write for good;\n // on SQLite, also text that is not JSON).\n // Each CASE tests the JSON type before any cast.\n const version = (column: string) => {\n if (pg) {\n const v = `(${column}->>'version')::numeric`;\n return `CASE WHEN jsonb_typeof(${column}->'version') <> 'number' THEN 0 WHEN ${v} % 1 = 0 AND ${v} BETWEEN 0 AND 9007199254740991 THEN ${v}::bigint ELSE 0 END`;\n }\n const v = `json_extract(${column}, '$.version')`;\n // Text that is not JSON at all (SQLite holds any) counts as 0 too, before json_type could fail on it.\n return `CASE WHEN NOT json_valid(${column}) THEN 0 WHEN json_type(${column}, '$.version') NOT IN ('integer', 'real') THEN 0 WHEN ${v} = CAST(${v} AS INTEGER) AND ${v} BETWEEN 0 AND 9007199254740991 THEN CAST(${v} AS INTEGER) ELSE 0 END`;\n };\n const sql = {\n upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,\n getJob: `SELECT * FROM ${p}jobs WHERE name = ?`,\n listJobs: `SELECT * FROM ${p}jobs ORDER BY ${byName}`,\n deleteRuns: `DELETE FROM ${p}runs WHERE job = ?`,\n deleteState: `DELETE FROM ${p}state WHERE job = ?`,\n deleteJob: `DELETE FROM ${p}jobs WHERE name = ?`,\n insertRun: `INSERT INTO ${p}runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n updateRun: `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?`,\n getRun: `SELECT * FROM ${p}runs WHERE id = ?`,\n listRuns: `SELECT * FROM ${p}runs WHERE job = ? ORDER BY started_at DESC, ${seq} DESC LIMIT ?`,\n runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,\n getState: `SELECT state FROM ${p}state WHERE job = ?`,\n setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,\n // compareAndSetState. Expecting version 0 also matches a missing row, so\n // that case inserts; any other version must find its row.\n casInsert: `INSERT INTO ${p}state (job, state) VALUES (?, ?)\n ON CONFLICT (job) DO UPDATE SET state = excluded.state WHERE ${version(`${p}state.state`)} = 0`,\n casUpdate: `UPDATE ${p}state SET state = ? WHERE job = ? AND ${version(\"state\")} = ?`,\n // Each job's newest run is kept whatever its age: without it, a job that\n // runs less often than the retention looks like it never ran.\n prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?\n AND started_at < (SELECT MAX(r.started_at) FROM ${p}runs r WHERE r.job = ${p}runs.job)`,\n };\n if (pg) {\n for (const key of Object.keys(sql) as (keyof typeof sql)[]) {\n let n = 0;\n sql[key] = sql[key].replace(/\\?/g, () => `$${++n}`);\n }\n }\n return sql;\n}\n\n/**\n * updateRunIf: the update above, only while the stored status is one of\n * `count` statuses. Built per count, since the list is bound value by value.\n */\nexport function updateRunIfSql(dialect: Dialect, p: string, count: number): string {\n const text = `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ? AND status IN (${Array.from({ length: count }, () => \"?\").join(\", \")})`;\n if (dialect !== \"postgres\") return text;\n let n = 0;\n return text.replace(/\\?/g, () => `$${++n}`);\n}\n\n/**\n * Postgres refuses U+0000 in TEXT and JSONB, and a refused write loses the\n * whole row, so every dialect writes text without it: a run's trigger,\n * output, error, and metric names, and every key and string of a definition\n * and a state. Identifiers (a job's name, a run's id) are written as given;\n * the client refuses one with a NUL before it gets here.\n */\nconst text = (value: string | null) => (value === null ? null : stripNul(value));\nconst jsonText = (value: unknown) => stripJsonNul(JSON.stringify(value));\n\n// Parameters in statement order, so both drivers bind the same values.\nexport const params = {\n upsertJob: (definition: StoredJobDefinition, now: number) => [definition.name, jsonText(definition), now, now],\n insertRun: (run: Run) => [\n run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), stripNul(run.trigger),\n ],\n updateRun: (run: Run) => [run.status, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), run.id],\n updateRunIf: (run: Run, fromStatuses: Run[\"status\"][]) => [\n run.status, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), run.id, ...fromStatuses,\n ],\n setState: (state: JobState) => [state.job, jsonText(state)],\n casInsert: (state: JobState) => [state.job, jsonText(state)],\n casUpdate: (state: JobState, expectedVersion: number) => [jsonText(state), state.job, expectedVersion],\n};\n\n// SQLite hands back JSON as TEXT and Postgres as parsed JSONB; Postgres returns BIGINT as a string.\ntype Json<T> = T | string;\ntype Int = number | string;\n\nexport interface JobRow { name: string; definition: Json<StoredJobDefinition>; created_at: Int; updated_at: Int }\nexport interface RunRow {\n id: string; job: string; status: Run[\"status\"]; started_at: Int; finished_at: Int | null;\n duration_ms: Int | null; error: string | null; output: string | null; metrics: Json<Record<string, number>> | null; trigger: string;\n}\nexport interface StateRow { state: Json<JobState> }\n\n/**\n * Rows are read leniently: a foreign, hand-edited, or damaged row (SQLite\n * keeps whatever type it is given, in any column) must affect only its own\n * job, never every read. JSON text that does not parse reads as null, which\n * the client takes as no state, or as an unreadable definition it reports.\n */\nconst json = (v: unknown): unknown => {\n if (typeof v !== \"string\") return v;\n try {\n return JSON.parse(v) as unknown;\n } catch {\n return null;\n }\n};\n/** A time, or a count of milliseconds, as a column holds it (a string from Postgres's BIGINT): NaN when it is not a number. */\nconst number = (v: unknown): number => (typeof v === \"number\" ? v : typeof v === \"string\" && v.trim() !== \"\" ? Number(v) : Number.NaN);\n/** A time that must be there: one that is not a finite number reads as 0. */\nconst time = (v: unknown): number => {\n const n = number(v);\n return Number.isFinite(n) ? n : 0;\n};\n/** A time or duration that may be absent: one that is not a finite number reads as null. */\nconst maybeTime = (v: unknown): number | null => {\n const n = number(v);\n return Number.isFinite(n) ? n : null;\n};\nconst textOrNull = (v: unknown): string | null => (typeof v === \"string\" ? v : null);\nconst isObject = (v: unknown): v is Record<string, number> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n\n/** A job's row. Its definition is as stored, or null when its text does not parse; the client reads the rest (see readStoredJob). */\nexport function rowToJob(r: JobRow): StoredJob {\n return { name: r.name, definition: json(r.definition) as StoredJobDefinition, createdAt: time(r.created_at), updatedAt: time(r.updated_at) };\n}\n\n/**\n * A run's row. A start that is not a finite number reads as 0, a finish or\n * duration as null; an error or output that is not text as null; metrics\n * that do not parse to an object as {}; a trigger that is not text as \"run\".\n */\nexport function rowToRun(r: RunRow): Run {\n const metrics = json(r.metrics);\n return {\n id: r.id, job: r.job, status: r.status, startedAt: time(r.started_at), finishedAt: maybeTime(r.finished_at),\n durationMs: maybeTime(r.duration_ms), error: textOrNull(r.error), output: textOrNull(r.output),\n metrics: isObject(metrics) ? metrics : {}, trigger: typeof r.trigger === \"string\" ? r.trigger : \"run\",\n };\n}\n\n/** A state's row, or null when its text does not parse (normalizeState reads anything else that is not an object as no state). */\nexport function rowToState(r: StateRow): JobState {\n return json(r.state) as JobState;\n}\n","import type { Store, StoredJob } from \"../types.js\";\nimport { params, rowToJob, rowToRun, rowToState, schema, statements, tablePrefix, updateRunIfSql, type JobRow, type RunRow, type StateRow } from \"./sql.js\";\n\n/**\n * The parts of a Cloudflare D1 binding this store uses. Written out here so\n * the SDK needs nothing from @cloudflare/workers-types: a Worker's\n * `env.DB: D1Database` fits it as it is.\n */\nexport interface D1PreparedStatementLike {\n bind(...values: unknown[]): D1PreparedStatementLike;\n first(): Promise<unknown>;\n all(): Promise<{ results?: unknown[] }>;\n run(): Promise<{ meta?: { changes?: number } }>;\n}\n\nexport interface D1DatabaseLike {\n prepare(query: string): D1PreparedStatementLike;\n batch(statements: D1PreparedStatementLike[]): Promise<unknown[]>;\n}\n\nexport interface D1Options {\n /** Table name prefix: lowercase letters, digits, and underscores. Default \"cronwatch_\". */\n prefix?: string;\n /**\n * Create the tables on first use (CREATE TABLE IF NOT EXISTS), once per\n * isolate. Default true. Pass false when a D1 migration creates them\n * instead (see the Cloudflare docs page for the file).\n */\n createTables?: boolean;\n}\n\n/** Databases (and prefixes) whose tables this isolate has already made sure of. */\nconst created = new WeakMap<object, Set<string>>();\n\n/**\n * Keeps everything in Cloudflare D1, for Workers. D1 is SQLite, so this is\n * the SQLite store's schema, statements, and row mapping over D1's\n * prepare, bind, and batch. Create it where the binding is, inside the\n * handler: `d1(env.DB)`.\n */\nexport function d1(database: D1DatabaseLike, options: D1Options = {}): Store {\n if (!database || typeof database.prepare !== \"function\" || typeof database.batch !== \"function\") {\n throw new Error(\"cronwatch: d1() needs a D1 database binding, such as env.DB\");\n }\n const p = tablePrefix(options.prefix);\n // D1 keeps its own tables under _cf_ and refuses writes to them.\n if (p.startsWith(\"_cf_\")) throw new Error(`cronwatch: invalid table prefix ${JSON.stringify(p)}. D1 reserves names starting with _cf_.`);\n const sql = statements(\"sqlite\", p);\n const createTables = options.createTables ?? true;\n\n const stmt = (text: string, values: unknown[] = []) => {\n const s = database.prepare(text);\n return values.length > 0 ? s.bind(...values) : s;\n };\n const first = async <T>(text: string, values: unknown[]): Promise<T | null> => ((await stmt(text, values).first()) as T | null) ?? null;\n const all = async <T>(text: string, values: unknown[] = []): Promise<T[]> => ((await stmt(text, values).all()).results ?? []) as T[];\n const changes = async (text: string, values: unknown[]): Promise<number> => (await stmt(text, values).run()).meta?.changes ?? 0;\n\n return {\n async init() {\n if (!createTables || created.get(database)?.has(p)) return;\n // exec() splits on newlines, so each statement goes in on its own, in one batch.\n const ddl = schema(\"sqlite\", p).split(\";\").map((s) => s.trim()).filter(Boolean);\n await database.batch(ddl.map((s) => database.prepare(s)));\n // Remembered only once it worked, and as a flag rather than a promise,\n // so no invocation ever waits on another's unfinished I/O.\n let prefixes = created.get(database);\n if (!prefixes) created.set(database, (prefixes = new Set()));\n prefixes.add(p);\n },\n async upsertJob(definition, now) {\n await stmt(sql.upsertJob, params.upsertJob(definition, now)).run();\n },\n async getJob(name) {\n const r = await first<JobRow>(sql.getJob, [name]);\n return r ? rowToJob(r) : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n return (await all<JobRow>(sql.listJobs)).map(rowToJob);\n },\n async deleteJob(name) {\n // A batch is one transaction.\n await database.batch([stmt(sql.deleteRuns, [name]), stmt(sql.deleteState, [name]), stmt(sql.deleteJob, [name])]);\n },\n async insertRun(run) {\n await stmt(sql.insertRun, params.insertRun(run)).run();\n },\n async updateRun(run) {\n await stmt(sql.updateRun, params.updateRun(run)).run();\n },\n async updateRunIf(run, fromStatuses) {\n if (fromStatuses.length === 0) return false;\n return (await changes(updateRunIfSql(\"sqlite\", p, fromStatuses.length), params.updateRunIf(run, fromStatuses))) > 0;\n },\n async getRun(id) {\n const r = await first<RunRow>(sql.getRun, [id]);\n return r ? rowToRun(r) : null;\n },\n async listRuns(job, limit) {\n return (await all<RunRow>(sql.listRuns, [job, limit])).map(rowToRun);\n },\n async lastRun(job) {\n const r = await first<RunRow>(sql.listRuns, [job, 1]);\n return r ? rowToRun(r) : null;\n },\n async runningRuns() {\n return (await all<RunRow>(sql.runningRuns)).map(rowToRun);\n },\n async getState(job) {\n const r = await first<StateRow>(sql.getState, [job]);\n return r ? rowToState(r) : null;\n },\n async setState(state) {\n await stmt(sql.setState, params.setState(state)).run();\n },\n async compareAndSetState(state, expectedVersion) {\n const written = expectedVersion === 0\n ? await changes(sql.casInsert, params.casInsert(state))\n : await changes(sql.casUpdate, params.casUpdate(state, expectedVersion));\n return written > 0;\n },\n async prune(before) {\n return changes(sql.prune, [before]);\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/output.ts","../src/stores/sql.ts","../src/stores/d1.ts"],"names":["text","json"],"mappings":"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","file":"d1.js","sourcesContent":["/** Output is capped so a chatty job cannot fill the store. The tail is kept. */\nexport const OUTPUT_CAP = 16 * 1024;\n\n/**\n * NUL characters are removed first, since Postgres refuses them in TEXT and\n * JSONB and the whole run row would be lost. The cap then applies to what is\n * left.\n */\nexport function capOutput(text: string): string {\n const clean = stripNul(text);\n if (clean.length <= OUTPUT_CAP) return clean;\n return TRIMMED + clean.slice(clean.length - OUTPUT_CAP);\n}\n\nconst TRIMMED = \"[earlier output trimmed]\\n\";\n\n/**\n * How much text before the kept tail redaction reads, and never keeps: three\n * times the longest secret a default pattern can match (a PEM key's 16 KB body\n * with its header and footer, under OUTPUT_CAP + 1024), since a replacement\n * grows what it replaces at most threefold.\n */\nexport const REDACT_EDGE = 3 * (OUTPUT_CAP + 1024);\n\n/**\n * Output or an error as it is stored: redacted, then capped like capOutput,\n * so the cut cannot fall inside a secret and keep what follows its label.\n * Text of at most OUTPUT_CAP + REDACT_EDGE is redacted whole. Longer text is\n * cut to that many units from its end first, and after redacting, the first\n * REDACT_EDGE units are never kept: a secret whose label fell before that cut\n * is left out with them. NULs go before and after `redact`.\n */\nexport function redactAndCap(text: string, redact: (text: string) => string): string {\n const clean = stripNul(text);\n const from = clean.length - (OUTPUT_CAP + REDACT_EDGE);\n if (from <= 0) return capOutput(redact(clean));\n const redacted = stripNul(redact(clean.slice(from)));\n return TRIMMED + redacted.slice(Math.max(redacted.length - OUTPUT_CAP, REDACT_EDGE));\n}\n\n/** Removes every U+0000. */\nexport function stripNul(text: string): string {\n return text.includes(\"\\u0000\") ? text.replace(/\\u0000/g, \"\") : text;\n}\n\n/**\n * Removes every U+0000 from JSON text, keys and strings alike, by dropping\n * each \\u0000 escape (a NUL can appear in JSON no other way). Escapes are\n * read left to right in pairs, so an escaped backslash followed by \"u0000\"\n * is left as it is.\n */\nexport function stripJsonNul(json: string): string {\n return json.includes(\"\\\\u0000\") ? json.replace(/\\\\(u0000|[\\s\\S])/g, (escape, next: string) => (next === \"u0000\" ? \"\" : escape)) : json;\n}\n\n/** \"Name: message\" and the first five stack frames, capped like output. */\nexport function errorMessage(error: unknown): string {\n return capOutput(describeError(error));\n}\n\n/** \"Name: message\" and the first five stack frames, not capped: see redactAndCap. */\nexport function describeError(error: unknown): string {\n if (error instanceof Error) {\n // The stack repeats the header (over several lines when the message has\n // newlines), so take only its frames.\n const frames = (error.stack ?? \"\").split(\"\\n\").filter((line) => /^\\s+at /.test(line)).slice(0, 5);\n return `${error.name}: ${error.message}${frames.length ? `\\n${frames.join(\"\\n\")}` : \"\"}`;\n }\n if (typeof error === \"string\") return error;\n try {\n return JSON.stringify(error) ?? String(error);\n } catch {\n return String(error);\n }\n}\n\nconst REDACTED = \"[redacted]\";\n\n// Bounded quantifiers throughout, so a long line cannot make these backtrack.\n// They apply in this order, each to the text the ones before it left.\nconst SECRET_PATTERNS: [RegExp, string | ((match: string, ...groups: string[]) => string)][] = [\n // A PEM private key, header to footer. Without a footer (the output was\n // trimmed) it runs to the end of the base64 body. A \"-\" that starts five\n // dashes ends the body, so the footer is never swallowed into it.\n [\n /-----BEGIN (?:[A-Z0-9]{1,20} ){0,3}PRIVATE KEY-----(?:[A-Za-z0-9+/=\\s,:]|-(?!----)){0,16384}(?:-----END (?:[A-Z0-9]{1,20} ){0,3}PRIVATE KEY-----)?/g,\n REDACTED,\n ],\n // password=..., API_KEY: ..., \"client_secret\": \"...\", TOKEN='...', token=...,\n // :password=>\"...\" (but not max_tokens: 800). A quoted value is blanked to\n // its closing quote, spaces and all, and keeps its quotes.\n [\n /\\b([A-Za-z0-9_-]{0,40}(?:secret|token|passw(?:or)?d|pwd|api[_-]?key|access[_-]?key|private[_-]?key|credential)[A-Za-z0-9_-]{0,40}(?<![Tt][Oo][Kk][Ee][Nn][Ss])\"?\\s{0,3}(?:=>|[=:])\\s{0,3})(?:(\")[^\"\\n]{1,4096}\"|(')[^'\\n]{1,4096}'|[\"']?[^\\s\"',;&]{1,4096})/gi,\n (_match, name: string, double?: string, single?: string) => {\n const quote = double ?? single ?? \"\";\n return `${name}${quote}${REDACTED}${quote}`;\n },\n ],\n // Authorization: Basic <base64> and Authorization: Token <token>, also as a JSON or hash entry.\n [/\\b((?:proxy-)?authorization[\"']?\\s{0,3}(?:=>|[=:])\\s{0,3}[\"']?\\s{0,3}(?:basic|token)\\s{1,3})[A-Za-z0-9._~+/=:-]{1,4096}/gi, `$1${REDACTED}`],\n // Credentials inside a URL: postgres://user:password@host. The password\n // runs to the last \"@\" before a \"/\" or a space, so one that contains \"@\"\n // is blanked whole.\n [/(\\b[a-z][a-z0-9+.-]{0,30}:\\/\\/[^\\s/:@]{0,256}:)[^\\s/]{1,256}@/gi, `$1${REDACTED}@`],\n // Authorization: Bearer <token>\n [/\\b(Bearer\\s{1,3})[A-Za-z0-9._~+/=-]{8,4096}/g, `$1${REDACTED}`],\n // A bare JWT: three base64url segments, the first starting eyJ.\n [/\\beyJ[A-Za-z0-9_-]{4,4096}\\.[A-Za-z0-9_-]{4,4096}\\.[A-Za-z0-9_-]{0,4096}/g, REDACTED],\n // Incoming webhook URLs carry their secret in the path.\n [/(\\bhooks\\.slack\\.com\\/(?:services|workflows|triggers)\\/)[A-Za-z0-9/_-]{1,255}/gi, `$1${REDACTED}`],\n [/(\\bdiscord(?:app)?\\.com\\/api\\/(?:v\\d{1,2}\\/)?webhooks\\/)[A-Za-z0-9/_-]{1,255}/gi, `$1${REDACTED}`],\n // Well-known token shapes: AWS, GitHub, Slack, Stripe, Anthropic, OpenAI, and Google style keys.\n [/\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b/g, REDACTED],\n [/\\b(?:gh[pousr]_[A-Za-z0-9]{30,255}|github_pat_[A-Za-z0-9_]{20,255})\\b/g, REDACTED],\n [/\\bxox[abposr]-[A-Za-z0-9-]{10,255}/g, REDACTED],\n [/\\b[rsp]k_(?:live|test)_[A-Za-z0-9]{10,255}\\b/g, REDACTED],\n [/\\bwhsec_[A-Za-z0-9+/=]{16,255}/g, REDACTED],\n [/\\bsk-[A-Za-z0-9_-]{20,255}/g, REDACTED],\n [/\\bAIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])/g, REDACTED],\n];\n\n/**\n * The default `redact`: blanks values that look like secrets (key=value pairs\n * with secret-ish names, Authorization headers, URL credentials, bearer\n * tokens, JWTs, PEM private keys, webhook URLs, and well-known token formats)\n * before output or an error is stored, shown, or sent anywhere.\n */\nexport function redactSecrets(text: string): string {\n let out = text;\n for (const [pattern, replacement] of SECRET_PATTERNS) out = out.replace(pattern, replacement as string);\n return out;\n}\n","import { stripJsonNul, stripNul } from \"../output.js\";\nimport type { JobState, Run, StoredJob, StoredJobDefinition } from \"../types.js\";\n\n/**\n * The schema, statements, and row mapping shared by the SQLite and Postgres\n * stores. Imports no driver, so either entry point can pull it in alone.\n * Statements are written with `?` placeholders; Postgres numbers them.\n */\n\nexport type Dialect = \"sqlite\" | \"postgres\";\n\nexport const DEFAULT_PREFIX = \"cronwatch_\";\n\n// Postgres truncates identifiers past 63 bytes; the longest name we build is the prefix plus \"runs_job_started\".\nconst MAX_PREFIX = 63 - \"runs_job_started\".length;\n\n/**\n * Table names are built from the prefix, so it must be a plain lowercase\n * identifier. Uppercase is refused rather than folded: Postgres lowercases\n * unquoted names, so \"Monitoring_\" would quietly become \"monitoring_\".\n */\nexport function tablePrefix(prefix: string = DEFAULT_PREFIX): string {\n if (!/^[a-z_][a-z0-9_]*$/.test(prefix) || prefix.length > MAX_PREFIX) {\n throw new Error(\n `cronwatch: invalid table prefix ${JSON.stringify(prefix)}. Use lowercase letters, digits, and underscores, ` +\n `not starting with a digit, at most ${MAX_PREFIX} characters.`,\n );\n }\n return prefix;\n}\n\nexport function schema(dialect: Dialect, p: string): string {\n const pg = dialect === \"postgres\";\n const int = pg ? \"BIGINT\" : \"INTEGER\";\n const json = pg ? \"JSONB\" : \"TEXT\";\n return `\n CREATE TABLE IF NOT EXISTS ${p}jobs (\n name TEXT PRIMARY KEY,\n definition ${json} NOT NULL,\n created_at ${int} NOT NULL,\n updated_at ${int} NOT NULL\n );\n CREATE TABLE IF NOT EXISTS ${p}runs (${pg ? \"\\n seq BIGSERIAL,\" : \"\"}\n id TEXT PRIMARY KEY,\n job TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at ${int} NOT NULL,\n finished_at ${int},\n duration_ms ${int},\n error TEXT,\n output TEXT,\n metrics ${json} NOT NULL DEFAULT '{}',\n trigger TEXT NOT NULL DEFAULT 'run'\n );\n CREATE INDEX IF NOT EXISTS ${p}runs_job_started ON ${p}runs (job, started_at DESC);\n CREATE INDEX IF NOT EXISTS ${p}runs_running ON ${p}runs (status) WHERE status = 'running';\n CREATE TABLE IF NOT EXISTS ${p}state (\n job TEXT PRIMARY KEY,\n state ${json} NOT NULL\n );\n `;\n}\n\nexport function statements(dialect: Dialect, p: string) {\n const pg = dialect === \"postgres\";\n // Insertion order, to break ties between runs that started in the same millisecond.\n const seq = pg ? \"seq\" : \"rowid\";\n // Byte order on both, so names sort the same whatever the database's collation.\n const byName = pg ? `name COLLATE \"C\"` : \"name\";\n // The version inside a state's JSON, as stateVersion() reads it: a whole\n // number from 0 to 2^53 - 1, else 0 (none, or a foreign row's 1.5 or \"x\",\n // which must neither fail the statement nor refuse every write for good;\n // on SQLite, also text that is not JSON).\n // Each CASE tests the JSON type before any cast.\n const version = (column: string) => {\n if (pg) {\n const v = `(${column}->>'version')::numeric`;\n return `CASE WHEN jsonb_typeof(${column}->'version') <> 'number' THEN 0 WHEN ${v} % 1 = 0 AND ${v} BETWEEN 0 AND 9007199254740991 THEN ${v}::bigint ELSE 0 END`;\n }\n const v = `json_extract(${column}, '$.version')`;\n // Text that is not JSON at all (SQLite holds any) counts as 0 too, before json_type could fail on it.\n return `CASE WHEN NOT json_valid(${column}) THEN 0 WHEN json_type(${column}, '$.version') NOT IN ('integer', 'real') THEN 0 WHEN ${v} = CAST(${v} AS INTEGER) AND ${v} BETWEEN 0 AND 9007199254740991 THEN CAST(${v} AS INTEGER) ELSE 0 END`;\n };\n const sql = {\n upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT (name) DO UPDATE SET definition = excluded.definition, updated_at = excluded.updated_at`,\n getJob: `SELECT * FROM ${p}jobs WHERE name = ?`,\n listJobs: `SELECT * FROM ${p}jobs ORDER BY ${byName}`,\n deleteRuns: `DELETE FROM ${p}runs WHERE job = ?`,\n deleteState: `DELETE FROM ${p}state WHERE job = ?`,\n deleteJob: `DELETE FROM ${p}jobs WHERE name = ?`,\n insertRun: `INSERT INTO ${p}runs (id, job, status, started_at, finished_at, duration_ms, error, output, metrics, trigger)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n updateRun: `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ?`,\n getRun: `SELECT * FROM ${p}runs WHERE id = ?`,\n listRuns: `SELECT * FROM ${p}runs WHERE job = ? ORDER BY started_at DESC, ${seq} DESC LIMIT ?`,\n runningRuns: `SELECT * FROM ${p}runs WHERE status = 'running' ORDER BY started_at, ${seq}`,\n getState: `SELECT state FROM ${p}state WHERE job = ?`,\n setState: `INSERT INTO ${p}state (job, state) VALUES (?, ?) ON CONFLICT (job) DO UPDATE SET state = excluded.state`,\n // compareAndSetState. Expecting version 0 also matches a missing row, so\n // that case inserts; any other version must find its row.\n casInsert: `INSERT INTO ${p}state (job, state) VALUES (?, ?)\n ON CONFLICT (job) DO UPDATE SET state = excluded.state WHERE ${version(`${p}state.state`)} = 0`,\n casUpdate: `UPDATE ${p}state SET state = ? WHERE job = ? AND ${version(\"state\")} = ?`,\n // Each job's newest run is kept whatever its age: without it, a job that\n // runs less often than the retention looks like it never ran.\n prune: `DELETE FROM ${p}runs WHERE status <> 'running' AND started_at < ?\n AND started_at < (SELECT MAX(r.started_at) FROM ${p}runs r WHERE r.job = ${p}runs.job)`,\n };\n if (pg) {\n for (const key of Object.keys(sql) as (keyof typeof sql)[]) {\n let n = 0;\n sql[key] = sql[key].replace(/\\?/g, () => `$${++n}`);\n }\n }\n return sql;\n}\n\n/**\n * updateRunIf: the update above, only while the stored status is one of\n * `count` statuses. Built per count, since the list is bound value by value.\n */\nexport function updateRunIfSql(dialect: Dialect, p: string, count: number): string {\n const text = `UPDATE ${p}runs SET status = ?, finished_at = ?, duration_ms = ?, error = ?, output = ?, metrics = ? WHERE id = ? AND status IN (${Array.from({ length: count }, () => \"?\").join(\", \")})`;\n if (dialect !== \"postgres\") return text;\n let n = 0;\n return text.replace(/\\?/g, () => `$${++n}`);\n}\n\n/**\n * Postgres refuses U+0000 in TEXT and JSONB, and a refused write loses the\n * whole row, so every dialect writes text without it: a run's trigger,\n * output, error, and metric names, and every key and string of a definition\n * and a state. Identifiers (a job's name, a run's id) are written as given;\n * the client refuses one with a NUL before it gets here.\n */\nconst text = (value: string | null) => (value === null ? null : stripNul(value));\nconst jsonText = (value: unknown) => stripJsonNul(JSON.stringify(value));\n\n// Parameters in statement order, so both drivers bind the same values.\nexport const params = {\n upsertJob: (definition: StoredJobDefinition, now: number) => [definition.name, jsonText(definition), now, now],\n insertRun: (run: Run) => [\n run.id, run.job, run.status, run.startedAt, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), stripNul(run.trigger),\n ],\n updateRun: (run: Run) => [run.status, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), run.id],\n updateRunIf: (run: Run, fromStatuses: Run[\"status\"][]) => [\n run.status, run.finishedAt, run.durationMs, text(run.error), text(run.output), jsonText(run.metrics), run.id, ...fromStatuses,\n ],\n setState: (state: JobState) => [state.job, jsonText(state)],\n casInsert: (state: JobState) => [state.job, jsonText(state)],\n casUpdate: (state: JobState, expectedVersion: number) => [jsonText(state), state.job, expectedVersion],\n};\n\n// SQLite hands back JSON as TEXT and Postgres as parsed JSONB; Postgres returns BIGINT as a string.\ntype Json<T> = T | string;\ntype Int = number | string;\n\nexport interface JobRow { name: string; definition: Json<StoredJobDefinition>; created_at: Int; updated_at: Int }\nexport interface RunRow {\n id: string; job: string; status: Run[\"status\"]; started_at: Int; finished_at: Int | null;\n duration_ms: Int | null; error: string | null; output: string | null; metrics: Json<Record<string, number>> | null; trigger: string;\n}\nexport interface StateRow { state: Json<JobState> }\n\n/**\n * Rows are read leniently: a foreign, hand-edited, or damaged row (SQLite\n * keeps whatever type it is given, in any column) must affect only its own\n * job, never every read. JSON text that does not parse reads as null, which\n * the client takes as no state, or as an unreadable definition it reports.\n */\nconst json = (v: unknown): unknown => {\n if (typeof v !== \"string\") return v;\n try {\n return JSON.parse(v) as unknown;\n } catch {\n return null;\n }\n};\n/** A time, or a count of milliseconds, as a column holds it (a string from Postgres's BIGINT): NaN when it is not a number. */\nconst number = (v: unknown): number => (typeof v === \"number\" ? v : typeof v === \"string\" && v.trim() !== \"\" ? Number(v) : Number.NaN);\n/** A time that must be there: one that is not a finite number reads as 0. */\nconst time = (v: unknown): number => {\n const n = number(v);\n return Number.isFinite(n) ? n : 0;\n};\n/** A time or duration that may be absent: one that is not a finite number reads as null. */\nconst maybeTime = (v: unknown): number | null => {\n const n = number(v);\n return Number.isFinite(n) ? n : null;\n};\nconst textOrNull = (v: unknown): string | null => (typeof v === \"string\" ? v : null);\nconst isObject = (v: unknown): v is Record<string, number> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n\n/** A job's row. Its definition is as stored, or null when its text does not parse; the client reads the rest (see readStoredJob). */\nexport function rowToJob(r: JobRow): StoredJob {\n return { name: r.name, definition: json(r.definition) as StoredJobDefinition, createdAt: time(r.created_at), updatedAt: time(r.updated_at) };\n}\n\n/**\n * A run's row. A start that is not a finite number reads as 0, a finish or\n * duration as null; an error or output that is not text as null; metrics\n * that do not parse to an object as {}; a trigger that is not text as \"run\".\n */\nexport function rowToRun(r: RunRow): Run {\n const metrics = json(r.metrics);\n return {\n id: r.id, job: r.job, status: r.status, startedAt: time(r.started_at), finishedAt: maybeTime(r.finished_at),\n durationMs: maybeTime(r.duration_ms), error: textOrNull(r.error), output: textOrNull(r.output),\n metrics: isObject(metrics) ? metrics : {}, trigger: typeof r.trigger === \"string\" ? r.trigger : \"run\",\n };\n}\n\n/** A state's row, or null when its text does not parse (normalizeState reads anything else that is not an object as no state). */\nexport function rowToState(r: StateRow): JobState {\n return json(r.state) as JobState;\n}\n","import type { Store, StoredJob } from \"../types.js\";\nimport { params, rowToJob, rowToRun, rowToState, schema, statements, tablePrefix, updateRunIfSql, type JobRow, type RunRow, type StateRow } from \"./sql.js\";\n\n/**\n * The parts of a Cloudflare D1 binding this store uses. Written out here so\n * the SDK needs nothing from @cloudflare/workers-types: a Worker's\n * `env.DB: D1Database` fits it as it is.\n */\nexport interface D1PreparedStatementLike {\n bind(...values: unknown[]): D1PreparedStatementLike;\n first(): Promise<unknown>;\n all(): Promise<{ results?: unknown[] }>;\n run(): Promise<{ meta?: { changes?: number } }>;\n}\n\nexport interface D1DatabaseLike {\n prepare(query: string): D1PreparedStatementLike;\n batch(statements: D1PreparedStatementLike[]): Promise<unknown[]>;\n}\n\nexport interface D1Options {\n /** Table name prefix: lowercase letters, digits, and underscores. Default \"cronwatch_\". */\n prefix?: string;\n /**\n * Create the tables on first use (CREATE TABLE IF NOT EXISTS), once per\n * isolate. Default true. Pass false when a D1 migration creates them\n * instead (see the Cloudflare docs page for the file).\n */\n createTables?: boolean;\n}\n\n/** Databases (and prefixes) whose tables this isolate has already made sure of. */\nconst created = new WeakMap<object, Set<string>>();\n\n/**\n * Keeps everything in Cloudflare D1, for Workers. D1 is SQLite, so this is\n * the SQLite store's schema, statements, and row mapping over D1's\n * prepare, bind, and batch. Create it where the binding is, inside the\n * handler: `d1(env.DB)`.\n */\nexport function d1(database: D1DatabaseLike, options: D1Options = {}): Store & Required<Pick<Store, \"lastRun\">> {\n if (!database || typeof database.prepare !== \"function\" || typeof database.batch !== \"function\") {\n throw new Error(\"cronwatch: d1() needs a D1 database binding, such as env.DB\");\n }\n const p = tablePrefix(options.prefix);\n // D1 keeps its own tables under _cf_ and refuses writes to them.\n if (p.startsWith(\"_cf_\")) throw new Error(`cronwatch: invalid table prefix ${JSON.stringify(p)}. D1 reserves names starting with _cf_.`);\n const sql = statements(\"sqlite\", p);\n const createTables = options.createTables ?? true;\n\n const stmt = (text: string, values: unknown[] = []) => {\n const s = database.prepare(text);\n return values.length > 0 ? s.bind(...values) : s;\n };\n const first = async <T>(text: string, values: unknown[]): Promise<T | null> => ((await stmt(text, values).first()) as T | null) ?? null;\n const all = async <T>(text: string, values: unknown[] = []): Promise<T[]> => ((await stmt(text, values).all()).results ?? []) as T[];\n const changes = async (text: string, values: unknown[]): Promise<number> => (await stmt(text, values).run()).meta?.changes ?? 0;\n\n return {\n async init() {\n if (!createTables || created.get(database)?.has(p)) return;\n // exec() splits on newlines, so each statement goes in on its own, in one batch.\n const ddl = schema(\"sqlite\", p).split(\";\").map((s) => s.trim()).filter(Boolean);\n await database.batch(ddl.map((s) => database.prepare(s)));\n // Remembered only once it worked, and as a flag rather than a promise,\n // so no invocation ever waits on another's unfinished I/O.\n let prefixes = created.get(database);\n if (!prefixes) created.set(database, (prefixes = new Set()));\n prefixes.add(p);\n },\n async upsertJob(definition, now) {\n await stmt(sql.upsertJob, params.upsertJob(definition, now)).run();\n },\n async getJob(name) {\n const r = await first<JobRow>(sql.getJob, [name]);\n return r ? rowToJob(r) : null;\n },\n async listJobs(): Promise<StoredJob[]> {\n return (await all<JobRow>(sql.listJobs)).map(rowToJob);\n },\n async deleteJob(name) {\n // A batch is one transaction.\n await database.batch([stmt(sql.deleteRuns, [name]), stmt(sql.deleteState, [name]), stmt(sql.deleteJob, [name])]);\n },\n async insertRun(run) {\n await stmt(sql.insertRun, params.insertRun(run)).run();\n },\n async updateRun(run) {\n await stmt(sql.updateRun, params.updateRun(run)).run();\n },\n async updateRunIf(run, fromStatuses) {\n if (fromStatuses.length === 0) return false;\n return (await changes(updateRunIfSql(\"sqlite\", p, fromStatuses.length), params.updateRunIf(run, fromStatuses))) > 0;\n },\n async getRun(id) {\n const r = await first<RunRow>(sql.getRun, [id]);\n return r ? rowToRun(r) : null;\n },\n async listRuns(job, limit) {\n return (await all<RunRow>(sql.listRuns, [job, limit])).map(rowToRun);\n },\n async lastRun(job) {\n const r = await first<RunRow>(sql.listRuns, [job, 1]);\n return r ? rowToRun(r) : null;\n },\n async runningRuns() {\n return (await all<RunRow>(sql.runningRuns)).map(rowToRun);\n },\n async getState(job) {\n const r = await first<StateRow>(sql.getState, [job]);\n return r ? rowToState(r) : null;\n },\n async setState(state) {\n await stmt(sql.setState, params.setState(state)).run();\n },\n async compareAndSetState(state, expectedVersion) {\n const written = expectedVersion === 0\n ? await changes(sql.casInsert, params.casInsert(state))\n : await changes(sql.casUpdate, params.casUpdate(state, expectedVersion));\n return written > 0;\n },\n async prune(before) {\n return changes(sql.prune, [before]);\n },\n };\n}\n"]}
package/dist/datadog.cjs CHANGED
@@ -23,16 +23,39 @@ var ERROR_BODY_MAX = 200;
23
23
  async function post(provider, url, init, secrets = []) {
24
24
  const response = await fetch(postable(url), { method: "POST", headers: init.headers, body: init.body, redirect: "error", signal: AbortSignal.timeout(TIMEOUT_MS) });
25
25
  if (!response.ok) {
26
- let text = "";
27
- try {
28
- text = await response.text();
29
- } catch {
30
- }
26
+ const text = await errorText(response);
31
27
  throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : ""}`);
32
28
  }
33
29
  await drain(response);
34
30
  return response;
35
31
  }
32
+ var ERROR_READ_MAX = 64 * 1024;
33
+ async function errorText(response) {
34
+ const reader = response.body?.getReader();
35
+ if (!reader) return "";
36
+ const chunks = [];
37
+ let size = 0;
38
+ try {
39
+ while (size < ERROR_READ_MAX) {
40
+ const { done, value } = await reader.read();
41
+ if (done) break;
42
+ const take = value.subarray(0, ERROR_READ_MAX - size);
43
+ chunks.push(take);
44
+ size += take.byteLength;
45
+ }
46
+ } catch {
47
+ } finally {
48
+ await reader.cancel().catch(() => {
49
+ });
50
+ }
51
+ const bytes = new Uint8Array(size);
52
+ let at = 0;
53
+ for (const chunk of chunks) {
54
+ bytes.set(chunk, at);
55
+ at += chunk.byteLength;
56
+ }
57
+ return new TextDecoder().decode(bytes);
58
+ }
36
59
  async function drain(response) {
37
60
  try {
38
61
  await response.body?.cancel();
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/datadog.ts"],"names":[],"mappings":"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","file":"datadog.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 await drain(response);\n return response;\n}\n\n/**\n * Lets go of a successful answer's body, which nothing reads, so its\n * connection goes back to the pool now rather than when it is collected.\n */\nexport async function drain(response: Response): Promise<void> {\n try {\n await response.body?.cancel();\n } catch {\n // Already read, or the connection is gone: nothing to let go of.\n }\n}\n\n/**\n * The parts of a webhook URL that are its credential, to cut from an error\n * body that echoes the request: its path and query together, then the last\n * segment of its path (Slack's and Discord's token) on its own.\n */\nexport function urlSecrets(url: string): string[] {\n try {\n const parsed = new URL(url);\n const last = parsed.pathname.split(\"/\").pop() ?? \"\";\n return [parsed.pathname + parsed.search, last].filter((part) => part.length > 0 && part !== \"/\");\n } catch {\n return [];\n }\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 * Datadog Events API v1.\n * API reference: https://docs.datadoghq.com/api/latest/events/ (Post an event)\n * Field limits: https://datadoghq.dev/datadog-api-client-typescript/classes/v1.EventCreateRequest.html\n * POST https://api.<site>/api/v1/events with DD-API-KEY. Answers 202.\n */\nimport type { Alert, AlertChannel } from \"../types.js\";\nimport { cut, plainText, post, sha256Hex, trimmed } from \"./shared.js\";\n\nexport interface DatadogOptions {\n /** An API key (not an application key). */\n apiKey: string;\n /** Your Datadog site: \"datadoghq.com\" (the default), \"datadoghq.eu\", \"us3.datadoghq.com\", \"us5.datadoghq.com\", \"ap1.datadoghq.com\", \"ddog-gov.com\". */\n site?: string;\n /** Extra tags, \"env:prod\" say. Every event also has cronwatch, job:<name>, and alert:<type>. */\n tags?: string[];\n /** Associates the event with a host and its tags. */\n host?: string;\n link?: (alert: Alert) => string;\n}\n\nconst ALERT_TYPE: Record<Alert[\"type\"], \"error\" | \"warning\" | \"success\"> = {\n missed: \"error\",\n failed: \"error\",\n stuck: \"error\",\n slow: \"warning\",\n over_budget: \"warning\",\n under_floor: \"warning\",\n recovered: \"success\",\n};\n\n/** Posts alerts to the Datadog event stream, aggregated per job and alert type. */\nexport function datadog(options: DatadogOptions): AlertChannel {\n // A pasted credential often carries a stray space or newline, which a header would refuse or send.\n const apiKey = trimmed(options.apiKey);\n if (!apiKey) throw new Error(\"datadog() needs an apiKey\");\n const site = (options.site ?? \"datadoghq.com\").replace(/^https?:\\/\\//, \"\").replace(/^(api|app)\\./, \"\").replace(/\\/+$/, \"\");\n if (!/^[a-z0-9.-]+$/i.test(site)) throw new Error(\"datadog() needs a site like datadoghq.com\");\n const url = `https://api.${site}/api/v1/events`;\n return {\n name: \"datadog\",\n async send(alert) {\n const link = options.link?.(alert);\n const event = {\n title: cut(alert.title, 500),\n text: cut(plainText(alert, link), 4000),\n alert_type: ALERT_TYPE[alert.type],\n aggregation_key: await aggregationKey(alert),\n date_happened: Math.floor(alert.at / 1000),\n priority: \"normal\",\n tags: [\"cronwatch\", `job:${alert.job}`, `alert:${alert.type}`, ...(options.tags ?? [])],\n ...(options.host ? { host: options.host } : {}),\n };\n await post(\"Datadog\", url, {\n headers: { \"content-type\": \"application/json\", accept: \"application/json\", \"dd-api-key\": apiKey },\n body: JSON.stringify(event),\n }, [apiKey]);\n },\n };\n}\n\n/** \"cronwatch:<job>:<type>\", or a hash of it when that passes Datadog's 100 characters. */\nasync function aggregationKey(alert: Alert): Promise<string> {\n const key = `cronwatch:${alert.job}:${alert.type}`;\n return key.length <= 100 ? key : `cronwatch:${(await sha256Hex(key)).slice(0, 40)}`;\n}\n"]}
1
+ {"version":3,"sources":["../src/alerts/shared.ts","../src/alerts/datadog.ts"],"names":[],"mappings":"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","file":"datadog.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 const text = await errorText(response);\n throw new Error(`${provider} ${origin(url)} answered ${response.status}${text ? `: ${errorBody(text, secrets)}` : \"\"}`);\n }\n await drain(response);\n return response;\n}\n\n/** The most of an error body read (64 KiB), far more than any secret plus ERROR_BODY_MAX needs. */\nexport const ERROR_READ_MAX = 64 * 1024;\n\n/**\n * The start of an error answer's body, at most ERROR_READ_MAX bytes read\n * from the stream and the rest let go, so a provider (or something posing\n * as one) cannot make a send hold a body of any size. \"\" when it cannot be\n * read. A character cut at the end decodes as U+FFFD, far past what\n * errorBody keeps.\n */\nexport async function errorText(response: Response): Promise<string> {\n const reader = response.body?.getReader();\n if (!reader) return \"\";\n const chunks: Uint8Array[] = [];\n let size = 0;\n try {\n while (size < ERROR_READ_MAX) {\n const { done, value } = await reader.read();\n if (done) break;\n const take = value.subarray(0, ERROR_READ_MAX - size);\n chunks.push(take);\n size += take.byteLength;\n }\n } catch {\n // The status is enough; whatever was read is kept.\n } finally {\n await reader.cancel().catch(() => {});\n }\n const bytes = new Uint8Array(size);\n let at = 0;\n for (const chunk of chunks) {\n bytes.set(chunk, at);\n at += chunk.byteLength;\n }\n return new TextDecoder().decode(bytes);\n}\n\n/**\n * Lets go of a successful answer's body, which nothing reads, so its\n * connection goes back to the pool now rather than when it is collected.\n */\nexport async function drain(response: Response): Promise<void> {\n try {\n await response.body?.cancel();\n } catch {\n // Already read, or the connection is gone: nothing to let go of.\n }\n}\n\n/**\n * The parts of a webhook URL that are its credential, to cut from an error\n * body that echoes the request: its path and query together, then the last\n * segment of its path (Slack's and Discord's token) on its own.\n */\nexport function urlSecrets(url: string): string[] {\n try {\n const parsed = new URL(url);\n const last = parsed.pathname.split(\"/\").pop() ?? \"\";\n return [parsed.pathname + parsed.search, last].filter((part) => part.length > 0 && part !== \"/\");\n } catch {\n return [];\n }\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 * Datadog Events API v1.\n * API reference: https://docs.datadoghq.com/api/latest/events/ (Post an event)\n * Field limits: https://datadoghq.dev/datadog-api-client-typescript/classes/v1.EventCreateRequest.html\n * POST https://api.<site>/api/v1/events with DD-API-KEY. Answers 202.\n */\nimport type { Alert, AlertChannel } from \"../types.js\";\nimport { cut, plainText, post, sha256Hex, trimmed } from \"./shared.js\";\n\nexport interface DatadogOptions {\n /** An API key (not an application key). */\n apiKey: string;\n /** Your Datadog site: \"datadoghq.com\" (the default), \"datadoghq.eu\", \"us3.datadoghq.com\", \"us5.datadoghq.com\", \"ap1.datadoghq.com\", \"ddog-gov.com\". */\n site?: string;\n /** Extra tags, \"env:prod\" say. Every event also has cronwatch, job:<name>, and alert:<type>. */\n tags?: string[];\n /** Associates the event with a host and its tags. */\n host?: string;\n link?: (alert: Alert) => string;\n}\n\nconst ALERT_TYPE: Record<Alert[\"type\"], \"error\" | \"warning\" | \"success\"> = {\n missed: \"error\",\n failed: \"error\",\n stuck: \"error\",\n slow: \"warning\",\n over_budget: \"warning\",\n under_floor: \"warning\",\n recovered: \"success\",\n};\n\n/** Posts alerts to the Datadog event stream, aggregated per job and alert type. */\nexport function datadog(options: DatadogOptions): AlertChannel {\n // A pasted credential often carries a stray space or newline, which a header would refuse or send.\n const apiKey = trimmed(options.apiKey);\n if (!apiKey) throw new Error(\"datadog() needs an apiKey\");\n const site = (options.site ?? \"datadoghq.com\").replace(/^https?:\\/\\//, \"\").replace(/^(api|app)\\./, \"\").replace(/\\/+$/, \"\");\n if (!/^[a-z0-9.-]+$/i.test(site)) throw new Error(\"datadog() needs a site like datadoghq.com\");\n const url = `https://api.${site}/api/v1/events`;\n return {\n name: \"datadog\",\n async send(alert) {\n const link = options.link?.(alert);\n const event = {\n title: cut(alert.title, 500),\n text: cut(plainText(alert, link), 4000),\n alert_type: ALERT_TYPE[alert.type],\n aggregation_key: await aggregationKey(alert),\n date_happened: Math.floor(alert.at / 1000),\n priority: \"normal\",\n tags: [\"cronwatch\", `job:${alert.job}`, `alert:${alert.type}`, ...(options.tags ?? [])],\n ...(options.host ? { host: options.host } : {}),\n };\n await post(\"Datadog\", url, {\n headers: { \"content-type\": \"application/json\", accept: \"application/json\", \"dd-api-key\": apiKey },\n body: JSON.stringify(event),\n }, [apiKey]);\n },\n };\n}\n\n/** \"cronwatch:<job>:<type>\", or a hash of it when that passes Datadog's 100 characters. */\nasync function aggregationKey(alert: Alert): Promise<string> {\n const key = `cronwatch:${alert.job}:${alert.type}`;\n return key.length <= 100 ? key : `cronwatch:${(await sha256Hex(key)).slice(0, 40)}`;\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { A as Alert, a as AlertChannel } from './types-D6t0Z846.cjs';
1
+ import { A as Alert, a as AlertChannel } from './types-B8IFVK5k.cjs';
2
2
 
3
3
  /**
4
4
  * Datadog Events API v1.
package/dist/datadog.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as Alert, a as AlertChannel } from './types-D6t0Z846.js';
1
+ import { A as Alert, a as AlertChannel } from './types-B8IFVK5k.js';
2
2
 
3
3
  /**
4
4
  * Datadog Events API v1.