qwen_2.5_model / src /modules /plugins /redact-config.spec.ts
Muhammad Noman
Deploy OpenWA to Hugging Face Spaces
46252cd
Raw
History Blame Contribute Delete
13 kB
import { SECRET_SENTINEL, redactSecretConfig, restoreSecretConfig } from './redact-config';
import type { PluginConfigSchema } from '../../core/plugins/plugin.interfaces';
// plugin config (incl. fields a plugin marks `secret`, e.g. an API key) was returned
// verbatim by the GET routes, readable by any key. Redact on read; restore on write so the
// dashboard PUTting the masked value back doesn't overwrite the real secret.
const schema: PluginConfigSchema = {
type: 'object',
properties: {
apiKey: { type: 'string', secret: true },
endpoint: { type: 'string' },
},
};
describe('redactSecretConfig', () => {
it('masks secret-flagged non-empty values, leaves non-secret fields intact', () => {
expect(redactSecretConfig({ apiKey: 's3cr3t', endpoint: 'https://x' }, schema)).toEqual({
apiKey: SECRET_SENTINEL,
endpoint: 'https://x',
});
});
it('does not mask an empty/absent secret (so "***" never implies a secret that is not set)', () => {
expect(redactSecretConfig({ apiKey: '', endpoint: 'https://x' }, schema)).toEqual({
apiKey: '',
endpoint: 'https://x',
});
expect(redactSecretConfig({ endpoint: 'https://x' }, schema)).toEqual({ endpoint: 'https://x' });
});
it('fails closed and masks every value when there is no schema (cannot tell which fields are secret)', () => {
const cfg = { apiKey: 's3cr3t', endpoint: 'https://x' };
const out = redactSecretConfig(cfg, undefined);
expect(out).toEqual({ apiKey: SECRET_SENTINEL, endpoint: SECRET_SENTINEL });
expect(out).not.toBe(cfg); // copy, not the same ref
});
it('round-trips a schemaless config: sentinel values restore the stored value, edited values persist', () => {
const masked = redactSecretConfig({ apiKey: 's3cr3t', region: 'us' }, undefined);
const restored = restoreSecretConfig({ ...masked, region: 'eu' }, { apiKey: 's3cr3t', region: 'us' }, undefined);
expect(restored).toEqual({ apiKey: 's3cr3t', region: 'eu' });
});
});
describe('restoreSecretConfig', () => {
it('keeps the existing stored secret when the incoming value is the sentinel (unchanged round-trip)', () => {
const merged = restoreSecretConfig(
{ apiKey: SECRET_SENTINEL, endpoint: 'https://new' },
{ apiKey: 'real-secret' },
schema,
);
expect(merged).toEqual({ apiKey: 'real-secret', endpoint: 'https://new' });
});
it('stores a genuinely new secret value', () => {
const merged = restoreSecretConfig({ apiKey: 'brand-new' }, { apiKey: 'real-secret' }, schema);
expect(merged.apiKey).toBe('brand-new');
});
it('drops a sentinel/empty secret when there is nothing stored to keep', () => {
expect(restoreSecretConfig({ apiKey: SECRET_SENTINEL }, {}, schema)).not.toHaveProperty('apiKey');
expect(restoreSecretConfig({ apiKey: '' }, undefined, schema)).not.toHaveProperty('apiKey');
});
});
// v0.7 richer schema: secrets can live inside a nested object or in each row of an array-of-rows,
// so redaction (read) and restoration (write) must recurse — else a nested secret leaks via GET, or
// gets clobbered by the mask on the round-trip PUT.
const nestedSchema: PluginConfigSchema = {
type: 'object',
properties: {
provider: {
type: 'object',
properties: {
apiKey: { type: 'string', secret: true },
region: { type: 'string' },
},
},
endpoints: {
type: 'array',
items: {
type: 'object',
properties: {
url: { type: 'string' },
token: { type: 'string', secret: true },
},
},
},
},
};
describe('redactSecretConfig (nested)', () => {
it('masks a secret nested inside an object', () => {
expect(redactSecretConfig({ provider: { apiKey: 'k', region: 'us' } }, nestedSchema)).toEqual({
provider: { apiKey: SECRET_SENTINEL, region: 'us' },
});
});
it('masks a secret sub-field in every row of an array-of-rows (leaving empty ones untouched)', () => {
expect(
redactSecretConfig(
{
endpoints: [
{ url: 'a', token: 't1' },
{ url: 'b', token: '' },
],
},
nestedSchema,
),
).toEqual({
endpoints: [
{ url: 'a', token: SECRET_SENTINEL },
{ url: 'b', token: '' },
],
});
});
});
describe('restoreSecretConfig (nested)', () => {
it('restores a nested-object secret when the incoming value is the sentinel', () => {
expect(
restoreSecretConfig(
{ provider: { apiKey: SECRET_SENTINEL, region: 'eu' } },
{ provider: { apiKey: 'real', region: 'us' } },
nestedSchema,
),
).toEqual({ provider: { apiKey: 'real', region: 'eu' } });
});
it('restores per-row array-of-rows secrets, keeping genuinely-new ones', () => {
expect(
restoreSecretConfig(
{
endpoints: [
{ url: 'a', token: SECRET_SENTINEL },
{ url: 'b', token: 'new' },
],
},
{
endpoints: [
{ url: 'a', token: 'real1' },
{ url: 'b', token: 'real2' },
],
},
nestedSchema,
),
).toEqual({
endpoints: [
{ url: 'a', token: 'real1' },
{ url: 'b', token: 'new' },
],
});
});
// Rows are matched to the stored secret by their non-secret content, NOT by array index — so
// adding/removing/reordering a row can never bind a sentinel to a different row's stored secret.
it('restores by content after a row is removed (no positional drift)', () => {
expect(
restoreSecretConfig(
{ endpoints: [{ url: 'b', token: SECRET_SENTINEL }] },
{
endpoints: [
{ url: 'a', token: 'real_a' },
{ url: 'b', token: 'real_b' },
],
},
nestedSchema,
),
).toEqual({ endpoints: [{ url: 'b', token: 'real_b' }] });
});
it('restores by content after rows are reordered', () => {
expect(
restoreSecretConfig(
{
endpoints: [
{ url: 'b', token: SECRET_SENTINEL },
{ url: 'a', token: SECRET_SENTINEL },
],
},
{
endpoints: [
{ url: 'a', token: 'real_a' },
{ url: 'b', token: 'real_b' },
],
},
nestedSchema,
),
).toEqual({
endpoints: [
{ url: 'b', token: 'real_b' },
{ url: 'a', token: 'real_a' },
],
});
});
it('does not mis-target a secret onto a newly-inserted row', () => {
// New row "NEW" has no stored counterpart → its sentinel is dropped (not filled with a's secret);
// existing row "a" keeps its own secret regardless of the index shift.
expect(
restoreSecretConfig(
{
endpoints: [
{ url: 'NEW', token: SECRET_SENTINEL },
{ url: 'a', token: SECRET_SENTINEL },
],
},
{ endpoints: [{ url: 'a', token: 'real1' }] },
nestedSchema,
),
).toEqual({
endpoints: [{ url: 'NEW' }, { url: 'a', token: 'real1' }],
});
});
// A row's signature is the row with its secret masked, so editing a NON-secret field changes the
// signature and the content-match misses. On an in-place edit (array length unchanged) the row at
// the same index is the same logical row, so its stored secret must be preserved — losing it on a
// routine rename is silent data loss.
it('keeps a row secret when only its non-secret field changed (same length)', () => {
expect(
restoreSecretConfig(
{ endpoints: [{ url: 'a-renamed', token: SECRET_SENTINEL }] },
{ endpoints: [{ url: 'a', token: 'real' }] },
nestedSchema,
),
).toEqual({ endpoints: [{ url: 'a-renamed', token: 'real' }] });
});
// Two rows with identical non-secret content collide on signature; on an unchanged round-trip both
// secrets must survive (positional fallback), not be dropped to nothing.
it('restores both secrets when two rows share non-secret content (no-op round-trip)', () => {
expect(
restoreSecretConfig(
{
endpoints: [
{ url: 'a', token: SECRET_SENTINEL },
{ url: 'a', token: SECRET_SENTINEL },
],
},
{
endpoints: [
{ url: 'a', token: 'real1' },
{ url: 'a', token: 'real2' },
],
},
nestedSchema,
),
).toEqual({
endpoints: [
{ url: 'a', token: 'real1' },
{ url: 'a', token: 'real2' },
],
});
});
});
// An array whose ITEMS are scalar secrets (e.g. a list of API keys) — every masked element shares the
// signature "***", so content-matching is impossible and restore must align by position. A no-op
// round-trip or an in-place edit must never wipe the stored secrets to null.
const scalarSecretArraySchema: PluginConfigSchema = {
type: 'object',
properties: {
keys: { type: 'array', items: { type: 'string', secret: true } },
},
};
describe('restoreSecretConfig (scalar-secret array)', () => {
it('restores every secret on an unchanged round-trip (no null pollution)', () => {
expect(
restoreSecretConfig(
{ keys: [SECRET_SENTINEL, SECRET_SENTINEL] },
{ keys: ['k1', 'k2'] },
scalarSecretArraySchema,
),
).toEqual({ keys: ['k1', 'k2'] });
});
it('keeps a genuinely-new secret element while restoring the unchanged ones', () => {
expect(
restoreSecretConfig({ keys: [SECRET_SENTINEL, 'brand-new'] }, { keys: ['k1', 'k2'] }, scalarSecretArraySchema),
).toEqual({ keys: ['k1', 'brand-new'] });
});
it('drops a sentinel element that has nothing stored to keep (no null in the array)', () => {
expect(restoreSecretConfig({ keys: [SECRET_SENTINEL] }, { keys: [] }, scalarSecretArraySchema)).toEqual({
keys: [],
});
});
it('preserves stored secrets when a new key is appended (length grows)', () => {
expect(
restoreSecretConfig(
{ keys: [SECRET_SENTINEL, SECRET_SENTINEL, SECRET_SENTINEL, 'brand-new'] },
{ keys: ['k1', 'k2', 'k3'] },
scalarSecretArraySchema,
),
).toEqual({ keys: ['k1', 'k2', 'k3', 'brand-new'] });
});
it('preserves stored secrets when a blank trailing entry is appended (the blank is dropped)', () => {
expect(
restoreSecretConfig(
{ keys: [SECRET_SENTINEL, SECRET_SENTINEL, SECRET_SENTINEL, ''] },
{ keys: ['k1', 'k2', 'k3'] },
scalarSecretArraySchema,
),
).toEqual({ keys: ['k1', 'k2', 'k3'] });
});
it('preserves the remaining stored secrets when the last key is removed (length shrinks)', () => {
expect(
restoreSecretConfig(
{ keys: [SECRET_SENTINEL, SECRET_SENTINEL] },
{ keys: ['k1', 'k2', 'k3'] },
scalarSecretArraySchema,
),
).toEqual({ keys: ['k1', 'k2'] });
});
});
// A composite field (object/array) that is itself marked secret:true must be masked as ONE unit on
// read and restored as a unit on write — recursing would leak its non-secret children in plaintext.
const compositeSecretSchema: PluginConfigSchema = {
type: 'object',
properties: {
creds: { type: 'object', secret: true, properties: { user: { type: 'string' }, pass: { type: 'string' } } },
keys: { type: 'array', secret: true, items: { type: 'string' } },
},
};
describe('redactSecretConfig (composite secret field)', () => {
it('masks a whole secret object so its children never leak', () => {
expect(redactSecretConfig({ creds: { user: 'u', pass: 'p' } }, compositeSecretSchema)).toEqual({
creds: SECRET_SENTINEL,
});
});
it('masks a whole secret array so its entries never leak', () => {
expect(redactSecretConfig({ keys: ['k1', 'k2'] }, compositeSecretSchema)).toEqual({ keys: SECRET_SENTINEL });
});
});
describe('restoreSecretConfig (composite secret field)', () => {
it('restores a secret object from the sentinel instead of clobbering it', () => {
expect(
restoreSecretConfig({ creds: SECRET_SENTINEL }, { creds: { user: 'u', pass: 'p' } }, compositeSecretSchema),
).toEqual({ creds: { user: 'u', pass: 'p' } });
});
it('restores a secret array from the sentinel', () => {
expect(restoreSecretConfig({ keys: SECRET_SENTINEL }, { keys: ['k1', 'k2'] }, compositeSecretSchema)).toEqual({
keys: ['k1', 'k2'],
});
});
it('stores a genuinely-new secret object as a unit', () => {
expect(
restoreSecretConfig(
{ creds: { user: 'x', pass: 'y' } },
{ creds: { user: 'u', pass: 'p' } },
compositeSecretSchema,
),
).toEqual({ creds: { user: 'x', pass: 'y' } });
});
it('drops a sentinel secret object when there is nothing stored', () => {
expect(restoreSecretConfig({ creds: SECRET_SENTINEL }, {}, compositeSecretSchema)).not.toHaveProperty('creds');
});
});