From ead163ffc30118fd3eb6fabf83fd2c255a1b56d7 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Tue, 27 Jan 2026 05:18:07 +0200 Subject: [PATCH 1/7] feat: update API documentation titles for EnclaveJS packages and add Babel transform support --- docs/api-reference/ast-guard.mdx | 6 +- docs/api-reference/enclave-vm.mdx | 6 +- docs/api-reference/enclavejs-broker.mdx | 2 +- docs/api-reference/enclavejs-client.mdx | 2 +- docs/api-reference/enclavejs-react.mdx | 2 +- docs/api-reference/enclavejs-runtime.mdx | 2 +- docs/api-reference/enclavejs-stream.mdx | 763 ++++++++++ docs/api-reference/enclavejs-types.mdx | 2 +- .../core-libraries/ast-guard/babel-preset.mdx | 229 +++ .../enclave-vm/babel-transform.mdx | 324 ++++ docs/docs.json | 47 +- libs/ast-guard/src/index.ts | 18 +- libs/ast-guard/src/presets/babel.preset.ts | 230 +++ libs/ast-guard/src/presets/index.ts | 9 + .../transforms/import-rewrite.transform.ts | 318 ++++ libs/ast-guard/src/transforms/index.ts | 9 + libs/enclave-vm/package.json | 1 + .../src/__tests__/babel-examples.spec.ts | 422 ++++++ .../src/__tests__/babel-examples.ts | 1299 +++++++++++++++++ .../babel-preset-comprehensive.spec.ts | 916 ++++++++++++ libs/enclave-vm/src/__tests__/babel.spec.ts | 447 ++++++ .../src/__tests__/import-rewrite.spec.ts | 316 ++++ .../src/__tests__/multi-file-babel.spec.ts | 538 +++++++ .../src/__tests__/perf/babel.perf.spec.ts | 534 +++++++ libs/enclave-vm/src/babel/babel-wrapper.ts | 511 +++++++ libs/enclave-vm/src/babel/index.ts | 24 + .../src/babel/multi-file-transform.ts | 469 ++++++ libs/enclave-vm/src/enclave.ts | 75 +- libs/enclave-vm/src/index.ts | 16 + libs/enclave-vm/src/types.ts | 5 +- package.json | 3 +- yarn.lock | 11 +- 32 files changed, 7517 insertions(+), 39 deletions(-) create mode 100644 docs/api-reference/enclavejs-stream.mdx create mode 100644 docs/core-libraries/ast-guard/babel-preset.mdx create mode 100644 docs/core-libraries/enclave-vm/babel-transform.mdx create mode 100644 libs/ast-guard/src/presets/babel.preset.ts create mode 100644 libs/ast-guard/src/transforms/import-rewrite.transform.ts create mode 100644 libs/enclave-vm/src/__tests__/babel-examples.spec.ts create mode 100644 libs/enclave-vm/src/__tests__/babel-examples.ts create mode 100644 libs/enclave-vm/src/__tests__/babel-preset-comprehensive.spec.ts create mode 100644 libs/enclave-vm/src/__tests__/babel.spec.ts create mode 100644 libs/enclave-vm/src/__tests__/import-rewrite.spec.ts create mode 100644 libs/enclave-vm/src/__tests__/multi-file-babel.spec.ts create mode 100644 libs/enclave-vm/src/__tests__/perf/babel.perf.spec.ts create mode 100644 libs/enclave-vm/src/babel/babel-wrapper.ts create mode 100644 libs/enclave-vm/src/babel/index.ts create mode 100644 libs/enclave-vm/src/babel/multi-file-transform.ts diff --git a/docs/api-reference/ast-guard.mdx b/docs/api-reference/ast-guard.mdx index 4a62dcd..b5693a8 100644 --- a/docs/api-reference/ast-guard.mdx +++ b/docs/api-reference/ast-guard.mdx @@ -1,9 +1,9 @@ --- -title: 'ast-guard API' -description: 'Complete API reference for the ast-guard package' +title: 'ast-guard' +description: 'API reference for the ast-guard package' --- -Complete API reference for the `ast-guard` package. +API reference for the `ast-guard` package. ## Installation diff --git a/docs/api-reference/enclave-vm.mdx b/docs/api-reference/enclave-vm.mdx index 54c8f98..5b9ed77 100644 --- a/docs/api-reference/enclave-vm.mdx +++ b/docs/api-reference/enclave-vm.mdx @@ -1,9 +1,9 @@ --- -title: 'enclave-vm API' -description: 'Complete API reference for the enclave-vm package' +title: 'enclave-vm' +description: 'API reference for the enclave-vm package' --- -Complete API reference for the `enclave-vm` package. +API reference for the `enclave-vm` package. ## Installation diff --git a/docs/api-reference/enclavejs-broker.mdx b/docs/api-reference/enclavejs-broker.mdx index 8fb8635..5df9bc9 100644 --- a/docs/api-reference/enclavejs-broker.mdx +++ b/docs/api-reference/enclavejs-broker.mdx @@ -1,5 +1,5 @@ --- -title: '@enclavejs/broker API' +title: '@enclavejs/broker' description: 'API reference for the EnclaveJS broker package' --- diff --git a/docs/api-reference/enclavejs-client.mdx b/docs/api-reference/enclavejs-client.mdx index 1629eff..fed619d 100644 --- a/docs/api-reference/enclavejs-client.mdx +++ b/docs/api-reference/enclavejs-client.mdx @@ -1,5 +1,5 @@ --- -title: '@enclavejs/client API' +title: '@enclavejs/client' description: 'API reference for the EnclaveJS client SDK' --- diff --git a/docs/api-reference/enclavejs-react.mdx b/docs/api-reference/enclavejs-react.mdx index 92ce440..443bb71 100644 --- a/docs/api-reference/enclavejs-react.mdx +++ b/docs/api-reference/enclavejs-react.mdx @@ -1,5 +1,5 @@ --- -title: '@enclavejs/react API' +title: '@enclavejs/react' description: 'API reference for the EnclaveJS React SDK' --- diff --git a/docs/api-reference/enclavejs-runtime.mdx b/docs/api-reference/enclavejs-runtime.mdx index f9d2369..e4e2bc6 100644 --- a/docs/api-reference/enclavejs-runtime.mdx +++ b/docs/api-reference/enclavejs-runtime.mdx @@ -1,5 +1,5 @@ --- -title: '@enclavejs/runtime API' +title: '@enclavejs/runtime' description: 'API reference for the EnclaveJS runtime package' --- diff --git a/docs/api-reference/enclavejs-stream.mdx b/docs/api-reference/enclavejs-stream.mdx new file mode 100644 index 0000000..82f7ab0 --- /dev/null +++ b/docs/api-reference/enclavejs-stream.mdx @@ -0,0 +1,763 @@ +--- +title: '@enclavejs/stream' +description: 'API reference for the EnclaveJS streaming protocol implementation' +--- + +API reference for the `@enclavejs/stream` package - streaming protocol implementation including NDJSON parsing, encryption, and reconnection handling. + +## Installation + +```bash +npm install @enclavejs/stream +``` + +## NDJSON Parsing + +### serializeEvent(event) + +Serialize an event to NDJSON format (single line). + +```ts +function serializeEvent(event: MaybeEncrypted): string +``` + +**Example:** +```ts +import { serializeEvent } from '@enclavejs/stream'; + +const line = serializeEvent({ + type: 'stdout', + sessionId: 'sess_123', + seq: 1, + payload: { data: 'Hello' } +}); +// '{"type":"stdout","sessionId":"sess_123","seq":1,"payload":{"data":"Hello"}}' +``` + +### serializeEvents(events) + +Serialize multiple events to NDJSON format. + +```ts +function serializeEvents(events: MaybeEncrypted[]): string +``` + +**Example:** +```ts +import { serializeEvents } from '@enclavejs/stream'; + +const ndjson = serializeEvents([event1, event2, event3]); +// Each event on its own line +``` + +### parseLine(line) + +Parse a single NDJSON line into an event. + +```ts +function parseLine(line: string): ParseResult + +type ParseResult = + | { success: true; data: T } + | { success: false; error: string; line: string }; +``` + +**Example:** +```ts +import { parseLine } from '@enclavejs/stream'; + +const result = parseLine('{"type":"stdout","sessionId":"sess_123","seq":1,"payload":{"data":"Hi"}}'); +if (result.success) { + console.log(result.data.type); // 'stdout' +} +``` + +### parseLines(data) + +Parse multiple NDJSON lines into events. + +```ts +function parseLines(data: string): { + events: (ParsedStreamEvent | ParsedEncryptedEnvelope)[]; + errors: Array<{ line: number; error: string; content: string }>; +} +``` + +**Example:** +```ts +import { parseLines } from '@enclavejs/stream'; + +const { events, errors } = parseLines(ndjsonData); +console.log(`Parsed ${events.length} events, ${errors.length} errors`); +``` + +### NdjsonStreamParser + +Incremental NDJSON parser for streaming data. Handles partial lines across chunks. + +```ts +class NdjsonStreamParser { + constructor(options: { + onEvent: (event: ParsedStreamEvent | ParsedEncryptedEnvelope) => void; + onError: (error: { line: number; error: string; content: string }) => void; + }); + + feed(chunk: string): void; + flush(): void; + reset(): void; + getLineNumber(): number; + hasPendingData(): boolean; +} +``` + +**Example:** +```ts +import { NdjsonStreamParser } from '@enclavejs/stream'; + +const parser = new NdjsonStreamParser({ + onEvent: (event) => console.log('Event:', event.type), + onError: (error) => console.error('Parse error:', error.error), +}); + +// Feed chunks as they arrive +parser.feed('{"type":"stdout"'); +parser.feed(',"sessionId":"sess_123","seq":1,"payload":{"data":"Hi"}}\n'); + +// Flush any remaining data when stream ends +parser.flush(); +``` + +### createNdjsonParseStream() + +Create a transform stream that parses NDJSON. Works with browser `fetch()` and Node.js streams. + +```ts +function createNdjsonParseStream(): TransformStream +``` + +**Example:** +```ts +import { createNdjsonParseStream } from '@enclavejs/stream'; + +const response = await fetch('/api/stream'); +const reader = response.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(createNdjsonParseStream()) + .getReader(); + +while (true) { + const { done, value } = await reader.read(); + if (done) break; + console.log('Event:', value.type); +} +``` + +### createNdjsonSerializeStream() + +Create a transform stream that serializes events to NDJSON. + +```ts +function createNdjsonSerializeStream(): TransformStream, string> +``` + +### parseNdjsonStream(stream) + +Async generator that parses NDJSON from a ReadableStream. + +```ts +async function* parseNdjsonStream( + stream: ReadableStream +): AsyncGenerator +``` + +**Example:** +```ts +import { parseNdjsonStream } from '@enclavejs/stream'; + +const response = await fetch('/api/stream'); + +for await (const event of parseNdjsonStream(response.body)) { + console.log('Event:', event.type); +} +``` + +## ECDH Key Exchange + +### generateKeyPair(curve?) + +Generate an ephemeral ECDH key pair. + +```ts +async function generateKeyPair(curve?: SupportedCurve): Promise + +interface EcdhKeyPair { + publicKey: CryptoKey; + privateKey: CryptoKey; +} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `curve` | `SupportedCurve` | `'P-256'` | Elliptic curve to use | + +**Example:** +```ts +import { generateKeyPair } from '@enclavejs/stream'; + +const keyPair = await generateKeyPair(); +// Or with specific curve +const keyPair384 = await generateKeyPair('P-384'); +``` + +### exportPublicKey(publicKey) + +Export a public key to base64 format. + +```ts +async function exportPublicKey(publicKey: CryptoKey): Promise + +interface SerializedPublicKey { + publicKeyB64: string; + curve: SupportedCurve; +} +``` + +### importPublicKey(publicKeyB64, curve?) + +Import a public key from base64 format. + +```ts +async function importPublicKey( + publicKeyB64: string, + curve?: SupportedCurve +): Promise +``` + +### deriveSharedSecret(privateKey, peerPublicKey) + +Derive shared secret from private key and peer's public key. + +```ts +async function deriveSharedSecret( + privateKey: CryptoKey, + peerPublicKey: CryptoKey +): Promise +``` + +**Example:** +```ts +import { generateKeyPair, deriveSharedSecret, importPublicKey } from '@enclavejs/stream'; + +// Client side +const clientKeyPair = await generateKeyPair(); +const serverPubKey = await importPublicKey(serverPublicKeyB64); +const sharedSecret = await deriveSharedSecret(clientKeyPair.privateKey, serverPubKey); +``` + +### createClientHello(keyPair) + +Create a client hello message for the encryption handshake. + +```ts +async function createClientHello(keyPair: EcdhKeyPair): Promise +``` + +### createServerHello(keyPair, keyId) + +Create a server hello message for the encryption handshake. + +```ts +async function createServerHello(keyPair: EcdhKeyPair, keyId: string): Promise +``` + +### processClientHello(clientHello) + +Process a client hello and generate server response. + +```ts +async function processClientHello(clientHello: ClientHello): Promise<{ + serverKeyPair: EcdhKeyPair; + peerPublicKey: CryptoKey; + serverHello: ServerHello; + keyId: string; +}> +``` + +### processServerHello(serverHello) + +Process a server hello and extract peer's public key. + +```ts +async function processServerHello(serverHello: ServerHello): Promise<{ + peerPublicKey: CryptoKey; + keyId: string; +}> +``` + +### EcdhError + +Error class for ECDH operations. + +```ts +class EcdhError extends Error { + readonly code: string; + constructor(message: string, code: string); +} +``` + +## Key Derivation (HKDF) + +### deriveKey(sharedSecret, salt, info, keyLength?) + +Derive a key using HKDF-SHA256. + +```ts +async function deriveKey( + sharedSecret: Uint8Array, + salt: Uint8Array | null, + info: string, + keyLength?: number +): Promise +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `sharedSecret` | `Uint8Array` | Required | Shared secret from ECDH | +| `salt` | `Uint8Array \| null` | `null` | Optional salt (defaults to zeros) | +| `info` | `string` | Required | Context info string | +| `keyLength` | `number` | `32` | Output key length in bytes | + +### deriveSessionKeys(sharedSecret, sessionId) + +Derive session keys for bidirectional communication. + +```ts +async function deriveSessionKeys( + sharedSecret: Uint8Array, + sessionId: string +): Promise<{ + clientToServerKey: Uint8Array; + serverToClientKey: Uint8Array; +}> +``` + +### importAesGcmKey(keyBytes) + +Import raw key bytes as a CryptoKey for AES-GCM. + +```ts +async function importAesGcmKey(keyBytes: Uint8Array): Promise +``` + +### deriveSessionCryptoKeys(sharedSecret, sessionId) + +Derive and import session keys as CryptoKeys. + +```ts +async function deriveSessionCryptoKeys( + sharedSecret: Uint8Array, + sessionId: string +): Promise<{ + clientToServerKey: CryptoKey; + serverToClientKey: CryptoKey; +}> +``` + +**Example:** +```ts +import { deriveSessionCryptoKeys, deriveSharedSecret } from '@enclavejs/stream'; + +const sharedSecret = await deriveSharedSecret(privateKey, peerPublicKey); +const { clientToServerKey, serverToClientKey } = await deriveSessionCryptoKeys( + sharedSecret, + 'sess_123' +); +``` + +### HkdfError + +Error class for HKDF operations. + +```ts +class HkdfError extends Error { + readonly code: string; + constructor(message: string, code: string); +} +``` + +## AES-GCM Encryption + +### encrypt(key, plaintext, nonce, additionalData?) + +Encrypt data using AES-GCM. + +```ts +async function encrypt( + key: CryptoKey, + plaintext: Uint8Array, + nonce: Uint8Array, + additionalData?: Uint8Array +): Promise +``` + +### decrypt(key, ciphertext, nonce, additionalData?) + +Decrypt data using AES-GCM. + +```ts +async function decrypt( + key: CryptoKey, + ciphertext: Uint8Array, + nonce: Uint8Array, + additionalData?: Uint8Array +): Promise +``` + +### encryptJson(key, keyId, data, nonce?) + +Encrypt a JSON object and create an encrypted envelope payload. + +```ts +async function encryptJson( + key: CryptoKey, + keyId: string, + data: unknown, + nonce?: Uint8Array +): Promise +``` + +### decryptJson(key, payload) + +Decrypt an encrypted envelope payload and parse as JSON. + +```ts +async function decryptJson( + key: CryptoKey, + payload: EncryptedEnvelopePayload +): Promise +``` + +### createEncryptedEnvelope(key, keyId, sessionId, seq, innerEvent, nonce?) + +Create an encrypted envelope from an event. + +```ts +async function createEncryptedEnvelope( + key: CryptoKey, + keyId: string, + sessionId: SessionId, + seq: number, + innerEvent: unknown, + nonce?: Uint8Array +): Promise +``` + +### generateNonce() + +Generate a random 12-byte nonce for AES-GCM. + +```ts +function generateNonce(): Uint8Array +``` + +### generateCounterNonce(prefix, counter) + +Generate a counter-based nonce (8 bytes prefix + 4 bytes counter). + +```ts +function generateCounterNonce(prefix: Uint8Array, counter: bigint): Uint8Array +``` + +### toBase64(bytes) + +Encode bytes to base64. + +```ts +function toBase64(bytes: Uint8Array): string +``` + +### fromBase64(base64) + +Decode base64 to bytes. + +```ts +function fromBase64(base64: string): Uint8Array +``` + +### AesGcmError + +Error class for AES-GCM operations. + +```ts +class AesGcmError extends Error { + readonly code: string; + constructor(message: string, code: string); +} +``` + +## SessionEncryptionContext + +Manages encryption key state for a session. + +```ts +class SessionEncryptionContext { + constructor(key: CryptoKey, keyInfo: SessionKeyInfo); + + // Properties + readonly keyId: string; + + // Methods + needsRotation(): boolean; + encrypt(plaintext: Uint8Array): Promise<{ ciphertext: Uint8Array; nonce: Uint8Array }>; + encryptJson(data: unknown): Promise; + createEnvelope(sessionId: SessionId, seq: number, innerEvent: unknown): Promise; + decrypt(payload: EncryptedEnvelopePayload): Promise; + decryptJson(payload: EncryptedEnvelopePayload): Promise; + getNonceCounter(): bigint; + toKeyInfo(): SessionKeyInfo; + + // Static methods + static fromKeyBytes(keyBytes: Uint8Array, keyId: string): Promise; +} +``` + +**Example:** +```ts +import { SessionEncryptionContext } from '@enclavejs/stream'; + +// Create from key bytes +const ctx = await SessionEncryptionContext.fromKeyBytes(keyBytes, 'key_123'); + +// Encrypt an event +const envelope = await ctx.createEnvelope('sess_123', 1, { + type: 'stdout', + payload: { data: 'Hello' } +}); + +// Check if key rotation is needed +if (ctx.needsRotation()) { + // Perform key rotation +} + +// Decrypt +const decrypted = await ctx.decryptJson(envelope.payload); +``` + +## Reconnection + +### ConnectionState + +Connection state enumeration. + +```ts +const ConnectionState = { + Disconnected: 'disconnected', + Connecting: 'connecting', + Connected: 'connected', + Reconnecting: 'reconnecting', + Failed: 'failed', + Closed: 'closed', +} as const; + +type ConnectionState = (typeof ConnectionState)[keyof typeof ConnectionState]; +``` + +### DEFAULT_RECONNECTION_CONFIG + +Default reconnection configuration. + +```ts +const DEFAULT_RECONNECTION_CONFIG: ReconnectionConfig = { + maxRetries: 5, + initialDelayMs: 1000, + maxDelayMs: 30000, + backoffMultiplier: 2, + jitter: true, + jitterFactor: 0.3, +}; + +interface ReconnectionConfig { + maxRetries: number; + initialDelayMs: number; + maxDelayMs: number; + backoffMultiplier: number; + jitter: boolean; + jitterFactor: number; +} +``` + +### ReconnectionStateMachine + +Manages connection state and automatic reconnection. + +```ts +class ReconnectionStateMachine { + constructor(options: { + config?: Partial; + onEvent: (event: ReconnectionEvent) => void; + }); + + getState(): ConnectionState; + getRetryCount(): number; + connect(): void; + onConnected(): void; + onDisconnected(reason?: string): void; + onFatalError(reason: string): void; + close(): void; + reset(): void; + canReconnect(): boolean; +} + +type ReconnectionEvent = + | { type: 'state_change'; state: ConnectionState; previousState: ConnectionState } + | { type: 'retry_scheduled'; attempt: number; delayMs: number } + | { type: 'retry_started'; attempt: number } + | { type: 'connected' } + | { type: 'disconnected'; reason?: string } + | { type: 'failed'; reason: string }; +``` + +**Example:** +```ts +import { ReconnectionStateMachine, ConnectionState } from '@enclavejs/stream'; + +const reconnect = new ReconnectionStateMachine({ + config: { maxRetries: 3 }, + onEvent: (event) => { + switch (event.type) { + case 'state_change': + console.log(`State: ${event.previousState} -> ${event.state}`); + break; + case 'retry_scheduled': + console.log(`Retry ${event.attempt} in ${event.delayMs}ms`); + break; + case 'connected': + console.log('Connected!'); + break; + case 'failed': + console.error('Connection failed:', event.reason); + break; + } + }, +}); + +reconnect.connect(); +// ... when connection succeeds +reconnect.onConnected(); +// ... when connection drops +reconnect.onDisconnected('Network error'); +``` + +### SequenceTracker + +Tracks sequence numbers and detects gaps for replay. + +```ts +class SequenceTracker { + constructor(maxGaps?: number); + + receive(seq: number): { gap: boolean; missingStart?: number; missingEnd?: number }; + getLastSeq(): number; + getGaps(): Array<{ start: number; end: number }>; + clearGap(start: number, end: number): void; + hasGaps(): boolean; + reset(): void; +} +``` + +**Example:** +```ts +import { SequenceTracker } from '@enclavejs/stream'; + +const tracker = new SequenceTracker(); + +tracker.receive(1); // { gap: false } +tracker.receive(2); // { gap: false } +tracker.receive(5); // { gap: true, missingStart: 3, missingEnd: 4 } + +console.log(tracker.getGaps()); // [{ start: 3, end: 4 }] +``` + +### EventBuffer + +Buffer for storing events during reconnection. + +```ts +class EventBuffer { + constructor(maxSize?: number); + + add(event: StreamEvent | EncryptedEnvelope): boolean; + getAll(): (StreamEvent | EncryptedEnvelope)[]; + drain(): (StreamEvent | EncryptedEnvelope)[]; + size(): number; + isFull(): boolean; + clear(): void; +} +``` + +**Example:** +```ts +import { EventBuffer } from '@enclavejs/stream'; + +const buffer = new EventBuffer(100); + +buffer.add(event1); +buffer.add(event2); + +// Get all events and clear buffer +const events = buffer.drain(); +``` + +### HeartbeatMonitor + +Monitors heartbeats to detect stale connections. + +```ts +class HeartbeatMonitor { + constructor(options: { timeoutMs: number; onTimeout: () => void }); + + start(): void; + stop(): void; + reset(): void; + onHeartbeat(): void; + getTimeSinceLastHeartbeat(): number; +} +``` + +**Example:** +```ts +import { HeartbeatMonitor } from '@enclavejs/stream'; + +const monitor = new HeartbeatMonitor({ + timeoutMs: 30000, + onTimeout: () => { + console.log('Connection stale, reconnecting...'); + reconnect(); + }, +}); + +monitor.start(); + +// When heartbeat received +monitor.onHeartbeat(); + +// When done +monitor.stop(); +``` + +## Re-exported Types + +This package re-exports all types from `@enclavejs/types`: + +```ts +export * from '@enclavejs/types'; +``` + +See [@enclavejs/types API](/api-reference/enclavejs-types) for the complete type reference. + +## Related + +- [Stream Overview](/enclavejs/stream) - Usage guide +- [@enclavejs/types](/api-reference/enclavejs-types) - Type definitions +- [@enclavejs/client](/api-reference/enclavejs-client) - Client SDK +- [Streaming Protocol](/concepts/streaming-protocol) - Protocol concepts diff --git a/docs/api-reference/enclavejs-types.mdx b/docs/api-reference/enclavejs-types.mdx index ae022b4..a13f5da 100644 --- a/docs/api-reference/enclavejs-types.mdx +++ b/docs/api-reference/enclavejs-types.mdx @@ -1,5 +1,5 @@ --- -title: '@enclavejs/types API' +title: '@enclavejs/types' description: 'Type definitions and Zod schemas for EnclaveJS' --- diff --git a/docs/core-libraries/ast-guard/babel-preset.mdx b/docs/core-libraries/ast-guard/babel-preset.mdx new file mode 100644 index 0000000..11220ad --- /dev/null +++ b/docs/core-libraries/ast-guard/babel-preset.mdx @@ -0,0 +1,229 @@ +--- +title: 'Babel Preset' +description: 'Security preset for Babel transforms inside the enclave sandbox' +--- + +The Babel preset extends the [AgentScript preset](/core-libraries/ast-guard/agentscript-preset) to enable secure TSX/JSX transformation inside the enclave. It adds the `Babel` global while maintaining all AgentScript security guarantees. + +## Overview + +The Babel preset provides: + +- **Babel.transform()** - Transform TSX/JSX to JavaScript +- **Security configs** - Per-level limits for input size, output size, timeouts +- **Preset whitelist** - Only allowed Babel presets can be used +- **Full AgentScript validation** - All blocked constructs remain blocked + +## Basic Usage + +```ts +import { createBabelPreset, JSAstValidator } from 'ast-guard'; + +const validator = new JSAstValidator(createBabelPreset({ + securityLevel: 'STANDARD', +})); + +const result = await validator.validate(` + const tsx = '
Hello
'; + const js = Babel.transform(tsx, { presets: ['react'] }); + return js.code; +`); +``` + +## Security Configurations + +The Babel preset defines security limits per security level: + +```ts +import { getBabelConfig, BABEL_SECURITY_CONFIGS } from 'ast-guard'; + +// Get config for a specific level +const config = getBabelConfig('SECURE'); + +// Or access all configs +console.log(BABEL_SECURITY_CONFIGS); +``` + +### Configuration by Security Level + +| Level | Max Input | Max Output | Timeout | Allowed Presets | +|-------|-----------|------------|---------|-----------------| +| `STRICT` | 100 KB | 500 KB | 5s | `react` | +| `SECURE` | 500 KB | 2 MB | 10s | `typescript`, `react` | +| `STANDARD` | 1 MB | 5 MB | 15s | `typescript`, `react` | +| `PERMISSIVE` | 5 MB | 25 MB | 30s | `typescript`, `react`, `env` | + + + Use `STRICT` for untrusted input where you only need JSX transformation. Use `STANDARD` for typical LLM-generated TypeScript+React code. + + +## Configuration Options + +```ts +interface BabelSecurityConfig { + /** Maximum input code size in bytes */ + maxInputSize: number; + + /** Maximum output code size in bytes */ + maxOutputSize: number; + + /** Transform timeout in milliseconds */ + transformTimeout: number; + + /** Allowed Babel preset names */ + allowedPresets: string[]; +} +``` + +### Default Configurations + +```ts +// STRICT - Minimal, JSX only +{ + maxInputSize: 100 * 1024, // 100 KB + maxOutputSize: 500 * 1024, // 500 KB + transformTimeout: 5000, // 5 seconds + allowedPresets: ['react'], +} + +// SECURE - TypeScript + React +{ + maxInputSize: 500 * 1024, // 500 KB + maxOutputSize: 2 * 1024 * 1024, // 2 MB + transformTimeout: 10000, // 10 seconds + allowedPresets: ['typescript', 'react'], +} + +// STANDARD - Default for most use cases +{ + maxInputSize: 1024 * 1024, // 1 MB + maxOutputSize: 5 * 1024 * 1024, // 5 MB + transformTimeout: 15000, // 15 seconds + allowedPresets: ['typescript', 'react'], +} + +// PERMISSIVE - Extended capabilities +{ + maxInputSize: 5 * 1024 * 1024, // 5 MB + maxOutputSize: 25 * 1024 * 1024, // 25 MB + transformTimeout: 30000, // 30 seconds + allowedPresets: ['typescript', 'react', 'env'], +} +``` + +## Allowed Globals + +The Babel preset adds these globals to the AgentScript allowlist: + +| Global | Description | +|--------|-------------| +| `Babel` | The restricted Babel transform API | +| `__safe_Babel` | Internal transformed version | + +All other [AgentScript allowed globals](/core-libraries/ast-guard/agentscript-preset#default-allowed-globals) remain available. + +## What's Blocked + +The Babel preset inherits all AgentScript security rules: + +- **Dangerous Babel options** - `plugins`, `sourceMaps`, `ast`, `babelrc`, `configFile` +- **Disallowed presets** - Any preset not in the security level's allowlist +- **All AgentScript blocked constructs** - `eval`, `Function`, `process`, etc. + + + Babel plugins are completely blocked because they can execute arbitrary code during transformation. Only presets from the allowlist can be used. + + +## Creating the Preset + +```ts +import { createBabelPreset } from 'ast-guard'; + +// Basic usage - inherits from AgentScript +const rules = createBabelPreset({ + securityLevel: 'STANDARD', +}); + +// With custom globals +const rulesWithGlobals = createBabelPreset({ + securityLevel: 'STANDARD', + allowedGlobals: ['customHelper'], +}); + +// With all AgentScript options +const fullRules = createBabelPreset({ + securityLevel: 'SECURE', + allowedGlobals: ['myGlobal'], + requireCallTool: true, + allowedLoops: { + allowFor: true, + allowForOf: true, + allowWhile: false, + }, +}); +``` + +## Using with Enclave + +The enclave automatically uses the Babel preset when configured: + +```ts +import { Enclave } from 'enclave-vm'; + +const enclave = new Enclave({ + preset: 'babel', // Uses createBabelPreset internally + securityLevel: 'STANDARD', // Determines Babel limits +}); + +// Now Babel.transform is available inside the sandbox +await enclave.run(` + const js = Babel.transform('
', { presets: ['react'] }); + return js.code; +`); +``` + +## API Reference + +### `createBabelPreset(options)` + +Creates validation rules for the Babel preset. + +```ts +function createBabelPreset(options?: BabelPresetOptions): ValidationRule[]; + +interface BabelPresetOptions extends AgentScriptOptions { + // All AgentScriptOptions are supported + // Security level determines Babel limits +} +``` + +### `getBabelConfig(level)` + +Gets Babel security configuration for a security level. + +```ts +function getBabelConfig(level?: SecurityLevel): BabelSecurityConfig; + +// Example +const config = getBabelConfig('SECURE'); +// Returns: { maxInputSize, maxOutputSize, transformTimeout, allowedPresets } +``` + +### `BABEL_SECURITY_CONFIGS` + +Direct access to all security configurations. + +```ts +const BABEL_SECURITY_CONFIGS: Record; + +// Example +const strictConfig = BABEL_SECURITY_CONFIGS.STRICT; +const standardConfig = BABEL_SECURITY_CONFIGS.STANDARD; +``` + +## Related + +- [AgentScript Preset](/core-libraries/ast-guard/agentscript-preset) - Base preset for code validation +- [Security Rules](/core-libraries/ast-guard/security-rules) - Rule reference +- [Babel Transform (enclave-vm)](/core-libraries/enclave-vm/babel-transform) - Using Babel in enclave +- [Security Levels](/core-libraries/enclave-vm/security-levels) - Security profiles diff --git a/docs/core-libraries/enclave-vm/babel-transform.mdx b/docs/core-libraries/enclave-vm/babel-transform.mdx new file mode 100644 index 0000000..ccc7baa --- /dev/null +++ b/docs/core-libraries/enclave-vm/babel-transform.mdx @@ -0,0 +1,324 @@ +--- +title: 'Babel Transform' +description: 'Transform TSX/JSX code to JavaScript inside the secure enclave sandbox' +--- + +The Babel transform feature enables secure TSX/JSX transformation inside the enclave sandbox. This allows LLM-generated React components to be compiled to JavaScript safely, without exposing the host system to code execution risks. + +## Overview + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Enclave Sandbox │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ TSX Input │─────▶│ Babel │─────▶│ JS Output │ │ +│ │ │ │ Transform │ │ │ │ +│ │ │ │ (Isolated) │ │ React.create│ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ │ +│ ┌────────┴────────┐ │ +│ │ Security Layer │ │ +│ │ - Preset allow │ │ +│ │ - Size limits │ │ +│ │ - Timeout │ │ +│ │ - No plugins │ │ +│ └─────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ +``` + +## Quick Start + +```ts +import { Enclave } from 'enclave-vm'; + +// Create enclave with babel preset +const enclave = new Enclave({ + preset: 'babel', + securityLevel: 'STANDARD', +}); + +// Transform TSX inside the sandbox +const result = await enclave.run(` + const tsx = \` + interface Props { name: string; } + const Greeting = ({ name }: Props) =>

Hello, {name}!

; + \`; + + const transformed = Babel.transform(tsx, { + presets: ['typescript', 'react'], + filename: 'Greeting.tsx', + }); + + return transformed.code; +`); + +console.log(result.value); +// Output: const Greeting = ({ name }) => /*#__PURE__*/React.createElement("h1", null, "Hello, ", name, "!"); + +enclave.dispose(); +``` + +## Security Model + +The Babel transform runs in an isolated VM context with multiple security layers: + +| Layer | Protection | Description | +|-------|------------|-------------| +| **Preset Whitelist** | Controlled transforms | Only allowed presets can be used (no arbitrary plugins) | +| **Input Size Limit** | DoS prevention | Maximum source code size varies by security level | +| **Output Size Limit** | Memory protection | Prevents output expansion attacks | +| **Transform Timeout** | Resource control | Prevents infinite compilation loops | +| **Isolated Context** | Sandbox escape | Babel runs without access to fs, process, require | +| **No Plugins** | Code execution | Plugins are completely blocked (they execute arbitrary code) | + +## Configuration + +### Security Levels + +Each security level provides different limits for Babel transforms: + +| Security Level | Max Input | Max Output | Timeout | Allowed Presets | +|----------------|-----------|------------|---------|-----------------| +| `STRICT` | 100 KB | 500 KB | 5s | `react` only | +| `SECURE` | 500 KB | 2 MB | 10s | `typescript`, `react` | +| `STANDARD` | 1 MB | 5 MB | 15s | `typescript`, `react` | +| `PERMISSIVE` | 5 MB | 25 MB | 30s | `typescript`, `react`, `env` | + +```ts +import { Enclave } from 'enclave-vm'; +import { getBabelConfig } from 'ast-guard'; + +// Get config for a security level +const config = getBabelConfig('SECURE'); +console.log(config); +// { +// maxInputSize: 524288, // 500 KB +// maxOutputSize: 2097152, // 2 MB +// transformTimeout: 10000, // 10 seconds +// allowedPresets: ['typescript', 'react'] +// } +``` + +### Creating a Babel Enclave + +```ts +const enclave = new Enclave({ + preset: 'babel', // Enable Babel preset + securityLevel: 'STANDARD', // Choose security level +}); +``` + +## Transform API + +Inside the enclave, the `Babel` global provides a restricted transform API: + +```ts +interface SafeTransformOptions { + filename?: string; // For error messages (sanitized) + presets?: string[]; // Must be in allowed list + sourceType?: 'module' | 'script'; // Default: 'module' +} + +interface SafeTransformResult { + code: string; // Transformed JavaScript +} + +// Usage inside enclave +const result = Babel.transform(code, options); +``` + +### Transform Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `filename` | `string` | `'input.tsx'` | Filename for error messages | +| `presets` | `string[]` | `[]` | Babel presets to apply | +| `sourceType` | `'module' \| 'script'` | `'module'` | How to parse the code | + + + The following Babel options are **blocked** for security: + - `plugins` - Completely blocked (execute arbitrary code) + - `sourceMaps` - Blocked (path leakage) + - `ast` - Blocked (not needed, reduces attack surface) + - `babelrc` / `configFile` - Blocked (no file system access) + + +## Common Use Cases + +### Transform React Components + +```ts +const code = ` + const tsx = \` + const Button = ({ onClick, children }) => ( + + ); + \`; + + return Babel.transform(tsx, { + presets: ['react'], + filename: 'Button.jsx', + }).code; +`; + +const result = await enclave.run(code); +// Result: React.createElement("button", { className: "btn", onClick }, children) +``` + +### Transform TypeScript + JSX + +```ts +const code = ` + const tsx = \` + interface UserCardProps { + user: { name: string; email: string; }; + onEdit?: () => void; + } + + const UserCard = ({ user, onEdit }: UserCardProps) => ( +
+

{user.name}

+

{user.email}

+ {onEdit && } +
+ ); + \`; + + return Babel.transform(tsx, { + presets: ['typescript', 'react'], + filename: 'UserCard.tsx', + }).code; +`; + +const result = await enclave.run(code); +// TypeScript types are stripped, JSX is transformed +``` + +### Tool Integration for Dynamic Components + +```ts +const enclave = new Enclave({ + preset: 'babel', + securityLevel: 'STANDARD', + toolHandler: async (toolName, args) => { + if (toolName === 'component:fetch') { + // Fetch TSX from database, API, or LLM + return `

Content

`; + } + return null; + }, +}); + +const code = ` + // Fetch component TSX from external source + const tsx = await callTool('component:fetch', { title: 'Welcome' }); + + // Transform to JavaScript + const js = Babel.transform(tsx, { + presets: ['react'], + filename: 'Card.jsx', + }).code; + + return js; +`; + +const result = await enclave.run(code); +``` + +## Error Handling + +Babel transform errors are sanitized to prevent path leakage: + +```ts +const code = ` + try { + // Invalid JSX + Babel.transform(' + The Babel context is cached between transforms. The first transform (cold start) takes ~20-50ms, subsequent transforms are much faster. + + +### Performance Tips + +1. **Batch transforms** - Transform multiple components in a single enclave run +2. **Reuse enclave** - Don't create/dispose for each transform +3. **Minimize types** - Complex TypeScript types increase transform time +4. **Use STANDARD level** - Good balance of security and performance + +```ts +// Good: Reuse enclave for multiple transforms +const enclave = new Enclave({ preset: 'babel' }); + +const components = ['Button', 'Card', 'Modal']; +const results = await enclave.run(` + const components = ${JSON.stringify(componentCode)}; + return components.map(tsx => + Babel.transform(tsx, { presets: ['react'] }).code + ); +`); + +enclave.dispose(); +``` + +## Direct API (Outside Enclave) + +For server-side use without the full enclave sandbox, use `createRestrictedBabel`: + +```ts +import { createRestrictedBabel } from 'enclave-vm'; +import { getBabelConfig } from 'ast-guard'; + +const config = getBabelConfig('STANDARD'); +const babel = createRestrictedBabel(config); + +const result = babel.transform(tsxCode, { + presets: ['typescript', 'react'], + filename: 'Component.tsx', +}); + +console.log(result.code); +``` + + + The direct API still runs Babel in an isolated VM context, but doesn't provide the full enclave sandbox features (tool calls, iteration limits, etc.). Use the full enclave for LLM-generated code. + + +## Related + +- [Overview](/core-libraries/enclave-vm/overview) - Enclave introduction +- [Security Levels](/core-libraries/enclave-vm/security-levels) - Security profiles +- [AgentScript Preset](/core-libraries/ast-guard/agentscript-preset) - AST validation for enclave code +- [Tool System](/core-libraries/enclave-vm/tool-system) - Integrating tools with enclave diff --git a/docs/docs.json b/docs/docs.json index a8a42bd..cec5f03 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -90,6 +90,7 @@ "core-libraries/enclave-vm/overview", "core-libraries/enclave-vm/security-levels", "core-libraries/enclave-vm/tool-system", + "core-libraries/enclave-vm/babel-transform", "core-libraries/enclave-vm/worker-pool", "core-libraries/enclave-vm/double-vm", "core-libraries/enclave-vm/ai-scoring", @@ -104,6 +105,7 @@ "core-libraries/ast-guard/overview", "core-libraries/ast-guard/pre-scanner", "core-libraries/ast-guard/agentscript-preset", + "core-libraries/ast-guard/babel-preset", "core-libraries/ast-guard/security-rules", "core-libraries/ast-guard/code-transform", "core-libraries/ast-guard/custom-rules" @@ -134,19 +136,6 @@ "guides/security-hardening" ] }, - { - "group": "API Reference", - "icon": "code", - "pages": [ - "api-reference/enclave-vm", - "api-reference/ast-guard", - "api-reference/enclavejs-types", - "api-reference/enclavejs-broker", - "api-reference/enclavejs-client", - "api-reference/enclavejs-react", - "api-reference/enclavejs-runtime" - ] - }, { "group": "Examples", "icon": "flask", @@ -177,6 +166,38 @@ ] } ] + }, + { + "dropdown": "API Reference", + "icon": "code", + "versions": [ + { + "version": "v2.0 (latest)", + "default": true, + "groups": [ + { + "group": "Core Libraries", + "icon": "cube", + "pages": [ + "api-reference/enclave-vm", + "api-reference/ast-guard" + ] + }, + { + "group": "EnclaveJS Packages", + "icon": "boxes-stacked", + "pages": [ + "api-reference/enclavejs-types", + "api-reference/enclavejs-stream", + "api-reference/enclavejs-broker", + "api-reference/enclavejs-client", + "api-reference/enclavejs-react", + "api-reference/enclavejs-runtime" + ] + } + ] + } + ] } ] } diff --git a/libs/ast-guard/src/index.ts b/libs/ast-guard/src/index.ts index 0eb4db9..7f46918 100644 --- a/libs/ast-guard/src/index.ts +++ b/libs/ast-guard/src/index.ts @@ -97,9 +97,19 @@ export { AGENTSCRIPT_PERMISSIVE_GLOBALS, AGENTSCRIPT_BASE_GLOBALS, // Legacy alias for STRICT getAgentScriptGlobals, + // Babel preset for TSX/JSX transformation + createBabelPreset, + getBabelConfig, + BABEL_SECURITY_CONFIGS, } from './presets'; -export type { PresetOptions, AgentScriptOptions, SecurityLevel } from './presets'; +export type { + PresetOptions, + AgentScriptOptions, + SecurityLevel, + BabelPresetOptions, + BabelSecurityConfig, +} from './presets'; // AgentScript tool descriptions for AI agents export { @@ -117,6 +127,10 @@ export { // Concatenation transformation transformConcatenation, transformTemplateLiterals, + // Import rewriting + rewriteImports, + isValidPackageName, + isValidSubpath, } from './transforms'; export type { @@ -124,6 +138,8 @@ export type { StringExtractionResult, ConcatTransformConfig, ConcatTransformResult, + ImportRewriteConfig, + ImportRewriteResult, } from './transforms'; // Pre-Scanner (Layer 0 Defense) diff --git a/libs/ast-guard/src/presets/babel.preset.ts b/libs/ast-guard/src/presets/babel.preset.ts new file mode 100644 index 0000000..7389d8a --- /dev/null +++ b/libs/ast-guard/src/presets/babel.preset.ts @@ -0,0 +1,230 @@ +/** + * Babel Preset for Enclave + * + * Extends the AgentScript preset with Babel.transform() support. + * Security limits are determined by the security level. + * + * @packageDocumentation + */ + +import { ValidationRule } from '../interfaces'; +import { createAgentScriptPreset, AgentScriptOptions, SecurityLevel } from './agentscript.preset'; + +/** + * Babel configuration per security level + * + * These limits control resource usage for Babel transforms: + * - maxInputSize: Maximum source code size to transform + * - maxOutputSize: Maximum transformed output size + * - transformTimeout: Maximum time for transformation (reserved) + * - allowedPresets: Which Babel presets can be used + * - Multi-file limits (maxFiles, maxTotalInputSize, maxTotalOutputSize) + */ +export interface BabelSecurityConfig { + /** + * Maximum input code size in bytes (single file) + */ + maxInputSize: number; + + /** + * Maximum output code size in bytes (single file) + */ + maxOutputSize: number; + + /** + * Transform timeout in milliseconds + */ + transformTimeout: number; + + /** + * Allowed Babel preset names + */ + allowedPresets: string[]; + + /** + * Maximum number of files for multi-file transform + */ + maxFiles: number; + + /** + * Maximum total input size across all files (bytes) + */ + maxTotalInputSize: number; + + /** + * Maximum total output size across all files (bytes) + */ + maxTotalOutputSize: number; +} + +/** + * Babel security configurations per security level + * + * STRICT: Minimal - only JSX transformation (react preset) + * SECURE: Standard - TypeScript + React + * STANDARD: Standard - TypeScript + React + * PERMISSIVE: Extended - TypeScript + React + env + * + * Multi-file limits per level: + * | Level | Max Files | Max Total Input | Max Total Output | + * |------------|-----------|-----------------|------------------| + * | STRICT | 3 | 200 KB | 1 MB | + * | SECURE | 10 | 1 MB | 5 MB | + * | STANDARD | 25 | 5 MB | 25 MB | + * | PERMISSIVE | 100 | 25 MB | 125 MB | + */ +export const BABEL_SECURITY_CONFIGS: Record = { + /** + * STRICT: Minimal Babel access + * - Small input limit (100KB) + * - Only react preset (JSX transformation) + * - No TypeScript (reduces attack surface) + * - Max 3 files for multi-file transform + */ + STRICT: { + maxInputSize: 100 * 1024, // 100KB + maxOutputSize: 500 * 1024, // 500KB + transformTimeout: 5000, // 5s + allowedPresets: ['react'], // Minimal - JSX only + maxFiles: 3, + maxTotalInputSize: 200 * 1024, // 200KB + maxTotalOutputSize: 1024 * 1024, // 1MB + }, + + /** + * SECURE: Standard Babel access + * - Medium input limit (500KB) + * - TypeScript + React presets + * - Max 10 files for multi-file transform + */ + SECURE: { + maxInputSize: 500 * 1024, // 500KB + maxOutputSize: 2 * 1024 * 1024, // 2MB + transformTimeout: 10000, // 10s + allowedPresets: ['typescript', 'react'], + maxFiles: 10, + maxTotalInputSize: 1024 * 1024, // 1MB + maxTotalOutputSize: 5 * 1024 * 1024, // 5MB + }, + + /** + * STANDARD: Standard Babel access (same as SECURE) + * - Medium input limit (1MB) + * - TypeScript + React presets + * - Max 25 files for multi-file transform + */ + STANDARD: { + maxInputSize: 1024 * 1024, // 1MB + maxOutputSize: 5 * 1024 * 1024, // 5MB + transformTimeout: 15000, // 15s + allowedPresets: ['typescript', 'react'], + maxFiles: 25, + maxTotalInputSize: 5 * 1024 * 1024, // 5MB + maxTotalOutputSize: 25 * 1024 * 1024, // 25MB + }, + + /** + * PERMISSIVE: Extended Babel access + * - Large input limit (5MB) + * - TypeScript + React + env presets + * - Max 100 files for multi-file transform + */ + PERMISSIVE: { + maxInputSize: 5 * 1024 * 1024, // 5MB + maxOutputSize: 25 * 1024 * 1024, // 25MB + transformTimeout: 30000, // 30s + allowedPresets: ['typescript', 'react', 'env'], + maxFiles: 100, + maxTotalInputSize: 25 * 1024 * 1024, // 25MB + maxTotalOutputSize: 125 * 1024 * 1024, // 125MB + }, +}; + +/** + * Options for Babel preset + * + * Extends AgentScriptOptions with Babel-specific options. + * The security level controls both AST validation and Babel limits. + */ +export type BabelPresetOptions = AgentScriptOptions; + +/** + * Get Babel configuration for a security level + * + * Use this to retrieve the Babel limits (input/output size, allowed presets) + * for a given security level. + * + * @param securityLevel - The security level (default: STANDARD) + * @returns Babel security configuration + * + * @example + * ```typescript + * import { getBabelConfig } from 'ast-guard'; + * + * const config = getBabelConfig('SECURE'); + * console.log(config.allowedPresets); // ['typescript', 'react'] + * console.log(config.maxInputSize); // 524288 (500KB) + * ``` + */ +export function getBabelConfig(securityLevel: SecurityLevel = 'STANDARD'): BabelSecurityConfig { + return BABEL_SECURITY_CONFIGS[securityLevel]; +} + +/** + * Create a Babel preset for AST validation + * + * This preset extends the AgentScript preset with: + * - `Babel` global (the restricted Babel.transform API) + * - `__safe_Babel` (transformed version) + * + * The Babel global provides: + * - `Babel.transform(code, options)` - Transform TSX/JSX code + * + * Security measures: + * - Preset whitelist per security level + * - Input/output size limits per security level + * - No plugins allowed (they execute arbitrary code) + * - No source maps (path leakage) + * - No AST output + * + * @param options - Babel preset options + * @returns Array of validation rules + * + * @example + * ```typescript + * import { createBabelPreset, JSAstValidator } from 'ast-guard'; + * + * const rules = createBabelPreset({ + * securityLevel: 'SECURE', + * }); + * + * const validator = new JSAstValidator(rules); + * const result = await validator.validate(code); + * ``` + * + * @example + * ```javascript + * // Inside enclave with babel preset: + * const js = Babel.transform(tsx, { + * filename: 'App.tsx', + * presets: ['typescript', 'react'], + * sourceType: 'module', + * }).code; + * + * return js; + * ``` + */ +export function createBabelPreset(options: BabelPresetOptions = {}): ValidationRule[] { + const securityLevel = options.securityLevel ?? 'STANDARD'; + const baseGlobals = options.allowedGlobals ?? []; + + return createAgentScriptPreset({ + ...options, + securityLevel, + allowedGlobals: [ + ...baseGlobals, + 'Babel', // The Babel global + '__safe_Babel', // Transformed version (for consistency) + ], + }); +} diff --git a/libs/ast-guard/src/presets/index.ts b/libs/ast-guard/src/presets/index.ts index 383fb81..e74485c 100644 --- a/libs/ast-guard/src/presets/index.ts +++ b/libs/ast-guard/src/presets/index.ts @@ -31,6 +31,15 @@ export { type SecurityLevel, } from './agentscript.preset'; +// Babel preset for TSX/JSX transformation +export { + createBabelPreset, + getBabelConfig, + BABEL_SECURITY_CONFIGS, + type BabelPresetOptions, + type BabelSecurityConfig, +} from './babel.preset'; + // Re-export for convenience import { ValidationRule } from '../interfaces'; import { ConfigurationError } from '../errors'; diff --git a/libs/ast-guard/src/transforms/import-rewrite.transform.ts b/libs/ast-guard/src/transforms/import-rewrite.transform.ts new file mode 100644 index 0000000..76cb8c6 --- /dev/null +++ b/libs/ast-guard/src/transforms/import-rewrite.transform.ts @@ -0,0 +1,318 @@ +/** + * Import Rewrite Transform + * + * Transforms npm package imports to ESM CDN URLs with specific versions. + * This enables sandboxed code to import npm packages from a trusted CDN + * without needing a bundler or package manager. + * + * SECURITY CONSIDERATIONS: + * - Only packages explicitly listed in packageVersions can be imported + * - Package names and subpaths are validated against strict regex patterns + * - CDN base URL must be HTTPS + * - Local imports (./foo, ../bar) are skipped by default + * + * @packageDocumentation + */ + +import * as acorn from 'acorn'; +import { generate } from 'astring'; +import type { ImportDeclaration, Program, Node, Literal } from 'estree'; + +/** + * Configuration for import rewriting + */ +export interface ImportRewriteConfig { + /** + * Whether import rewriting is enabled + */ + enabled: boolean; + + /** + * Base URL for the ESM CDN (must be HTTPS) + * @example 'https://esm.agentfront.dev' + */ + cdnBaseUrl: string; + + /** + * Package versions to use for each npm package. + * - Specify a version string to pin: `'react': '18.2.0'` → `/react@18.2.0` + * - Use empty string for latest: `'react': ''` → `/react` (no @version) + * + * Only packages listed here (or in allowedPackages) can be imported. + * @example { 'react': '18.2.0', '@mui/material': '' } + */ + packageVersions: Record; + + /** + * Optional allowlist of packages that can be imported without specifying a version. + * Packages in this list but not in packageVersions will use latest (no @version in URL). + * @example ['react', 'react-dom', '@mui/material'] + */ + allowedPackages?: string[]; + + /** + * Whether to skip local imports (./foo, ../bar) + * @default true + */ + skipLocalImports?: boolean; +} + +/** + * Result of import rewriting + */ +export interface ImportRewriteResult { + /** + * The rewritten code + */ + code: string; + + /** + * List of imports that were rewritten + */ + rewrittenImports: Array<{ + /** + * Original import source + * @example '@mui/material/Button' + */ + original: string; + + /** + * Rewritten CDN URL + * @example 'https://esm.agentfront.dev/@mui/material@5.15.0/Button' + */ + rewritten: string; + }>; + + /** + * List of imports that were not rewritten (e.g., local imports) + */ + skippedImports: string[]; +} + +/** + * Regex to validate npm package names + * Matches: 'react', '@mui/material', '@scope/package-name' + * Based on npm package naming rules + */ +const PACKAGE_NAME_REGEX = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i; + +/** + * Regex to validate subpaths (no path traversal) + * Matches: 'Button', 'components/Button', 'esm/index' + * Rejects: '../foo', './bar', paths with '..' + */ +const SUBPATH_REGEX = /^[a-zA-Z0-9\-_./]+$/; + +/** + * Check if an import is a local import (starts with ./ or ../) + */ +function isLocalImport(source: string): boolean { + return source.startsWith('./') || source.startsWith('../'); +} + +/** + * Parse an import source into package name and subpath + * + * @example + * parseImportSource('react') => { packageName: 'react', subpath: undefined } + * parseImportSource('@mui/material/Button') => { packageName: '@mui/material', subpath: 'Button' } + * parseImportSource('lodash/debounce') => { packageName: 'lodash', subpath: 'debounce' } + */ +function parseImportSource(source: string): { packageName: string; subpath?: string } { + // Handle scoped packages (@scope/name) + if (source.startsWith('@')) { + const parts = source.split('/'); + if (parts.length >= 2) { + const packageName = `${parts[0]}/${parts[1]}`; + const subpath = parts.length > 2 ? parts.slice(2).join('/') : undefined; + return { packageName, subpath }; + } + } + + // Handle regular packages + const parts = source.split('/'); + const packageName = parts[0]; + const subpath = parts.length > 1 ? parts.slice(1).join('/') : undefined; + + return { packageName, subpath }; +} + +/** + * Validate a package name against security rules + */ +function validatePackageName(packageName: string): void { + if (!PACKAGE_NAME_REGEX.test(packageName)) { + throw new Error(`Invalid package name: "${packageName}". Package names must follow npm naming conventions.`); + } +} + +/** + * Validate a subpath against security rules + */ +function validateSubpath(subpath: string): void { + if (!SUBPATH_REGEX.test(subpath)) { + throw new Error(`Invalid subpath: "${subpath}". Subpaths must not contain path traversal sequences.`); + } + + // Additional check for path traversal + if (subpath.includes('..')) { + throw new Error(`Invalid subpath: "${subpath}". Path traversal is not allowed.`); + } +} + +/** + * Validate the CDN base URL + */ +function validateCdnUrl(cdnBaseUrl: string): void { + try { + const url = new URL(cdnBaseUrl); + if (url.protocol !== 'https:') { + throw new Error(`CDN URL must use HTTPS: "${cdnBaseUrl}"`); + } + } catch (error) { + if (error instanceof Error && error.message.includes('HTTPS')) { + throw error; + } + throw new Error(`Invalid CDN URL: "${cdnBaseUrl}"`); + } +} + +/** + * Rewrite imports in JavaScript/TypeScript code to use CDN URLs + * + * @param code - The source code to transform + * @param config - Import rewrite configuration + * @returns The transformed code and list of rewritten imports + * + * @example + * ```typescript + * const result = rewriteImports( + * `import React from 'react'; + * import Button from '@mui/material/Button';`, + * { + * enabled: true, + * cdnBaseUrl: 'https://esm.agentfront.dev', + * packageVersions: { + * 'react': '18.2.0', + * '@mui/material': '5.15.0' + * } + * } + * ); + * + * // result.code: + * // import React from 'https://esm.agentfront.dev/react@18.2.0'; + * // import Button from 'https://esm.agentfront.dev/@mui/material@5.15.0/Button'; + * ``` + */ +export function rewriteImports(code: string, config: ImportRewriteConfig): ImportRewriteResult { + // Return early if disabled + if (!config.enabled) { + return { + code, + rewrittenImports: [], + skippedImports: [], + }; + } + + // Validate CDN URL + validateCdnUrl(config.cdnBaseUrl); + + const skipLocalImports = config.skipLocalImports ?? true; + + // If allowedPackages is provided, use it as the exclusive allowlist + // Otherwise, use packageVersions keys as the allowlist + const allowedPackages = config.allowedPackages + ? new Set(config.allowedPackages) + : new Set(Object.keys(config.packageVersions)); + + // Parse the code into an AST + let ast: Program; + try { + ast = acorn.parse(code, { + ecmaVersion: 'latest', + sourceType: 'module', + }) as unknown as Program; + } catch (error) { + throw new Error(`Failed to parse code for import rewriting: ${(error as Error).message}`); + } + + const rewrittenImports: Array<{ original: string; rewritten: string }> = []; + const skippedImports: string[] = []; + + // Walk through all import declarations + for (const node of ast.body) { + if (node.type === 'ImportDeclaration') { + const importNode = node as ImportDeclaration; + const source = importNode.source.value as string; + + // Skip local imports if configured + if (isLocalImport(source)) { + if (skipLocalImports) { + skippedImports.push(source); + continue; + } + } + + // Parse the import source + const { packageName, subpath } = parseImportSource(source); + + // Check if package is allowed + if (!allowedPackages.has(packageName)) { + throw new Error( + `Package "${packageName}" is not allowed. ` + + `Only packages listed in packageVersions or allowedPackages can be imported: ${[...allowedPackages].join(', ')}`, + ); + } + + // Validate package name + validatePackageName(packageName); + + // Validate subpath if present + if (subpath) { + validateSubpath(subpath); + } + + // Get version from config (may be empty string or undefined for latest) + const version = config.packageVersions[packageName]; + + // Construct CDN URL + // If version is empty or undefined, don't include @version (use latest) + const packageWithVersion = version ? `${packageName}@${version}` : packageName; + const cdnUrl = subpath + ? `${config.cdnBaseUrl}/${packageWithVersion}/${subpath}` + : `${config.cdnBaseUrl}/${packageWithVersion}`; + + // Update the import source + (importNode.source as Literal).value = cdnUrl; + (importNode.source as Literal).raw = JSON.stringify(cdnUrl); + + rewrittenImports.push({ + original: source, + rewritten: cdnUrl, + }); + } + } + + // Generate code from the modified AST + const rewrittenCode = generate(ast); + + return { + code: rewrittenCode, + rewrittenImports, + skippedImports, + }; +} + +/** + * Check if a string is a valid npm package name + */ +export function isValidPackageName(name: string): boolean { + return PACKAGE_NAME_REGEX.test(name); +} + +/** + * Check if a string is a valid subpath (no path traversal) + */ +export function isValidSubpath(subpath: string): boolean { + return SUBPATH_REGEX.test(subpath) && !subpath.includes('..'); +} diff --git a/libs/ast-guard/src/transforms/index.ts b/libs/ast-guard/src/transforms/index.ts index e51812f..8003c29 100644 --- a/libs/ast-guard/src/transforms/index.ts +++ b/libs/ast-guard/src/transforms/index.ts @@ -21,3 +21,12 @@ export { ConcatTransformConfig, ConcatTransformResult, } from './concat.transform'; + +// Import rewriting +export { + rewriteImports, + isValidPackageName, + isValidSubpath, + type ImportRewriteConfig, + type ImportRewriteResult, +} from './import-rewrite.transform'; diff --git a/libs/enclave-vm/package.json b/libs/enclave-vm/package.json index 0ad5204..5c67bc6 100644 --- a/libs/enclave-vm/package.json +++ b/libs/enclave-vm/package.json @@ -36,6 +36,7 @@ } }, "dependencies": { + "@babel/standalone": "^7.26.0", "@enclavejs/types": "0.1.0", "ast-guard": "2.4.0", "acorn": "8.15.0", diff --git a/libs/enclave-vm/src/__tests__/babel-examples.spec.ts b/libs/enclave-vm/src/__tests__/babel-examples.spec.ts new file mode 100644 index 0000000..555ab8a --- /dev/null +++ b/libs/enclave-vm/src/__tests__/babel-examples.spec.ts @@ -0,0 +1,422 @@ +/** + * Babel Transform Examples Tests + * + * Unit tests validating all 50 TSX/JSX examples transform correctly. + * + * @packageDocumentation + */ + +import { createRestrictedBabel, resetBabelContext, BabelWrapperConfig } from '../babel'; +import { + BABEL_EXAMPLES, + COMPLEXITY_LEVELS, + getExamplesByLevel, + getLevelStats, + ComplexityLevel, + ComponentExample, +} from './babel-examples'; + +describe('Babel Transform Examples', () => { + const defaultConfig: BabelWrapperConfig = { + maxInputSize: 1024 * 1024, // 1MB + maxOutputSize: 5 * 1024 * 1024, // 5MB + allowedPresets: ['typescript', 'react'], + transformTimeout: 15000, + }; + + let babel: ReturnType; + + beforeAll(() => { + babel = createRestrictedBabel(defaultConfig); + }); + + afterAll(() => { + resetBabelContext(); + }); + + describe('Example Coverage', () => { + it('should have exactly 50 examples', () => { + expect(BABEL_EXAMPLES.length).toBe(50); + }); + + it('should have 10 examples per complexity level', () => { + const stats = getLevelStats(); + + for (const level of COMPLEXITY_LEVELS) { + expect(stats[level].count).toBe(10); + } + }); + + it('should have unique IDs for all examples', () => { + const ids = BABEL_EXAMPLES.map((e) => e.id); + const uniqueIds = new Set(ids); + expect(uniqueIds.size).toBe(BABEL_EXAMPLES.length); + }); + + it('should have IDs from 1 to 50', () => { + const ids = BABEL_EXAMPLES.map((e) => e.id).sort((a, b) => a - b); + expect(ids).toEqual(Array.from({ length: 50 }, (_, i) => i + 1)); + }); + + it('should have unique names for all examples', () => { + const names = BABEL_EXAMPLES.map((e) => e.name); + const uniqueNames = new Set(names); + expect(uniqueNames.size).toBe(BABEL_EXAMPLES.length); + }); + }); + + describe.each(COMPLEXITY_LEVELS)('Level: %s', (level: ComplexityLevel) => { + const examples = getExamplesByLevel(level); + + describe('Transform validation', () => { + it.each(examples.map((e) => [e.name, e] as const))( + '%s - transforms without errors', + (_name: string, example: ComponentExample) => { + const result = babel.transform(example.code, { + presets: ['typescript', 'react'], + filename: `${example.name}.tsx`, + }); + + expect(result).toBeDefined(); + expect(result.code).toBeDefined(); + expect(typeof result.code).toBe('string'); + expect(result.code.length).toBeGreaterThan(0); + }, + ); + + it.each(examples.map((e) => [e.name, e] as const))( + '%s - contains expected patterns', + (_name: string, example: ComponentExample) => { + const result = babel.transform(example.code, { + presets: ['typescript', 'react'], + filename: `${example.name}.tsx`, + }); + + for (const pattern of example.expectedPatterns) { + expect(result.code).toContain(pattern); + } + }, + ); + + it.each(examples.map((e) => [e.name, e] as const))( + '%s - does not contain forbidden patterns', + (_name: string, example: ComponentExample) => { + const result = babel.transform(example.code, { + presets: ['typescript', 'react'], + filename: `${example.name}.tsx`, + }); + + for (const pattern of example.forbiddenPatterns ?? []) { + expect(result.code).not.toContain(pattern); + } + }, + ); + }); + }); + + describe('TypeScript Type Stripping', () => { + const examplesWithTypes = BABEL_EXAMPLES.filter((e) => e.forbiddenPatterns && e.forbiddenPatterns.length > 0); + + it('should have examples with TypeScript types to strip', () => { + expect(examplesWithTypes.length).toBeGreaterThan(0); + }); + + it.each(examplesWithTypes.map((e) => [e.name, e] as const))( + '%s - strips all TypeScript types', + (_name: string, example: ComponentExample) => { + const result = babel.transform(example.code, { + presets: ['typescript', 'react'], + filename: `${example.name}.tsx`, + }); + + // Verify no TypeScript-specific syntax remains + expect(result.code).not.toMatch(/\binterface\s+\w+/); + expect(result.code).not.toMatch(/\btype\s+\w+\s*=/); + expect(result.code).not.toMatch(/:\s*\w+\[\]/); + expect(result.code).not.toMatch(/<\w+>/); // Generic brackets (when not JSX) + }, + ); + }); + + describe('JSX Transformation', () => { + it('should transform JSX to React.createElement calls', () => { + for (const example of BABEL_EXAMPLES) { + const result = babel.transform(example.code, { + presets: ['typescript', 'react'], + filename: `${example.name}.tsx`, + }); + + // All examples should produce React.createElement calls + expect(result.code).toContain('React.createElement'); + } + }); + + it('should not contain JSX syntax in output', () => { + for (const example of BABEL_EXAMPLES) { + const result = babel.transform(example.code, { + presets: ['typescript', 'react'], + filename: `${example.name}.tsx`, + }); + + // No JSX opening/closing tags should remain + expect(result.code).not.toMatch(/<[A-Z][a-zA-Z]*[^>]*>/); + expect(result.code).not.toMatch(/<\/[A-Z][a-zA-Z]*>/); + expect(result.code).not.toMatch(/<[a-z]+[^>]*>/); + expect(result.code).not.toMatch(/<\/[a-z]+>/); + // Fragment syntax + expect(result.code).not.toMatch(/<>/); + expect(result.code).not.toMatch(/<\/>/); + } + }); + }); + + describe('Output Validation', () => { + it('should produce valid JavaScript for all examples', () => { + for (const example of BABEL_EXAMPLES) { + const result = babel.transform(example.code, { + presets: ['typescript', 'react'], + filename: `${example.name}.tsx`, + }); + + // Attempting to parse the output should not throw + expect(() => { + // Basic syntax check - if this throws, the output is invalid JS + new Function(result.code); + }).not.toThrow(); + } + }); + + it('should produce non-empty code for all examples', () => { + for (const example of BABEL_EXAMPLES) { + const result = babel.transform(example.code, { + presets: ['typescript', 'react'], + filename: `${example.name}.tsx`, + }); + + // All examples should produce non-trivial output + expect(result.code.length).toBeGreaterThan(50); + // Should contain at least one const or function declaration + expect(result.code).toMatch(/\b(const|function|class)\s+\w+/); + } + }); + }); + + describe('Complexity Level Characteristics', () => { + describe('L1_MINIMAL', () => { + const l1Examples = getExamplesByLevel('L1_MINIMAL'); + + it('should have simple, single-element components', () => { + for (const example of l1Examples) { + // L1 examples should be relatively short + expect(example.code.length).toBeLessThan(500); + + const result = babel.transform(example.code, { + presets: ['typescript', 'react'], + filename: `${example.name}.tsx`, + }); + + // Output should also be concise + expect(result.code.length).toBeLessThan(1000); + } + }); + }); + + describe('L2_SIMPLE', () => { + const l2Examples = getExamplesByLevel('L2_SIMPLE'); + + it('should have function components with parameters', () => { + for (const example of l2Examples) { + const result = babel.transform(example.code, { + presets: ['typescript', 'react'], + filename: `${example.name}.tsx`, + }); + + // L2 examples should contain React.createElement and function definitions + expect(result.code).toContain('React.createElement'); + // Should have either a const arrow function or function declaration + expect(result.code).toMatch(/const\s+\w+\s*=|function\s+\w+/); + } + }); + }); + + describe('L3_STYLED', () => { + const l3Examples = getExamplesByLevel('L3_STYLED'); + + it('should have style-related code', () => { + for (const example of l3Examples) { + const result = babel.transform(example.code, { + presets: ['typescript', 'react'], + filename: `${example.name}.tsx`, + }); + + // L3 examples should have style or className + const hasStyles = + result.code.includes('style:') || result.code.includes('className:') || result.code.includes('styles.'); + expect(hasStyles).toBe(true); + } + }); + }); + + describe('L4_COMPOSITE', () => { + const l4Examples = getExamplesByLevel('L4_COMPOSITE'); + + it('should have multiple component definitions or complex patterns', () => { + for (const example of l4Examples) { + // L4 examples should have more code + expect(example.code.length).toBeGreaterThan(200); + + const result = babel.transform(example.code, { + presets: ['typescript', 'react'], + filename: `${example.name}.tsx`, + }); + + // Multiple React.createElement calls expected + const createElementCount = (result.code.match(/React\.createElement/g) || []).length; + expect(createElementCount).toBeGreaterThan(1); + } + }); + }); + + describe('L5_COMPLEX', () => { + const l5Examples = getExamplesByLevel('L5_COMPLEX'); + + it('should have TypeScript types to strip', () => { + for (const example of l5Examples) { + // All L5 examples should have forbidden patterns (types) + expect(example.forbiddenPatterns).toBeDefined(); + expect(example.forbiddenPatterns!.length).toBeGreaterThan(0); + } + }); + + it('should be the most complex examples', () => { + const l5Stats = getLevelStats()['L5_COMPLEX']; + const l1Stats = getLevelStats()['L1_MINIMAL']; + + // L5 average size should be significantly larger than L1 + expect(l5Stats.avgSize).toBeGreaterThan(l1Stats.avgSize * 3); + }); + }); + }); + + describe('Edge Cases', () => { + it('should handle empty fragment', () => { + const result = babel.transform('const Empty = () => <>;', { + presets: ['typescript', 'react'], + }); + expect(result.code).toContain('React.createElement'); + expect(result.code).toContain('React.Fragment'); + }); + + it('should handle deeply nested JSX', () => { + const deeplyNested = ` + const Deep = () => ( +
+
+
+
+
+ Deep +
+
+
+
+
+ ); + `; + const result = babel.transform(deeplyNested, { + presets: ['typescript', 'react'], + }); + expect(result.code).toContain('React.createElement'); + const createElementCount = (result.code.match(/React\.createElement/g) || []).length; + expect(createElementCount).toBe(6); + }); + + it('should handle mixed expressions and elements', () => { + const mixed = ` + const Mixed = ({ items }: { items: string[] }) => ( +
    + {items.length === 0 &&
  • No items
  • } + {items.length > 0 && items.map((item, i) =>
  • {item}
  • )} + {items.length > 10 &&
  • ...and more
  • } +
+ ); + `; + const result = babel.transform(mixed, { + presets: ['typescript', 'react'], + }); + expect(result.code).toContain('React.createElement'); + expect(result.code).not.toContain('interface'); + expect(result.code).not.toContain(': string[]'); + }); + + it('should handle class components with lifecycle methods', () => { + const classComponent = ` + class LifecycleComponent extends React.Component<{ name: string }, { mounted: boolean }> { + state = { mounted: false }; + componentDidMount() { this.setState({ mounted: true }); } + componentWillUnmount() { console.log('unmounting'); } + render() { + return
{this.state.mounted ? 'Mounted' : 'Not mounted'}
; + } + } + `; + const result = babel.transform(classComponent, { + presets: ['typescript', 'react'], + }); + expect(result.code).toContain('React.createElement'); + expect(result.code).toContain('componentDidMount'); + expect(result.code).toContain('componentWillUnmount'); + expect(result.code).not.toContain('<{ name: string }'); + }); + + it('should handle JSX spread attributes', () => { + const spread = ` + const Spread = (props: { id: string; className: string }) => { + const extra = { 'data-test': 'value', role: 'button' }; + return
Content
; + }; + `; + const result = babel.transform(spread, { + presets: ['typescript', 'react'], + }); + expect(result.code).toContain('React.createElement'); + expect(result.code).not.toContain(': { id: string'); + }); + + it('should handle template literals in className', () => { + const template = ` + const Template = ({ active }: { active: boolean }) => ( +
Content
+ ); + `; + const result = babel.transform(template, { + presets: ['typescript', 'react'], + }); + expect(result.code).toContain('React.createElement'); + expect(result.code).toContain('active'); + expect(result.code).not.toContain(': { active: boolean }'); + }); + }); + + describe('React-only transforms (no TypeScript)', () => { + it('should transform pure JSX without TypeScript preset', () => { + const jsx = 'const Pure = () =>
Pure JSX
;'; + const result = babel.transform(jsx, { + presets: ['react'], + filename: 'Pure.jsx', + }); + expect(result.code).toContain('React.createElement'); + expect(result.code).toContain('"div"'); + }); + + it('should fail on TypeScript syntax with only react preset', () => { + const tsx = 'const Typed = ({ name }: { name: string }) =>
{name}
;'; + expect(() => { + babel.transform(tsx, { + presets: ['react'], + filename: 'Typed.jsx', + }); + }).toThrow(); + }); + }); +}); diff --git a/libs/enclave-vm/src/__tests__/babel-examples.ts b/libs/enclave-vm/src/__tests__/babel-examples.ts new file mode 100644 index 0000000..9a9b705 --- /dev/null +++ b/libs/enclave-vm/src/__tests__/babel-examples.ts @@ -0,0 +1,1299 @@ +/** + * Babel Transform Test Examples + * + * 50 TSX/JSX component examples ranging from minimal to complex. + * Used for testing the Babel preset transform capability. + * + * @packageDocumentation + */ + +/** + * Complexity levels for component examples + */ +export type ComplexityLevel = 'L1_MINIMAL' | 'L2_SIMPLE' | 'L3_STYLED' | 'L4_COMPOSITE' | 'L5_COMPLEX'; + +/** + * Array of all complexity levels for iteration + */ +export const COMPLEXITY_LEVELS: ComplexityLevel[] = [ + 'L1_MINIMAL', + 'L2_SIMPLE', + 'L3_STYLED', + 'L4_COMPOSITE', + 'L5_COMPLEX', +]; + +/** + * Component example for testing Babel transforms + */ +export interface ComponentExample { + /** Unique identifier (1-50) */ + id: number; + /** Component name */ + name: string; + /** Complexity level */ + level: ComplexityLevel; + /** Human-readable description */ + description: string; + /** Source TSX/JSX code */ + code: string; + /** Patterns that MUST appear in transformed output */ + expectedPatterns: string[]; + /** Patterns that must NOT appear in output (e.g., TypeScript types) */ + forbiddenPatterns?: string[]; +} + +/** + * 50 TSX/JSX component examples organized by complexity level + */ +export const BABEL_EXAMPLES: ComponentExample[] = [ + // ========================================================================== + // L1: MINIMAL (1-10) - Single elements, no props, basic JSX + // ========================================================================== + { + id: 1, + name: 'PlainText', + level: 'L1_MINIMAL', + description: 'Plain text element', + code: `const PlainText = () =>
Hello World
;`, + expectedPatterns: ['React.createElement', '"div"', '"Hello World"'], + }, + { + id: 2, + name: 'SelfClosing', + level: 'L1_MINIMAL', + description: 'Self-closing element', + code: `const SelfClosing = () => ;`, + expectedPatterns: ['React.createElement', '"input"', 'type:', '"text"'], + }, + { + id: 3, + name: 'WithExpression', + level: 'L1_MINIMAL', + description: 'Element with expression', + code: `const WithExpression = () => {1 + 1};`, + expectedPatterns: ['React.createElement', '"span"', '1 + 1'], + }, + { + id: 4, + name: 'WithFragment', + level: 'L1_MINIMAL', + description: 'Fragment with children', + code: `const WithFragment = () => <>AB;`, + expectedPatterns: ['React.createElement', 'React.Fragment', '"span"'], + }, + { + id: 5, + name: 'Siblings', + level: 'L1_MINIMAL', + description: 'Multiple sibling elements', + code: `const Siblings = () =>
OneTwo
;`, + expectedPatterns: ['React.createElement', '"div"', '"span"', '"One"', '"Two"'], + }, + { + id: 6, + name: 'NestedElements', + level: 'L1_MINIMAL', + description: 'Deeply nested elements', + code: `const NestedElements = () =>

Deep

;`, + expectedPatterns: ['React.createElement', '"div"', '"section"', '"article"', '"p"', '"Deep"'], + }, + { + id: 7, + name: 'WithClassName', + level: 'L1_MINIMAL', + description: 'Element with className', + code: `const WithClassName = () =>
Content
;`, + expectedPatterns: ['React.createElement', '"div"', 'className:', '"container"'], + }, + { + id: 8, + name: 'WithDataAttribute', + level: 'L1_MINIMAL', + description: 'Element with data attribute', + code: `const WithDataAttribute = () =>
Test
;`, + expectedPatterns: ['React.createElement', '"div"', 'data-testid', '"my-element"'], + }, + { + id: 9, + name: 'WithSpreadProps', + level: 'L1_MINIMAL', + description: 'Element with spread props', + code: `const props = { id: 'main', role: 'main' }; +const WithSpreadProps = () =>
Spread
;`, + expectedPatterns: ['React.createElement', '"div"', 'props'], + }, + { + id: 10, + name: 'ArrowComponent', + level: 'L1_MINIMAL', + description: 'Arrow function component', + code: `const ArrowComponent = () => ;`, + expectedPatterns: ['React.createElement', '"button"', '"Click"'], + }, + + // ========================================================================== + // L2: SIMPLE (11-20) - Props, events, basic patterns + // ========================================================================== + { + id: 11, + name: 'PropsDestructuring', + level: 'L2_SIMPLE', + description: 'Props destructuring', + code: `interface ButtonProps { label: string; onClick: () => void; } +const Button = ({ label, onClick }: ButtonProps) => ( + +);`, + expectedPatterns: ['React.createElement', '"button"', 'onClick', 'label'], + forbiddenPatterns: ['interface', 'ButtonProps', ': string', ': void'], + }, + { + id: 12, + name: 'ChildrenProp', + level: 'L2_SIMPLE', + description: 'Children prop usage', + code: `interface WrapperProps { children: React.ReactNode; } +const Wrapper = ({ children }: WrapperProps) => ( +
{children}
+);`, + expectedPatterns: ['React.createElement', '"div"', 'children', '"wrapper"'], + forbiddenPatterns: ['interface', 'WrapperProps', 'ReactNode'], + }, + { + id: 13, + name: 'EventHandler', + level: 'L2_SIMPLE', + description: 'Event handler', + code: `const EventHandler = () => ( + +);`, + expectedPatterns: ['React.createElement', '"button"', 'onClick', 'console.log', 'e.target'], + }, + { + id: 14, + name: 'ConditionalRendering', + level: 'L2_SIMPLE', + description: 'Conditional rendering', + code: `interface ShowProps { show: boolean; } +const Conditional = ({ show }: ShowProps) => ( +
{show ? Visible : Hidden}
+);`, + expectedPatterns: ['React.createElement', '"div"', '"span"', 'show', '"Visible"', '"Hidden"'], + forbiddenPatterns: ['interface', 'ShowProps', ': boolean'], + }, + { + id: 15, + name: 'ArrayMap', + level: 'L2_SIMPLE', + description: 'Array map rendering', + code: `interface ListProps { items: string[]; } +const List = ({ items }: ListProps) => ( +
    {items.map((item, i) =>
  • {item}
  • )}
+);`, + expectedPatterns: ['React.createElement', '"ul"', '"li"', 'map', 'key:'], + forbiddenPatterns: ['interface', 'ListProps', 'string[]'], + }, + { + id: 16, + name: 'OptionalChaining', + level: 'L2_SIMPLE', + description: 'Optional chaining in JSX', + code: `interface UserProps { user?: { name: string; }; } +const UserName = ({ user }: UserProps) => ( + {user?.name ?? 'Anonymous'} +);`, + expectedPatterns: ['React.createElement', '"span"', 'user', 'Anonymous'], + forbiddenPatterns: ['interface', 'UserProps'], + }, + { + id: 17, + name: 'DefaultProps', + level: 'L2_SIMPLE', + description: 'Default props pattern', + code: `interface GreetingProps { name?: string; } +const Greeting = ({ name = 'World' }: GreetingProps) => ( +

Hello, {name}!

+);`, + expectedPatterns: ['React.createElement', '"h1"', 'World', 'name'], + forbiddenPatterns: ['interface', 'GreetingProps'], + }, + { + id: 18, + name: 'ChildrenArray', + level: 'L2_SIMPLE', + description: 'Rendering children array', + code: `const items = [A, B]; +const ChildrenArray = () =>
{items}
;`, + expectedPatterns: ['React.createElement', '"div"', '"span"', 'items'], + }, + { + id: 19, + name: 'KeyPropList', + level: 'L2_SIMPLE', + description: 'Key prop in list', + code: `interface DataItem { id: string; text: string; } +interface DataListProps { items: DataItem[]; } +const DataList = ({ items }: DataListProps) => ( +
    {items.map(item =>
  • {item.text}
  • )}
+);`, + expectedPatterns: ['React.createElement', '"ul"', '"li"', 'key:', 'item.id', 'item.text'], + forbiddenPatterns: ['interface DataItem', 'interface DataListProps', 'DataItem[]'], + }, + { + id: 20, + name: 'ComponentComposition', + level: 'L2_SIMPLE', + description: 'Component composition', + code: `const Header = () =>

Title

; +const Footer = () =>

Footer

; +const Page = () =>
Content
;`, + expectedPatterns: ['React.createElement', 'Header', 'Footer', '"header"', '"footer"', '"main"'], + }, + + // ========================================================================== + // L3: STYLED (21-30) - Inline styles, dynamic styling, CSS patterns + // ========================================================================== + { + id: 21, + name: 'InlineStyle', + level: 'L3_STYLED', + description: 'Inline style object', + code: `const StyledBox = () => ( +
+ Styled content +
+);`, + expectedPatterns: ['React.createElement', '"div"', 'style:', 'padding:', 'backgroundColor:', 'borderRadius:'], + }, + { + id: 22, + name: 'DynamicStyles', + level: 'L3_STYLED', + description: 'Dynamic styles based on props', + code: `interface BoxProps { size: 'sm' | 'md' | 'lg'; } +const DynamicBox = ({ size }: BoxProps) => { + const sizes = { sm: 8, md: 16, lg: 24 }; + return
Content
; +};`, + expectedPatterns: ['React.createElement', '"div"', 'style:', 'sizes', 'size'], + forbiddenPatterns: ['interface', 'BoxProps', "'sm' | 'md' | 'lg'"], + }, + { + id: 23, + name: 'ConditionalClassName', + level: 'L3_STYLED', + description: 'Conditional className', + code: `interface AlertProps { type: 'success' | 'error' | 'warning'; message: string; } +const Alert = ({ type, message }: AlertProps) => ( +
{message}
+);`, + expectedPatterns: ['React.createElement', '"div"', 'className:', 'alert', 'type'], + forbiddenPatterns: ['interface', 'AlertProps'], + }, + { + id: 24, + name: 'CSSModulesPattern', + level: 'L3_STYLED', + description: 'CSS modules pattern', + code: `const styles = { container: 'container_abc123', title: 'title_def456' }; +const CSSModules = () => ( +
+

Styled Title

+
+);`, + expectedPatterns: ['React.createElement', '"div"', '"h1"', 'styles.container', 'styles.title'], + }, + { + id: 25, + name: 'StyleVariables', + level: 'L3_STYLED', + description: 'CSS variables in style', + code: `interface ThemeProps { primaryColor: string; } +const ThemedButton = ({ primaryColor }: ThemeProps) => ( + +);`, + expectedPatterns: ['React.createElement', '"button"', 'style:', '--primary', 'primaryColor'], + forbiddenPatterns: ['interface', 'ThemeProps', 'CSSProperties'], + }, + { + id: 26, + name: 'ResponsiveStyles', + level: 'L3_STYLED', + description: 'Responsive style helper', + code: `interface CardProps { compact?: boolean; } +const ResponsiveCard = ({ compact = false }: CardProps) => ( +
+ Card Content +
+);`, + expectedPatterns: ['React.createElement', '"div"', 'style:', 'compact', 'padding:', 'maxWidth:'], + forbiddenPatterns: ['interface', 'CardProps'], + }, + { + id: 27, + name: 'ThemeAwareStyles', + level: 'L3_STYLED', + description: 'Theme-aware styles', + code: `interface ColorPalette { colors: { primary: string; secondary: string; }; } +interface ColorBoxProps { palette: ColorPalette; } +const ThemeAwareBox = ({ palette }: ColorBoxProps) => ( +
+ Themed Box +
+);`, + expectedPatterns: ['React.createElement', '"div"', 'palette.colors.primary', 'palette.colors.secondary'], + forbiddenPatterns: ['interface ColorPalette', 'interface ColorBoxProps'], + }, + { + id: 28, + name: 'AnimationStyles', + level: 'L3_STYLED', + description: 'Animation styles', + code: `interface AnimatedProps { isVisible: boolean; } +const AnimatedBox = ({ isVisible }: AnimatedProps) => ( +
+ Animated Content +
+);`, + expectedPatterns: ['React.createElement', '"div"', 'opacity:', 'transform:', 'transition:', 'isVisible'], + forbiddenPatterns: ['interface', 'AnimatedProps'], + }, + { + id: 29, + name: 'PseudoClassPatterns', + level: 'L3_STYLED', + description: 'Hover state pattern (inline)', + code: `const HoverButton = () => { + const [isHovered, setIsHovered] = [false, (v: boolean) => {}]; + return ( + + ); +};`, + expectedPatterns: ['React.createElement', '"button"', 'onMouseEnter', 'onMouseLeave', 'isHovered'], + }, + { + id: 30, + name: 'MediaQueryStyles', + level: 'L3_STYLED', + description: 'Media query responsive pattern', + code: `interface ResponsiveProps { isMobile: boolean; } +const ResponsiveLayout = ({ isMobile }: ResponsiveProps) => ( +
+ +
Main Content
+
+);`, + expectedPatterns: ['React.createElement', '"div"', '"aside"', '"main"', 'flexDirection', 'isMobile'], + forbiddenPatterns: ['interface', 'ResponsiveProps'], + }, + + // ========================================================================== + // L4: COMPOSITE (31-40) - Multi-component patterns, render props, HOCs + // ========================================================================== + { + id: 31, + name: 'ParentChildProps', + level: 'L4_COMPOSITE', + description: 'Parent-child prop passing', + code: `interface CardProps { title: string; children: React.ReactNode; } +const Card = ({ title, children }: CardProps) => ( +
+

{title}

+
{children}
+
+); + +const CardExample = () => ( + +

Card content goes here

+
+);`, + expectedPatterns: ['React.createElement', '"div"', '"h2"', 'title', 'children', 'Card'], + forbiddenPatterns: ['interface', 'CardProps', 'ReactNode'], + }, + { + id: 32, + name: 'RenderProps', + level: 'L4_COMPOSITE', + description: 'Render props pattern', + code: `interface MouseTrackerProps { render: (x: number, y: number) => React.ReactNode; } +const MouseTracker = ({ render }: MouseTrackerProps) => { + const position = { x: 0, y: 0 }; + return
{}}>{render(position.x, position.y)}
; +}; + +const TrackerExample = () => ( + Mouse: {x}, {y}} /> +);`, + expectedPatterns: ['React.createElement', 'render', 'position', 'MouseTracker'], + forbiddenPatterns: ['interface', 'MouseTrackerProps', 'ReactNode'], + }, + { + id: 33, + name: 'CompoundComponents', + level: 'L4_COMPOSITE', + description: 'Compound components pattern', + code: `const TabsContext = { activeTab: 0 }; + +interface TabProps { index: number; children: React.ReactNode; } +const Tab = ({ index, children }: TabProps) => ( +
{children}
+); + +interface TabsProps { children: React.ReactNode; } +const Tabs = ({ children }: TabsProps) => ( +
{children}
+); + +const TabsExample = () => ( + + Tab 1 Content + Tab 2 Content + +);`, + expectedPatterns: ['React.createElement', 'Tab', 'Tabs', 'TabsContext', 'activeTab', 'index'], + forbiddenPatterns: ['interface', 'TabProps', 'TabsProps'], + }, + { + id: 34, + name: 'SlotPattern', + level: 'L4_COMPOSITE', + description: 'Slot pattern', + code: `interface LayoutProps { + header?: React.ReactNode; + sidebar?: React.ReactNode; + children: React.ReactNode; + footer?: React.ReactNode; +} +const Layout = ({ header, sidebar, children, footer }: LayoutProps) => ( +
+ {header &&
{header}
} +
+ {sidebar && } +
{children}
+
+ {footer &&
{footer}
} +
+);`, + expectedPatterns: ['React.createElement', 'header', 'sidebar', 'children', 'footer', '"layout"'], + forbiddenPatterns: ['interface', 'LayoutProps', 'ReactNode'], + }, + { + id: 35, + name: 'HOCPattern', + level: 'L4_COMPOSITE', + description: 'Higher-order component pattern', + code: `interface WithLoadingProps { isLoading: boolean; } +function withLoading

(Component: React.ComponentType

) { + return ({ isLoading, ...props }: P & WithLoadingProps) => { + if (isLoading) return

Loading...
; + return ; + }; +} + +interface DataProps { data: string; } +const DataDisplay = ({ data }: DataProps) =>
{data}
; +const DataDisplayWithLoading = withLoading(DataDisplay);`, + expectedPatterns: ['React.createElement', 'isLoading', 'Loading', 'Component', 'withLoading'], + forbiddenPatterns: ['interface', 'WithLoadingProps', 'DataProps', 'ComponentType'], + }, + { + id: 36, + name: 'ContextConsumer', + level: 'L4_COMPOSITE', + description: 'Context consumer pattern', + code: `interface ThemeContextType { theme: 'light' | 'dark'; toggleTheme: () => void; } +const ThemeContext = { theme: 'light' as const, toggleTheme: () => {} }; + +const ThemedComponent = () => { + const { theme, toggleTheme } = ThemeContext; + return ( +
+ +

Current theme: {theme}

+
+ ); +};`, + expectedPatterns: ['React.createElement', 'theme', 'toggleTheme', 'ThemeContext'], + forbiddenPatterns: ['interface', 'ThemeContextType'], + }, + { + id: 37, + name: 'ForwardRefPattern', + level: 'L4_COMPOSITE', + description: 'Forward ref pattern', + code: `interface InputProps { + label: string; + placeholder?: string; +} + +const ForwardedInput = React.forwardRef( + ({ label, placeholder }, ref) => ( +
+ + +
+ ) +); + +const InputExample = () => { + const inputRef = { current: null }; + return ; +};`, + expectedPatterns: ['React.createElement', 'forwardRef', 'ref', 'label', 'placeholder'], + forbiddenPatterns: ['interface', 'InputProps', 'HTMLInputElement'], + }, + { + id: 38, + name: 'ControlledComponent', + level: 'L4_COMPOSITE', + description: 'Controlled component pattern', + code: `interface ControlledInputProps { + value: string; + onChange: (value: string) => void; + label: string; +} + +const ControlledInput = ({ value, onChange, label }: ControlledInputProps) => ( +
+ + onChange(e.target.value)} + /> + {value.length} characters +
+);`, + expectedPatterns: ['React.createElement', 'value', 'onChange', 'e.target.value', 'value.length'], + forbiddenPatterns: ['interface', 'ControlledInputProps'], + }, + { + id: 39, + name: 'FormWithFields', + level: 'L4_COMPOSITE', + description: 'Form with multiple fields', + code: `interface ContactFormProps { onSubmit: (data: FormData) => void; } +const ContactForm = ({ onSubmit }: ContactFormProps) => ( +
{ e.preventDefault(); onSubmit(new FormData(e.target as HTMLFormElement)); }}> +
+ + +
+
+ + +
+
+ +