ddd628ab6f
IIOS becomes the tenant's integration/credential hub for anything it sends. New IiosProviderCredential (one row per scope+providerType) seals the secret with AES-256-GCM under IIOS_CRED_KEY (secret-crypto.ts); only non-secret displayHints are ever read back. ProviderCredentialService upsert/resolve/status; TwilioSmsProvider resolves the caller's own creds per scope at send time and POSTs to Twilio (fail-closed NOT_CONFIGURED when unset). Registered over the SMS sandbox only when the platform key + store are present. PUT/GET /v1/providers/:type/credentials (scope-fenced, masked reads). 16 new unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
80 lines
3.5 KiB
TypeScript
80 lines
3.5 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { ProviderCredentialService } from './provider-credential.service';
|
|
|
|
const KEY_B64 = Buffer.alloc(32, 7).toString('base64');
|
|
|
|
/** Minimal in-memory stand-in for prisma.iiosProviderCredential (upsert/findUnique). */
|
|
function fakePrisma() {
|
|
const rows = new Map<string, Record<string, unknown>>();
|
|
const k = (scopeId: string, providerType: string) => `${scopeId}::${providerType}`;
|
|
return {
|
|
_rows: rows,
|
|
iiosProviderCredential: {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
async upsert({ where, create, update }: any) {
|
|
const key = k(where.scopeId_providerType.scopeId, where.scopeId_providerType.providerType);
|
|
const existing = rows.get(key);
|
|
const row = existing ? { ...existing, ...update } : { ...create };
|
|
rows.set(key, row);
|
|
return row;
|
|
},
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
async findUnique({ where }: any) {
|
|
return rows.get(k(where.scopeId_providerType.scopeId, where.scopeId_providerType.providerType)) ?? null;
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function make(prisma: ReturnType<typeof fakePrisma>, env: NodeJS.ProcessEnv): ProviderCredentialService {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const s = new ProviderCredentialService(prisma as any);
|
|
s.env = env;
|
|
return s;
|
|
}
|
|
|
|
describe('ProviderCredentialService', () => {
|
|
let prisma: ReturnType<typeof fakePrisma>;
|
|
let svc: ProviderCredentialService;
|
|
|
|
beforeEach(() => {
|
|
prisma = fakePrisma();
|
|
svc = make(prisma, { IIOS_CRED_KEY: KEY_B64 } as NodeJS.ProcessEnv);
|
|
});
|
|
|
|
it('seals the secret at rest — plaintext token never stored', async () => {
|
|
await svc.upsert('scope_1', 'TWILIO_SMS', { accountSid: 'AC123', authToken: 'tok_secret', fromNumber: '+15550001111' });
|
|
const stored = JSON.stringify([...prisma._rows.values()]);
|
|
expect(stored).not.toContain('tok_secret');
|
|
expect(stored).toContain('cipherText');
|
|
});
|
|
|
|
it('resolves back the exact config it sealed', async () => {
|
|
const cfg = { accountSid: 'AC123', authToken: 'tok_secret', fromNumber: '+15550001111' };
|
|
await svc.upsert('scope_1', 'TWILIO_SMS', cfg);
|
|
expect(await svc.resolve('scope_1', 'TWILIO_SMS')).toEqual(cfg);
|
|
});
|
|
|
|
it('status is masked — reveals hints, never the token', async () => {
|
|
await svc.upsert('scope_1', 'TWILIO_SMS', { accountSid: 'AC1234567', authToken: 'tok_secret', fromNumber: '+15550001111' });
|
|
const status = await svc.status('scope_1', 'TWILIO_SMS');
|
|
expect(status).toMatchObject({ configured: true, enabled: true, hints: { fromNumber: '+15550001111', sidLast4: '4567' } });
|
|
expect(JSON.stringify(status)).not.toContain('tok_secret');
|
|
});
|
|
|
|
it('reports not-configured for an unknown scope', async () => {
|
|
expect(await svc.status('nope', 'TWILIO_SMS')).toEqual({ configured: false });
|
|
expect(await svc.resolve('nope', 'TWILIO_SMS')).toBeNull();
|
|
});
|
|
|
|
it('does not resolve a disabled credential', async () => {
|
|
await svc.upsert('scope_1', 'TWILIO_SMS', { accountSid: 'AC123', authToken: 't', fromNumber: '+1' }, { enabled: false });
|
|
expect(await svc.resolve('scope_1', 'TWILIO_SMS')).toBeNull();
|
|
});
|
|
|
|
it('throws when the platform key is absent (fail closed, never store plaintext)', async () => {
|
|
const noKey = make(prisma, {} as NodeJS.ProcessEnv);
|
|
await expect(noKey.upsert('s', 'TWILIO_SMS', { accountSid: 'A', authToken: 't', fromNumber: '+1' })).rejects.toThrow(/IIOS_CRED_KEY/);
|
|
});
|
|
});
|