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
+94
View File
@@ -23,6 +23,100 @@ export type Result<T, E = Error> =
*/
export type AsyncResult<T, E = Error> = Promise<Result<T, E>>;
/**
* Branded types for type-level security
* These prevent mixing sensitive/non-sensitive values at compile time
*/
/** Unique symbol for branding plaintext values */
declare const PlaintextBrand: unique symbol;
/** Unique symbol for branding encrypted values */
declare const EncryptedBrand: unique symbol;
/** Unique symbol for branding base64-encoded values */
declare const Base64Brand: unique symbol;
/** Unique symbol for branding PEM certificates */
declare const PEMCertBrand: unique symbol;
/**
* Plaintext sensitive value (not yet encrypted)
* Must be explicitly created via PlaintextValue()
*/
export type PlaintextValue = string & { readonly [PlaintextBrand]: typeof PlaintextBrand };
/**
* Encrypted value (already encrypted)
* Created by encryption functions
*/
export type EncryptedValue = string & { readonly [EncryptedBrand]: typeof EncryptedBrand };
/**
* Base64-encoded string
* Created by base64 encoding functions
*/
export type Base64String = string & { readonly [Base64Brand]: typeof Base64Brand };
/**
* PEM-encoded certificate
* Created by certificate parsing functions
*/
export type PEMCertificate = string & { readonly [PEMCertBrand]: typeof PEMCertBrand };
/**
* Create a branded plaintext value
* Use this to mark user input as plaintext before encryption
*
* @example
* const secret = PlaintextValue('my-password');
*/
export function PlaintextValue(value: string): PlaintextValue {
return value as PlaintextValue;
}
/**
* Create a branded encrypted value
* This is typically used by encryption functions
*
* @example
* return Ok(EncryptedValue(encryptedString));
*/
export function EncryptedValue(value: string): EncryptedValue {
return value as EncryptedValue;
}
/**
* Create a branded base64 string
*
* @example
* return Ok(Base64String(encoded));
*/
export function Base64String(value: string): Base64String {
return value as Base64String;
}
/**
* Create a branded PEM certificate
*
* @example
* return Ok(PEMCertificate(certPem));
*/
export function PEMCertificate(value: string): PEMCertificate {
return value as PEMCertificate;
}
/**
* Unwrap a branded type to get the raw string
* Use sparingly - only when you need the raw value
*
* @example
* const rawValue = unwrap(plaintextValue);
*/
export function unwrap<T extends string>(value: T): string {
return value;
}
/**
* Helper to create a success result
*