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>
138 lines
4.6 KiB
TypeScript
138 lines
4.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { describe, it } from 'node:test';
|
|
import { storePluginSyncSecretsThenConnect } from './pluginSyncConnectWithSecrets.ts';
|
|
|
|
describe('storePluginSyncSecretsThenConnect', () => {
|
|
it('deletes just-created secret keys when connect rejects', async () => {
|
|
const putCalls: Array<{ key: string; value: string }> = [];
|
|
const deletedKeys: string[][] = [];
|
|
let connectCalls = 0;
|
|
|
|
await assert.rejects(
|
|
() => storePluginSyncSecretsThenConnect({
|
|
providerId: 'plugin:sync.example',
|
|
secrets: [
|
|
{ secretKey: 'sync-credential', value: 'bad-password' },
|
|
{ secretKey: 'sync-credential:token', value: 'bad-token' },
|
|
],
|
|
putSecret: async ({ key, value }) => {
|
|
putCalls.push({ key, value });
|
|
return { kind: 'secret', id: `ref-${key}`, key };
|
|
},
|
|
deleteSecrets: async ({ keys }) => {
|
|
deletedKeys.push([...keys]);
|
|
return { deleted: keys.length };
|
|
},
|
|
connect: async () => {
|
|
connectCalls += 1;
|
|
throw new Error('auth failed');
|
|
},
|
|
}),
|
|
/auth failed/,
|
|
);
|
|
|
|
assert.equal(connectCalls, 1);
|
|
assert.deepEqual(putCalls, [
|
|
{ key: 'sync-credential', value: 'bad-password' },
|
|
{ key: 'sync-credential:token', value: 'bad-token' },
|
|
]);
|
|
assert.deepEqual(deletedKeys, [['sync-credential', 'sync-credential:token']]);
|
|
});
|
|
|
|
it('does not delete secrets when connect succeeds', async () => {
|
|
const deletedKeys: string[][] = [];
|
|
await storePluginSyncSecretsThenConnect({
|
|
providerId: 'plugin:sync.example',
|
|
secrets: [{ secretKey: 'sync-credential', value: 'ok' }],
|
|
putSecret: async ({ key }) => ({ kind: 'secret', id: 'ref-1', key }),
|
|
deleteSecrets: async ({ keys }) => {
|
|
deletedKeys.push([...keys]);
|
|
return { deleted: keys.length };
|
|
},
|
|
connect: async () => {},
|
|
});
|
|
assert.deepEqual(deletedKeys, []);
|
|
});
|
|
|
|
it('does not delete when reusing an existing credential without new secrets', async () => {
|
|
const deletedKeys: string[][] = [];
|
|
const existing = { kind: 'secret' as const, id: 'existing-ref', key: 'sync-credential' };
|
|
await assert.rejects(
|
|
() => storePluginSyncSecretsThenConnect({
|
|
providerId: 'plugin:sync.example',
|
|
secrets: [],
|
|
existingCredential: existing,
|
|
putSecret: async () => {
|
|
throw new Error('put should not run');
|
|
},
|
|
deleteSecrets: async ({ keys }) => {
|
|
deletedKeys.push([...keys]);
|
|
return { deleted: keys.length };
|
|
},
|
|
connect: async () => {
|
|
throw new Error('network failed');
|
|
},
|
|
}),
|
|
/network failed/,
|
|
);
|
|
assert.deepEqual(deletedKeys, []);
|
|
});
|
|
|
|
it('deletes secrets that were put before a later put fails', async () => {
|
|
const deletedKeys: string[][] = [];
|
|
await assert.rejects(
|
|
() => storePluginSyncSecretsThenConnect({
|
|
providerId: 'plugin:sync.example',
|
|
secrets: [
|
|
{ secretKey: 'sync-credential', value: 'first' },
|
|
{ secretKey: 'sync-credential:token', value: 'second' },
|
|
],
|
|
putSecret: async ({ key }) => {
|
|
if (key === 'sync-credential:token') throw new Error('put failed');
|
|
return { kind: 'secret', id: 'ref-1', key };
|
|
},
|
|
deleteSecrets: async ({ keys }) => {
|
|
deletedKeys.push([...keys]);
|
|
return { deleted: keys.length };
|
|
},
|
|
connect: async () => {
|
|
throw new Error('connect should not run');
|
|
},
|
|
}),
|
|
/put failed/,
|
|
);
|
|
assert.deepEqual(deletedKeys, [['sync-credential']]);
|
|
});
|
|
|
|
it('keeps overwritten secrets when reconnect connect rejects', async () => {
|
|
const deletedKeys: string[][] = [];
|
|
const restoredKeys: string[][] = [];
|
|
await assert.rejects(
|
|
() => storePluginSyncSecretsThenConnect({
|
|
providerId: 'plugin:sync.example',
|
|
secrets: [{ secretKey: 'sync-credential', value: 'bad-password' }],
|
|
putSecret: async ({ key }) => ({
|
|
kind: 'secret',
|
|
id: 'ref-overwritten',
|
|
key,
|
|
created: false,
|
|
}),
|
|
deleteSecrets: async ({ keys }) => {
|
|
deletedKeys.push([...keys]);
|
|
return { deleted: keys.length };
|
|
},
|
|
restoreSecrets: async ({ keys }) => {
|
|
restoredKeys.push([...keys]);
|
|
return { restored: keys.length };
|
|
},
|
|
connect: async () => {
|
|
throw new Error('auth failed');
|
|
},
|
|
}),
|
|
/auth failed/,
|
|
);
|
|
assert.deepEqual(deletedKeys, []);
|
|
assert.deepEqual(restoredKeys, [['sync-credential']]);
|
|
});
|
|
});
|