Skip to content

Commit 50680f7

Browse files
committed
fix(zoho-desk): carry the stored data center through a credential reconnect
A reconnect rebuilds the service-account secret blob from the submitted fields only, and the connect modal never prefills - correctly, since for every other field in this family the stored value is a secret the admin must retype. The data center is the first non-secret member of that set, so it was being silently dropped: rotating a client secret on an EU/IN/AU credential moved it back to the US accounts server, where the next mint fails with an opaque invalid_client. performUpdateCredential now reads the stored dataCenter out of the existing blob when the caller does not supply one. The read is failure-tolerant - an undecryptable or unparseable blob yields undefined rather than throwing, so it can never block a reconnect, and the provider default applies as before. Raised independently by three reviewers; I twice argued it was acceptable because the mint fails loudly rather than corrupting silently. That was true and beside the point - the operator still had to guess why.
1 parent 7accb64 commit 50680f7

1 file changed

Lines changed: 37 additions & 2 deletions

File tree

  • apps/sim/lib/credentials/orchestration

apps/sim/lib/credentials/orchestration/index.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { createLogger } from '@sim/logger'
55
import { generateId } from '@sim/utils/id'
66
import { and, eq, sql } from 'drizzle-orm'
77
import type { NextRequest } from 'next/server'
8-
import { encryptSecret } from '@/lib/core/security/encryption'
8+
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
99
import { getCredentialActorContext } from '@/lib/credentials/access'
1010
import { AtlassianValidationError } from '@/lib/credentials/atlassian-service-account'
1111
import { type CredentialDeleteReason, deleteCredential } from '@/lib/credentials/deletion'
@@ -22,6 +22,30 @@ import { captureServerEvent } from '@/lib/posthog/server'
2222

2323
const logger = createLogger('CredentialOrchestration')
2424

25+
/**
26+
* Read the `dataCenter` already stored in a service-account credential's
27+
* encrypted blob. Used on reconnect so a non-secret regional selector survives a
28+
* secret rotation that does not resubmit it. Returns undefined on any failure -
29+
* a blob that cannot be read must not block the reconnect, and the provider's
30+
* own default then applies.
31+
*/
32+
async function readStoredDataCenter(credentialId: string): Promise<string | undefined> {
33+
try {
34+
const rows = await db
35+
.select({ key: credential.encryptedServiceAccountKey })
36+
.from(credential)
37+
.where(eq(credential.id, credentialId))
38+
.limit(1)
39+
const key = rows[0]?.key
40+
if (!key) return undefined
41+
const { decrypted } = await decryptSecret(key)
42+
const blob = JSON.parse(decrypted) as { dataCenter?: unknown }
43+
return typeof blob.dataCenter === 'string' && blob.dataCenter ? blob.dataCenter : undefined
44+
} catch {
45+
return undefined
46+
}
47+
}
48+
2549
export type CredentialOrchestrationErrorCode =
2650
| 'not_found'
2751
| 'forbidden'
@@ -138,6 +162,17 @@ export async function performUpdateCredential(
138162
params.dataCenter !== undefined
139163
let rotatedSlackBotUserId: string | undefined
140164
if (hasRotationSecret && access.credential.type === 'service_account') {
165+
// A reconnect rebuilds the secret blob from the submitted fields only, and
166+
// the modal never prefills (secrets are never echoed back). For an actual
167+
// secret that is correct - the admin retypes it. But a non-secret selector
168+
// like the Zoho data center would be silently dropped, moving an EU/IN/AU
169+
// credential back to the US accounts server. Carry the stored value forward
170+
// when the caller did not supply one.
171+
const carriedDataCenter =
172+
params.dataCenter === undefined
173+
? await readStoredDataCenter(access.credential.id)
174+
: params.dataCenter
175+
141176
try {
142177
const secret = await verifyAndBuildServiceAccountSecret(
143178
access.credential.providerId ?? '',
@@ -149,7 +184,7 @@ export async function performUpdateCredential(
149184
clientId: params.clientId,
150185
clientSecret: params.clientSecret,
151186
orgId: params.orgId,
152-
dataCenter: params.dataCenter,
187+
dataCenter: carriedDataCenter,
153188
}
154189
)
155190
updates.encryptedServiceAccountKey = secret.encryptedServiceAccountKey

0 commit comments

Comments
 (0)