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>
55 lines
2.2 KiB
TypeScript
55 lines
2.2 KiB
TypeScript
import { BadRequestException, Body, Controller, Get, Headers, Param, Put } from '@nestjs/common';
|
|
import { SessionVerifier } from '../platform/session.verifier';
|
|
import { ActorResolver, type MessagePrincipal } from '../identity/actor.resolver';
|
|
import { ProviderCredentialService } from './provider-credential.service';
|
|
import { TwilioCredentialsDto } from './provider-credential.dto';
|
|
|
|
const SUPPORTED = new Set(['TWILIO_SMS']);
|
|
|
|
/**
|
|
* Per-scope BYO integration credentials. The caller's attested session decides the scope, so a
|
|
* tenant can only read/write ITS OWN provider credentials. The secret is sealed by the service;
|
|
* GET returns a masked status (never the token). be-crm gates this behind tenant-admin policy.
|
|
*/
|
|
@Controller('v1/providers')
|
|
export class ProviderCredentialController {
|
|
constructor(
|
|
private readonly session: SessionVerifier,
|
|
private readonly actors: ActorResolver,
|
|
private readonly credentials: ProviderCredentialService,
|
|
) {}
|
|
|
|
@Put(':providerType/credentials')
|
|
async put(
|
|
@Param('providerType') providerType: string,
|
|
@Body() body: TwilioCredentialsDto,
|
|
@Headers('authorization') authorization?: string,
|
|
) {
|
|
this.assertSupported(providerType);
|
|
const scope = await this.actors.resolveScope(this.principal(authorization));
|
|
await this.credentials.upsert(scope.id, providerType, {
|
|
accountSid: body.accountSid,
|
|
authToken: body.authToken,
|
|
fromNumber: body.fromNumber,
|
|
});
|
|
return this.credentials.status(scope.id, providerType);
|
|
}
|
|
|
|
@Get(':providerType/credentials')
|
|
async get(@Param('providerType') providerType: string, @Headers('authorization') authorization?: string) {
|
|
this.assertSupported(providerType);
|
|
const scope = await this.actors.resolveScope(this.principal(authorization));
|
|
return this.credentials.status(scope.id, providerType);
|
|
}
|
|
|
|
private assertSupported(providerType: string): void {
|
|
if (!SUPPORTED.has(providerType)) throw new BadRequestException(`unsupported providerType: ${providerType}`);
|
|
}
|
|
|
|
private principal(authorization?: string): MessagePrincipal {
|
|
const token = (authorization ?? '').replace(/^Bearer\s+/i, '');
|
|
if (!token) throw new BadRequestException('Authorization bearer token is required');
|
|
return this.session.verify(token);
|
|
}
|
|
}
|