@stonyx/cron 0.2.1-alpha.46 → 0.2.1-alpha.48

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 (2) hide show
  1. package/dist/service.js +35 -5
  2. package/package.json +1 -1
package/dist/service.js CHANGED
@@ -12,6 +12,28 @@ import { locked } from './locked.js';
12
12
  import { normalizeJobInput, recoverFlatParams } from './normalize.js';
13
13
  import RunLog from './run-log.js';
14
14
  const MAX_TIMER_DELAY_MS = 60_000;
15
+ /** Longest error text that may reach a log line. Anything past this is truncated. */
16
+ const MAX_LOGGED_ERROR_LENGTH = 512;
17
+ /** Longest job name that may reach a log line. Anything past this is truncated. */
18
+ const MAX_LOGGED_NAME_LENGTH = 120;
19
+ /**
20
+ * Flatten a value for interpolation into a single log line.
21
+ *
22
+ * Chronicle writes `${timestamp} ${content}\n` to a newline-delimited file, so
23
+ * any `\r` or `\n` inside `content` ends the record early and everything after
24
+ * it is read back as a separate, attacker-shaped entry — including a forged
25
+ * `[timestamp] Cron — ...` prefix that is indistinguishable from a real one.
26
+ * Both values that reach these lines are untrusted: `job.name` is passed
27
+ * through `createJob` unvalidated and `normalize.ts` exists specifically to
28
+ * accept AI-shaped input, and an error message is arbitrary consumer-callback
29
+ * text. Newlines become the literal two characters so the content survives for
30
+ * a reader, and the length cap keeps one pathological value from swamping the
31
+ * file.
32
+ */
33
+ function forLog(value, maxLength) {
34
+ const flattened = value.replace(/\r\n|[\r\n\u2028\u2029]/g, '\\n');
35
+ return flattened.length > maxLength ? `${flattened.slice(0, maxLength)}...` : flattened;
36
+ }
15
37
  /**
16
38
  * Describe a thrown value without ever throwing.
17
39
  *
@@ -19,12 +41,20 @@ const MAX_TIMER_DELAY_MS = 60_000;
19
41
  * `toString`/`Symbol.toPrimitive` throws, raises "Cannot convert object to
20
42
  * primitive value". Consumer callbacks throw arbitrary values, so the error
21
43
  * handler must not become a second failure source of its own.
44
+ *
45
+ * The `instanceof Error` branch needs the same guard as the other one. `Error`
46
+ * is subclassable and `message` is a plain writable property, so a consumer can
47
+ * hand back an `Error` whose `message` is a getter that throws, or one that is
48
+ * an object whose `toString` throws — `Error.prototype.message` is typed
49
+ * `string`, so TypeScript sees nothing wrong and the coercion is deferred to
50
+ * the caller's template literal, outside every guard here. That made `run()`
51
+ * reject instead of returning an `ExecuteResult`: a contract violation in the
52
+ * function written to prevent exactly that. Reading and coercing `message`
53
+ * inside the `try` is what closes it.
22
54
  */
23
55
  function describeError(err) {
24
- if (err instanceof Error)
25
- return err.message;
26
56
  try {
27
- return String(err);
57
+ return err instanceof Error ? String(err.message) : String(err);
28
58
  }
29
59
  catch {
30
60
  return 'unknown error';
@@ -276,7 +306,7 @@ export default class CronService {
276
306
  // half. Deliberately not awaited: the batch must not block on a log
277
307
  // transport, and `void` marks the floated promise as intentional.
278
308
  try {
279
- void Promise.resolve(log.error(`Cron — Job "${job.name}" (${job.id}) execution failed unexpectedly: ${describeError(err)}`)).catch(() => {
309
+ void Promise.resolve(log.error(`Cron — Job "${forLog(job.name, MAX_LOGGED_NAME_LENGTH)}" (${job.id}) execution failed unexpectedly: ${forLog(describeError(err), MAX_LOGGED_ERROR_LENGTH)}`)).catch(() => {
280
310
  // Nothing left to report to.
281
311
  });
282
312
  }
@@ -391,7 +421,7 @@ export default class CronService {
391
421
  catch (err) {
392
422
  status = 'error';
393
423
  error = describeError(err);
394
- this.log(`Job "${job.name}" (${job.id}) failed: ${error}`);
424
+ this.log(`Job "${forLog(job.name, MAX_LOGGED_NAME_LENGTH)}" (${job.id}) failed: ${forLog(error, MAX_LOGGED_ERROR_LENGTH)}`);
395
425
  }
396
426
  }
397
427
  finally {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "keywords": [
4
4
  "stonyx-module"
5
5
  ],
6
- "version": "0.2.1-alpha.46",
6
+ "version": "0.2.1-alpha.48",
7
7
  "description": "Cron/job scheduler for Stonyx framework",
8
8
  "main": "dist/main.js",
9
9
  "types": "dist/main.d.ts",