Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/core/utils/continuous-loop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
export interface ContinuousLoopOptions {
/** Interval between cycles in seconds */
readonly intervalSeconds: number;
/**
* Callback to run on each cycle.
*
* @param isShutdownRequested - Function to check if shutdown has been requested during the cycle
* @returns Promise that resolves when the cycle is complete (return value is ignored)
*/
readonly runCycle: (isShutdownRequested: () => boolean) => Promise<unknown>;
/** Optional callback for cleanup on shutdown */
readonly onShutdown?: () => Promise<void>;
/** Optional callback for per-cycle errors */
readonly onCycleError?: (error: unknown) => void;
}

/**
* Run a function in a continuous loop with graceful shutdown support.
*/
export async function runContinuousLoop(options: ContinuousLoopOptions): Promise<void> {
const { intervalSeconds, runCycle, onShutdown, onCycleError } = options;

let shutdownRequested = false;
let sleepResolve: (() => void) | null = null;
let sleepTimeout: NodeJS.Timeout | null = null;

const handleShutdown = (_signal: string): void => {
shutdownRequested = true;
if (sleepTimeout) {
clearTimeout(sleepTimeout);
sleepTimeout = null;
}
if (sleepResolve) {
sleepResolve();
sleepResolve = null;
}
};

const sigintHandler = (): void => handleShutdown('SIGINT');
const sigtermHandler = (): void => handleShutdown('SIGTERM');

process.on('SIGINT', sigintHandler);
process.on('SIGTERM', sigtermHandler);

const isShutdownRequested = (): boolean => shutdownRequested;

try {
for (;;) {
if (isShutdownRequested()) {
break;
}
try {
await runCycle(isShutdownRequested);
} catch (error) {
try {
onCycleError?.(error);
} catch {
// Prevent a failing error callback from terminating the loop
}
}

if (isShutdownRequested()) {
break;
}
await new Promise<void>((resolve) => {
sleepResolve = resolve;
sleepTimeout = setTimeout(() => {
sleepTimeout = null;
sleepResolve = null;
resolve();
}, intervalSeconds * 1000);
});
Comment on lines 20 to 72
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and examine its structure
find . -name "continuous-loop.ts" -type f

Repository: Fairmint/canton-node-sdk

Length of output: 103


🏁 Script executed:

# Look at the ContinuousLoopOptions definition
rg -n "interface ContinuousLoopOptions|type ContinuousLoopOptions" -A 10 --type=ts

Repository: Fairmint/canton-node-sdk

Length of output: 1261


🏁 Script executed:

# Check the full content of the continuous-loop.ts file
cat -n src/core/utils/continuous-loop.ts 2>/dev/null || echo "File not found"

Repository: Fairmint/canton-node-sdk

Length of output: 2852


🏁 Script executed:

# Find all call sites of runContinuousLoop
rg -nP 'runContinuousLoop\s*\(' -B 2 -A 3 --type=ts

Repository: Fairmint/canton-node-sdk

Length of output: 562


🏁 Script executed:

# Search for imports of runContinuousLoop in the codebase
rg -n "runContinuousLoop" --type=ts

Repository: Fairmint/canton-node-sdk

Length of output: 302


🏁 Script executed:

# Also check if there are any tests that might show expected usage
find . -name "*.test.ts" -o -name "*.spec.ts" | head -20

Repository: Fairmint/canton-node-sdk

Length of output: 1148


🏁 Script executed:

# Search for continuous-loop usage in tests
rg -n "continuous-loop\|runContinuousLoop" --type=ts -l

Repository: Fairmint/canton-node-sdk

Length of output: 50


🏁 Script executed:

# Search for files that import from the utils index where runContinuousLoop is exported
rg -n "from.*utils|from.*core" --type=ts | grep -v node_modules | head -30

Repository: Fairmint/canton-node-sdk

Length of output: 3792


🏁 Script executed:

# Look for any import of ContinuousLoopOptions as well
rg -n "ContinuousLoopOptions" --type=ts

Repository: Fairmint/canton-node-sdk

Length of output: 379


🏁 Script executed:

# Check git to see if this function has recent usage
git log --oneline -p --all -- src/core/utils/continuous-loop.ts 2>/dev/null | head -100

Repository: Fairmint/canton-node-sdk

Length of output: 2649


🏁 Script executed:

# Check if there are any tests for continuous-loop
find . -name "*continuous*" -type f | grep -i test

Repository: Fairmint/canton-node-sdk

Length of output: 50


🏁 Script executed:

# Also check if the function appears anywhere else in comments, docs, or examples
rg -i "continuous.*loop|runContinuousLoop" --type=md --type=txt

Repository: Fairmint/canton-node-sdk

Length of output: 50


Add validation for intervalSeconds to prevent hot loops from invalid values.

The intervalSeconds parameter lacks type-level constraints and is passed directly to setTimeout at line 67. Invalid inputs (≤0, NaN, or non-finite) would cause setTimeout to resolve immediately, creating a CPU-pegging hot loop. Add a guard at function entry:

Proposed fix
export async function runContinuousLoop(options: ContinuousLoopOptions): Promise<void> {
  const { intervalSeconds, runCycle, onShutdown, onCycleError } = options;

+  if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
+    throw new Error('intervalSeconds must be a positive, finite number');
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function runContinuousLoop(options: ContinuousLoopOptions): Promise<void> {
const { intervalSeconds, runCycle, onShutdown, onCycleError } = options;
let shutdownRequested = false;
let sleepResolve: (() => void) | null = null;
let sleepTimeout: NodeJS.Timeout | null = null;
const handleShutdown = (_signal: string): void => {
shutdownRequested = true;
if (sleepTimeout) {
clearTimeout(sleepTimeout);
sleepTimeout = null;
}
if (sleepResolve) {
sleepResolve();
sleepResolve = null;
}
};
const sigintHandler = (): void => handleShutdown('SIGINT');
const sigtermHandler = (): void => handleShutdown('SIGTERM');
process.on('SIGINT', sigintHandler);
process.on('SIGTERM', sigtermHandler);
const isShutdownRequested = (): boolean => shutdownRequested;
try {
for (;;) {
if (isShutdownRequested()) {
break;
}
try {
await runCycle(isShutdownRequested);
} catch (error) {
onCycleError?.(error);
}
if (isShutdownRequested()) {
break;
}
await new Promise<void>((resolve) => {
sleepResolve = resolve;
sleepTimeout = setTimeout(() => {
sleepTimeout = null;
sleepResolve = null;
resolve();
}, intervalSeconds * 1000);
});
export async function runContinuousLoop(options: ContinuousLoopOptions): Promise<void> {
const { intervalSeconds, runCycle, onShutdown, onCycleError } = options;
if (!Number.isFinite(intervalSeconds) || intervalSeconds <= 0) {
throw new Error('intervalSeconds must be a positive, finite number');
}
let shutdownRequested = false;
let sleepResolve: (() => void) | null = null;
let sleepTimeout: NodeJS.Timeout | null = null;
const handleShutdown = (_signal: string): void => {
shutdownRequested = true;
if (sleepTimeout) {
clearTimeout(sleepTimeout);
sleepTimeout = null;
}
if (sleepResolve) {
sleepResolve();
sleepResolve = null;
}
};
const sigintHandler = (): void => handleShutdown('SIGINT');
const sigtermHandler = (): void => handleShutdown('SIGTERM');
process.on('SIGINT', sigintHandler);
process.on('SIGTERM', sigtermHandler);
const isShutdownRequested = (): boolean => shutdownRequested;
try {
for (;;) {
if (isShutdownRequested()) {
break;
}
try {
await runCycle(isShutdownRequested);
} catch (error) {
onCycleError?.(error);
}
if (isShutdownRequested()) {
break;
}
await new Promise<void>((resolve) => {
sleepResolve = resolve;
sleepTimeout = setTimeout(() => {
sleepTimeout = null;
sleepResolve = null;
resolve();
}, intervalSeconds * 1000);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/utils/continuous-loop.ts` around lines 20 - 68, The
runContinuousLoop function must validate the intervalSeconds input to avoid hot
loops; at the top of runContinuousLoop (before any await or setTimeout use)
assert that intervalSeconds is a finite number > 0 (e.g.,
Number.isFinite(intervalSeconds) && intervalSeconds > 0) and if not, throw a
RangeError with a clear message; this prevents passing invalid values into
setTimeout (used where sleepTimeout is set) and avoids immediate-resolving
timers that create CPU-pegging loops.

}
} finally {
process.off('SIGINT', sigintHandler);
process.off('SIGTERM', sigtermHandler);
if (typeof onShutdown === 'function') {
await onShutdown();
}
}
}
2 changes: 2 additions & 0 deletions src/core/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { waitForCondition, type WaitForConditionOptions } from './polling';
export { extractString, hasStringProperty, isNonEmptyString, isNumber, isRecord, isString } from './type-guards';
export { runContinuousLoop, type ContinuousLoopOptions } from './continuous-loop';
export { SyncStateTracker, type SyncState, type SyncStateTrackerOptions } from './sync-state-tracker';
154 changes: 154 additions & 0 deletions src/core/utils/sync-state-tracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
Comment on lines +1 to +3
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Run the full test command sequence for this change.

npm test is still pending, but the guidelines require running lint/build/test after any code change. Please run the full sequence and update the PR status.

As per coding guidelines "Run npm run lint && npm run build && npm test after any code change".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/utils/sync-state-tracker.ts` around lines 1 - 3, Run the full CI
checks locally for the change in src/core/utils/sync-state-tracker.ts: execute
"npm run lint && npm run build && npm test" and fix any lint/build/test failures
that appear from the edits to the imports or related code, then push the fixes
and update the PR status to indicate the checks passed; specifically run the
lint/build/test sequence after modifying sync-state-tracker.ts (the module
importing fs, os, path) and include the resulting test output or remediation
commits in the PR.


export interface SyncState {
/** The last offset we successfully processed */
readonly lastSuccessfulOffset: number;
/** The ledger end at the time of our last successful sync */
readonly ledgerEndAtSync: number;
/** ISO timestamp of last update */
readonly lastUpdated: string;
/** Additional metadata for debugging */
readonly metadata?: {
readonly hostname?: string;
readonly processId?: number;
};
}

export interface SyncStateTrackerOptions {
/** Network name (mainnet, devnet) */
readonly network: string;
/** Provider name (intellect, 5n) */
readonly provider: string;
/** Optional: Custom directory for state files (defaults to ~/.canton-sync-state) */
readonly stateDirectory?: string;
}

function getDefaultStateDirectory(): string {
const envDir = process.env['CANTON_SYNC_STATE_DIR'];
if (envDir) {
return envDir;
}

const homeDir = os.homedir();
return path.join(homeDir, '.canton-sync-state');
}

/**
* Local file-based tracking of ledger sync progress.
*/
export class SyncStateTracker {
private readonly stateFilePath: string;
private readonly directoryWritable: boolean;
private cachedState: SyncState | null = null;

constructor(options: SyncStateTrackerOptions) {
const stateDir = options.stateDirectory ?? getDefaultStateDirectory();

let isWritable = true;
if (!fs.existsSync(stateDir)) {
try {
fs.mkdirSync(stateDir, { recursive: true });
} catch {
isWritable = false;
}
}

this.directoryWritable = isWritable;
this.stateFilePath = path.join(stateDir, `${options.network}-${options.provider}.json`);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the sync-state-tracker.ts file
cat -n src/core/utils/sync-state-tracker.ts

Repository: Fairmint/canton-node-sdk

Length of output: 5026


🏁 Script executed:

# Search for type definitions of SyncStateTrackerOptions
rg -nP 'interface\s+SyncStateTrackerOptions|type\s+SyncStateTrackerOptions' --type=ts -A 10

Repository: Fairmint/canton-node-sdk

Length of output: 1107


🏁 Script executed:

# Search for SyncStateTracker instantiations
rg -nP 'new\s+SyncStateTracker|SyncStateTracker\(' --type=ts -B 3 -A 3

Repository: Fairmint/canton-node-sdk

Length of output: 50


🏁 Script executed:

# Check for any sanitization or validation functions already in the codebase
rg -nP 'sanitize|validate.*path|pathTraversal' --type=ts -i

Repository: Fairmint/canton-node-sdk

Length of output: 2372


🏁 Script executed:

# Search for SyncStateTracker references with broader pattern
rg -nP 'SyncStateTracker' --type=ts -B 2 -A 2

Repository: Fairmint/canton-node-sdk

Length of output: 1581


🏁 Script executed:

# Look in test files to see how it's constructed
find . -name '*.test.ts' -o -name '*.spec.ts' | xargs grep -l 'SyncStateTracker' 2>/dev/null | head -5

Repository: Fairmint/canton-node-sdk

Length of output: 50


🏁 Script executed:

# Check if there are any tests for sync-state-tracker
fd -e 'test.ts' -e 'spec.ts' sync-state-tracker

Repository: Fairmint/canton-node-sdk

Length of output: 50


🏁 Script executed:

# Search for instantiation patterns more broadly
rg -nP 'new\s+SyncStateTracker|SyncStateTracker\s*\(' --type=ts

Repository: Fairmint/canton-node-sdk

Length of output: 50


🏁 Script executed:

# Find all test files in the repository
fd -e 'test.ts' -e 'spec.ts' | head -20

Repository: Fairmint/canton-node-sdk

Length of output: 1108


🏁 Script executed:

# Check if sync-state-tracker has any tests at all
find . -path '*/test*' -o -path '*/spec*' | xargs grep -l 'SyncStateTracker' 2>/dev/null

Repository: Fairmint/canton-node-sdk

Length of output: 52


🏁 Script executed:

# Search in the entire codebase for where SyncStateTracker is instantiated
rg -nP 'SyncStateTracker\s*\{|new\s+SyncStateTracker' --type=ts -A 5

Repository: Fairmint/canton-node-sdk

Length of output: 527


🏁 Script executed:

# Let's verify what path.join does with path traversal attempts
node <<'EOF'
const path = require('path');

// Test cases
const stateDir = '/home/user/.canton-sync-state';

// Case 1: Normal input
const normal = path.join(stateDir, 'mainnet-intellect.json');
console.log('Normal:', normal);

// Case 2: Traversal attempt
const traversal = path.join(stateDir, '../../etc-passwd.json');
console.log('Traversal attempt:', traversal);

// Case 3: Another traversal
const traversal2 = path.join(stateDir, '../../../sensitive.json');
console.log('Traversal attempt 2:', traversal2);

// Case 4: What if network contains traversal
const network = '../../etc';
const provider = 'passwd';
const result = path.join(stateDir, `${network}-${provider}.json`);
console.log('Network with traversal:', result);
EOF

Repository: Fairmint/canton-node-sdk

Length of output: 252


🏁 Script executed:

# Check if there are any validation or sanitization of network/provider elsewhere
rg -nP 'network|provider' --type=ts -B 2 -A 2 src/core/ | head -100

Repository: Fairmint/canton-node-sdk

Length of output: 5798


🏁 Script executed:

# Look for how these values might be passed from external sources
rg -nP 'SyncStateTrackerOptions|network.*provider' --type=ts -B 5 -A 5 | head -80

Repository: Fairmint/canton-node-sdk

Length of output: 5780


Sanitize network and provider inputs to prevent path traversal.

The network and provider values are directly interpolated into the file path without validation. Node.js path.join() does not prevent path traversal sequences; malicious input like ../../etc-passwd would escape the intended stateDir. Since this is exported as a public API, downstream consumers could pass untrusted input, potentially writing state files outside the intended directory.

🛡️ Proposed fix to sanitize inputs
   constructor(options: SyncStateTrackerOptions) {
     const stateDir = options.stateDirectory ?? getDefaultStateDirectory();
+
+    // Sanitize inputs to prevent path traversal
+    const sanitize = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_');
+    const safeNetwork = sanitize(options.network);
+    const safeProvider = sanitize(options.provider);

     let isWritable = true;
     if (!fs.existsSync(stateDir)) {
       try {
         fs.mkdirSync(stateDir, { recursive: true });
       } catch {
         isWritable = false;
       }
     }

     this.directoryWritable = isWritable;
-    this.stateFilePath = path.join(stateDir, `${options.network}-${options.provider}.json`);
+    this.stateFilePath = path.join(stateDir, `${safeNetwork}-${safeProvider}.json`);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.stateFilePath = path.join(stateDir, `${options.network}-${options.provider}.json`);
constructor(options: SyncStateTrackerOptions) {
const stateDir = options.stateDirectory ?? getDefaultStateDirectory();
// Sanitize inputs to prevent path traversal
const sanitize = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_');
const safeNetwork = sanitize(options.network);
const safeProvider = sanitize(options.provider);
let isWritable = true;
if (!fs.existsSync(stateDir)) {
try {
fs.mkdirSync(stateDir, { recursive: true });
} catch {
isWritable = false;
}
}
if (isWritable) {
try {
fs.accessSync(stateDir, fs.constants.W_OK);
} catch {
isWritable = false;
}
}
this.directoryWritable = isWritable;
this.stateFilePath = path.join(stateDir, `${safeNetwork}-${safeProvider}.json`);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/utils/sync-state-tracker.ts` at line 59, The code currently builds
this.stateFilePath by interpolating options.network and options.provider
directly, which allows path traversal; update the constructor/initialization
that sets stateFilePath to sanitize and validate options.network and
options.provider before joining with stateDir: either strip path separators and
".." (e.g., replace / and \ with '-', reject empty or '.'/'..'), or enforce a
strict whitelist regex (e.g., /^[a-zA-Z0-9._-]+$/) and throw on invalid values;
alternatively use path.basename on each value to drop directory components and
then join with stateDir and the `.json` suffix so stateFilePath always stays
inside stateDir. Ensure any validation error surfaces clearly from the
constructor that sets stateFilePath.

}

isWritable(): boolean {
return this.directoryWritable;
}
Comment on lines +46 to +64
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's view the actual file to see the current state
head -100 src/core/utils/sync-state-tracker.ts

Repository: Fairmint/canton-node-sdk

Length of output: 2644


🏁 Script executed:

#!/bin/bash
# Find all usages of isWritable() to understand downstream reliance
rg -nP '\.isWritable\s*\(' -C3 --type=ts

Repository: Fairmint/canton-node-sdk

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Try different patterns to find isWritable() usages
rg 'isWritable' --type=ts -n

Repository: Fairmint/canton-node-sdk

Length of output: 349


🏁 Script executed:

#!/bin/bash
# View the rest of the file to see full updateState method and any other relevant code
tail -50 src/core/utils/sync-state-tracker.ts

Repository: Fairmint/canton-node-sdk

Length of output: 1301


Add explicit write permission check for existing directories to prevent silent persistence failures.

If the directory exists but lacks write permissions, isWritable() will incorrectly return true, causing updateState() to silently fail when attempting file writes. Add an explicit access check using fs.accessSync() with fs.constants.W_OK after the mkdir attempt.

Proposed fix
   constructor(options: SyncStateTrackerOptions) {
     const stateDir = options.stateDirectory ?? getDefaultStateDirectory();

     let isWritable = true;
     if (!fs.existsSync(stateDir)) {
       try {
         fs.mkdirSync(stateDir, { recursive: true });
       } catch {
         isWritable = false;
       }
     }
+
+    if (isWritable) {
+      try {
+        fs.accessSync(stateDir, fs.constants.W_OK);
+      } catch {
+        isWritable = false;
+      }
+    }

     this.directoryWritable = isWritable;
     this.stateFilePath = path.join(stateDir, `${options.network}-${options.provider}.json`);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/utils/sync-state-tracker.ts` around lines 46 - 64, The constructor
currently assumes an existing directory is writable and sets directoryWritable =
true, which makes isWritable() return true even when writes will fail; modify
the SyncStateTracker constructor to perform an explicit write-access check after
ensuring the directory exists: after the mkdirSync attempt (or if
fs.existsSync(stateDir) is true) call fs.accessSync(stateDir, fs.constants.W_OK)
inside a try/catch and set isWritable = false on any error so directoryWritable
correctly reflects writability; keep stateFilePath assignment and isWritable()
method unchanged but ensure updateState() will rely on the corrected
directoryWritable flag.


getState(): SyncState | null {
if (this.cachedState) {
return this.cachedState;
}

if (!this.directoryWritable) {
return null;
}

try {
if (!fs.existsSync(this.stateFilePath)) {
return null;
}

const content = fs.readFileSync(this.stateFilePath, 'utf-8');
const state = JSON.parse(content) as SyncState;

if (
typeof state.lastSuccessfulOffset !== 'number' ||
typeof state.ledgerEndAtSync !== 'number' ||
typeof state.lastUpdated !== 'string'
) {
return null;
}
Comment on lines +83 to +89
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider validating non-negative integers.

The validation checks types but doesn't verify that lastSuccessfulOffset and ledgerEndAtSync are non-negative integers. Negative or fractional values could cause unexpected behavior in getStartingOffset.

🛡️ Optional enhancement
       if (
         typeof state.lastSuccessfulOffset !== 'number' ||
         typeof state.ledgerEndAtSync !== 'number' ||
-        typeof state.lastUpdated !== 'string'
+        typeof state.lastUpdated !== 'string' ||
+        !Number.isInteger(state.lastSuccessfulOffset) ||
+        !Number.isInteger(state.ledgerEndAtSync) ||
+        state.lastSuccessfulOffset < 0 ||
+        state.ledgerEndAtSync < 0
       ) {
         return null;
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
typeof state.lastSuccessfulOffset !== 'number' ||
typeof state.ledgerEndAtSync !== 'number' ||
typeof state.lastUpdated !== 'string'
) {
return null;
}
if (
typeof state.lastSuccessfulOffset !== 'number' ||
typeof state.ledgerEndAtSync !== 'number' ||
typeof state.lastUpdated !== 'string' ||
!Number.isInteger(state.lastSuccessfulOffset) ||
!Number.isInteger(state.ledgerEndAtSync) ||
state.lastSuccessfulOffset < 0 ||
state.ledgerEndAtSync < 0
) {
return null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/utils/sync-state-tracker.ts` around lines 83 - 89, The current
validation in the sync-state-tracker is only checking types but not that offsets
are non-negative integers; update the guard in the function that reads state
(used by getStartingOffset) to ensure state.lastSuccessfulOffset and
state.ledgerEndAtSync pass Number.isInteger(...) and are >= 0 (in addition to
being numbers), and keep the existing check for state.lastUpdated being a
string; if either offset fails these non-negative integer checks, return null so
getStartingOffset won't receive negative or fractional offsets.


this.cachedState = state;
return state;
} catch {
return null;
}
}

updateState(offset: number, ledgerEnd: number): void {
if (!this.directoryWritable) {
return;
}

const state: SyncState = {
lastSuccessfulOffset: offset,
ledgerEndAtSync: ledgerEnd,
lastUpdated: new Date().toISOString(),
metadata: {
hostname: process.env['HOSTNAME'] ?? 'unknown',
processId: process.pid,
},
};

this.cachedState = state;

try {
const tempPath = `${this.stateFilePath}.tmp`;
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2));
fs.renameSync(tempPath, this.stateFilePath);
} catch {
// Ignore local persistence errors and continue with in-memory progress.
}
}

getStartingOffset(currentLedgerEnd: number): { offset: number; wasReset: boolean } | null {
const state = this.getState();

if (!state) {
return null;
}

if (state.lastSuccessfulOffset > currentLedgerEnd) {
this.resetState();
return { offset: 0, wasReset: true };
}

const safeOffset = Math.max(0, state.lastSuccessfulOffset - 1);
return { offset: safeOffset, wasReset: false };
}

resetState(): void {
this.cachedState = null;
try {
if (fs.existsSync(this.stateFilePath)) {
fs.unlinkSync(this.stateFilePath);
}
} catch {
// Best-effort deletion; in-memory cache is already cleared.
}
}

getStateFilePath(): string {
return this.stateFilePath;
}
}