mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-09-24 15:49:09 +00:00
2ea5efacba
* fix(plugins): address residual Codex sync secret and backup gaps Retain provider→plugin secret bindings so disconnect can wipe credentials after a plugin is disabled, keep overwritten secrets on failed reconnect, and snapshot live-empty sidecars in protective backups. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(plugins): restore overwritten sync secrets on rejected reconnect Stash prior plaintext and SecretRef id on overwrite, restore them when connect fails, and discard the stash after a successful reconnect so saved provider credentials keep resolving. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(plugins): make sync secret overwrite stash opt-in Only the syncPutSecret path stashes prior plaintext/SecretRef so ordinary secrets.set overwrites do not leave credentials in process memory. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(plugins): host-own sync provider bindings and clear failed stashes Move provider→plugin bindings into a dedicated DB table outside plugin-writable secrets, validate provider namespace ownership, and drop overwrite stashes when replacement writes fail. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(plugins): keep overwrite stash until restore write succeeds Only drop the prior plaintext after encrypt+upsert succeeds so a failed restore can still retry and recover the saved SecretRef. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(plugins): preserve overwrite stash across retries and prefix deletes Do not replace an existing restore stash on later puts, and clear matching stash entries when deleteByKeyPrefix wipes sync credentials on disconnect. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
108 lines
3.3 KiB
TypeScript
108 lines
3.3 KiB
TypeScript
/**
|
|
* Persist plugin sync secrets then connect; roll back just-created secrets if
|
|
* connect (or a later put) fails so bad passwords are not left in OS storage.
|
|
* Overwritten keys are restored from the host-side overwrite stash so a
|
|
* rejected reconnect does not leave a broken SecretRef / rejected credential.
|
|
*/
|
|
|
|
export type PluginSyncSecretRef = { kind: 'secret'; id: string; key: string };
|
|
|
|
export interface PluginSyncConnectSecretInput {
|
|
secretKey: string;
|
|
value: string;
|
|
}
|
|
|
|
export interface PluginSyncPutSecretResult extends PluginSyncSecretRef {
|
|
/** False when the durable (plugin,key) row already existed and was overwritten. */
|
|
created?: boolean;
|
|
}
|
|
|
|
export interface StorePluginSyncSecretsThenConnectParams {
|
|
providerId: string;
|
|
secrets: readonly PluginSyncConnectSecretInput[];
|
|
/** Reused when `secrets` is empty (edit non-secret fields / reconnect). */
|
|
existingCredential?: PluginSyncSecretRef;
|
|
putSecret: (params: {
|
|
providerId: string;
|
|
key: string;
|
|
value: string;
|
|
}) => Promise<PluginSyncPutSecretResult>;
|
|
deleteSecrets: (params: {
|
|
providerId: string;
|
|
keys: string[];
|
|
}) => Promise<unknown>;
|
|
/**
|
|
* Restore host-stashed plaintext for overwritten keys (`discard: false`),
|
|
* or drop the stash after a successful connect (`discard: true`).
|
|
*/
|
|
restoreSecrets?: (params: {
|
|
providerId: string;
|
|
keys: string[];
|
|
discard?: boolean;
|
|
}) => Promise<unknown>;
|
|
connect: (credential: PluginSyncSecretRef | undefined) => Promise<void>;
|
|
}
|
|
|
|
export async function storePluginSyncSecretsThenConnect(
|
|
params: StorePluginSyncSecretsThenConnectParams,
|
|
): Promise<void> {
|
|
const createdKeys: string[] = [];
|
|
const overwrittenKeys: string[] = [];
|
|
let credential: PluginSyncSecretRef | undefined = params.existingCredential;
|
|
|
|
try {
|
|
if (params.secrets.length > 0) {
|
|
credential = undefined;
|
|
for (const secret of params.secrets) {
|
|
const ref = await params.putSecret({
|
|
providerId: params.providerId,
|
|
key: secret.secretKey,
|
|
value: secret.value,
|
|
});
|
|
if (ref.created === false) {
|
|
overwrittenKeys.push(secret.secretKey);
|
|
} else {
|
|
createdKeys.push(secret.secretKey);
|
|
}
|
|
// SyncConnectPayload.credential carries the primary (first) secret;
|
|
// additional secrets remain addressable via secrets.get(key).
|
|
if (!credential) credential = ref;
|
|
}
|
|
}
|
|
await params.connect(credential);
|
|
if (overwrittenKeys.length > 0 && params.restoreSecrets) {
|
|
try {
|
|
await params.restoreSecrets({
|
|
providerId: params.providerId,
|
|
keys: [...overwrittenKeys],
|
|
discard: true,
|
|
});
|
|
} catch {
|
|
/* best-effort stash cleanup */
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (createdKeys.length > 0) {
|
|
try {
|
|
await params.deleteSecrets({
|
|
providerId: params.providerId,
|
|
keys: [...createdKeys],
|
|
});
|
|
} catch {
|
|
/* best-effort; surface the original connect/put error */
|
|
}
|
|
}
|
|
if (overwrittenKeys.length > 0 && params.restoreSecrets) {
|
|
try {
|
|
await params.restoreSecrets({
|
|
providerId: params.providerId,
|
|
keys: [...overwrittenKeys],
|
|
});
|
|
} catch {
|
|
/* best-effort; surface the original connect/put error */
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
}
|