feat: implement branded types for type-level security (Phase 1.2)

Add branded types to prevent mixing plaintext, encrypted, and certificate
values at compile time. This provides an additional layer of type safety
without any runtime cost.

## Changes

### Type System (src/types.ts)
- Add PlaintextValue branded type for user input
- Add EncryptedValue branded type for encrypted data
- Add Base64String branded type for base64-encoded values
- Add PEMCertificate branded type for PEM certificates
- Add constructor functions for each branded type
- Add unwrap() utility for extracting raw strings

### Crypto Module (src/lib/crypto.ts)
- Update parsePublicKeyFromCert() to require PEMCertificate
- Update encryptValue() to accept PlaintextValue, return Base64String
- Update encryptKeyValues() to accept PlaintextValue[], return Base64String[]
- Update validateCertificate() to require PEMCertificate

### Controller API (src/lib/controller.ts)
- Update fetchPublicCertificate() to return PEMCertificate
- Brand certificate at source when fetching from API

### UI Components
- EncryptDialog: Brand user input as PlaintextValue before encryption
- SealingKeysView: Brand certificates as PEMCertificate when parsing

## Benefits

- Zero runtime cost (types erased at compile time)
- Prevents passing plaintext where encrypted expected
- Prevents passing encrypted where plaintext expected
- Self-documenting function signatures
- TypeScript enforces correct value handling

## Verification

- TypeScript: 0 errors
- Linting: 0 errors
- Build: Success (340.20 kB, 93.41 kB gzipped)
- Build time: 3.99s (improved from 4.64s)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
2026-02-11 21:17:45 -05:00
parent 286e88fece
commit fcf0ace106
6 changed files with 618 additions and 21 deletions
@@ -27,7 +27,7 @@ import React from 'react';
import { fetchPublicCertificate, getPluginConfig } from '../lib/controller';
import { encryptKeyValues, parsePublicKeyFromCert } from '../lib/crypto';
import { SealedSecret } from '../lib/SealedSecretCRD';
import { SealedSecretScope,SecretKeyValue } from '../types';
import { PlaintextValue, SealedSecretScope, SecretKeyValue } from '../types';
interface EncryptDialogProps {
open: boolean;
@@ -111,7 +111,7 @@ export function EncryptDialog({ open, onClose }: EncryptDialogProps) {
// 3. Encrypt all values client-side
const encryptResult = encryptKeyValues(
keyResult.value,
validKeyValues.map(kv => ({ key: kv.key, value: kv.value })),
validKeyValues.map(kv => ({ key: kv.key, value: PlaintextValue(kv.value) })),
namespace,
name,
scope
@@ -11,6 +11,7 @@ import forge from 'node-forge';
import { useSnackbar } from 'notistack';
import React from 'react';
import { fetchPublicCertificate, getPluginConfig } from '../lib/controller';
import { PEMCertificate } from '../types';
interface SealingKey {
name: string;
@@ -23,7 +24,7 @@ interface SealingKey {
/**
* Parse certificate dates from TLS secret
*/
function parseCertificateDates(certPem: string): { notBefore?: string; notAfter?: string } {
function parseCertificateDates(certPem: PEMCertificate): { notBefore?: string; notAfter?: string } {
try {
const cert = forge.pki.certificateFromPem(certPem);
return {
@@ -57,7 +58,7 @@ export function SealingKeysView() {
| 'active'
| 'compromised';
const certPem = secret.data?.['tls.crt'] ? atob(secret.data['tls.crt']) : '';
const dates = certPem ? parseCertificateDates(certPem) : {};
const dates = certPem ? parseCertificateDates(PEMCertificate(certPem)) : {};
return {
name: secret.metadata.name!,