Skip to content
Merged
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
7 changes: 5 additions & 2 deletions web/scripts/coderouter/migrate-encrypted-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ for (const teamId of teams) {
verified += 1;
}
const source = await readTeamVault(teamId);
if (Object.keys(source.accounts).length !== encrypted.length) {
const encryptedIds = new Set(encrypted.map((value) => value.accountId));
const missing = Object.keys(source.accounts)
.filter((accountId) => !encryptedIds.has(accountId));
if (missing.length > 0) {
throw new Error(
`refusing to delete source for team ${teamId}: account counts differ`,
`refusing to delete source for team ${teamId}: ${missing.length} source accounts are not encrypted`,
);
}
await clearTeamVault(teamId);
Expand Down
25 changes: 19 additions & 6 deletions web/services/coderouter/repository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createHash, randomBytes, randomUUID } from "node:crypto";
import { and, eq, gt, isNull, lte, notInArray, or, sql } from "drizzle-orm";
import { and, eq, gt, isNull, lt, lte, notInArray, or, sql } from "drizzle-orm";
import { cloudDb } from "../../db/client";
import {
coderouterAccounts,
Expand Down Expand Up @@ -237,16 +237,28 @@ export async function importEncryptedCredential(input: {
readonly encrypted: EncryptedCredential;
}): Promise<void> {
await cloudDb().transaction(async (tx) => {
await tx
const [inserted] = await tx
.insert(coderouterCredentials)
.values(encryptedValues(input.encrypted))
.onConflictDoUpdate({
.onConflictDoNothing({
target: coderouterCredentials.accountId,
set: {
})
.returning({ accountId: coderouterCredentials.accountId });
if (!inserted) {
await tx
.update(coderouterCredentials)
.set({
...encryptedValues(input.encrypted),
updatedAt: new Date(),
},
});
})
.where(and(
eq(coderouterCredentials.accountId, input.encrypted.accountId),
lt(
coderouterCredentials.credentialRevision,
input.encrypted.credentialRevision,
),
));
}
Comment on lines +240 to +261

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'export const coderouterAccounts|vaultRevision|credentialRevision|importEncryptedCredential' \
  web/db/schema.ts \
  web/services/coderouter \
  web/scripts/coderouter

rg -n -C 8 \
  'importEncryptedCredential|vaultRevision|credentialRevision' \
  web --glob '*test*' --glob '*spec*' || true

Repository: manaflow-ai/cmux

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repository.ts around importEncryptedCredential =="
sed -n '150,285p' web/services/coderouter/repository.ts

echo
echo "== migration callers and importEncryptedCredential call sites =="
rg -n -C 5 'importEncryptedCredential|encryptCredential\(\{[\s\S]*?credentialRevision: input\.encrypted\.credentialRevision|credentialRevision: account\.revision|credentialRevision: expectedRevision \+ 1' web/services/coderouter web/scripts/coderouter

echo
echo "== importEncryptedCredential call sites =="
rg -n -C 4 'importEncryptedCredential' web

Repository: manaflow-ai/cmux

Length of output: 11046


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== vault.ts relevant contents =="
ast-grep outline web/services/coderouter/vault.ts --view expanded || true
sed -n '1,260p' web/services/coderouter/vault.ts

echo
echo "== coderouter schema relevant insert/update migration/statements context =="
rg -n -C 6 'coderouter_accounts|coderouter_credentials|vault_revision|credential_revision' web/db web/migrations web --glob '*.ts' --glob '*.tsx' --glob '*.sql' || true

echo
echo "== deterministic SQL-like behavior for current import predicate cases =="
python3 - <<'PY'
def update_result(input_revision, stored_credential_revision=None, stored_account_revision=None):
    if stored_credential_revision is None:
        credential_inserted = True
        credential_updated = False
    elif stored_credential_revision < input_revision:
        credential_inserted = False
        credential_updated = True
    else:
        credential_inserted = False
        credential_updated = False
    if stored_account_revision is not None and stored_account_revision < input_revision:
        account_updated = True
    else:
        account_updated = False
    return {
        "credential_inserted": credential_inserted,
        "credential_updated": credential_updated,
        "account_updated": account_updated,
        "stored_credential": stored_credential_revision,
        "stored_account": stored_account_revision,
        "input": input_revision,
    }

cases = [
    {"label": "missing credential, stored account newer", "stored_credential": None, "stored_account": 10, "input": 5},
    {"label": "older credential, stored account newer", "stored_credential": 1, "stored_account": 10, "input": 5},
    {"label": "credential newer, stored account newer", "stored_credential": 20, "stored_account": 10, "input": 5},
    {"label": "both newer than import", "stored_credential": 20, "stored_account": 15, "input": 5},
]
for c in cases:
    print(c["label"], json := update_result(c["input"], c["stored_credential"], c["stored_account"]))
PY

Repository: manaflow-ai/cmux

Length of output: 31655


Gate both credential and account updates with one monotonic revision.

importEncryptedCredential reads only coderouterCredentials.credentialRevision for the credential write and coderouterAccounts.vaultRevision for the metadata write. If the imported revision is newer than the stored credential revision but older than the account revision, account metadata such as label and credentialExpiresAt is overwritten with stale data while the credential row remains newer. Make both statements accept one revision from the credential source, and add coverage for missing credentials plus divergent stored revisions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/services/coderouter/repository.ts` around lines 240 - 261, Update
importEncryptedCredential so both the credential upsert and coderouterAccounts
metadata update are gated by the same monotonic revision from the imported
credential source. Use that revision for the account vaultRevision comparison as
well as coderouterCredentials.credentialRevision, preventing stale metadata
updates when stored revisions diverge. Add coverage for missing credentials and
divergent stored revisions.

Source: Path instructions

await tx
.update(coderouterAccounts)
.set({
Expand All @@ -258,6 +270,7 @@ export async function importEncryptedCredential(input: {
.where(and(
eq(coderouterAccounts.id, input.encrypted.accountId),
eq(coderouterAccounts.teamId, input.encrypted.teamId),
lt(coderouterAccounts.vaultRevision, input.encrypted.credentialRevision),
));
});
}
Expand Down