{"record":{"id":"98e88b124e44b5da","repo":"can1357/oh-my-pi","slug":"no-credential-with-id-id","errorCode":null,"errorMessage":"No credential with id=${id}","messagePattern":"No credential with id=(.+?)","errorType":"validation","errorClass":"AIError.ValidationError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/auth-storage.ts","lineNumber":6756,"sourceCode":"\t\t\t\ttype: \"oauth\",\n\t\t\t\taccess: refreshed.access,\n\t\t\t\trefresh: refreshed.refresh,\n\t\t\t\texpires: refreshed.expires,\n\t\t\t\taccountId: refreshed.accountId ?? attempted.accountId,\n\t\t\t\temail: refreshed.email ?? attempted.email,\n\t\t\t\tprojectId: refreshed.projectId ?? attempted.projectId,\n\t\t\t\tenterpriseUrl: refreshed.enterpriseUrl ?? attempted.enterpriseUrl,\n\t\t\t\tapiEndpoint: refreshed.apiEndpoint ?? attempted.apiEndpoint,\n\t\t\t\torgId: refreshed.orgId ?? attempted.orgId,\n\t\t\t\torgName: refreshed.orgName ?? attempted.orgName,\n\t\t\t\tauthorizedAt: refreshed.authorizedAt ?? attempted.authorizedAt,\n\t\t\t};\n\t\t\t// Persist by id: the array may have been reordered/shrunk while the\n\t\t\t// refresh was in flight, so the pre-await positional index is unsafe. A\n\t\t\t// -1 means the row was disabled/removed mid-refresh — surface that as a\n\t\t\t// miss rather than implying a live row the snapshot won't contain.\n\t\t\tif (this.#replaceCredentialById(provider, id, updated) === -1) {\n\t\t\t\tthrow new AIError.ValidationError(`No credential with id=${id}`);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tid,\n\t\t\t\tprovider,\n\t\t\t\tcredential: { ...updated, refresh: REMOTE_REFRESH_SENTINEL },\n\t\t\t\tidentityKey: resolveCredentialIdentityKey(provider, updated),\n\t\t\t};\n\t\t}\n\t\tthrow new AIError.ValidationError(`No credential with id=${id}`);\n\t}\n\n\t/**\n\t * Disable the credential with the given id and emit a\n\t * {@link CredentialDisabledEvent}. Used by the auth-broker server to honour\n\t * `POST /v1/credential/:id/disable`. Returns `false` when no such row exists.\n\t */\n\tdisableCredentialById(id: number, disabledCause: string): boolean {\n\t\tfor (const [provider, entries] of this.#data) {","sourceCodeStart":6738,"sourceCodeEnd":6774,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/auth-storage.ts#L6738-L6774","documentation":"Thrown by the auth storage's credential refresh persistence path when the refreshed credential can no longer be found by id. After an async refresh completes, the code re-looks-up the row positionally because the array may have been reordered or shrunk while awaiting; if #replaceCredentialById returns -1, the credential was disabled or removed mid-refresh and there is no live row to persist into, so a ValidationError is raised instead of writing to a stale index.","triggerScenarios":"Calling the credential update/refresh API while another caller concurrently disables or removes the same credential id; refreshing a credential whose id no longer exists in the provider's credential array; a race between an auth-broker refresh and a concurrent credential list mutation.","commonSituations":"Multiple processes (e.g. the auth-broker server and a CLI session) sharing auth storage where one deletes or disables a credential while another is mid-refresh; stale UI or cached id passed after credential removal; credential rotation that drops rows during a long-running token refresh.","solutions":["Re-fetch the credential list and confirm the id still exists before refreshing","Retry the refresh with a freshly resolved credential id, or re-authenticate to create a new credential","Check whether another tool/process disabled or removed the credential (auth-broker disable endpoint) and restore it","Wrap the refresh call in try-catch for AIError.ValidationError and treat it as 'credential gone' rather than retrying with the same id"],"exampleFix":"// before\nconst updated = await refresh(credential); // may take seconds\nstorage.replaceByIndex(originalIndex, updated); // stale index\n// after\nconst updated = await refresh(credential);\ntry {\n  storage.updateCredential(provider, id, updated);\n} catch (e) {\n  // credential was removed/disabled mid-refresh — re-acquire instead\n  const fresh = await authenticate(provider);\n}","handlingStrategy":"try-catch","validationCode":"const exists = (await storage.listCredentials(provider)).some(c => c.id === id);\nif (!exists) throw new Error(`Credential ${id} no longer exists; re-authenticate`);","typeGuard":null,"tryCatchPattern":"try {\n  await storage.updateCredential(provider, id, refreshed);\n} catch (e) {\n  if (e instanceof AIError.ValidationError && /No credential with id=/.test(e.message)) {\n    const fresh = await authenticate(provider); // credential vanished mid-flight\n  } else throw e;\n}","preventionTips":["Serialize credential mutations through a single owner/lock to avoid refresh vs. disable races","Re-validate credential ids immediately before refresh","Listen for CredentialDisabledEvent/removed events and cancel in-flight refreshes","Persist provider+id references and re-resolve them on miss instead of failing hard"],"tags":["auth","concurrency","race-condition","validation"],"backgroundTag":"credential-not-found","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}