Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

feat(sync): add oc sync orchestration#62

Open
ndycode wants to merge 8 commits intosplit/pr58-backend-primitivesfrom
split/pr58-sync-orchestrator
Open

feat(sync): add oc sync orchestration#62
ndycode wants to merge 8 commits intosplit/pr58-backend-primitivesfrom
split/pr58-sync-orchestrator

Conversation

@ndycode
Copy link
Owner

@ndycode ndycode commented Mar 10, 2026

Summary

  • add the plan/apply orchestration layer for oc-chatgpt sync on top of the backend primitives branch
  • keep planner/apply behavior, backup-export result handling, and their direct tests isolated from the lower-level storage and merge-preview code
  • harden apply writes and error reporting so downstream settings flows can call this safely

Why

This branch is the orchestration layer between the backend primitives and the interactive settings flow. Splitting it out keeps the write/error-handling logic reviewable on its own.

Verification

  • npm exec vitest run test/oc-chatgpt-orchestrator.test.ts
  • npm run typecheck
  • npm run build

Notes

  • split replacement for the orchestration portion of #58
  • downstream settings work in #61 builds on this branch

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this pr adds the plan/apply orchestration layer for oc-chatgpt sync, sitting between the backend storage primitives and the upcoming settings ui. the atomic temp+rename write and renameWithRetry are solid improvements over a direct writeFile, and the discriminated-union error handling is clean and well-exercised. three concrete gaps remain:

  1. windows token-safety on temp files: fs.mkdir in persistMergedDefault has no retry loop (unlike renameWithRetry), and mode: 0o600 in fs.writeFile is silently ignored on windows, leaving plaintext refresh tokens world-readable until rename completes.

  2. planOcChatgptSync throws but return type hides it: non-ENOENT storage errors from loadTargetStorage propagate as unhandled rejections; the function's return type has no error variant, so standalone callers (e.g. preview flows) face a hidden failure mode with zero direct test coverage.

  3. missing mkdir failure test: the existing test suite covers writeFile and rename failures, but not the mkdir path which sits outside the inner try/catch and would propagate directly to the outer error handler.

key findings:

  • mkdir no retry + mode: 0o600 no-op on windows = token-leakage window
  • planOcChatgptSync return type doesn't reflect throws contract
  • missing test for mkdir failure in persistMergedDefault

Confidence Score: 3/5

  • safe to merge for unix-only deployments; windows token-safety gaps should be resolved before shipping to desktop users.
  • the orchestration logic is well-structured and the discriminated-union contract is clean. the temp+rename atomic write with rename retry is the right pattern. however, mkdir having no retry on windows EPERM/EBUSY and mode: 0o600 being a no-op on windows are concrete token-safety gaps on the primary target platform (user desktops running windows with antivirus). the planOcChatgptSync throw-contract issue also means any new caller of the exported function faces a hidden failure mode with no test coverage.
  • lib/oc-chatgpt-orchestrator.ts lines 193–198 (mkdir/writeFile in persistMergedDefault) and lines 88–124 (planOcChatgptSync throw contract); test suite missing coverage for mkdir failure path.

Important Files Changed

Filename Overview
lib/oc-chatgpt-orchestrator.ts new orchestration layer for oc-chatgpt sync; adds plan/apply with temp+rename atomic write and retry logic — two token-safety gaps: mkdir has no retry on windows EPERM/EBUSY, and mode: 0o600 in writeFile is silently ignored on windows leaving temp files world-readable; planOcChatgptSync return type also hides that it can throw on non-ENOENT storage errors.
test/oc-chatgpt-orchestrator.test.ts comprehensive vitest suite covering blocked detection, apply happy path, persist retry, rename exhaustion, and error recovery; missing a test for mkdir failure in persistMergedDefault — this is a separate execution path outside the inner try/catch that should be covered explicitly.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant applyOcChatgptSync
    participant planOcChatgptSync
    participant detectOcChatgptMultiAuthTarget
    participant loadTargetStorage
    participant previewOcChatgptImportMerge
    participant persistMergedDefault
    participant fs

    Caller->>applyOcChatgptSync: call(source, destination?, options)
    applyOcChatgptSync->>planOcChatgptSync: call(source, destination?, deps)
    planOcChatgptSync->>detectOcChatgptMultiAuthTarget: detect(detectOptions)
    detectOcChatgptMultiAuthTarget-->>planOcChatgptSync: none|ambiguous|target
    alt blocked
        planOcChatgptSync-->>applyOcChatgptSync: blocked-none|blocked-ambiguous
        applyOcChatgptSync-->>Caller: blocked-none|blocked-ambiguous
    else target found
        planOcChatgptSync->>loadTargetStorage: load(descriptor)
        loadTargetStorage-->>planOcChatgptSync: AccountStorageV3|null (or throws)
        planOcChatgptSync->>previewOcChatgptImportMerge: preview(source, destination)
        previewOcChatgptImportMerge-->>planOcChatgptSync: OcChatgptMergePreview
        planOcChatgptSync-->>applyOcChatgptSync: ready(target, preview, payload, destination)
        applyOcChatgptSync->>persistMergedDefault: persist(target, merged)
        persistMergedDefault->>fs: mkdir(dirname(accountPath))
        fs-->>persistMergedDefault: ok (or EPERM/EBUSY — no retry)
        persistMergedDefault->>fs: writeFile(tempPath, json, mode:0o600)
        Note over fs: mode 0o600 ignored on Windows
        persistMergedDefault->>fs: rename(tempPath → accountPath) [retry x5]
        fs-->>persistMergedDefault: ok
        persistMergedDefault-->>applyOcChatgptSync: accountPath
        applyOcChatgptSync-->>Caller: applied(target, preview, merged, persistedPath)
    end
    alt any error thrown
        applyOcChatgptSync->>detectOcChatgptMultiAuthTarget: recovery detect(detectOptions)
        applyOcChatgptSync-->>Caller: error(target?, error)
    end
Loading

Fix All in Codex

Last reviewed commit: c0cd5c8

Greptile also left 2 inline comments on this PR.

Context used:

  • Rule used - What: Every code change must explain how it defend... (source)

@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

Warning

Rate limit exceeded

@ndycode has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 27 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ea06d0a0-00db-4e72-b960-ad73645f0af1

📥 Commits

Reviewing files that changed from the base of the PR and between 776d5d0 and c0cd5c8.

📒 Files selected for processing (2)
  • lib/oc-chatgpt-orchestrator.ts
  • test/oc-chatgpt-orchestrator.test.ts
📝 Walkthrough

Walkthrough

new orchestrator module that plans and applies oc-chatgpt syncs and exports named backups via dependency-injected detect, preview, load, and persist functions. it returns blocked/ready plan states, applies merges to disk, and handles backup collisions. (see lib/oc-chatgpt-orchestrator.ts:268, test/oc-chatgpt-orchestrator.test.ts:349)

Changes

Cohort / File(s) Summary
orchestrator implementation
lib/oc-chatgpt-orchestrator.ts
new module implementing planOcChatgptSync, applyOcChatgptSync, persistMergedDefault, runNamedBackupExport, extractCollisionPath, and related types. dependency injection points for detect, preview, load, persist, and export. (new file, ~268 lines).
orchestrator tests
test/oc-chatgpt-orchestrator.test.ts
new comprehensive tests covering blocked-none/ambiguous/ready plan flows, apply persistence and error paths, loadTargetStorage integration, backup export collisions and path extraction, and various error propagation cases (~349 lines).

Sequence Diagram(s)

sequenceDiagram
    participant client
    participant orchestrator
    participant detector
    participant storage
    participant merger

    client->>orchestrator: planOcChatgptSync(source, destination?)
    orchestrator->>detector: detectOcChatgptMultiAuthTarget(deps)
    alt detection ambiguous/none
        detector-->>orchestrator: blocked-none / blocked-ambiguous
        orchestrator-->>client: BlockedDetection
    else detection ready
        detector-->>orchestrator: target descriptor
        orchestrator->>storage: loadTargetStorage(target)
        storage-->>orchestrator: destination account | null
        orchestrator->>merger: previewOcChatgptImportMerge(source, destination, target)
        merger-->>orchestrator: preview + payload
        orchestrator-->>client: OcChatgptSyncPlanReady(preview,payload,destination)
    end
Loading
sequenceDiagram
    participant client
    participant orchestrator
    participant persister
    participant backup
    participant detector

    client->>orchestrator: applyOcChatgptSync(...)
    orchestrator->>orchestrator: planOcChatgptSync() [see plan flow]
    alt plan blocked
        orchestrator-->>client: BlockedDetection
    else plan ready
        orchestrator->>persister: persistMerged(target, merged)
        alt persist succeeds
            persister-->>orchestrator: persistedPath
            orchestrator->>backup: exportNamedBackup(name, force?)
            alt backup exported
                backup-->>orchestrator: exported(path)
                orchestrator-->>client: applied result (merged, persistedPath)
            else backup collision
                backup-->>orchestrator: collision(error)
                orchestrator-->>client: applied result (merged, collision path)
            else backup error
                backup-->>orchestrator: error
                orchestrator->>detector: detectOcChatgptMultiAuthTarget() // re-eval for error result
                orchestrator-->>client: error result (target preserved)
            end
        else persist fails
            persister-->>orchestrator: error
            orchestrator-->>client: error result (target preserved)
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

key review flags

  • missing regression tests: tests cover many flows but do not exercise real multi-auth detection scenarios or exportNamedBackup integration. add integration/regression tests for detectOcChatgptMultiAuthTarget and exportNamedBackup in real environments (lib/oc-chatgpt-orchestrator.ts:268, test/oc-chatgpt-orchestrator.test.ts:349).
  • windows path edge cases: persistMergedDefault writes to target.accountPath and uses JSON writes. verify path separators and permissions on windows. check persistMergedDefault behaviors and loadTargetStorageDefault ENOENT handling for windows paths (lib/oc-chatgpt-orchestrator.ts:line ~200).
  • concurrency risks: applyOcChatgptSync performs detection during plan and may re-run detection on backup errors, allowing mismatch between persisted data and later detection. consider locking or documenting race conditions (lib/oc-chatgpt-orchestrator.ts:268).

Suggested labels: bug

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning pr description covers summary, rationale, and verification steps, but omits required validation checklist items and docs/governance sections from template. add completed validation checklist (npm run lint, typecheck, test, build), docs updates checklist, risk/rollback section, and confirm all checks passed before merge.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed Title follows conventional commits format with type 'feat', scope 'sync', and lowercase imperative summary under 72 chars.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch split/pr58-sync-orchestrator

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +155 to +172
export async function applyOcChatgptSync(
options: ApplyOcChatgptSyncOptions,
): Promise<OcChatgptSyncApplyResult> {
const dependencies = options.dependencies ?? {};
const plan = await planOcChatgptSync({
source: options.source,
destination: options.destination,
detectOptions: options.detectOptions,
dependencies: {
detectTarget: dependencies.detectTarget,
previewMerge: dependencies.previewMerge,
loadTargetStorage: dependencies.loadTargetStorage,
},
});

if (plan.kind !== "ready") {
return plan;
}
Copy link

Choose a reason for hiding this comment

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

The applyOcChatgptSync function doesn't wrap the planning phase in a try-catch, which means if loadTargetStorage throws an error (e.g., permission denied, invalid JSON, etc.), the function will throw instead of returning a structured error result with kind: "error". This is inconsistent with how persist errors are handled (lines 175-187).

export async function applyOcChatgptSync(
	options: ApplyOcChatgptSyncOptions,
): Promise<OcChatgptSyncApplyResult> {
	const dependencies = options.dependencies ?? {};
	let plan;
	try {
		plan = await planOcChatgptSync({
			source: options.source,
			destination: options.destination,
			detectOptions: options.detectOptions,
			dependencies: {
				detectTarget: dependencies.detectTarget,
				previewMerge: dependencies.previewMerge,
				loadTargetStorage: dependencies.loadTargetStorage,
			},
		});
	} catch (error) {
		// Need target info, but we don't have it if planning failed early
		// May need to adjust error result structure
		return { kind: "error", target: /* need to handle this */, error };
	}

	if (plan.kind !== "ready") {
		return plan;
	}
	// ... rest of function
}

Note: The error result structure requires a target property which may not be available if planning fails, so the error type may need adjustment.

Suggested change
export async function applyOcChatgptSync(
options: ApplyOcChatgptSyncOptions,
): Promise<OcChatgptSyncApplyResult> {
const dependencies = options.dependencies ?? {};
const plan = await planOcChatgptSync({
source: options.source,
destination: options.destination,
detectOptions: options.detectOptions,
dependencies: {
detectTarget: dependencies.detectTarget,
previewMerge: dependencies.previewMerge,
loadTargetStorage: dependencies.loadTargetStorage,
},
});
if (plan.kind !== "ready") {
return plan;
}
export async function applyOcChatgptSync(
options: ApplyOcChatgptSyncOptions,
): Promise<OcChatgptSyncApplyResult> {
const dependencies = options.dependencies ?? {};
let plan;
try {
plan = await planOcChatgptSync({
source: options.source,
destination: options.destination,
detectOptions: options.detectOptions,
dependencies: {
detectTarget: dependencies.detectTarget,
previewMerge: dependencies.previewMerge,
loadTargetStorage: dependencies.loadTargetStorage,
},
});
} catch (error) {
return { kind: "error", target: null, error };
}
if (plan.kind !== "ready") {
return plan;
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 2 files

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/oc-chatgpt-orchestrator.ts`:
- Around line 166-208: The catch re-runs detection which can flip results;
instead call detection once at the start of applyOcChatgptSync (use
dependencies.detectTarget ?? detectOcChatgptMultiAuthTarget with
options.detectOptions) and stash its result/descriptor for reuse in the catch
handler; on error, mapDetectionToBlocked against the original detection and if
not blocked and detection.kind === "target" return { kind: "error", target:
originalDescriptor, error } so the original target and error are preserved;
update applyOcChatgptSync to use persistedPath logic unchanged but reference the
saved detection, and add vitest regression tests (tests mentioned around
test/oc-chatgpt-orchestrator.test.ts:188-226 and 316-348) that simulate the
second detection returning none/ambiguous and assert applyOcChatgptSync still
returns kind: "error" for the original target and that queues handle EBUSY/429
per repo guidelines.
- Around line 236-252: The extractCollisionPath function currently treats any
EEXIST with a path as a collision; change it to first check the error message
for the "file already exists: ..." pattern (use the same regex /already exists:
(?<path>.+)$/i) and then require that the code is "EEXIST" and the message-match
both before returning a path (update the EEXIST branch in extractCollisionPath
accordingly), and add a regression unit test that simulates an EEXIST error
lacking the collision message (e.g., mkdir failing because a file exists) to
assert extractCollisionPath returns undefined for that case.

In `@test/oc-chatgpt-orchestrator.test.ts`:
- Around line 144-348: Tests currently only use injected doubles so the real
fs-backed branches in applyOcChatgptSync and runNamedBackupExport (and their
helpers loadTargetStorage/persistMerged/exportBackup) aren't covered; add
deterministic vitest tests that use a temp directory and the library's default
fs-backed loader/persistor to exercise those branches, simulate Windows-style
errors by producing NodeJS.ErrnoException objects with code
"EACCES"/"EEXIST"/"EBUSY" (or by creating files to trigger EEXIST), and add a
controlled concurrency regression: start two applyOcChatgptSync calls targeting
the same accountPath but make the fs-persist step deterministic (use a deferred
Promise or hook in the test to pause the first persistMerged) so one call wins
and the other yields a structured collision/error result; assert error.kind and
preserved original error where appropriate and reuse the symbols
applyOcChatgptSync, runNamedBackupExport, loadTargetStorage, persistMerged, and
exportBackup to locate the code to test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bbca39ef-b24b-4adb-b2a3-02bd8108c3ca

📥 Commits

Reviewing files that changed from the base of the PR and between cfd7e9b and ccfa36e.

📒 Files selected for processing (2)
  • lib/oc-chatgpt-orchestrator.ts
  • test/oc-chatgpt-orchestrator.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/oc-chatgpt-orchestrator.test.ts
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/oc-chatgpt-orchestrator.ts

activeIndex: 0,
};

describe("oc-chatgpt orchestrator", () => {
Copy link

Choose a reason for hiding this comment

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

missing coverage: applyOcChatgptSync blocked-none / blocked-ambiguous path

applyOcChatgptSync lines 183-185 forward a blocked-* plan result directly to the caller:

if (plan.kind !== "ready") {
    return plan;
}

every test that calls applyOcChatgptSync injects a detectTarget that always resolves to "target", so this path has zero coverage. a blocked-none or blocked-ambiguous result from planOcChatgptSync during an apply call would reach callers completely untested. given this is a new orchestration layer that callers will pattern-match on, both missing cases should have explicit tests — especially because on windows a deleted-between-plan-and-apply target directory would hit blocked-none here instead of error.

suggested additions:

it("returns blocked-none from apply when detection finds no target", async () => {
    const result = await applyOcChatgptSync({
        source: sourceStorage,
        dependencies: {
            detectTarget: () => ({ kind: "none", reason: "missing", tried: [] }),
        },
    });
    expect(result.kind).toBe("blocked-none");
});

it("returns blocked-ambiguous from apply when detection is ambiguous", async () => {
    const result = await applyOcChatgptSync({
        source: sourceStorage,
        dependencies: {
            detectTarget: () => ({ kind: "ambiguous", reason: "multiple", candidates: [] }),
        },
    });
    expect(result.kind).toBe("blocked-ambiguous");
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: test/oc-chatgpt-orchestrator.test.ts
Line: 40

Comment:
**missing coverage: `applyOcChatgptSync` blocked-none / blocked-ambiguous path**

`applyOcChatgptSync` lines 183-185 forward a `blocked-*` plan result directly to the caller:

```ts
if (plan.kind !== "ready") {
    return plan;
}
```

every test that calls `applyOcChatgptSync` injects a `detectTarget` that always resolves to `"target"`, so this path has zero coverage. a `blocked-none` or `blocked-ambiguous` result from `planOcChatgptSync` during an apply call would reach callers completely untested. given this is a new orchestration layer that callers will pattern-match on, both missing cases should have explicit tests — especially because on windows a deleted-between-plan-and-apply target directory would hit `blocked-none` here instead of `error`.

suggested additions:
```ts
it("returns blocked-none from apply when detection finds no target", async () => {
    const result = await applyOcChatgptSync({
        source: sourceStorage,
        dependencies: {
            detectTarget: () => ({ kind: "none", reason: "missing", tried: [] }),
        },
    });
    expect(result.kind).toBe("blocked-none");
});

it("returns blocked-ambiguous from apply when detection is ambiguous", async () => {
    const result = await applyOcChatgptSync({
        source: sourceStorage,
        dependencies: {
            detectTarget: () => ({ kind: "ambiguous", reason: "multiple", candidates: [] }),
        },
    });
    expect(result.kind).toBe("blocked-ambiguous");
});
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/oc-chatgpt-orchestrator.ts">

<violation number="1" location="lib/oc-chatgpt-orchestrator.ts:165">
P1: Write the temp account file with `mode: 0o600`; otherwise rename can widen final token-file permissions to default umask values.</violation>

<violation number="2" location="lib/oc-chatgpt-orchestrator.ts:170">
P2: Add retry/backoff around `fs.rename` for transient filesystem lock errors to avoid spurious apply failures.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@ndycode
Copy link
Owner Author

ndycode commented Mar 10, 2026

Pushed follow-up c0cd5c8 on split/pr58-sync-orchestrator.

This closes the remaining orchestration test gap by:

  • changing persistedPath to the cleaner optional string shape
  • adding rename-exhaustion coverage that verifies the temp apply file is unlinked after all retry attempts are exhausted

Validated with npm exec vitest run test/oc-chatgpt-orchestrator.test.ts.

Comment on lines +193 to +198
await fs.mkdir(dirname(path), { recursive: true });
try {
await fs.writeFile(tempPath, `${JSON.stringify(merged, null, 2)}\n`, {
encoding: "utf-8",
mode: 0o600,
});
Copy link

Choose a reason for hiding this comment

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

mkdir has no retry; mode: 0o600 is a no-op on Windows

two distinct token-safety problems on this block:

  1. fs.mkdir (line 193) sits outside the try/catch and has no EPERM/EBUSY retry. on windows, antivirus scanning the parent directory can hold a lock during directory creation for hundreds of milliseconds. a single EPERM here causes the entire apply to fail immediately, with no temp file created yet and no chance to retry — the same window-lock scenario that renameWithRetry was added to handle.

  2. mode: 0o600 (line 197) is silently ignored by node.js on windows. the temp file lands with the default windows acl (typically readable by any local user) and sits on disk until the rename completes. during that window, a concurrent process with the same user session can read the temp file containing plaintext refresh tokens. on unix the mode is correctly enforced; this is purely a windows token-leakage gap.

for (1) add a mkdirWithRetry mirroring renameWithRetry. for (2) you can't fix mode on windows via fs.writeFile — the correct approach is to write to the temp file path and then call fs.chmod before the rename, or accept that this protection is unix-only and document the gap.

// (1) retry mkdir to match renameWithRetry coverage
await mkdirWithRetry(dirname(path));
// (2) on unix this restricts permissions; on windows the ACL
//     must be set explicitly via icacls or the gap is documented
await fs.writeFile(tempPath, `${JSON.stringify(merged, null, 2)}\n`, {
    encoding: "utf-8",
    mode: 0o600,
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/oc-chatgpt-orchestrator.ts
Line: 193-198

Comment:
`mkdir` has no retry; `mode: 0o600` is a no-op on Windows

two distinct token-safety problems on this block:

1. `fs.mkdir` (line 193) sits outside the try/catch and has no `EPERM`/`EBUSY` retry. on windows, antivirus scanning the parent directory can hold a lock during directory creation for hundreds of milliseconds. a single `EPERM` here causes the entire apply to fail immediately, with no temp file created yet and no chance to retry — the same window-lock scenario that `renameWithRetry` was added to handle.

2. `mode: 0o600` (line 197) is silently ignored by node.js on windows. the temp file lands with the default windows acl (typically readable by any local user) and sits on disk until the rename completes. during that window, a concurrent process with the same user session can read the temp file containing plaintext refresh tokens. on unix the mode is correctly enforced; this is purely a windows token-leakage gap.

for (1) add a `mkdirWithRetry` mirroring `renameWithRetry`. for (2) you can't fix mode on windows via `fs.writeFile` — the correct approach is to write to the temp file path and then call `fs.chmod` before the rename, or accept that this protection is unix-only and document the gap.

```ts
// (1) retry mkdir to match renameWithRetry coverage
await mkdirWithRetry(dirname(path));
// (2) on unix this restricts permissions; on windows the ACL
//     must be set explicitly via icacls or the gap is documented
await fs.writeFile(tempPath, `${JSON.stringify(merged, null, 2)}\n`, {
    encoding: "utf-8",
    mode: 0o600,
});
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment on lines +88 to +124
export async function planOcChatgptSync(
options: PlanOcChatgptSyncOptions,
): Promise<OcChatgptSyncPlanResult> {
const detectTarget =
options.dependencies?.detectTarget ?? detectOcChatgptMultiAuthTarget;
const previewMerge =
options.dependencies?.previewMerge ?? previewOcChatgptImportMerge;

const detection = detectTarget(options.detectOptions);
const blocked = mapDetectionToBlocked(detection);
if (blocked) {
return blocked;
}
if (detection.kind !== "target") {
throw new Error("Unexpected oc target detection result");
}

const descriptor = detection.descriptor;
const destination =
options.destination === undefined
? await (
options.dependencies?.loadTargetStorage ?? loadTargetStorageDefault
)(descriptor)
: options.destination;
const preview = previewMerge({
source: options.source,
destination,
});

return {
kind: "ready",
target: descriptor,
preview,
payload: preview.payload,
destination,
};
}
Copy link

Choose a reason for hiding this comment

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

planOcChatgptSync can throw but its return type hides this

planOcChatgptSync returns Promise<OcChatgptSyncPlanResult>, a union that has no error variant. yet loadTargetStorage (or its default loadTargetStorageDefault) can throw non-ENOENT errors — EACCES, EBUSY, SyntaxError from malformed json — and those propagate as unhandled rejections from planOcChatgptSync.

applyOcChatgptSync handles this correctly by wrapping the plan call in try/catch. but callers that use planOcChatgptSync standalone (e.g. a "dry-run preview" flow in the settings ui) have no type-level signal that they must wrap in try/catch. the only tests for the throwing-loadTargetStorage path go through applyOcChatgptSync — there is zero direct test coverage for planOcChatgptSync itself when loadTargetStorage throws.

either add an error variant to OcChatgptSyncPlanResult and catch inside planOcChatgptSync, or document the throws contract explicitly with a jsdoc @throws so callers know to wrap it. also add a vitest case calling planOcChatgptSync directly with a throwing loadTargetStorage to pin this behavior.

Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/oc-chatgpt-orchestrator.ts
Line: 88-124

Comment:
`planOcChatgptSync` can throw but its return type hides this

`planOcChatgptSync` returns `Promise<OcChatgptSyncPlanResult>`, a union that has no error variant. yet `loadTargetStorage` (or its default `loadTargetStorageDefault`) can throw non-`ENOENT` errors — `EACCES`, `EBUSY`, `SyntaxError` from malformed json — and those propagate as unhandled rejections from `planOcChatgptSync`.

`applyOcChatgptSync` handles this correctly by wrapping the plan call in try/catch. but callers that use `planOcChatgptSync` standalone (e.g. a "dry-run preview" flow in the settings ui) have no type-level signal that they must wrap in try/catch. the only tests for the throwing-loadTargetStorage path go through `applyOcChatgptSync` — there is zero direct test coverage for `planOcChatgptSync` itself when `loadTargetStorage` throws.

either add an `error` variant to `OcChatgptSyncPlanResult` and catch inside `planOcChatgptSync`, or document the throws contract explicitly with a jsdoc `@throws` so callers know to wrap it. also add a vitest case calling `planOcChatgptSync` directly with a throwing `loadTargetStorage` to pin this behavior.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant