mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
fix: serialize managed credential refresh with reads and secret edits
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
feba1f926f
commit
7cf8df553d
7 files changed
+109
-17
No files matched your search
@@ -11,7 +11,7 @@ import {
|
||||
} from "@paperclipai/shared";
|
||||
import { aiConnectionService } from "./ai-connections.js";
|
||||
import { secretService } from "./secrets.js";
|
||||
import { decideCodexAuthMerge } from "@paperclipai/adapter-codex-local/server";
|
||||
import { decideCodexAuthMerge, withAccountHomeSecretMutationLock } from "@paperclipai/adapter-codex-local/server";
|
||||
import type { AdapterExecutionTarget } from "@paperclipai/adapter-utils/execution-target";
|
||||
import { runAdapterExecutionTargetProcess } from "@paperclipai/adapter-utils/execution-target";
|
||||
import { decideGrokAuthMerge } from "@paperclipai/adapter-grok-local/server";
|
||||
@@ -314,7 +314,7 @@ export async function prepareManagedAiRuntime(
|
||||
if (subscriptionFile) {
|
||||
const refreshed = await readFile(authFile, "utf8");
|
||||
if (refreshed !== value)
|
||||
await db.transaction(async (tx) => {
|
||||
await withAccountHomeSecretMutationLock(undefined, input.companyId, () => db.transaction(async (tx) => {
|
||||
const [grant] = await tx
|
||||
.select()
|
||||
.from(connectionGrants)
|
||||
@@ -374,7 +374,7 @@ export async function prepareManagedAiRuntime(
|
||||
.update(connectionGrants)
|
||||
.set({ updatedAt: new Date() })
|
||||
.where(eq(connectionGrants.id, grant.id));
|
||||
});
|
||||
}));
|
||||
}
|
||||
} finally {
|
||||
if (home) await rm(home, { recursive: true, force: true });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { withAccountHomeSecretMutationLock } from "@paperclipai/adapter-codex-local/server";
|
||||
import { syncConnectionCredentialBindings } from "./connection-credential-bindings.js";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { and, eq, inArray, or, sql } from "drizzle-orm";
|
||||
@@ -402,6 +403,9 @@ export function aiConnectionService(db: Db) {
|
||||
};
|
||||
}
|
||||
async function credential(row: Pick<Awaited<ReturnType<typeof select>>, "connection" | "grant">) {
|
||||
return withAccountHomeSecretMutationLock(undefined, row.connection.companyId, () => credentialUnlocked(row));
|
||||
}
|
||||
async function credentialUnlocked(row: Pick<Awaited<ReturnType<typeof select>>, "connection" | "grant">) {
|
||||
const ref = row.grant.credentialSecretRefs.find(
|
||||
(r) => r.configPath === "ai.credential",
|
||||
);
|
||||
@@ -498,7 +502,7 @@ export function aiConnectionService(db: Db) {
|
||||
);
|
||||
const id = reconnect?.connection.id ?? randomUUID();
|
||||
const grantId = reconnect?.grant.id ?? randomUUID();
|
||||
return db.transaction(async (tx) => {
|
||||
return withAccountHomeSecretMutationLock(undefined, companyId, () => db.transaction(async (tx) => {
|
||||
const secrets = secretService(tx);
|
||||
if (sessionId) {
|
||||
const [session] = await tx
|
||||
@@ -803,7 +807,7 @@ export function aiConnectionService(db: Db) {
|
||||
details: { provider: input.provider, method: input.method, grantId },
|
||||
});
|
||||
return { connectionId: id, grantId };
|
||||
});
|
||||
}));
|
||||
}
|
||||
/** A late failure must never invalidate credentials that were refreshed or reconnected meanwhile. */
|
||||
async function markAuthenticationFailed(input: {
|
||||
@@ -813,7 +817,7 @@ export function aiConnectionService(db: Db) {
|
||||
runStartedAt: Date;
|
||||
attribution: AiConnectionAttribution & { identity: string };
|
||||
}) {
|
||||
return db.transaction(async (tx) => {
|
||||
return withAccountHomeSecretMutationLock(undefined, input.companyId, () => db.transaction(async (tx) => {
|
||||
const { attribution } = input;
|
||||
const [grant] = await tx.select().from(connectionGrants).where(and(
|
||||
eq(connectionGrants.companyId, input.companyId),
|
||||
@@ -847,11 +851,11 @@ export function aiConnectionService(db: Db) {
|
||||
entityType: "tool_connection", entityId: connection.id,
|
||||
details: { provider: attribution.provider, grantId: grant.id },
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
/** Refresh only the selected vaulted identity, serializing rotation and reconnect. */
|
||||
async function refreshQuotaCredential(row: Parameters<typeof credential>[0], failedValue: string, signal: AbortSignal) {
|
||||
return db.transaction(async (tx) => {
|
||||
return withAccountHomeSecretMutationLock(undefined, row.connection.companyId, () => db.transaction(async (tx) => {
|
||||
const companyId = row.connection.companyId;
|
||||
const [grant] = await tx.select().from(connectionGrants).where(and(
|
||||
eq(connectionGrants.id, row.grant.id), eq(connectionGrants.companyId, companyId),
|
||||
@@ -904,7 +908,7 @@ export function aiConnectionService(db: Db) {
|
||||
action: "ai_connection.credential_refreshed", entityType: "tool_connection", entityId: row.connection.id,
|
||||
details: { provider: "openai", grantId: grant.id } });
|
||||
return refreshed;
|
||||
});
|
||||
}));
|
||||
}
|
||||
// A quota read uses the same credential audience as execution. Operator status
|
||||
// alone never grants access to another member's personal subscription.
|
||||
|
||||
Reference in new issue
Block a user