@cronwatch/sdk 0.9.0 → 0.11.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 (86) hide show
  1. package/README.md +4 -4
  2. package/dist/anthropic.d.cts +1 -1
  3. package/dist/anthropic.d.ts +1 -1
  4. package/dist/bugsnag.d.cts +1 -1
  5. package/dist/bugsnag.d.ts +1 -1
  6. package/dist/{client-D35cfnNV.d.cts → client-CRMfhylq.d.cts} +129 -37
  7. package/dist/{client-CQm-8-mR.d.ts → client-D2Xk2600.d.ts} +129 -37
  8. package/dist/d1.cjs +29 -11
  9. package/dist/d1.cjs.map +1 -1
  10. package/dist/d1.d.cts +1 -1
  11. package/dist/d1.d.ts +1 -1
  12. package/dist/d1.js +29 -11
  13. package/dist/d1.js.map +1 -1
  14. package/dist/datadog.d.cts +1 -1
  15. package/dist/datadog.d.ts +1 -1
  16. package/dist/discord.cjs +25 -4
  17. package/dist/discord.cjs.map +1 -1
  18. package/dist/discord.d.cts +20 -4
  19. package/dist/discord.d.ts +20 -4
  20. package/dist/discord.js +22 -3
  21. package/dist/discord.js.map +1 -1
  22. package/dist/{email-BxCp9RQV.d.ts → email-BWuUiCkw.d.ts} +1 -1
  23. package/dist/{email-D1d5niMy.d.cts → email-Dcqe3k8n.d.cts} +1 -1
  24. package/dist/honeybadger.d.cts +1 -1
  25. package/dist/honeybadger.d.ts +1 -1
  26. package/dist/index.cjs +380 -123
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +15 -9
  29. package/dist/index.d.ts +15 -9
  30. package/dist/index.js +379 -124
  31. package/dist/index.js.map +1 -1
  32. package/dist/mailgun.d.cts +2 -2
  33. package/dist/mailgun.d.ts +2 -2
  34. package/dist/newrelic.d.cts +1 -1
  35. package/dist/newrelic.d.ts +1 -1
  36. package/dist/pg-cron.cjs +19 -8
  37. package/dist/pg-cron.cjs.map +1 -1
  38. package/dist/pg-cron.d.cts +15 -12
  39. package/dist/pg-cron.d.ts +15 -12
  40. package/dist/pg-cron.js +16 -5
  41. package/dist/pg-cron.js.map +1 -1
  42. package/dist/postgres.cjs +28 -10
  43. package/dist/postgres.cjs.map +1 -1
  44. package/dist/postgres.d.cts +1 -1
  45. package/dist/postgres.d.ts +1 -1
  46. package/dist/postgres.js +28 -10
  47. package/dist/postgres.js.map +1 -1
  48. package/dist/postmark.d.cts +2 -2
  49. package/dist/postmark.d.ts +2 -2
  50. package/dist/resend.d.cts +2 -2
  51. package/dist/resend.d.ts +2 -2
  52. package/dist/rollbar.d.cts +1 -1
  53. package/dist/rollbar.d.ts +1 -1
  54. package/dist/sendgrid.d.cts +2 -2
  55. package/dist/sendgrid.d.ts +2 -2
  56. package/dist/sentry.cjs +4 -1
  57. package/dist/sentry.cjs.map +1 -1
  58. package/dist/sentry.d.cts +5 -2
  59. package/dist/sentry.d.ts +5 -2
  60. package/dist/sentry.js +4 -1
  61. package/dist/sentry.js.map +1 -1
  62. package/dist/ses.d.cts +2 -2
  63. package/dist/ses.d.ts +2 -2
  64. package/dist/slack.d.cts +1 -1
  65. package/dist/slack.d.ts +1 -1
  66. package/dist/sqlite.cjs +29 -11
  67. package/dist/sqlite.cjs.map +1 -1
  68. package/dist/sqlite.d.cts +1 -1
  69. package/dist/sqlite.d.ts +1 -1
  70. package/dist/sqlite.js +29 -11
  71. package/dist/sqlite.js.map +1 -1
  72. package/dist/twilio.cjs +10 -4
  73. package/dist/twilio.cjs.map +1 -1
  74. package/dist/twilio.d.cts +12 -7
  75. package/dist/twilio.d.ts +12 -7
  76. package/dist/twilio.js +7 -1
  77. package/dist/twilio.js.map +1 -1
  78. package/dist/{types-Cz47Boks.d.cts → types-DLnlUie8.d.cts} +18 -2
  79. package/dist/{types-Cz47Boks.d.ts → types-DLnlUie8.d.ts} +18 -2
  80. package/dist/webhook.cjs +10 -5
  81. package/dist/webhook.cjs.map +1 -1
  82. package/dist/webhook.d.cts +18 -7
  83. package/dist/webhook.d.ts +18 -7
  84. package/dist/webhook.js +10 -6
  85. package/dist/webhook.js.map +1 -1
  86. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { S as Store, a as AlertChannel, n as TriageFn, i as JobOptions, R as Run, A as Alert, D as Duration, J as JobDefinition, f as CheckResult, k as JobSummary, j as JobState } from './types-Cz47Boks.js';
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-DLnlUie8.js';
2
2
 
3
3
  /** What a job function receives. */
4
4
  interface JobContext {
@@ -19,10 +19,14 @@ interface RoutesOptions {
19
19
  * Required to reach anything. Send it as `Authorization: Bearer <token>`,
20
20
  * or open the dashboard once with `?token=<token>` and a cookie is set.
21
21
  * Defaults to process.env.CRONWATCH_TOKEN (on Cloudflare Workers, which
22
- * have no process, pass env.CRONWATCH_TOKEN); an empty string counts as unset.
23
- * With no token while NODE_ENV is "development" or "test", the routes make
24
- * a random one and print a sign-in link to the server log on their first
25
- * request; with no token otherwise they answer 503. Pass `null` to opt out
22
+ * have no process, pass env.CRONWATCH_TOKEN); an empty string or one of
23
+ * only whitespace, given here or in the variable, counts as unset, and any
24
+ * other type (false, a number) throws.
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
30
  * and serve them open everywhere, for example behind your own auth.
27
31
  *
28
32
  * The check endpoint (/api/check) also accepts the client's cronSecret, so
@@ -62,12 +66,11 @@ interface Routes {
62
66
  DELETE: FetchHandler;
63
67
  }
64
68
  /**
65
- * A fetch-style handler serving the dashboard and a small JSON API. Mount it
66
- * in a Next.js app at app/cronwatch/[[...path]]/route.ts:
67
- *
68
- * export const { GET, POST, DELETE } = cw.routes();
69
+ * The dashboard and its JSON API for a client.
70
+ * @deprecated Use `cw.routes(options)`, the one way to mount the dashboard
71
+ * in every port. This name still works through 1.x and goes in 2.0.
69
72
  */
70
- declare function createRoutes(cw: CronWatch, options?: RoutesOptions): Routes;
73
+ declare function createRoutes(cw: Cronwatch, options?: RoutesOptions): Routes;
71
74
 
72
75
  type JobFn<T> = (job: JobContext) => Promise<T> | T;
73
76
  type HandlerFn<T> = (job: JobContext, request: Request) => Promise<T> | T;
@@ -75,9 +78,12 @@ interface HandlerOptions {
75
78
  /**
76
79
  * Callers must send `Authorization: Bearer <secret>`. Defaults to the
77
80
  * client's cronSecret, which defaults to process.env.CRON_SECRET (what
78
- * Vercel sends its cron requests with). An empty string counts as unset.
79
- * With no secret at all the handler answers 503 unless NODE_ENV is
80
- * "development" or "test". Pass null to allow anyone.
81
+ * Vercel sends its cron requests with). An empty string, or one of only
82
+ * whitespace, counts as unset; any other type (false, a number) throws.
83
+ * With no secret at all the handler answers 503 unless the app is in
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.
81
87
  */
82
88
  secret?: string | null;
83
89
  }
@@ -162,12 +168,21 @@ interface RunHandle {
162
168
  }
163
169
  /**
164
170
  * What a Source may use of the client: declare jobs, read the store, and
165
- * record runs it found elsewhere. A CronWatch is one.
171
+ * record runs it found elsewhere. A Cronwatch is one.
166
172
  */
167
173
  interface SourceHost {
168
- /** Declare a job, as CronWatch.job(). Throws for an invalid name or option. */
174
+ /** Declare a job, as Cronwatch.job(). Throws for an invalid name or option. */
169
175
  job(name: string, options?: JobOptions): unknown;
170
- /** See CronWatch.recordRun(). */
176
+ /**
177
+ * The definitions declared now, as Cronwatch.definedJobs(): a source reads
178
+ * it to declare again a job forgotten since (the dashboard's forget), whose
179
+ * definition has not changed. A host without it is taken to keep every
180
+ * job the source declared.
181
+ */
182
+ definedJobs?(): {
183
+ name: string;
184
+ }[];
185
+ /** See Cronwatch.recordRun(). */
171
186
  recordRun(run: Run, options?: RecordRunOptions): Promise<Alert[]>;
172
187
  readonly store: Store;
173
188
  readonly now: () => number;
@@ -187,7 +202,7 @@ interface RecordRunOptions {
187
202
  /** False stores the run without evaluating it, for history imported on first sight. Default true. */
188
203
  evaluate?: boolean;
189
204
  }
190
- interface CronWatchOptions {
205
+ interface CronwatchOptions {
191
206
  /** Where jobs, runs and state live. Defaults to an in-memory store that forgets on restart. */
192
207
  store?: Store;
193
208
  /**
@@ -203,8 +218,9 @@ interface CronWatchOptions {
203
218
  /**
204
219
  * Shared secret that handler() requests must carry. Defaults to
205
220
  * process.env.CRON_SECRET (on Cloudflare Workers, which have no process,
206
- * pass env.CRON_SECRET); an empty string counts as unset. Pass null to
207
- * let handlers run without one.
221
+ * pass env.CRON_SECRET); an empty string or one of only whitespace, given
222
+ * here or in the variable, counts as unset. Pass null to let handlers run
223
+ * without one. Any other type (false, a number) throws.
208
224
  */
209
225
  cronSecret?: string | null;
210
226
  /** How long finished runs are kept. Default "30d". */
@@ -234,7 +250,7 @@ interface CronWatchOptions {
234
250
  /** The clock. Tests use this. */
235
251
  now?: () => number;
236
252
  }
237
- declare class CronWatch {
253
+ declare class Cronwatch {
238
254
  readonly store: Store;
239
255
  readonly alerts: AlertChannel[];
240
256
  readonly triage: TriageFn | undefined;
@@ -247,7 +263,7 @@ declare class CronWatch {
247
263
  /** cronSecret was passed as null: handlers may run without a secret. */
248
264
  private readonly secretOptOut;
249
265
  private readonly redact;
250
- /** "check": queue alerts for another process's check instead of sending them. See CronWatchOptions.deliver. */
266
+ /** "check": queue alerts for another process's check instead of sending them. See CronwatchOptions.deliver. */
251
267
  private readonly deferDelivery;
252
268
  private readonly defaults;
253
269
  private readonly definitions;
@@ -266,7 +282,9 @@ declare class CronWatch {
266
282
  private usingDefaultStore;
267
283
  private warnedNoSecret;
268
284
  private warnedDeferredStart;
269
- constructor(options?: CronWatchOptions);
285
+ /** Stored jobs read with a definition that was not a JSON object (see readJob). */
286
+ private readonly unreadable;
287
+ constructor(options?: CronwatchOptions);
270
288
  /** Declare a job. Call it once, at module level, and keep the handle. */
271
289
  job(name: string, options?: JobOptions): JobHandle;
272
290
  /** Run a job by name without keeping a handle. Defines it on first use. */
@@ -287,9 +305,27 @@ declare class CronWatch {
287
305
  * that replaced it, never its own over it, and one forgotten since writes
288
306
  * its own. The writes of one name take turns, in the order they were asked
289
307
  * for, so one still under way cannot land after a later one; and a name
290
- * declared again while its write was under way is still to be written.
308
+ * declared again while its write was under way is still to be written. A
309
+ * name is marked as written only while that same declaration stands, so a
310
+ * forget that lands during the write (deleting the row after it) leaves
311
+ * the name to be written again, as does one forgotten before it.
312
+ *
313
+ * With `confirm`, as a run starts, a name already written is read back:
314
+ * another process may have forgotten the job since, and a job still
315
+ * declared here comes back on its next run.
291
316
  */
292
317
  private sync;
318
+ /**
319
+ * Every stored job, once each declaration has been written. A job declared
320
+ * here that the store no longer has was forgotten by another process after
321
+ * this one wrote it: it is written again, as its next run would, so it is
322
+ * checked and shown while any process still declares it.
323
+ */
324
+ private storedJobs;
325
+ /** A stored job read leniently (readStoredJob); one whose definition is unreadable is remembered, so that evaluating it throws. */
326
+ private readJob;
327
+ /** Throws for a job whose stored definition was not a JSON object: reported, and shown as failing, while the others carry on. */
328
+ private evaluable;
293
329
  /**
294
330
  * Runs `fn` after every earlier state update for the same job has settled,
295
331
  * so two runs (or a run and a check) in this process never read and write
@@ -321,7 +357,8 @@ declare class CronWatch {
321
357
  private execute;
322
358
  /**
323
359
  * Sets a finished run's status and error from how it ended, then redacts
324
- * its output and error. Shared by execute() and RunHandle.finish().
360
+ * its output and error and caps them, in that order. Shared by execute()
361
+ * and RunHandle.finish().
325
362
  */
326
363
  private conclude;
327
364
  /**
@@ -374,17 +411,28 @@ declare class CronWatch {
374
411
  * alone and reported. A finished run is judged as if it had been wrapped
375
412
  * here (expect, failures, duration, budgets) and its output and error are
376
413
  * redacted the same way; one finishing after a check marked it timeout is
377
- * judged only when it succeeded, as RunHandle.finish() does. Returns the
378
- * alerts it sent.
414
+ * judged only when it succeeded, as RunHandle.finish() does. A metric that
415
+ * is not a finite number throws before anything is written, as
416
+ * job.metric() does. Returns the alerts it sent.
379
417
  */
380
418
  recordRun(input: Run, options?: RecordRunOptions): Promise<Alert[]>;
381
419
  /** recordRun() for a run already stored. */
382
420
  private recordOver;
383
421
  /**
384
422
  * Evaluate a finished run (ok, failed, or timed out by a check), already
385
- * written, against the job's state and send what that produces. Never throws.
423
+ * written, against the job's state and send what that produces. The
424
+ * alerts are written with that state (see outbox()). Never throws.
386
425
  */
387
426
  private finishRun;
427
+ /**
428
+ * An evaluation as it is written: its drafts composed into alerts and held
429
+ * in the same state (holdAlerts), so the write that opens a condition also
430
+ * keeps its alerts, and a process that stops before sending them does not
431
+ * lose them. Called inside updateState(), so it only computes.
432
+ */
433
+ private outbox;
434
+ /** Reports alerts let go because a job's queue was full. */
435
+ private reportDropped;
388
436
  /**
389
437
  * The runs before `run`, newest first, with up to BASELINE_WINDOW
390
438
  * successful ones when the store has them. One small read normally; a
@@ -410,31 +458,62 @@ declare class CronWatch {
410
458
  job: JobSummary;
411
459
  runs: Run[];
412
460
  }[]>;
461
+ /** A job's summary, or null for one the store does not have. One declared here and forgotten elsewhere is written again, as storedJobs() does. */
413
462
  jobSummary(name: string): Promise<JobSummary | null>;
414
463
  /** A job's runs, newest first. `limit` is a whole number from 1 to 500. */
415
464
  runs(name: string, limit?: number): Promise<Run[]>;
416
465
  getRun(id: string): Promise<Run | null>;
417
- /** Stop alerts for a job for a while. State keeps updating underneath. */
466
+ /**
467
+ * Stop alerts for a job for a while. State keeps updating underneath. The
468
+ * end is a whole millisecond, held at 2^53 - 1 (see silenceEnd).
469
+ */
418
470
  silence(name: string, duration: Duration): Promise<JobState>;
419
471
  unsilence(name: string): Promise<JobState>;
420
472
  /** Read, change and write one job's state, in turn with every other update to it. */
421
473
  private patchState;
422
- /** Remove a job and its runs from the store. A job still declared in code comes back on its next run. */
474
+ /**
475
+ * Remove a job and its runs from the store. A job still declared in code
476
+ * comes back: here on its next run, and in any other process that declares
477
+ * it on its next run there, or at that process's next check or dashboard read.
478
+ */
423
479
  forget(name: string): Promise<void>;
424
- /** The dashboard and JSON API as fetch-style handlers. See createRoutes(). */
480
+ /**
481
+ * The dashboard and JSON API as fetch-style handlers: the one way to mount
482
+ * them. A Next.js app exports them from app/cronwatch/[[...path]]/route.ts:
483
+ *
484
+ * export const { GET, POST, DELETE } = cw.routes();
485
+ */
425
486
  routes(options?: RoutesOptions): Routes;
426
487
  /**
427
488
  * Check on an interval, for long-running servers. Default every minute.
428
489
  * Not for serverless functions or Cloudflare Workers, where nothing runs
429
- * between requests: call check() from a cron there instead.
490
+ * between requests: call check() from a cron there instead. (A job's
491
+ * start() opens a run; this starts the checks.)
492
+ */
493
+ startChecking(every?: Duration): void;
494
+ /**
495
+ * startChecking(), under its former name.
496
+ * @deprecated Renamed `startChecking`, since a job's `start()` opens a run.
497
+ * This name still works through 1.x and goes in 2.0.
430
498
  */
431
499
  start(every?: Duration): void;
500
+ /** Stop the checks startChecking() began. */
432
501
  stop(): void;
502
+ /**
503
+ * Stop the interval, wait for a check already under way (bounded by its
504
+ * own channel, triage and retry timeouts; what it throws was reported to
505
+ * whoever started it), then close the store, so that check neither writes
506
+ * after the store is closed nor loses the alerts it would queue.
507
+ */
433
508
  close(): Promise<void>;
434
509
  /**
435
- * Compose, triage and send each draft. The state was saved before this
436
- * (updateState), so a slow channel holds up nothing else; afterwards only
437
- * the delivery fields are written back, onto a fresh read of the state.
510
+ * Triage and send each alert the outbox holds (see outbox()). The state,
511
+ * with the alerts in it, was saved before this, so a slow channel holds up
512
+ * nothing else; afterwards only the delivery fields are written back, onto
513
+ * a fresh read of the state, and the alerts leave `sending`. Triage is made
514
+ * here, never stored with the held alert: the write that opens a condition
515
+ * cannot wait for it, and a retry triages an alert that has none. With
516
+ * deliver: "check" the alerts were queued for a check elsewhere instead.
438
517
  */
439
518
  private dispatch;
440
519
  /**
@@ -447,8 +526,10 @@ declare class CronWatch {
447
526
  private retryUndelivered;
448
527
  /**
449
528
  * Mark delivered alerts done, drop stale ones, and keep failed ones for the
450
- * next check. A failed alert replaces its stored copy, so a triage made on
451
- * this attempt is kept. lastAlertAt moves only on a delivery.
529
+ * next check, taking them all out of `sending` (recordSent). A failed alert
530
+ * replaces its stored copy, so a triage made on this attempt is kept.
531
+ * lastAlertAt moves only on a delivery. When this write fails, alerts
532
+ * still in `sending` are retried once their lease runs out.
452
533
  */
453
534
  private recordDelivery;
454
535
  /** Send to every channel at once. True when at least one accepted it, or there are none. */
@@ -461,4 +542,15 @@ declare function consoleChannel(): AlertChannel;
461
542
  /** Wrap any function as an alert channel. */
462
543
  declare function custom(name: string, send: (alert: Alert) => Promise<void> | void): AlertChannel;
463
544
 
464
- export { type CronWatchOptions as C, type FetchHandler as F, type HandlerFn as H, type JobContext as J, type RecordRunOptions as R, type Source as S, CronWatch as a, type HandlerOptions as b, type JobFn as c, type JobHandle as d, type Routes as e, type RoutesOptions as f, type RunHandle as g, type RunOutcome as h, type SourceHost as i, type StartOptions as j, consoleChannel as k, createRoutes as l, custom as m };
545
+ /**
546
+ * The client's former name.
547
+ * @deprecated Renamed `Cronwatch`, the spelling every port uses. This name
548
+ * still works through 1.x and goes in 2.0.
549
+ */
550
+ declare const CronWatch: typeof Cronwatch;
551
+ /** @deprecated Renamed `Cronwatch`; this name goes in 2.0. */
552
+ type CronWatch = Cronwatch;
553
+ /** @deprecated Renamed `CronwatchOptions`; this name goes in 2.0. */
554
+ type CronWatchOptions = CronwatchOptions;
555
+
556
+ export { type CronwatchOptions as C, type FetchHandler as F, type HandlerFn as H, type JobContext as J, type RecordRunOptions as R, type Source as S, Cronwatch as a, CronWatch as b, type CronWatchOptions as c, type HandlerOptions as d, type JobFn as e, type JobHandle as f, type Routes as g, type RoutesOptions as h, type RunHandle as i, type RunOutcome as j, type SourceHost as k, type StartOptions as l, consoleChannel as m, createRoutes as n, custom as o };
package/dist/d1.cjs CHANGED
@@ -54,7 +54,7 @@ function statements(dialect, p) {
54
54
  const byName = "name";
55
55
  const version = (column) => {
56
56
  const v = `json_extract(${column}, '$.version')`;
57
- return `CASE 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`;
57
+ 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`;
58
58
  };
59
59
  const sql = {
60
60
  upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)
@@ -119,23 +119,41 @@ var params = {
119
119
  casInsert: (state) => [state.job, jsonText(state)],
120
120
  casUpdate: (state, expectedVersion) => [jsonText(state), state.job, expectedVersion]
121
121
  };
122
- var json = (v) => typeof v === "string" ? JSON.parse(v) : v;
123
- var num = (v) => v === null ? null : Number(v);
122
+ var json = (v) => {
123
+ if (typeof v !== "string") return v;
124
+ try {
125
+ return JSON.parse(v);
126
+ } catch {
127
+ return null;
128
+ }
129
+ };
130
+ var number = (v) => typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : Number.NaN;
131
+ var time = (v) => {
132
+ const n = number(v);
133
+ return Number.isFinite(n) ? n : 0;
134
+ };
135
+ var maybeTime = (v) => {
136
+ const n = number(v);
137
+ return Number.isFinite(n) ? n : null;
138
+ };
139
+ var textOrNull = (v) => typeof v === "string" ? v : null;
140
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
124
141
  function rowToJob(r) {
125
- return { name: r.name, definition: json(r.definition), createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };
142
+ return { name: r.name, definition: json(r.definition), createdAt: time(r.created_at), updatedAt: time(r.updated_at) };
126
143
  }
127
144
  function rowToRun(r) {
145
+ const metrics = json(r.metrics);
128
146
  return {
129
147
  id: r.id,
130
148
  job: r.job,
131
149
  status: r.status,
132
- startedAt: Number(r.started_at),
133
- finishedAt: num(r.finished_at),
134
- durationMs: num(r.duration_ms),
135
- error: r.error,
136
- output: r.output,
137
- metrics: r.metrics === null ? {} : json(r.metrics),
138
- trigger: r.trigger
150
+ startedAt: time(r.started_at),
151
+ finishedAt: maybeTime(r.finished_at),
152
+ durationMs: maybeTime(r.duration_ms),
153
+ error: textOrNull(r.error),
154
+ output: textOrNull(r.output),
155
+ metrics: isObject(metrics) ? metrics : {},
156
+ trigger: typeof r.trigger === "string" ? r.trigger : "run"
139
157
  };
140
158
  }
141
159
  function rowToState(r) {
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 \"[earlier output trimmed]\\n\" + clean.slice(clean.length - OUTPUT_CAP);\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\nfunction 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 // 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 return `CASE 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\nconst json = <T>(v: Json<T>): T => (typeof v === \"string\" ? (JSON.parse(v) as T) : v);\nconst num = (v: Int | null): number | null => (v === null ? null : Number(v));\n\nexport function rowToJob(r: JobRow): StoredJob {\n return { name: r.name, definition: json(r.definition), createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };\n}\n\nexport function rowToRun(r: RunRow): Run {\n return {\n id: r.id, job: r.job, status: r.status, startedAt: Number(r.started_at), finishedAt: num(r.finished_at),\n durationMs: num(r.duration_ms), error: r.error, output: r.output, metrics: r.metrics === null ? {} : json(r.metrics), trigger: r.trigger,\n };\n}\n\nexport function rowToState(r: StateRow): JobState {\n return json(r.state);\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":";;;AAyCO,SAAS,SAASA,KAAAA,EAAsB;AAC7C,EAAA,OAAOA,KAAAA,CAAK,SAAS,IAAQ,CAAA,GAAIA,MAAK,OAAA,CAAQ,SAAA,EAAW,EAAE,CAAA,GAAIA,KAAAA;AACjE;AAQO,SAAS,aAAaC,KAAAA,EAAsB;AACjD,EAAA,OAAOA,KAAAA,CAAK,QAAA,CAAS,SAAS,CAAA,GAAIA,MAAK,OAAA,CAAQ,mBAAA,EAAqB,CAAC,MAAA,EAAQ,IAAA,KAAkB,IAAA,KAAS,OAAA,GAAU,EAAA,GAAK,MAAO,CAAA,GAAIA,KAAAA;AACpI;;;AC1CO,IAAM,cAAA,GAAiB,YAAA;AAG9B,IAAM,UAAA,GAAa,KAAK,kBAAA,CAAmB,MAAA;AAOpC,SAAS,WAAA,CAAY,SAAiB,cAAA,EAAwB;AACnE,EAAA,IAAI,CAAC,oBAAA,CAAqB,IAAA,CAAK,MAAM,CAAA,IAAK,MAAA,CAAO,SAAS,UAAA,EAAY;AACpE,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,mCAAmC,IAAA,CAAK,SAAA,CAAU,MAAM,CAAC,uFACjB,UAAU,CAAA,YAAA;AAAA,KACpD;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AAEO,SAAS,MAAA,CAAO,SAAkB,CAAA,EAAmB;AAE1D,EAAA,MAAM,GAAA,GAAsB,SAAA;AAC5B,EAAA,MAAMA,KAAAA,GAAsB,MAAA;AAC5B,EAAA,OAAO;AAAA,+BAAA,EACwB,CAAC,CAAA;AAAA;AAAA,iBAAA,EAEfA,KAAI,CAAA;AAAA,iBAAA,EACJ,GAAG,CAAA;AAAA,iBAAA,EACH,GAAG,CAAA;AAAA;AAAA,+BAAA,EAEW,CAAC,CAAA,MAAA,EAAyC,EAAE;AAAA;AAAA;AAAA;AAAA,iBAAA,EAI1D,GAAG,CAAA;AAAA,kBAAA,EACF,GAAG,CAAA;AAAA,kBAAA,EACH,GAAG,CAAA;AAAA;AAAA;AAAA,cAAA,EAGPA,KAAI,CAAA;AAAA;AAAA;AAAA,+BAAA,EAGa,CAAC,uBAAuB,CAAC,CAAA;AAAA,+BAAA,EACzB,CAAC,mBAAmB,CAAC,CAAA;AAAA,+BAAA,EACrB,CAAC,CAAA;AAAA;AAAA,YAAA,EAEpBA,KAAI,CAAA;AAAA;AAAA,EAAA,CAAA;AAGlB;AAEO,SAAS,UAAA,CAAW,SAAkB,CAAA,EAAW;AAGtD,EAAA,MAAM,GAAA,GAAmB,OAAA;AAEzB,EAAA,MAAM,MAAA,GAAmC,MAAA;AAMzC,EAAA,MAAM,OAAA,GAAU,CAAC,MAAA,KAAmB;AAKlC,IAAA,MAAM,CAAA,GAAI,gBAAgB,MAAM,CAAA,cAAA,CAAA;AAEhC,IAAA,OAAO,CAAA,yBAAA,EAA4B,MAAM,CAAA,wBAAA,EAA2B,MAAM,CAAA,sDAAA,EAAyD,CAAC,CAAA,QAAA,EAAW,CAAC,CAAA,iBAAA,EAAoB,CAAC,CAAA,0CAAA,EAA6C,CAAC,CAAA,uBAAA,CAAA;AAAA,EACrN,CAAA;AACA,EAAA,MAAM,GAAA,GAAM;AAAA,IACV,SAAA,EAAW,eAAe,CAAC,CAAA;AAAA,yGAAA,CAAA;AAAA,IAE3B,MAAA,EAAQ,iBAAiB,CAAC,CAAA,mBAAA,CAAA;AAAA,IAC1B,QAAA,EAAU,CAAA,cAAA,EAAiB,CAAC,CAAA,cAAA,EAAiB,MAAM,CAAA,CAAA;AAAA,IACnD,UAAA,EAAY,eAAe,CAAC,CAAA,kBAAA,CAAA;AAAA,IAC5B,WAAA,EAAa,eAAe,CAAC,CAAA,mBAAA,CAAA;AAAA,IAC7B,SAAA,EAAW,eAAe,CAAC,CAAA,mBAAA,CAAA;AAAA,IAC3B,SAAA,EAAW,eAAe,CAAC,CAAA;AAAA,2CAAA,CAAA;AAAA,IAE3B,SAAA,EAAW,UAAU,CAAC,CAAA,sGAAA,CAAA;AAAA,IACtB,MAAA,EAAQ,iBAAiB,CAAC,CAAA,iBAAA,CAAA;AAAA,IAC1B,QAAA,EAAU,CAAA,cAAA,EAAiB,CAAC,CAAA,6CAAA,EAAgD,GAAG,CAAA,aAAA,CAAA;AAAA,IAC/E,WAAA,EAAa,CAAA,cAAA,EAAiB,CAAC,CAAA,mDAAA,EAAsD,GAAG,CAAA,CAAA;AAAA,IACxF,QAAA,EAAU,qBAAqB,CAAC,CAAA,mBAAA,CAAA;AAAA,IAChC,QAAA,EAAU,eAAe,CAAC,CAAA,uFAAA,CAAA;AAAA;AAAA;AAAA,IAG1B,SAAA,EAAW,eAAe,CAAC,CAAA;AAAA,mEAAA,EACsC,OAAA,CAAQ,CAAA,EAAG,CAAC,CAAA,WAAA,CAAa,CAAC,CAAA,IAAA,CAAA;AAAA,IAC3F,WAAW,CAAA,OAAA,EAAU,CAAC,CAAA,sCAAA,EAAyC,OAAA,CAAQ,OAAO,CAAC,CAAA,IAAA,CAAA;AAAA;AAAA;AAAA,IAG/E,KAAA,EAAO,eAAe,CAAC,CAAA;AAAA,sDAAA,EAC6B,CAAC,wBAAwB,CAAC,CAAA,SAAA;AAAA,GAChF;AAOA,EAAA,OAAO,GAAA;AACT;AAMO,SAAS,cAAA,CAAe,OAAA,EAAkB,CAAA,EAAW,KAAA,EAAuB;AACjF,EAAA,MAAMD,KAAAA,GAAO,CAAA,OAAA,EAAU,CAAC,CAAA,sHAAA,EAAyH,MAAM,IAAA,CAAK,EAAE,MAAA,EAAQ,KAAA,IAAS,MAAM,GAAG,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA,CAAA;AACpM,EAA4B,OAAOA,KAAAA;AAGrC;AASA,IAAM,OAAO,CAAC,KAAA,KAA0B,UAAU,IAAA,GAAO,IAAA,GAAO,SAAS,KAAK,CAAA;AAC9E,IAAM,WAAW,CAAC,KAAA,KAAmB,aAAa,IAAA,CAAK,SAAA,CAAU,KAAK,CAAC,CAAA;AAGhE,IAAM,MAAA,GAAS;AAAA,EACpB,SAAA,EAAW,CAAC,UAAA,EAAiC,GAAA,KAAgB,CAAC,UAAA,CAAW,IAAA,EAAM,QAAA,CAAS,UAAU,CAAA,EAAG,GAAA,EAAK,GAAG,CAAA;AAAA,EAC7G,SAAA,EAAW,CAAC,GAAA,KAAa;AAAA,IACvB,GAAA,CAAI,EAAA;AAAA,IAAI,GAAA,CAAI,GAAA;AAAA,IAAK,GAAA,CAAI,MAAA;AAAA,IAAQ,GAAA,CAAI,SAAA;AAAA,IAAW,GAAA,CAAI,UAAA;AAAA,IAAY,GAAA,CAAI,UAAA;AAAA,IAAY,IAAA,CAAK,IAAI,KAAK,CAAA;AAAA,IAAG,IAAA,CAAK,IAAI,MAAM,CAAA;AAAA,IAAG,QAAA,CAAS,IAAI,OAAO,CAAA;AAAA,IAAG,QAAA,CAAS,IAAI,OAAO;AAAA,GAC5J;AAAA,EACA,SAAA,EAAW,CAAC,GAAA,KAAa,CAAC,IAAI,MAAA,EAAQ,GAAA,CAAI,UAAA,EAAY,GAAA,CAAI,UAAA,EAAY,IAAA,CAAK,IAAI,KAAK,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA,EAAG,SAAS,GAAA,CAAI,OAAO,CAAA,EAAG,GAAA,CAAI,EAAE,CAAA;AAAA,EACtI,WAAA,EAAa,CAAC,GAAA,EAAU,YAAA,KAAkC;AAAA,IACxD,GAAA,CAAI,MAAA;AAAA,IAAQ,GAAA,CAAI,UAAA;AAAA,IAAY,GAAA,CAAI,UAAA;AAAA,IAAY,IAAA,CAAK,IAAI,KAAK,CAAA;AAAA,IAAG,IAAA,CAAK,IAAI,MAAM,CAAA;AAAA,IAAG,QAAA,CAAS,IAAI,OAAO,CAAA;AAAA,IAAG,GAAA,CAAI,EAAA;AAAA,IAAI,GAAG;AAAA,GACnH;AAAA,EACA,QAAA,EAAU,CAAC,KAAA,KAAoB,CAAC,MAAM,GAAA,EAAK,QAAA,CAAS,KAAK,CAAC,CAAA;AAAA,EAC1D,SAAA,EAAW,CAAC,KAAA,KAAoB,CAAC,MAAM,GAAA,EAAK,QAAA,CAAS,KAAK,CAAC,CAAA;AAAA,EAC3D,SAAA,EAAW,CAAC,KAAA,EAAiB,eAAA,KAA4B,CAAC,SAAS,KAAK,CAAA,EAAG,KAAA,CAAM,GAAA,EAAK,eAAe;AACvG,CAAA;AAmBA,IAAM,IAAA,GAAO,CAAC,CAAA,KAAwB;AACpC,EAAA,IAAI,OAAO,CAAA,KAAM,QAAA,EAAU,OAAO,CAAA;AAClC,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,MAAM,CAAC,CAAA;AAAA,EACrB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF,CAAA;AAEA,IAAM,SAAS,CAAC,CAAA,KAAwB,OAAO,CAAA,KAAM,WAAW,CAAA,GAAI,OAAO,CAAA,KAAM,QAAA,IAAY,EAAE,IAAA,EAAK,KAAM,KAAK,MAAA,CAAO,CAAC,IAAI,MAAA,CAAO,GAAA;AAElI,IAAM,IAAA,GAAO,CAAC,CAAA,KAAuB;AACnC,EAAA,MAAM,CAAA,GAAI,OAAO,CAAC,CAAA;AAClB,EAAA,OAAO,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA,GAAI,CAAA,GAAI,CAAA;AAClC,CAAA;AAEA,IAAM,SAAA,GAAY,CAAC,CAAA,KAA8B;AAC/C,EAAA,MAAM,CAAA,GAAI,OAAO,CAAC,CAAA;AAClB,EAAA,OAAO,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA,GAAI,CAAA,GAAI,IAAA;AAClC,CAAA;AACA,IAAM,aAAa,CAAC,CAAA,KAA+B,OAAO,CAAA,KAAM,WAAW,CAAA,GAAI,IAAA;AAC/E,IAAM,QAAA,GAAW,CAAC,CAAA,KAA4C,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,KAAM,IAAA,IAAQ,CAAC,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA;AAG9G,SAAS,SAAS,CAAA,EAAsB;AAC7C,EAAA,OAAO,EAAE,IAAA,EAAM,CAAA,CAAE,MAAM,UAAA,EAAY,IAAA,CAAK,EAAE,UAAU,CAAA,EAA0B,SAAA,EAAW,IAAA,CAAK,EAAE,UAAU,CAAA,EAAG,WAAW,IAAA,CAAK,CAAA,CAAE,UAAU,CAAA,EAAE;AAC7I;AAOO,SAAS,SAAS,CAAA,EAAgB;AACvC,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,CAAA,CAAE,OAAO,CAAA;AAC9B,EAAA,OAAO;AAAA,IACL,IAAI,CAAA,CAAE,EAAA;AAAA,IAAI,KAAK,CAAA,CAAE,GAAA;AAAA,IAAK,QAAQ,CAAA,CAAE,MAAA;AAAA,IAAQ,SAAA,EAAW,IAAA,CAAK,CAAA,CAAE,UAAU,CAAA;AAAA,IAAG,UAAA,EAAY,SAAA,CAAU,CAAA,CAAE,WAAW,CAAA;AAAA,IAC1G,UAAA,EAAY,SAAA,CAAU,CAAA,CAAE,WAAW,CAAA;AAAA,IAAG,KAAA,EAAO,UAAA,CAAW,CAAA,CAAE,KAAK,CAAA;AAAA,IAAG,MAAA,EAAQ,UAAA,CAAW,CAAA,CAAE,MAAM,CAAA;AAAA,IAC7F,OAAA,EAAS,QAAA,CAAS,OAAO,CAAA,GAAI,UAAU,EAAC;AAAA,IAAG,SAAS,OAAO,CAAA,CAAE,OAAA,KAAY,QAAA,GAAW,EAAE,OAAA,GAAU;AAAA,GAClG;AACF;AAGO,SAAS,WAAW,CAAA,EAAuB;AAChD,EAAA,OAAO,IAAA,CAAK,EAAE,KAAK,CAAA;AACrB;;;ACxLA,IAAM,OAAA,uBAAc,OAAA,EAA6B;AAQ1C,SAAS,EAAA,CAAG,QAAA,EAA0B,OAAA,GAAqB,EAAC,EAAU;AAC3E,EAAA,IAAI,CAAC,YAAY,OAAO,QAAA,CAAS,YAAY,UAAA,IAAc,OAAO,QAAA,CAAS,KAAA,KAAU,UAAA,EAAY;AAC/F,IAAA,MAAM,IAAI,MAAM,6DAA6D,CAAA;AAAA,EAC/E;AACA,EAAA,MAAM,CAAA,GAAI,WAAA,CAAY,OAAA,CAAQ,MAAM,CAAA;AAEpC,EAAA,IAAI,CAAA,CAAE,UAAA,CAAW,MAAM,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,CAAA,gCAAA,EAAmC,IAAA,CAAK,SAAA,CAAU,CAAC,CAAC,CAAA,uCAAA,CAAyC,CAAA;AACvI,EAAA,MAAM,GAAA,GAAM,UAAA,CAAW,QAAA,EAAU,CAAC,CAAA;AAClC,EAAA,MAAM,YAAA,GAAe,QAAQ,YAAA,IAAgB,IAAA;AAE7C,EAAA,MAAM,IAAA,GAAO,CAACA,KAAAA,EAAc,MAAA,GAAoB,EAAC,KAAM;AACrD,IAAA,MAAM,CAAA,GAAI,QAAA,CAAS,OAAA,CAAQA,KAAI,CAAA;AAC/B,IAAA,OAAO,OAAO,MAAA,GAAS,CAAA,GAAI,EAAE,IAAA,CAAK,GAAG,MAAM,CAAA,GAAI,CAAA;AAAA,EACjD,CAAA;AACA,EAAA,MAAM,KAAA,GAAQ,OAAUA,KAAAA,EAAc,MAAA,KAA2C,MAAM,KAAKA,KAAAA,EAAM,MAAM,CAAA,CAAE,KAAA,EAAM,IAAmB,IAAA;AACnI,EAAA,MAAM,GAAA,GAAM,OAAUA,KAAAA,EAAc,MAAA,GAAoB,EAAC,KAAA,CAAsB,MAAM,IAAA,CAAKA,KAAAA,EAAM,MAAM,CAAA,CAAE,GAAA,EAAI,EAAG,WAAW,EAAC;AAC3H,EAAA,MAAM,OAAA,GAAU,OAAOA,KAAAA,EAAc,MAAA,KAAA,CAAwC,MAAM,IAAA,CAAKA,KAAAA,EAAM,MAAM,CAAA,CAAE,GAAA,EAAI,EAAG,IAAA,EAAM,OAAA,IAAW,CAAA;AAE9H,EAAA,OAAO;AAAA,IACL,MAAM,IAAA,GAAO;AACX,MAAA,IAAI,CAAC,gBAAgB,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA,EAAG,GAAA,CAAI,CAAC,CAAA,EAAG;AAEpD,MAAA,MAAM,MAAM,MAAA,CAAO,QAAA,EAAU,CAAC,CAAA,CAAE,MAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,MAAM,CAAA,CAAE,IAAA,EAAM,CAAA,CAAE,OAAO,OAAO,CAAA;AAC9E,MAAA,MAAM,QAAA,CAAS,KAAA,CAAM,GAAA,CAAI,GAAA,CAAI,CAAC,MAAM,QAAA,CAAS,OAAA,CAAQ,CAAC,CAAC,CAAC,CAAA;AAGxD,MAAA,IAAI,QAAA,GAAW,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA;AACnC,MAAA,IAAI,CAAC,UAAU,OAAA,CAAQ,GAAA,CAAI,UAAW,QAAA,mBAAW,IAAI,KAAM,CAAA;AAC3D,MAAA,QAAA,CAAS,IAAI,CAAC,CAAA;AAAA,IAChB,CAAA;AAAA,IACA,MAAM,SAAA,CAAU,UAAA,EAAY,GAAA,EAAK;AAC/B,MAAA,MAAM,IAAA,CAAK,IAAI,SAAA,EAAW,MAAA,CAAO,UAAU,UAAA,EAAY,GAAG,CAAC,CAAA,CAAE,GAAA,EAAI;AAAA,IACnE,CAAA;AAAA,IACA,MAAM,OAAO,IAAA,EAAM;AACjB,MAAA,MAAM,IAAI,MAAM,KAAA,CAAc,IAAI,MAAA,EAAQ,CAAC,IAAI,CAAC,CAAA;AAChD,MAAA,OAAO,CAAA,GAAI,QAAA,CAAS,CAAC,CAAA,GAAI,IAAA;AAAA,IAC3B,CAAA;AAAA,IACA,MAAM,QAAA,GAAiC;AACrC,MAAA,OAAA,CAAQ,MAAM,GAAA,CAAY,GAAA,CAAI,QAAQ,CAAA,EAAG,IAAI,QAAQ,CAAA;AAAA,IACvD,CAAA;AAAA,IACA,MAAM,UAAU,IAAA,EAAM;AAEpB,MAAA,MAAM,QAAA,CAAS,KAAA,CAAM,CAAC,IAAA,CAAK,GAAA,CAAI,YAAY,CAAC,IAAI,CAAC,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,aAAa,CAAC,IAAI,CAAC,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAAA,IACjH,CAAA;AAAA,IACA,MAAM,UAAU,GAAA,EAAK;AACnB,MAAA,MAAM,IAAA,CAAK,IAAI,SAAA,EAAW,MAAA,CAAO,UAAU,GAAG,CAAC,EAAE,GAAA,EAAI;AAAA,IACvD,CAAA;AAAA,IACA,MAAM,UAAU,GAAA,EAAK;AACnB,MAAA,MAAM,IAAA,CAAK,IAAI,SAAA,EAAW,MAAA,CAAO,UAAU,GAAG,CAAC,EAAE,GAAA,EAAI;AAAA,IACvD,CAAA;AAAA,IACA,MAAM,WAAA,CAAY,GAAA,EAAK,YAAA,EAAc;AACnC,MAAA,IAAI,YAAA,CAAa,MAAA,KAAW,CAAA,EAAG,OAAO,KAAA;AACtC,MAAA,OAAQ,MAAM,OAAA,CAAQ,cAAA,CAAe,QAAA,EAAU,CAAA,EAAG,YAAA,CAAa,MAAM,CAAA,EAAG,MAAA,CAAO,WAAA,CAAY,GAAA,EAAK,YAAY,CAAC,CAAA,GAAK,CAAA;AAAA,IACpH,CAAA;AAAA,IACA,MAAM,OAAO,EAAA,EAAI;AACf,MAAA,MAAM,IAAI,MAAM,KAAA,CAAc,IAAI,MAAA,EAAQ,CAAC,EAAE,CAAC,CAAA;AAC9C,MAAA,OAAO,CAAA,GAAI,QAAA,CAAS,CAAC,CAAA,GAAI,IAAA;AAAA,IAC3B,CAAA;AAAA,IACA,MAAM,QAAA,CAAS,GAAA,EAAK,KAAA,EAAO;AACzB,MAAA,OAAA,CAAQ,MAAM,GAAA,CAAY,GAAA,CAAI,QAAA,EAAU,CAAC,KAAK,KAAK,CAAC,CAAA,EAAG,GAAA,CAAI,QAAQ,CAAA;AAAA,IACrE,CAAA;AAAA,IACA,MAAM,QAAQ,GAAA,EAAK;AACjB,MAAA,MAAM,CAAA,GAAI,MAAM,KAAA,CAAc,GAAA,CAAI,UAAU,CAAC,GAAA,EAAK,CAAC,CAAC,CAAA;AACpD,MAAA,OAAO,CAAA,GAAI,QAAA,CAAS,CAAC,CAAA,GAAI,IAAA;AAAA,IAC3B,CAAA;AAAA,IACA,MAAM,WAAA,GAAc;AAClB,MAAA,OAAA,CAAQ,MAAM,GAAA,CAAY,GAAA,CAAI,WAAW,CAAA,EAAG,IAAI,QAAQ,CAAA;AAAA,IAC1D,CAAA;AAAA,IACA,MAAM,SAAS,GAAA,EAAK;AAClB,MAAA,MAAM,IAAI,MAAM,KAAA,CAAgB,IAAI,QAAA,EAAU,CAAC,GAAG,CAAC,CAAA;AACnD,MAAA,OAAO,CAAA,GAAI,UAAA,CAAW,CAAC,CAAA,GAAI,IAAA;AAAA,IAC7B,CAAA;AAAA,IACA,MAAM,SAAS,KAAA,EAAO;AACpB,MAAA,MAAM,IAAA,CAAK,IAAI,QAAA,EAAU,MAAA,CAAO,SAAS,KAAK,CAAC,EAAE,GAAA,EAAI;AAAA,IACvD,CAAA;AAAA,IACA,MAAM,kBAAA,CAAmB,KAAA,EAAO,eAAA,EAAiB;AAC/C,MAAA,MAAM,OAAA,GAAU,oBAAoB,CAAA,GAChC,MAAM,QAAQ,GAAA,CAAI,SAAA,EAAW,OAAO,SAAA,CAAU,KAAK,CAAC,CAAA,GACpD,MAAM,QAAQ,GAAA,CAAI,SAAA,EAAW,OAAO,SAAA,CAAU,KAAA,EAAO,eAAe,CAAC,CAAA;AACzE,MAAA,OAAO,OAAA,GAAU,CAAA;AAAA,IACnB,CAAA;AAAA,IACA,MAAM,MAAM,MAAA,EAAQ;AAClB,MAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,KAAA,EAAO,CAAC,MAAM,CAAC,CAAA;AAAA,IACpC;AAAA,GACF;AACF","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"]}
package/dist/d1.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { S as Store } from './types-Cz47Boks.cjs';
1
+ import { S as Store } from './types-DLnlUie8.cjs';
2
2
 
3
3
  /**
4
4
  * The parts of a Cloudflare D1 binding this store uses. Written out here so
package/dist/d1.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { S as Store } from './types-Cz47Boks.js';
1
+ import { S as Store } from './types-DLnlUie8.js';
2
2
 
3
3
  /**
4
4
  * The parts of a Cloudflare D1 binding this store uses. Written out here so
package/dist/d1.js CHANGED
@@ -52,7 +52,7 @@ function statements(dialect, p) {
52
52
  const byName = "name";
53
53
  const version = (column) => {
54
54
  const v = `json_extract(${column}, '$.version')`;
55
- return `CASE 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`;
55
+ 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`;
56
56
  };
57
57
  const sql = {
58
58
  upsertJob: `INSERT INTO ${p}jobs (name, definition, created_at, updated_at) VALUES (?, ?, ?, ?)
@@ -117,23 +117,41 @@ var params = {
117
117
  casInsert: (state) => [state.job, jsonText(state)],
118
118
  casUpdate: (state, expectedVersion) => [jsonText(state), state.job, expectedVersion]
119
119
  };
120
- var json = (v) => typeof v === "string" ? JSON.parse(v) : v;
121
- var num = (v) => v === null ? null : Number(v);
120
+ var json = (v) => {
121
+ if (typeof v !== "string") return v;
122
+ try {
123
+ return JSON.parse(v);
124
+ } catch {
125
+ return null;
126
+ }
127
+ };
128
+ var number = (v) => typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : Number.NaN;
129
+ var time = (v) => {
130
+ const n = number(v);
131
+ return Number.isFinite(n) ? n : 0;
132
+ };
133
+ var maybeTime = (v) => {
134
+ const n = number(v);
135
+ return Number.isFinite(n) ? n : null;
136
+ };
137
+ var textOrNull = (v) => typeof v === "string" ? v : null;
138
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
122
139
  function rowToJob(r) {
123
- return { name: r.name, definition: json(r.definition), createdAt: Number(r.created_at), updatedAt: Number(r.updated_at) };
140
+ return { name: r.name, definition: json(r.definition), createdAt: time(r.created_at), updatedAt: time(r.updated_at) };
124
141
  }
125
142
  function rowToRun(r) {
143
+ const metrics = json(r.metrics);
126
144
  return {
127
145
  id: r.id,
128
146
  job: r.job,
129
147
  status: r.status,
130
- startedAt: Number(r.started_at),
131
- finishedAt: num(r.finished_at),
132
- durationMs: num(r.duration_ms),
133
- error: r.error,
134
- output: r.output,
135
- metrics: r.metrics === null ? {} : json(r.metrics),
136
- trigger: r.trigger
148
+ startedAt: time(r.started_at),
149
+ finishedAt: maybeTime(r.finished_at),
150
+ durationMs: maybeTime(r.duration_ms),
151
+ error: textOrNull(r.error),
152
+ output: textOrNull(r.output),
153
+ metrics: isObject(metrics) ? metrics : {},
154
+ trigger: typeof r.trigger === "string" ? r.trigger : "run"
137
155
  };
138
156
  }
139
157
  function rowToState(r) {