# base-stream-wrappers — Formation Recipe (FvW v8 §38)

**Module ID:** `base-stream-wrappers`
**Module name:** Stream Wrappers
**Lifecycle phase:** formation
**Status:** INTENT ONLY. No code yet.
**Date:** 2026-07-21

---

## 1. Intent (the "why")
We need a robust, secure abstraction layer for handling binary data streams in FreshCloud's email processing pipeline. This module provides:  
- Binary-safe file operations for MIME processing  
- Temporary file lifecycle management with automatic cleanup  
- Cross-platform line ending normalization  
- Stream multiplexing for composite resources  
All while maintaining strict isolation between users and accounts to prevent data leaks. This is a foundational layer for all email data operations in FreshCloud.

## 2. User-facing behaviour
| Actor           | Supported Operations                                                                 |
|-----------------|--------------------------------------------------------------------------------------|
| Other modules   | Open streams via `fswrapper://` schemes, read/write data, seek positions, close handles |
| FreshCloud core | Stream wrapper registration, configuration injection, cleanup commands                |
| Users           | Indirectly benefits from safe email data processing with zero leaks or artifacts     |

## 3. Public contract (data shapes + signatures)
```typescript
namespace FreshCloud\Mail\StreamWrappers;

interface IStreamWrapper {
  // Core operations
  stream_open(path: string, mode: string, options: int, opened_path: string): bool;
  stream_read(count: int): string;
  stream_write(data: string): int;
  stream_seek(offset: int, whence: int = SEEK_SET): bool;
  stream_tell(): int;
  stream_eof(): bool;
  stream_close(): void;

  // Metadata
  stream_stat(): array;
  stream_cast(as_type: int): resource|null;
}

interface IStreamFilter {
  filter(in: string, closing: bool): string;
}

class LineEndingsFilter implements IStreamFilter {
  static register(): void;
  filter(in: string, closing: bool): string;
}

class TempFileWrapper implements IStreamWrapper {
  create(): string; // Returns temp file path
  unlink(): void;
}

// Scheme constants
const SCHEME_BINARY = 'freshcloud-bin';
const SCHEME_TEMP = 'freshcloud-temp';
const SCHEME_LITERAL = 'freshcloud-literal';
const SCHEME_SUBSTREAMS = 'freshcloud-composite';
```

## 4. Persistence (what gets stored where)
- **Temporary files**: Local filesystem in `/var/tmp/freshcloud/streams/`  
  - Path pattern: `temp_[user_id]_[account_id]_[unique_hash]`  
  - Automatic cleanup on script termination or when refcount=0  
- **Audit logs**: `email_audit` table (if audit sweep enabled)  
  - Records: `stream_open`, `stream_close`, `stream_write` events  
- **No persistent state**: All stream data is ephemeral, never saved to PocketBase  

## 5. Dependencies (what this module uses, what uses this)
### Dependencies
- **Runtime**: PHP 8.0+ stream API, mbstring extension  
- **FreshCloud modules**:  
  - `core:logger` (for trace logging)  
  - `core:security` (for temp file naming)  
- **External**: None (pure PHP runtime)  

### Dependent modules
- `protocol:imap`, `protocol:smtp` (for MIME stream handling)  
- `storage:cache` (temp file lifecycle)  
- `renderer:mime` (line normalization)  

## 6. Behaviour inventory (the N public methods)
| Method                 | Signature                 | Purpose                             | Side Effects         | Errors                         |
|------------------------|---------------------------|-------------------------------------|----------------------|--------------------------------|
| `stream_open`          | `(path, mode, options)`   | Initialize stream from URI          | Temp file creation   | `invalid_scheme`, `permission_denied` |
| `stream_read`          | `(count)`                 | Read data from current position     | Buffer updates       | `eof_reached`, `invalid_state` |
| `stream_write`         | `(data)`                  | Write data to current position      | File modification    | `readonly_stream`, `disk_full` |
| `stream_seek`          | `(offset, whence)`        | Change read/write position          | Position update      | `invalid_offset`, `closed_stream` |
| `stream_tell`          | `(): int`                 | Get current position                | None                 | `closed_stream`                |
| `stream_close`         | `(): void`                | Release resources                   | File deletion (temp) | `already_closed`               |
| `stream_stat`          | `(): array`               | Get metadata (size, mod time etc.)   | None                 | `not_implemented`              |
| `LineEndingsFilter::register` | `(): void`         | Register line-ending filter         | Global registry update | `already_registered`          |

## 7. State machine
```
[INIT] → [OPENED] → [READING/WRITING] → [EOF] → [CLOSED]
   ↑            ↓            ↓           ↑         ↑
   └────────────┴────────────┴─────────────┘
      (register) (open)     (seek/write)     (read)
```
- **INIT**: Wrapper registered but not opened  
- **OPENED**: Stream handle active  
- **READING/WRITING**: Data operations in progress  
- **EOF**: End of file reached  
- **CLOSED**: Resources released  

Transitions:  
- `stream_open()` → OPENED (if valid scheme)  
- `stream_read()`/`stream_write()` → READING/WRITING  
- `stream_tell()`+position >= size → EOF  
- `stream_close()` → CLOSED (final state)  

## 8. Side effects
### Reads
- Temporary file system reads  
- Stream buffer reads (in-memory cache)  

### Writes
- Temporary file writes  
- Audit log writes (if enabled)  
- Stream buffer writes  

### Deletes
- Temporary file deletion on close/unlink  
- Registry cleanup on deregister  

### Emits
- `trace_log`: Stream operation events  
- `email_audit`: Audit events (if enabled)  

## 9. Input validation
| Input         | Validation Rule                                                                 | Fallback if Invalid                     |
|---------------|---------------------------------------------------------------------------------|------------------------------------------|
| Stream path   | Must match scheme regex: `^freshcloud-(bin|temp|literal|composite)://.+$`       | Throw `invalid_scheme` exception        |
| Write data    | Max 10MB per write chunk                                                        | Truncate with warning                    |
| Seek offset   | Must be >= 0 and ≤ file size (when SEEK_END)                                    | Clamp to valid range                     |
| Encoding      | Must be supported by mbstring                                                   | Default to UTF-8                        |
| Temp dir      | Must be writable by web server                                                 | Fallback to system temp dir             |

## 10. Failure modes
| Failure                     | Handling                                  | Caller Sees                     |
|-----------------------------|-------------------------------------------|----------------------------------|
| Disk full during write      | Rollback partial writes, close stream     | `IOException` with code 28      |
| Permission denied           | Close all handles, log warning             | `PermissionDeniedException`      |
| Invalid stream scheme       | Return false from stream_open             | `invalid_scheme` PHP error       |
| Read beyond EOF             | Return empty string, position unchanged    | No exception (normal read behavior) |
| Stream already closed       | Log error, return false                   | PHP warning, false return        |

## 11. User simulation
### Scenario 1: Binary attachment download
**Given** a user opens `freshcloud-bin://attachments/user_123/456/abc.bin`  
**When** they read 1024 bytes  
**Then** exact binary data returned without modification  

### Scenario 2: Temporary file for email draft
**Given** a user creates a temp file `freshcloud-temp://drafts/user_456`  
**When** writing MIME data to it  
**Then** data persists until closed/timeout  

### Scenario 3: Cross-platform email processing
**Given** an email with mixed CRLF/LF line endings  
**When** processed through LineEndingsFilter  
**Then** normalized to consistent LF  

### Scenario 4: Composite stream processing
**Given** multiple email parts to process  
**When** opened as `freshcloud-composite://part1,part2,part3`  
**Then** stream reads concatenated content seamlessly  

### Scenario 5: Multi-user isolation
**Given** two users uploading files  
**When** both create `freshcloud-temp://uploads`  
**Then** distinct files with user-specific paths  

### Scenario 6: Audit logging
**Given** audit enabled for user_789  
**When** they open `freshcloud-bin://inbox`  
**Then** event logged in `email_audit` table  

## 12. Reproducibility test
1. Install PHP 8.0+ with mbstring  
2. Set `open_basedir=/tmp/freshcloud` in php.ini  
3. Run `composer require freshcloud/core`  
4. Register wrapper in module index: `StreamWrappers\Binary::register()`  
5. Open test file: `$handle = fopen('freshcloud-bin://test.bin', 'rb')`  
6. Read data: `$data = fread($handle, 1024)`  
7. Write data: `fwrite($handle, 'test')`  
8. Close stream: `fclose($handle)`  
9. Verify temp file deleted automatically  

## 13. Substitutability (FvW v8 §37.4)
A drop-in alternative must provide:  
1. Same 5 stream schemes (`freshcloud-bin://`, `freshcloud-temp://`, etc.)  
2. Identical method signatures for IStreamWrapper/IStreamFilter  
3. Same state machine transitions  
4. Equivalent audit event schema (if enabled)  
5. Compatible temp file lifecycle  
Optional substitutions:  
- Alternate line ending normalization logic  
- Different temporary file directory strategy  

## 14. Invariants (the 5 things that must always hold)
1. **User Isolation**: No two streams from different (user, account) pairs can share the same temp file  
2. **Atomicity**: Stream writes must complete fully or not at all (no partial corrupt files)  
3. **Encoding Safety**: Binary data must never be mangled by character encoding assumptions  
4. **Resource Cleanup**: All streams must release handles/files on object destruction  
5. **Scheme Security**: Only FreshCloud-registered schemes (`freshcloud-*`) are operable  

## 15. Sweep fields applied (the 14 design intent fields)
### Sweep 1: Multi-user + PocketBase
- **user_scoping**: Per-user temp file paths using `user_id` in filename  
- **pb_integration**: None - operates below data plane (no PB API calls)  
- **auth_boundary**: Auth-agnostic (no user context passed to wrapper functions)  
- **state_isolation**: File-scoped temp files with refcounting for lifecycle  
- **logout_behaviour**: No action - streams auto-cleanup on process exit  
- **multi_account**: Account-specific path segments in temp file names  

### Sweep 2: World-class Abilities
- **connection_pool**: ✗ (Not applicable - no network connections)  
- **offline_cache**: ✓ Temp files serve as offline cache for MIME parts  
- **sieve_integration**: ✗ (Operates below Sieve layer)  
- **audit_logging**: ✓ Optional audit events to `email_audit` table  
- **ux_primitives**: ✗ (No user-visible behaviors)  

### Sweep 3: Migration + Observability + Tests
- **migration**: ✓ Zero migration - pure internal mechanism  
- **observability**: ✓ Stream operation counters and trace logging  
- **tests**: ✓ PHPUnit tests for all wrapper behaviors + edge cases  

## 16. Cross-references
- **FvW v8 §38**: Formation recipe lifecycle  
- **FvW v8 §37**: Fragment publishable contract  
- **FvW v8 §19**: Recipe extraction from legacy code  
- **Module**: `protocol:imap` (consumer of binary streams)  
- **Module**: `storage:cache` (temp file lifecycle management)  

## 17. What we are intentionally NOT doing (deferred to future)
1. **SubStreams wrapper** - MailSo had it but we defer until composite MIME parts needed  
2. **Network stream schemes** - No `http://` or `ftp://` wrappers (security boundary)  
3. **Persistent stream caching** - Temp files are ephemeral only  
4. **Async I/O** - Synchronous only (defer to ReactPHP in protocol layer)  
5. **Stream compression** - Not in this phase (defer to dedicated compression module)  
6. **PHP session integration** - Stateless design  
7. **AGPL legacy code** - Clean-room implementation only  
8. **GUI for streams** - No admin UI for stream management  
9. **Stream validation** - No MIME-type checking (defer to parser layer)  
10. **Custom error pages** - PHP stream errors only  
11. **Stream analytics** - No click tracking for raw streams  
12. **Multi-tenant isolation beyond paths** - No separate containers  
13. **Stream encryption** - Handled at protocol layer (TLS)  
14. **Resource limits enforcement** - Relies on OS limits for simplicity  
15. **Stream pipelining** - Linear read/write only (no buffering optimizations)