@goodandready/dsh-cron 0.2.48 → 0.2.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1102,6 +1102,54 @@ window.__ModuleLoader__.load({
1102
1102
  return (typeof t === 'function' ? t : translate)(key, vars);
1103
1103
  }
1104
1104
 
1105
+ /**
1106
+ * Issue #307: universal fetch with AbortController timeout (default 15s).
1107
+ * Connects with caller signal if supplied and fails closed on timeouts.
1108
+ */
1109
+ const fetchWithTimeout = async (url, options = {}, timeoutMs = 15000) => {
1110
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
1111
+ const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
1112
+ if (options && options.signal && controller) {
1113
+ if (options.signal.aborted) {
1114
+ controller.abort();
1115
+ } else {
1116
+ options.signal.addEventListener('abort', () => controller.abort(), { once: true });
1117
+ }
1118
+ }
1119
+ try {
1120
+ const res = await fetch(url, {
1121
+ ...options,
1122
+ signal: controller ? controller.signal : (options ? options.signal : undefined),
1123
+ });
1124
+ return res;
1125
+ } catch (err) {
1126
+ if (err && (err.name === 'AbortError' || controller?.signal?.aborted)) {
1127
+ const timeoutErr = new Error(`Request timed out after ${Math.round(timeoutMs / 1000)}s`);
1128
+ timeoutErr.isTimeout = true;
1129
+ throw timeoutErr;
1130
+ }
1131
+ throw err;
1132
+ } finally {
1133
+ if (timer) clearTimeout(timer);
1134
+ }
1135
+ };
1136
+
1137
+ /**
1138
+ * Issue #307: universal JSON fetch helper with timeout and aborted body protection.
1139
+ */
1140
+ const fetchJsonWithTimeout = async (url, options = {}, timeoutMs = 15000) => {
1141
+ const res = await fetchWithTimeout(url, options, timeoutMs);
1142
+ let data = {};
1143
+ try {
1144
+ data = await res.json();
1145
+ } catch (jsonErr) {
1146
+ if (jsonErr && jsonErr.name === 'AbortError') {
1147
+ throw jsonErr;
1148
+ }
1149
+ }
1150
+ return { res, data };
1151
+ };
1152
+
1105
1153
  function pickDeliverySettings(source) {
1106
1154
  const out = {};
1107
1155
  for (const key of DELIVERY_SETTING_KEYS.concat(TEMPLATE_SETTING_KEYS)) {
@@ -1391,8 +1439,12 @@ window.__ModuleLoader__.load({
1391
1439
  const gen = ++fetchGenRef.current;
1392
1440
  setLoading(true);
1393
1441
  setFetchError(null);
1442
+ const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
1443
+ const timer = controller ? setTimeout(() => controller.abort(), 15000) : null;
1394
1444
  try {
1395
- const res = await fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query));
1445
+ const res = await fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query), {
1446
+ signal: controller ? controller.signal : undefined,
1447
+ });
1396
1448
  const data = await res.json();
1397
1449
  if (gen !== fetchGenRef.current) return;
1398
1450
  if (data && data.ok) {
@@ -1407,6 +1459,7 @@ window.__ModuleLoader__.load({
1407
1459
  console.error('[dsh-cron] fetch tasks error:', err);
1408
1460
  setFetchError(err.message || T_KEY(t, 'list.loadErrorPrefix'));
1409
1461
  } finally {
1462
+ if (timer) clearTimeout(timer);
1410
1463
  if (gen === fetchGenRef.current) {
1411
1464
  setLoading(false);
1412
1465
  }
@@ -1414,9 +1467,11 @@ window.__ModuleLoader__.load({
1414
1467
  };
1415
1468
 
1416
1469
  const loadModels = async (providerName) => {
1470
+ const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
1471
+ const timer = controller ? setTimeout(() => controller.abort(), 15000) : null;
1417
1472
  try {
1418
1473
  const url = providerName ? '/dsh-cron/models?provider=' + encodeURIComponent(providerName) : '/dsh-cron/models';
1419
- const res = await fetch(url);
1474
+ const res = await fetch(url, { signal: controller ? controller.signal : undefined });
1420
1475
  const data = await res.json();
1421
1476
  if (data && data.ok) {
1422
1477
  if (!providerName) {
@@ -1431,6 +1486,8 @@ window.__ModuleLoader__.load({
1431
1486
  }
1432
1487
  } catch (err) {
1433
1488
  console.error('[dsh-cron] load models error:', err);
1489
+ } finally {
1490
+ if (timer) clearTimeout(timer);
1434
1491
  }
1435
1492
  };
1436
1493
 
@@ -1455,6 +1512,7 @@ window.__ModuleLoader__.load({
1455
1512
  const fetchOpts = { headers };
1456
1513
  if (pollController) fetchOpts.signal = pollController.signal;
1457
1514
 
1515
+ const pollTimer = pollController ? setTimeout(() => { try { pollController.abort(); } catch (abortErr) { void abortErr; } }, 15000) : null;
1458
1516
  fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query), fetchOpts)
1459
1517
  .then(res => {
1460
1518
  if (isDestroyed || pollGen !== fetchGenRef.current) return null;
@@ -1478,6 +1536,7 @@ window.__ModuleLoader__.load({
1478
1536
  if (err && err.name === 'AbortError') return;
1479
1537
  })
1480
1538
  .finally(() => {
1539
+ if (pollTimer) clearTimeout(pollTimer);
1481
1540
  if (isDestroyed) return;
1482
1541
  // Adaptive interval: 30s when tab is hidden, 8s when active (#134)
1483
1542
  const intervalMs = (typeof document !== 'undefined' && document.hidden) ? 30000 : 8000;
@@ -1515,10 +1574,17 @@ window.__ModuleLoader__.load({
1515
1574
  const fetchJsonWithTimeout = async (url, options = {}, timeoutMs = 15000) => {
1516
1575
  const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
1517
1576
  const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
1577
+ if (options && options.signal && controller) {
1578
+ if (options.signal.aborted) {
1579
+ controller.abort();
1580
+ } else {
1581
+ options.signal.addEventListener('abort', () => controller.abort(), { once: true });
1582
+ }
1583
+ }
1518
1584
  try {
1519
1585
  const res = await fetch(url, {
1520
1586
  ...options,
1521
- signal: controller ? controller.signal : options.signal,
1587
+ signal: controller ? controller.signal : (options ? options.signal : undefined),
1522
1588
  });
1523
1589
  let data = {};
1524
1590
  try {
@@ -1589,8 +1655,7 @@ window.__ModuleLoader__.load({
1589
1655
  /** #41: duplicate a task; the server copies config, resets run state (#41). */
1590
1656
  const handleDryRun = async (taskId) => {
1591
1657
  try {
1592
- const res = await fetch('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/dry-run', { method: 'POST' });
1593
- const data = await res.json();
1658
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/dry-run', { method: 'POST' });
1594
1659
  setDryRunResult(data);
1595
1660
  } catch (err) {
1596
1661
  alert('Dry run error: ' + err.message);
@@ -1603,8 +1668,7 @@ window.__ModuleLoader__.load({
1603
1668
  setArchiveLoading(true);
1604
1669
  setArchiveModalOpen(true);
1605
1670
  try {
1606
- const res = await fetch('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/archive?limit=15&offset=' + offset);
1607
- const data = await res.json();
1671
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/archive?limit=15&offset=' + offset);
1608
1672
  if (data.ok) {
1609
1673
  setArchiveRuns(prev => offset === 0 ? (data.runs || []) : [...prev, ...(data.runs || [])]);
1610
1674
  setArchiveTotal(data.total || 0);
@@ -1618,12 +1682,11 @@ window.__ModuleLoader__.load({
1618
1682
 
1619
1683
  const handlePreviewSchedule = async (scheduleExpr) => {
1620
1684
  try {
1621
- const res = await fetch('/dsh-cron/schedule/preview', {
1685
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/schedule/preview', {
1622
1686
  method: 'POST',
1623
1687
  headers: { 'Content-Type': 'application/json' },
1624
1688
  body: JSON.stringify({ schedule: scheduleExpr, timezone: formTimezone })
1625
1689
  });
1626
- const data = await res.json();
1627
1690
  if (data.ok && data.runs) {
1628
1691
  setSchedulePreviewRuns(data.runs);
1629
1692
  } else {
@@ -1636,8 +1699,7 @@ window.__ModuleLoader__.load({
1636
1699
 
1637
1700
  const handleDuplicate = async (task) => {
1638
1701
  try {
1639
- const res = await fetch('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/duplicate', { method: 'POST' });
1640
- const data = await res.json();
1702
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(task.id) + '/duplicate', { method: 'POST' });
1641
1703
  if (!data.ok) {
1642
1704
  alert(T_KEY(t, 'duplicate.failed', { error: (data && data.error) || '' }));
1643
1705
  return;
@@ -1678,8 +1740,7 @@ window.__ModuleLoader__.load({
1678
1740
  const promptMsg = T_KEY(t, 'transfer.exportPrompt') || 'Choose export mode:\nOK = Sanitized (removes custom env vars & headers)\nCancel = Full raw export';
1679
1741
  const wantSanitized = confirm(promptMsg);
1680
1742
  const exportUrl = '/dsh-cron/tasks/export' + (wantSanitized ? '?sanitize=true' : '');
1681
- const res = await fetch(exportUrl);
1682
- const data = await res.json();
1743
+ const { res, data } = await fetchJsonWithTimeout(exportUrl);
1683
1744
  if (!data.ok) {
1684
1745
  alert(T_KEY(t, 'transfer.exportFailed', { error: (data && data.error) || '' }));
1685
1746
  return;
@@ -1717,12 +1778,11 @@ window.__ModuleLoader__.load({
1717
1778
  alert(T_KEY(t, 'transfer.badFile', { error: parseErr.message }));
1718
1779
  return;
1719
1780
  }
1720
- const res = await fetch('/dsh-cron/tasks/import', {
1781
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/import', {
1721
1782
  method: 'POST',
1722
1783
  headers: importHeaders(document_),
1723
1784
  body: JSON.stringify({ document: document_, dryRun: true, strategy: importStrategy })
1724
1785
  });
1725
- const data = await res.json();
1726
1786
  if (!data.ok) {
1727
1787
  alert(T_KEY(t, 'transfer.importFailed', { error: (data && data.error) || '' }));
1728
1788
  return;
@@ -1740,12 +1800,11 @@ window.__ModuleLoader__.load({
1740
1800
  if (!importDoc) return;
1741
1801
  setTransferBusy(true);
1742
1802
  try {
1743
- const res = await fetch('/dsh-cron/tasks/import', {
1803
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/import', {
1744
1804
  method: 'POST',
1745
1805
  headers: importHeaders(importDoc),
1746
1806
  body: JSON.stringify({ document: importDoc, strategy: importStrategy })
1747
1807
  });
1748
- const data = await res.json();
1749
1808
  if (!data.ok) {
1750
1809
  alert(T_KEY(t, 'transfer.importFailed', { error: (data && data.error) || '' }));
1751
1810
  return;
@@ -1777,12 +1836,11 @@ window.__ModuleLoader__.load({
1777
1836
  'If the change makes the task execute code (script/node/python/ssh/docker), confirm it with me explicitly before applying it.',
1778
1837
  ].filter(Boolean);
1779
1838
  try {
1780
- const res = await fetch('/dsh-cron/chat/start', {
1839
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/chat/start', {
1781
1840
  method: 'POST',
1782
1841
  headers: { 'Content-Type': 'application/json' },
1783
1842
  body: JSON.stringify({ prompt: lines.join(String.fromCharCode(10)) })
1784
1843
  });
1785
- const data = await res.json();
1786
1844
  if (!data.ok || !data.sessionId) {
1787
1845
  alert(T_KEY(t, 'tune.startFailed', { error: (data && data.error) || '' }));
1788
1846
  return;
@@ -1798,7 +1856,7 @@ window.__ModuleLoader__.load({
1798
1856
  const handleDelete = async (id) => {
1799
1857
  if (!confirm(T_KEY(t, 'confirm.delete'))) return;
1800
1858
  try {
1801
- await fetch('/dsh-cron/tasks/' + encodeURIComponent(id), { method: 'DELETE' });
1859
+ await fetchWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(id), { method: 'DELETE' });
1802
1860
  fetchTasks();
1803
1861
  } catch (err) {
1804
1862
  console.debug('[dsh-cron] delete failed', err);
@@ -1809,8 +1867,7 @@ window.__ModuleLoader__.load({
1809
1867
  setTestStatus(null);
1810
1868
  setSettingsModalOpen(true);
1811
1869
  try {
1812
- const res = await fetch('/dsh-cron/settings');
1813
- const data = await res.json();
1870
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/settings');
1814
1871
  if (data && data.ok && data.settings) {
1815
1872
  setSettingsBotToken(data.settings.botToken || '');
1816
1873
  setSettingsChatId(data.settings.chatId || '');
@@ -1829,7 +1886,7 @@ window.__ModuleLoader__.load({
1829
1886
  const handleSaveSettings = async (e) => {
1830
1887
  e.preventDefault();
1831
1888
  try {
1832
- const res = await fetch('/dsh-cron/settings', {
1889
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/settings', {
1833
1890
  method: 'POST',
1834
1891
  headers: { 'Content-Type': 'application/json' },
1835
1892
  body: JSON.stringify(Object.assign({
@@ -1840,7 +1897,6 @@ window.__ModuleLoader__.load({
1840
1897
  kanbanBaseUrl: settingsKanbanUrl,
1841
1898
  }, settingsDelivery))
1842
1899
  });
1843
- const data = await res.json();
1844
1900
  if (data && data.ok) {
1845
1901
  setSettingsModalOpen(false);
1846
1902
  } else {
@@ -1881,7 +1937,7 @@ window.__ModuleLoader__.load({
1881
1937
 
1882
1938
  const handleTestTelegram = async () => { setTestStatus({ loading: true, message: T_KEY(t, 'settings.sending') });
1883
1939
  try {
1884
- const res = await fetch('/dsh-cron/telegram/test', {
1940
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/telegram/test', {
1885
1941
  method: 'POST',
1886
1942
  headers: { 'Content-Type': 'application/json' },
1887
1943
  body: JSON.stringify({
@@ -1889,7 +1945,6 @@ window.__ModuleLoader__.load({
1889
1945
  chatId: settingsChatId
1890
1946
  })
1891
1947
  });
1892
- const data = await res.json();
1893
1948
  if (data && data.ok) {
1894
1949
  setTestStatus({ loading: false, success: true, message: T_KEY(t, 'settings.testOk') });
1895
1950
  } else {
@@ -1969,8 +2024,7 @@ window.__ModuleLoader__.load({
1969
2024
  const loadTaskHistory = async (taskId) => {
1970
2025
  setHistoryLoading(true);
1971
2026
  try {
1972
- const res = await fetch('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/history');
1973
- const data = await res.json();
2027
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks/' + encodeURIComponent(taskId) + '/history');
1974
2028
  if (data.ok) {
1975
2029
  setTaskHistory(data.history || []);
1976
2030
  }
@@ -2119,12 +2173,11 @@ window.__ModuleLoader__.load({
2119
2173
  Boolean(payload.preflightCommand && payload.preflightCommand.trim()) ||
2120
2174
  Boolean(payload.selfHealingCommand && payload.selfHealingCommand.trim());
2121
2175
  if (hasCodeExec) headers[SCRIPT_CONFIRM_HEADER] = 'script';
2122
- const res = await fetch('/dsh-cron/tasks', {
2176
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks', {
2123
2177
  method: 'POST',
2124
2178
  headers,
2125
2179
  body: JSON.stringify(payload)
2126
2180
  });
2127
- const data = await res.json();
2128
2181
  if (!data.ok) { alert(data.error || T_KEY(t, 'settings.saveError', { error: '' })); return; }
2129
2182
  setManualModalOpen(false);
2130
2183
  fetchTasks();
@@ -2143,12 +2196,11 @@ window.__ModuleLoader__.load({
2143
2196
 
2144
2197
  setDshLoading(true);
2145
2198
  try {
2146
- const res = await fetch('/dsh-cron/chat/start', {
2199
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/chat/start', {
2147
2200
  method: 'POST',
2148
2201
  headers: { 'Content-Type': 'application/json' },
2149
2202
  body: JSON.stringify({ prompt: text })
2150
2203
  });
2151
- const data = await res.json();
2152
2204
  if (!data.ok || !data.sessionId) {
2153
2205
  alert(T_KEY(t, 'dsh.startFailed', { error: data.error || '' }));
2154
2206
  setDshLoading(false);
@@ -3532,8 +3584,7 @@ window.__ModuleLoader__.load({
3532
3584
  const checkUpdates = async () => {
3533
3585
  setUpdateState(prev => ({ ...prev, checking: true, error: null, notice: null }));
3534
3586
  try {
3535
- const res = await fetch('/api/dsh-cron/update');
3536
- const data = await res.json();
3587
+ const { res, data } = await fetchJsonWithTimeout('/api/dsh-cron/update');
3537
3588
  if (data && data.currentVersion) {
3538
3589
  setUpdateState(prev => ({
3539
3590
  ...prev,
@@ -3554,11 +3605,10 @@ window.__ModuleLoader__.load({
3554
3605
  const runUpdate = async () => {
3555
3606
  setUpdateState(prev => ({ ...prev, updating: true, error: null, notice: null }));
3556
3607
  try {
3557
- const res = await fetch('/api/dsh-cron/update', {
3608
+ const { res, data } = await fetchJsonWithTimeout('/api/dsh-cron/update', {
3558
3609
  method: 'POST',
3559
3610
  headers: { 'Content-Type': 'application/json', 'x-dsh-plugin-update': '1' }
3560
3611
  });
3561
- const data = await res.json();
3562
3612
  if (data && data.updated) {
3563
3613
  setUpdateState(prev => ({
3564
3614
  ...prev,
@@ -3587,9 +3637,8 @@ window.__ModuleLoader__.load({
3587
3637
  React.useEffect(() => {
3588
3638
  let active = true;
3589
3639
  setLoadState('loading');
3590
- fetch('/dsh-cron/settings')
3591
- .then(r => r.json())
3592
- .then(data => {
3640
+ fetchJsonWithTimeout('/dsh-cron/settings')
3641
+ .then(({ data }) => {
3593
3642
  if (!active) return;
3594
3643
  if (data && data.ok && data.settings) {
3595
3644
  setToken(data.settings.botToken || '');
@@ -3615,7 +3664,7 @@ window.__ModuleLoader__.load({
3615
3664
  const saveSettings = async (e) => {
3616
3665
  if (e) e.preventDefault();
3617
3666
  try {
3618
- const res = await fetch('/dsh-cron/settings', {
3667
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/settings', {
3619
3668
  method: 'POST',
3620
3669
  headers: { 'Content-Type': 'application/json' },
3621
3670
  body: JSON.stringify(Object.assign({
@@ -3627,7 +3676,6 @@ window.__ModuleLoader__.load({
3627
3676
  kanbanBaseUrl: kanbanUrl
3628
3677
  }, delivery))
3629
3678
  });
3630
- const data = await res.json();
3631
3679
  if (data && data.ok) {
3632
3680
  setSaved(true);
3633
3681
  setTimeout(() => setSaved(false), 2500);
@@ -3640,12 +3688,11 @@ window.__ModuleLoader__.load({
3640
3688
  const testTelegram = async () => {
3641
3689
  setTestInfo({ loading: true, message: T_KEY(t, 'settings.sending') });
3642
3690
  try {
3643
- const res = await fetch('/dsh-cron/telegram/test', {
3691
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/telegram/test', {
3644
3692
  method: 'POST',
3645
3693
  headers: { 'Content-Type': 'application/json' },
3646
3694
  body: JSON.stringify({ botToken: token, chatId })
3647
3695
  });
3648
- const data = await res.json();
3649
3696
  if (data && data.ok) {
3650
3697
  setTestInfo({ success: true, message: T_KEY(t, 'settings.testOk') });
3651
3698
  } else {
@@ -3659,12 +3706,11 @@ window.__ModuleLoader__.load({
3659
3706
  const testKanban = async () => {
3660
3707
  setTestInfo({ loading: true, message: T_KEY(t, 'settings.checkingKanban') });
3661
3708
  try {
3662
- const res = await fetch('/dsh-cron/kanban/test', {
3709
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/kanban/test', {
3663
3710
  method: 'POST',
3664
3711
  headers: { 'Content-Type': 'application/json' },
3665
3712
  body: JSON.stringify({ kanbanBaseUrl: kanbanUrl })
3666
3713
  });
3667
- const data = await res.json();
3668
3714
  if (data && data.ok) {
3669
3715
  setTestInfo({ success: true, message: T_KEY(t, 'settings.kanbanOk') });
3670
3716
  } else {
@@ -3971,8 +4017,7 @@ window.__ModuleLoader__.load({
3971
4017
 
3972
4018
  async function loadSidebarJobs(state) {
3973
4019
  try {
3974
- const res = await fetch('/dsh-cron/tasks?status=active');
3975
- const data = await res.json();
4020
+ const { res, data } = await fetchJsonWithTimeout('/dsh-cron/tasks?status=active');
3976
4021
  if (data && data.ok) renderSidebarJobs(state, data.tasks || []);
3977
4022
  } catch (err) {
3978
4023
  console.debug('[dsh-cron] load sidebar jobs failed', err);
package/lib/http-utils.js CHANGED
@@ -180,7 +180,13 @@ export async function readBody(req, res) {
180
180
  */
181
181
  export function redactTaskSecrets(task) {
182
182
  if (!task || typeof task !== 'object') return task;
183
- const clone = { ...task };
183
+ // Issue #310 / GH #5: sanitize to lossless JSON by omitting any undefined properties
184
+ const clone = {};
185
+ for (const [k, v] of Object.entries(task)) {
186
+ if (v !== undefined) {
187
+ clone[k] = v;
188
+ }
189
+ }
184
190
 
185
191
  if (clone.env && typeof clone.env === 'object') {
186
192
  const redactedEnv = {};
package/lib/runtimes.js CHANGED
@@ -124,8 +124,6 @@ export function buildSshInvocation(task) {
124
124
  return { file: 'ssh', args };
125
125
  }
126
126
 
127
- export const DOCKER_HOST_ENV_ALLOWLIST = ['TZ', 'LANG', 'LC_ALL'];
128
-
129
127
  /** #8/#226: build Docker run invocation without leaking host process secrets into container. */
130
128
  export function buildDockerInvocation(task, explicitEnv) {
131
129
  const image = String(task.dockerImage || '').trim();
package/lib/store.js CHANGED
@@ -555,8 +555,8 @@ export class TaskStore {
555
555
  type: task.type !== undefined ? task.type : (prev.type || 'llm'),
556
556
  status: task.status !== undefined ? task.status : (prev.status || 'active'),
557
557
  delivery: task.delivery !== undefined ? task.delivery : (prev.delivery || 'isolated'),
558
- provider: task.provider !== undefined ? task.provider : prev.provider,
559
- model: task.model !== undefined ? task.model : prev.model,
558
+ provider: task.provider !== undefined ? task.provider : (prev.provider || null),
559
+ model: task.model !== undefined ? task.model : (prev.model || null),
560
560
  notifyTelegram: task.notifyTelegram !== undefined
561
561
  ? (task.notifyTelegram === null || task.notifyTelegram === 'inherit' ? undefined : Boolean(task.notifyTelegram))
562
562
  : prev.notifyTelegram,
@@ -591,6 +591,12 @@ export class TaskStore {
591
591
  lastDurationMs: task.lastDurationMs !== undefined ? task.lastDurationMs : (prev.lastDurationMs || 0),
592
592
  nextRunAt: task.nextRunAt !== undefined ? task.nextRunAt : (prev.nextRunAt || null),
593
593
  };
594
+ // Issue #310 / GH #5: strip any undefined properties to guarantee lossless JSON
595
+ for (const key of Object.keys(record)) {
596
+ if (record[key] === undefined) {
597
+ delete record[key];
598
+ }
599
+ }
594
600
  this.tasks.set(id, record);
595
601
  try {
596
602
  this.save();
@@ -9,7 +9,7 @@ export function executeCreateTask(store, scheduler, args) {
9
9
  const typeError = validateTaskType(taskType, args);
10
10
  if (typeError) throw new Error(typeError);
11
11
  const parsed = parseScheduleExpression(args.schedule);
12
- const task = store.set({
12
+ const taskData = {
13
13
  title: args.title,
14
14
  schedule: parsed.cronPattern || args.schedule,
15
15
  scheduleText: parsed.humanText,
@@ -72,7 +72,14 @@ export function executeCreateTask(store, scheduler, args) {
72
72
  costLimitUsd: Number(args.costLimitUsd) > 0 ? Number(args.costLimitUsd) : undefined,
73
73
  dailyCostLimitUsd: Number(args.dailyCostLimitUsd) > 0 ? Number(args.dailyCostLimitUsd) : undefined,
74
74
  tokenLimit: Number(args.tokenLimit) > 0 ? Number(args.tokenLimit) : undefined,
75
- });
75
+ };
76
+ // Issue #310 / GH #5: strip any undefined properties before store.set()
77
+ for (const k of Object.keys(taskData)) {
78
+ if (taskData[k] === undefined) {
79
+ delete taskData[k];
80
+ }
81
+ }
82
+ const task = store.set(taskData);
76
83
  scheduler.scheduleTask(task);
77
84
  return {
78
85
  success: true,
package/lib/task-patch.js CHANGED
@@ -80,7 +80,7 @@ export function buildTaskPatch(current, body) {
80
80
  patch.oneShot = Boolean(parsed.isOneShot || (body && body.oneShot));
81
81
  if (scheduleChanged) {
82
82
  patch.nextRunAt = parsed.isOneShot ? parsed.targetTimestamp : (parsed.nextRun || null);
83
- patch.targetTimestamp = parsed.isOneShot ? parsed.targetTimestamp : undefined;
83
+ patch.targetTimestamp = parsed.isOneShot ? parsed.targetTimestamp : null;
84
84
  }
85
85
  } catch (err) {
86
86
  return { ok: false, error: err.message };
@@ -95,6 +95,12 @@ export function buildTaskPatch(current, body) {
95
95
  if (patch.tokenLimit !== undefined) {
96
96
  patch.tokenLimit = patch.tokenLimit === null ? null : (Number(patch.tokenLimit) > 0 ? Number(patch.tokenLimit) : null);
97
97
  }
98
+ // Issue #310 / GH #5: strip undefined properties from patch
99
+ for (const k of Object.keys(patch)) {
100
+ if (patch[k] === undefined) {
101
+ delete patch[k];
102
+ }
103
+ }
98
104
  return { ok: true, patch };
99
105
  }
100
106
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.2.48",
3
+ "version": "0.2.49",
4
4
  "description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",